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.

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.

stats.lucent.ts
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 };
}
See the C++
m_stats.cpp
// 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.

clipboard.lucent.ts
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,
);
}
}
See the C++
m_clipboard.mm
// 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")

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.

  • 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: async exports run on a background thread.
  • 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, eval or dynamic property access. Lucent compiles a checked subset of TypeScript.
  • 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.