How a module becomes native code
Five steps turn a .lucent.ts file into C++ in your app binary, and into a proxy in your JS bundle.
-
The TypeScript checker checks your module
Section titled “The TypeScript checker checks your module”Lucent runs the real TypeScript compiler, in strict mode with
noUncheckedIndexedAccess. Every type comes from the checker, including narrowing, so your editor andlucent buildsee the same types.Then Lucent applies its own rules: no
any, novar, no dynamic property access. Breaking one stops the build with aLUCENTcode, and nothing is written. The diagnostics list every code. -
Lucent writes C++
Section titled “Lucent writes C++”greet.lucent.ts export function greet(name: string): string {const trimmed = name.trim();return trimmed.length > 0 ? `Hello, ${trimmed}!` : "Hello, stranger!";}See the C++
m_greet.cpp // Generated by Lucent from greet.lucent.ts. Do not edit.#include "m_greet.h"#pragma push_macro("greet")#undef greet#pragma push_macro("trimmed")#undef trimmednamespace lucent_app {lucent::String m_greet::greet(lucent::String p0_) {#line 2 "greet.lucent.ts"lucent::String trimmed = p0_.trim();#line 3 "greet.lucent.ts"double v3_ = static_cast<double>(trimmed.length());bool v5_ = v3_ > 0.0;lucent::String v12_{};if (v5_) {v12_ = lucent::String(lucent::String(LUCENT_STR("Hello, ")) + trimmed) + LUCENT_STR("!");} else {v12_ = LUCENT_STR("Hello, stranger!");}#line 3 "greet.lucent.ts"return v12_;}void m_greet::init() {}} // namespace lucent_app#pragma pop_macro("trimmed")#pragma pop_macro("greet")Each module becomes one C++ file and one header. The
#linedirectives point back to your source, so compiler errors, the debugger and crash reports name.lucent.tslines.The C++ keeps JavaScript's behavior. A
numberis adoublewith JavaScript's arithmetic, strings are UTF-16, and objects are shared by reference. The few differences are listed. -
Section titled “lucent build writes the native package”lucent buildwrites the native packageIn .lucent/What it holds native/cpp/lucent/,native/cpp/rn/The C++ runtime, and Lucent, the one TurboModule that serves every module.native/cpp/generated/Your modules' C++, with a folder per platform when a module has platform code. native/LucentNative.podspec,native/ios/The CocoaPods spec, and the iOS registration. native/android/The CMake project, the Gradle file and the Android manifest. native/js/One proxy per module, and the loader they share. native/types/Declarations of the lucent:*imports, for your editor.native/manifest.jsonThe module list, and a hash of the build's inputs. check.json,android-classpath*.jsonWhat lucent checkand the Android dependency step remember between runs..lucent/is generated, so keep it out of git. When nothing changed,lucent buildreturns at once. Otherwise it rewrites only the files whose content changed. -
Your app build compiles it
Section titled “Your app build compiles it”Autolinking finds the native package through the
lucententry inreact-native.config.js. On iOS, CocoaPods compiles it, and it registers itself with React Native when the app loads. On Android, Gradle adds its CMake project to the app's native build.Either way, one C++ TurboModule named
Lucentserves every module. There's no codegen step, and no Swift or Kotlin. -
Metro swaps the import for a proxy
Section titled “Metro swaps the import for a proxy”.lucent/native/js/greet.js // Generated by Lucent. Do not edit."use strict";Object.defineProperty(exports, "__esModule", { value: true });const { loadModule, lucentClass } = require("./_lucent/runtime.js");const m = loadModule("greet",() => require("react-native").TurboModuleRegistry,require("./_lucent/identity.js"),);exports.greet = m.greet;Your editor and
tscread the.lucent.tssource, so the import is typed. Metro bundles this proxy instead, so none of the module's code is in the JS bundle.If the module was never built, the proxy throws
Lucent: greet.lucent.ts has not been compiled. Runlucent build, then rebuild the app.