What is Lucent
Lucent brings native logic and views to React Native through TypeScript. Write modules in .lucent.ts and experimental native components in .lucent.tsx.
export type Summary = { count: number; mean: number; max: number };
export function summarize(values: number[]): Summary { let sum = 0; let max = -Infinity; for (const value of values) { sum += value; if (value > max) max = value; } const mean = values.length > 0 ? sum / values.length : 0; return { count: values.length, mean, max };}import { summarize } from "./src/stats.lucent";
summarize([3, 1, 4, 1, 5]); // { count: 5, mean: 2.8, max: 5 }See the C++
// Generated by Lucent from stats.lucent.ts. Do not edit.#include "m_stats.h"
#pragma push_macro("count")#undef count#pragma push_macro("max")#undef max#pragma push_macro("mean")#undef mean#pragma push_macro("sum")#undef sum#pragma push_macro("summarize")#undef summarize#pragma push_macro("value")#undef value
namespace lucent_app {
lucent::Ref<lucent_app::S_Summary> m_stats::summarize(lucent::Array<double> p0_) {#line 4 "stats.lucent.ts" double sum = 0.0;#line 5 "stats.lucent.ts" double max = -lucent::kInfinity;#line 6 "stats.lucent.ts" { auto coll0_ = p0_; for (size_t i0_ = 0; i0_ < coll0_.size(); i0_++) { double v4_ = coll0_.at(i0_); { double value = v4_;#line 7 "stats.lucent.ts" sum = sum + value;#line 8 "stats.lucent.ts" bool v10_ = value > max; if (v10_) { max = value; } } } }#line 10 "stats.lucent.ts" double v12_ = p0_.length(); bool v14_ = v12_ > 0.0; double v19_{}; if (v14_) { double v16_ = p0_.length(); v19_ = sum / v16_; } else { v19_ = 0.0; }#line 10 "stats.lucent.ts" double mean = v19_;#line 11 "stats.lucent.ts" double v20_ = p0_.length(); return ({ auto obj_1 = std::make_shared<lucent_app::S_Summary>(); obj_1->count = v20_; obj_1->mean = mean; obj_1->max = max; obj_1; });}
void m_stats::init() {}
} // namespace lucent_app
#pragma pop_macro("value")#pragma pop_macro("summarize")#pragma pop_macro("sum")#pragma pop_macro("mean")#pragma pop_macro("max")#pragma pop_macro("count")// Generated by Lucent from stats.lucent.ts. Do not edit.#include "m_stats.h"
#pragma push_macro("count")#undef count#pragma push_macro("max")#undef max#pragma push_macro("mean")#undef mean#pragma push_macro("sum")#undef sum#pragma push_macro("summarize")#undef summarize#pragma push_macro("value")#undef value
namespace lucent_app {
lucent::Ref<lucent_app::S_Summary> m_stats::summarize(lucent::Array<double> p0_) {#line 4 "stats.lucent.ts" double sum = 0.0;#line 5 "stats.lucent.ts" double max = -lucent::kInfinity;#line 6 "stats.lucent.ts" { auto coll0_ = p0_; for (size_t i0_ = 0; i0_ < coll0_.size(); i0_++) { double v4_ = coll0_.at(i0_); { double value = v4_;#line 7 "stats.lucent.ts" sum = sum + value;#line 8 "stats.lucent.ts" bool v10_ = value > max; if (v10_) { max = value; } } } }#line 10 "stats.lucent.ts" double v12_ = p0_.length(); bool v14_ = v12_ > 0.0; double v19_{}; if (v14_) { double v16_ = p0_.length(); v19_ = sum / v16_; } else { v19_ = 0.0; }#line 10 "stats.lucent.ts" double mean = v19_;#line 11 "stats.lucent.ts" double v20_ = p0_.length(); return ({ auto obj_1 = std::make_shared<lucent_app::S_Summary>(); obj_1->count = v20_; obj_1->mean = mean; obj_1->max = max; obj_1; });}
void m_stats::init() {}
} // namespace lucent_app
#pragma pop_macro("value")#pragma pop_macro("summarize")#pragma pop_macro("sum")#pragma pop_macro("mean")#pragma pop_macro("max")#pragma pop_macro("count")The TypeScript checker checks the module, Lucent compiles it to C++, and your app links that C++. The import in App.tsx stays an ordinary, typed import. When Metro bundles the app, it swaps the module for a small proxy that calls the C++.
No JavaScript engine runs your module, and there's no Swift or Kotlin to write. Modules can also call iOS and Android APIs directly.
Call iOS and Android
Section titled “Call iOS and Android”import { PLATFORM } from "lucent:platform";import { UIPasteboard } from "lucent:ios/UIKit";import { ClipboardManager, ClipDescription } from "lucent:android/android.content";import { appContext } from "lucent:android";import { main } from "lucent:thread";
export async function hasStringAsync(): Promise<boolean> { if (PLATFORM === "ios") { return UIPasteboard.general.hasStrings; } else { return main( () => appContext() .getSystemService(ClipboardManager) ?.getPrimaryClipDescription() ?.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN) ?? false, ); }}import { hasStringAsync } from "./src/clipboard.lucent";
await hasStringAsync(); // true when the clipboard holds textSee the C++
// Generated by Lucent from clipboard.lucent.ts. Do not edit.#import <UIKit/UIKit.h>#include <lucent/platform/ios.h>#include "m_clipboard.h"
#pragma push_macro("hasStringAsync")#undef hasStringAsync
namespace lucent_app {
lucent::Promise<bool> m_clipboard::hasStringAsync() {#line 8 "clipboard.lucent.ts" {#line 9 "clipboard.lucent.ts" lucent::NativeRef v0_ = lucent::objc::wrap([UIPasteboard generalPasteboard], "UIPasteboard.general"); co_return static_cast<bool>([((UIPasteboard*)lucent::objc::unwrap(v0_)) hasStrings]); }}
void m_clipboard::init() {}
} // namespace lucent_app
#pragma pop_macro("hasStringAsync")// Generated by Lucent from clipboard.lucent.ts. Do not edit.#include <lucent/platform/android.h>#include "m_clipboard.h"
#pragma push_macro("hasStringAsync")#undef hasStringAsync#pragma push_macro("value")#undef value
namespace lucent_app {
lucent::Promise<bool> m_clipboard::hasStringAsync() {#line 8 "clipboard.lucent.ts" {#line 12 "clipboard.lucent.ts" lucent::Fn<bool()> v0_ = lucent::Fn<bool()>([]() mutable -> bool {#line 13 "clipboard.lucent.ts" lucent::NativeRef v0_ = lucent::jni::appContext(); lucent::Opt<lucent::NativeRef> v1_ = [&]() -> lucent::Opt<lucent::NativeRef> { JNIEnv* env = lucent::jni::env(); auto recv_ = v0_; lucent::jni::LocalFrame frame_(env); static jclass cls_ = lucent::jni::findClass("android/content/Context"); static auto id_ = lucent::jni::method(cls_, "getSystemService", "(Ljava/lang/Class;)Ljava/lang/Object;"); auto r_ = env->CallObjectMethod(lucent::jni::unwrap(recv_), id_, lucent::jni::findClass("android/content/ClipboardManager")); lucent::jni::check(env); return lucent::jni::wrapOpt(env, r_); }(); bool v3_ = !v1_.has(); lucent::Opt<bool> v17_{}; if (v3_) { v17_ = lucent::Opt<bool>(lucent::undefined); } else { lucent::NativeRef v4_ = v1_.value(); lucent::Opt<lucent::NativeRef> v5_ = [&]() -> lucent::Opt<lucent::NativeRef> { JNIEnv* env = lucent::jni::env(); auto recv_ = v4_; lucent::jni::LocalFrame frame_(env); static jclass cls_ = lucent::jni::findClass("android/content/ClipboardManager"); static auto id_ = lucent::jni::method(cls_, "getPrimaryClipDescription", "()Landroid/content/ClipDescription;"); auto r_ = env->CallObjectMethod(lucent::jni::unwrap(recv_), id_); lucent::jni::check(env); return lucent::jni::wrapOpt(env, r_); }(); bool v7_ = !v5_.has(); lucent::Opt<bool> v14_{}; if (v7_) { v14_ = lucent::Opt<bool>(lucent::undefined); } else { lucent::NativeRef v8_ = v5_.value();#line 16 "clipboard.lucent.ts" lucent::String v9_ = LUCENT_STR("text/plain");#line 13 "clipboard.lucent.ts" bool v10_ = [&]() -> bool { JNIEnv* env = lucent::jni::env(); auto recv_ = v8_; lucent::jni::LocalFrame frame_(env); static jclass cls_ = lucent::jni::findClass("android/content/ClipDescription"); static auto id_ = lucent::jni::method(cls_, "hasMimeType", "(Ljava/lang/String;)Z"); auto r_ = env->CallBooleanMethod(lucent::jni::unwrap(recv_), id_, ({ auto s_ = lucent::Opt<lucent::String>(v9_); s_.has() ? lucent::jni::toJString(env, s_.get()) : nullptr; })); lucent::jni::check(env); return r_ == JNI_TRUE; }(); v14_ = lucent::Opt<bool>(v10_); }#line 13 "clipboard.lucent.ts" v17_ = v14_; }#line 13 "clipboard.lucent.ts" bool v19_ = !v17_.has(); bool v22_{}; if (v19_) { v22_ = false; } else { v22_ = v17_.value(); }#line 13 "clipboard.lucent.ts" return v22_; });#line 11 "clipboard.lucent.ts" lucent::Promise<bool> v1_ = lucent::runOnMain(v0_); bool v2_ = co_await v1_; co_return v2_; }}
void m_clipboard::init() {}
} // namespace lucent_app
#pragma pop_macro("value")#pragma pop_macro("hasStringAsync")A module imports the SDKs directly, typed from your Xcode and Android SDK. Each platform's build compiles its own branch. It's one function of the expo-clipboard port; the examples have the whole port.
When to use it
Section titled “When to use it”- Logic you want in native code: parsers, codecs, geometry, data structures.
- Calls to iOS and Android APIs, written once in TypeScript instead of in Swift and Kotlin.
- Work that must stay off the JS thread:
asyncexports run on a background thread.
When not to use it
Section titled “When not to use it”- Shipping native views to production. The views implementation is experimental, behind
LUCENT_VIEWS=fabric, and still needs physical-device validation. - Production apps. Lucent is experimental, and its APIs change without a migration path.
- Code that needs
any,evalor dynamic property access. Lucent compiles a checked subset of TypeScript.
How it compares
Section titled “How it compares”- Expo Modules and Turbo Native Modules: you write Swift and Kotlin, and a JavaScript API over them.
- Nitro Modules: you write Swift, Kotlin or C++ against a TypeScript spec.
- Lucent: you write TypeScript, and the compiler writes the C++ and the calls into the SDK.
The comparison has the details, with dates and sources.