Skip to content
Lucent
SEARCH LUCENT

Search guides, APIs, and examples.

GitHub

Very early and experimental. The language, the generated native code and every package API change without notice. Do not use Lucent in production.

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.

  1. The build, step 1: TypeScript checkergreet.lucent.tsyour moduleTypeScript checkerLucent's rules · LUCENT codes1C++one file per module · #line to your source2.lucent/nativethe native package3Xcode · GradleCocoaPods · CMake · into your app4Metroeach import → a proxy in the JS bundle5

    Lucent runs the real TypeScript compiler, in strict mode with noUncheckedIndexedAccess. Every type comes from the checker, including narrowing, so your editor and lucent build see the same types.

    Then Lucent applies its own rules: no any, no var, no dynamic property access. Breaking one stops the build with a LUCENT code, and nothing is written. The diagnostics list every code.

  2. The build, step 2: C++greet.lucent.tsyour moduleTypeScript checkerLucent's rules · LUCENT codes1C++one file per module · #line to your source2.lucent/nativethe native package3Xcode · GradleCocoaPods · CMake · into your app4Metroeach import → a proxy in the JS bundle5
    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 trimmed
    namespace 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 #line directives point back to your source, so compiler errors, the debugger and crash reports name .lucent.ts lines.

    The C++ keeps JavaScript's behavior. A number is a double with JavaScript's arithmetic, strings are UTF-16, and objects are shared by reference. The few differences are listed.

  3. The build, step 3: .lucent/nativegreet.lucent.tsyour moduleTypeScript checkerLucent's rules · LUCENT codes1C++one file per module · #line to your source2.lucent/nativethe native package3Xcode · GradleCocoaPods · CMake · into your app4Metroeach import → a proxy in the JS bundle5
    In .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 check and the Android dependency step remember between runs.

    .lucent/ is generated, so keep it out of git. When nothing changed, lucent build returns at once. Otherwise it rewrites only the files whose content changed.

  4. The build, step 4: Xcode · Gradlegreet.lucent.tsyour moduleTypeScript checkerLucent's rules · LUCENT codes1C++one file per module · #line to your source2.lucent/nativethe native package3Xcode · GradleCocoaPods · CMake · into your app4Metroeach import → a proxy in the JS bundle5

    Autolinking finds the native package through the lucent entry in react-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 Lucent serves every module. There's no codegen step, and no Swift or Kotlin.

  5. The build, step 5: Metrogreet.lucent.tsyour moduleTypeScript checkerLucent's rules · LUCENT codes1C++one file per module · #line to your source2.lucent/nativethe native package3Xcode · GradleCocoaPods · CMake · into your app4Metroeach import → a proxy in the JS bundle5
    .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 tsc read the .lucent.ts source, 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. Run lucent build, then rebuild the app.