@keel-ai/modules-core
v0.1.0
Published
Native foundation for Keel modules — runtime (KeelModule base + JSI) + codegen (Swift Macros + Kotlin KSP), mirrored Apple↔Google. Sibling packages @keel-ai/modules-autolinking + @keel-ai/module-scripts + create-keel-module split out the per-module toolin
Readme
@keel-ai/modules-core
Foundation for the Keel native-module framework — spiritual sibling of Expo Modules API, optimized for the China-friendly RN stack (no Expo runtime dep).
One npm/SPM package, two responsibilities — runtime + compile-time codegen — mirrored on iOS and Android:
| | iOS | Android |
|---|---|---|
| Runtime (base classes, JSI install, install-context) | ios/KeelModuleCore/*.{swift,mm} + cpp/*.cpp → built into KeelModuleCore.xcframework (binary) | android/ → built into keel-sdk.aar / libkeel.so |
| Codegen (@KeelModule / @Constant / @Function / @AsyncFunction) | ios-macros/ — SwiftPM compiler plugin (KeelMacrosImpl + library KeelMacros) | android-ksp/ — Kotlin KSP processor (keel-annotations + keel-ksp) |
Module discovery + provider generation (the "autolinking" piece — Expo ships it as a sibling
expo-modules-autolinkingpackage) is NOT in this package's scope. Today the snippet helpers below cover the per-module link/install path; scan→generate-KeelModulesProvideris planned in a separate package (parallel to Expo's split).
Package architecture (SPM, model B)
Package.swift exposes one package, two products:
products: [
.library(name: "KeelModuleCore", targets: ["KeelModuleCore"]), // binary (xcframework)
.library(name: "KeelMacros", targets: ["KeelMacros"]), // source (+ .macro plugin)
]Leaf @keel-ai/* packages declare one package dependency on modules-core,
then pick which products to use:
.target(
name: "KeelDevice",
dependencies: [
.product(name: "KeelModuleCore", package: "modules-core"), // KeelModule base + InstallContext
.product(name: "KeelMacros", package: "modules-core"), // @KeelModule macro expansion
]
)Two products are required because SPM cannot merge a .binaryTarget and
a source .target into one library product — they're fundamentally
different artifacts.
Why both KeelModuleCore.podspec and Package.swift
KeelModuleCore cannot be packaged as a source-level SPM target — SPM
rejects mixed Swift + ObjC++ + C++ in one target, and the internal
Swift↔ObjC++ coupling is cyclic. It must be pre-built in Xcode (which
co-compiles the mix into one framework module). The build pipeline:
KeelModuleCore.podspec (BUILD recipe: source_files + RN C++ header
paths + C++20 + -undefined dynamic_lookup +
React-Core / React-jsi / ReactCommon deps)
↓ pod install (in keel/go/ios — provides the RN-pod context)
↓ xcodebuild × 2 slices (iphoneos + iphonesimulator)
↓ xcodebuild -create-xcframework
keel/build/ios/KeelModuleCore.xcframework
↓ modules-core/Package.swift .binaryTarget(path: "../build/ios/KeelModuleCore.xcframework")
↓ consumed by leaf @keel-ai/* + sdk SPM packages (model B graph)Reproduce with keel/modules-core/packaging/build-ios.sh. Same pattern Expo uses for
ExpoModulesCore: pod-built, SPM-distributed.
The KeelMacros half ships as source SPM target — no podspec, no
prebuilt step. A leaf SPM consumer importing the KeelMacros library
gets -load-plugin-executable auto-injected by SPM (zero-hack macro
codegen, no manual flag / vendored declaration / shell build-phase).
Library API (TypeScript)
The npm side of @keel-ai/modules-core also ships a small TS surface used by
the per-module CLI:
loadModuleSpec()/validateModuleSpec()— parse + lintmodule.ymlgeneratePodfileSnippet()/generateGradleSnippet()— emit per-module autolink snippets (used bykeel-module link/install)
CLI
Daily-dev only (scaffolding lives in the standalone create-keel-module
package — npm create keel-module <name>):
keel-module validate— lintmodule.yml+ check declared sources existkeel-module link— print Podfile + Gradle autolink snippets for the current modulekeel-module install— in-place inject those snippets into the host app'sPodfile+settings.gradle.kts. Idempotent (fenced begin/end comments — re-running replaces, doesn't duplicate)keel-module test— run the module's test suite in a minimal host-app contextkeel-module publish— validate + (optionally) bump version acrosspackage.json+module.yml+npm publish
module.yml
name: keel-module-wechat
version: 1.0.0
platforms: [ios, android]
ios:
pod: KeelModuleWechat
pod_path: .
min_ios: "13.0"
android:
aar: keel-module-wechat
gradle_path: ./android
min_sdk: 24
js:
entry: ./src/index.ts
peer_runtime: "^1.0.0"Distribution model
Flat — every Keel module is an independent npm package. Two artifact families ship from this repo:
SDK runtime —
keel/build/ios/{KeelModuleCore,Keel}.xcframework+keel/build/android/keel-sdk.aar, built bykeel/packaging/build-ios.shandbuild-android.sh. Hosts add SPM packages explicitly (mirrors Expo's pattern; no@_exportedumbrella):@keel-ai/keel→Keellibrary (KeelView + KeelRuntimeHost, via.binaryTargetonKeel.xcframework).@keel-ai/modules-core→KeelModuleCore+KeelMacrosproducts.- Each used leaf (
@keel-ai/{application,constants,…}) → its own SPM library.keel/go/ios/project.yml'spackages:+dependencies:blocks are the worked example. The 6 leaf packages no longer ship podspecs — they're pure source SPM consumed only via the manifests.
Keel npm packages — under
keel/packages/(native default-kernel- pure-TS) and
keel/modules/(opt-in: 5 vendor modules + first-party apple-signin / google-signin). Each independently versioned + published.
- pure-TS) and
See keel/docs/architecture.md § Modules API for the full picture.
