@lynxship/expo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # @lynxship/expo
2
+
3
+ Expo Modules API integration for embedding an official Lynx `LynxView` inside
4
+ an Expo/React Native application. The native view uses the LynxShip Android or
5
+ iOS OTA client when an OTA endpoint is configured and otherwise renders the
6
+ embedded `main.lynx.bundle` fallback.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npx expo install @lynxship/expo
12
+ npx expo prebuild
13
+ npx pod-install
14
+ ```
15
+
16
+ When the project uses a static `app.json` or `app.config.json`, the Expo CLI
17
+ automatically adds `@lynxship/expo` to `expo.plugins` during `npx expo install`.
18
+ The plugin's defaults are safe for the first build, so no manual config edit
19
+ is required for the basic integration. `npx expo prebuild` then applies the
20
+ Android and iOS native changes before the native build.
21
+
22
+ If the project uses a dynamic `app.config.js`/`app.config.ts`, or if custom OTA
23
+ options are needed, add the config plugin manually:
24
+
25
+ ```json
26
+ {
27
+ "expo": {
28
+ "plugins": [
29
+ [
30
+ "@lynxship/expo",
31
+ {
32
+ "endpoint": "https://api.example.com",
33
+ "projectId": "00000000-0000-4000-8000-000000000000",
34
+ "channel": "production",
35
+ "runtimeVersion": "lynx-runtime-2026-01",
36
+ "publicKeys": {
37
+ "release-key-1": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
38
+ },
39
+ "embeddedBundle": "main.lynx.bundle",
40
+ "lynxVersion": "auto"
41
+ }
42
+ ]
43
+ ]
44
+ }
45
+ }
46
+ ```
47
+
48
+ For a dynamic config, keep the same `plugins` entry in the returned Expo
49
+ configuration. Expo cannot safely rewrite JavaScript or TypeScript config
50
+ files automatically.
51
+
52
+ The endpoint must be HTTPS outside localhost. The public verification key is
53
+ safe to ship in the application; private signing keys must remain in the
54
+ LynxShip CLI or CI secret store.
55
+
56
+ `lynxVersion` defaults to `auto`. Android resolves the current Lynx release
57
+ through Gradle and iOS resolves the current CocoaPods release. Gradle and
58
+ CocoaPods lockfiles retain the concrete versions selected by the first native
59
+ install, so normal rebuilds remain reproducible. Set an exact semver only when
60
+ you intentionally operate a pinned native compatibility lane; do not put
61
+ `latest` in a production lockfile without reviewing the resulting native
62
+ build.
63
+
64
+ ## Use the view
65
+
66
+ ```tsx
67
+ import { LynxView } from "@lynxship/expo";
68
+
69
+ export function LynxScreen() {
70
+ return (
71
+ <LynxView
72
+ style={{ flex: 1 }}
73
+ bundle="main.lynx.bundle"
74
+ initialData="{}"
75
+ reloadOnUpdate
76
+ />
77
+ );
78
+ }
79
+ ```
80
+
81
+ The native module initializes Lynx, creates the official `LynxView`, and
82
+ provides a template provider. The provider first reads the verified active OTA
83
+ asset and falls back to the embedded bundle. An OTA release is accepted only
84
+ after its runtime, manifest signature, asset paths, sizes and SHA-256 hashes
85
+ pass validation. `onReady` is emitted after Lynx reports its first screen, not
86
+ merely after the render request is queued. Native code, permissions, native
87
+ modules and Lynx runtime changes still require a new binary.
88
+
89
+ ## Build workflow
90
+
91
+ From the Expo project:
92
+
93
+ ```bash
94
+ lynxship init
95
+ lynxship doctor --platform android
96
+ lynxship doctor --platform ios
97
+ lynxship build --platform android --profile production
98
+ lynxship build --platform ios --profile production
99
+ lynxship ota doctor --platform android
100
+ lynxship ota doctor --platform ios
101
+ ```
102
+
103
+ The package does not silently replace an existing native host. `expo prebuild`
104
+ and the official Lynx dependencies remain the source of truth for native
105
+ project generation. iOS builds still require macOS/Xcode; Android builds use
106
+ the Android SDK and Gradle toolchain.
107
+
108
+ ## Official compatibility boundary
109
+
110
+ This package follows the official brownfield Lynx integration: `LynxView` is a
111
+ native Android/iOS view and the host supplies the template provider. Lynx's
112
+ engine does not itself implement application-specific network downloads, so
113
+ the provider is the integration point for the LynxShip OTA cache.
114
+
115
+ References:
116
+
117
+ - [Lynx integration with existing apps](https://lynxjs.org/3.8/guide/start/integrate-with-existing-apps.html)
118
+ - [Expo native view modules](https://docs.expo.dev/modules/native-view-tutorial/)
119
+ - [Expo module configuration](https://docs.expo.dev/modules/module-config/)
120
+ - [LynxShip OTA security and compatibility](../../docs/compatibility.md)
121
+
122
+ The native targets must be built on their real platform. The repository's
123
+ Windows checks validate the JavaScript API, package metadata and generated
124
+ configuration; they cannot replace a real Android Gradle or macOS Xcode
125
+ runtime build.
@@ -0,0 +1,40 @@
1
+ plugins {
2
+ id "com.android.library"
3
+ }
4
+
5
+ // `latest.release` is used only when the app does not provide an exact
6
+ // version. Gradle dependency locking then records the resolved version for
7
+ // reproducible subsequent builds.
8
+ def lynxVersion = project.findProperty("LYNXSHIP_LYNX_VERSION") ?: "latest.release"
9
+
10
+ android {
11
+ namespace "com.lynxship.expo"
12
+ compileSdk 35
13
+
14
+ defaultConfig {
15
+ minSdk 24
16
+ consumerProguardFiles "consumer-rules.pro"
17
+ }
18
+
19
+ compileOptions {
20
+ sourceCompatibility JavaVersion.VERSION_17
21
+ targetCompatibility JavaVersion.VERSION_17
22
+ }
23
+ }
24
+
25
+ dependencies {
26
+ implementation "org.lynxsdk.lynx:lynx:${lynxVersion}"
27
+ implementation "org.lynxsdk.lynx:lynx-jssdk:${lynxVersion}"
28
+ implementation "org.lynxsdk.lynx:lynx-trace:${lynxVersion}"
29
+ implementation "org.lynxsdk.lynx:primjs:${lynxVersion}"
30
+ implementation "org.lynxsdk.lynx:lynx-service-image:${lynxVersion}"
31
+ implementation "org.lynxsdk.lynx:lynx-service-log:${lynxVersion}"
32
+ implementation "org.lynxsdk.lynx:lynx-service-http:${lynxVersion}"
33
+ implementation "com.facebook.fresco:fresco:2.3.0"
34
+ implementation "com.facebook.fresco:animated-gif:2.3.0"
35
+ implementation "com.facebook.fresco:animated-webp:2.3.0"
36
+ implementation "com.facebook.fresco:webpsupport:2.3.0"
37
+ implementation "com.facebook.fresco:animated-base:2.3.0"
38
+ implementation "com.squareup.okhttp3:okhttp:4.9.0"
39
+ implementation project(":lynxship-sdk-android")
40
+ }
@@ -0,0 +1,34 @@
1
+ # These rules are the public keep rules required by the Lynx Android runtime
2
+ # when an application enables R8/minification. Keep this file in the Expo
3
+ # module so consuming apps do not need to copy framework rules manually.
4
+ -dontwarn android.support.annotation.Keep
5
+ -keep @android.support.annotation.Keep class **
6
+ -keep @android.support.annotation.Keep class ** {
7
+ @android.support.annotation.Keep <fields>;
8
+ @android.support.annotation.Keep <methods>;
9
+ }
10
+ -dontwarn androidx.annotation.Keep
11
+ -keep @androidx.annotation.Keep class **
12
+ -keep @androidx.annotation.Keep class ** {
13
+ @androidx.annotation.Keep <fields>;
14
+ @androidx.annotation.Keep <methods>;
15
+ }
16
+ -keepclasseswithmembers,includedescriptorclasses class * {
17
+ native <methods>;
18
+ }
19
+ -keepclasseswithmembers class * {
20
+ @com.lynx.tasm.base.CalledByNative <methods>;
21
+ }
22
+ -keepclasseswithmembers class * {
23
+ @com.lynx.jsbridge.LynxMethod <methods>;
24
+ }
25
+ -keepclassmembers class * {
26
+ @com.lynx.tasm.behavior.LynxProp <methods>;
27
+ @com.lynx.tasm.behavior.LynxPropGroup <methods>;
28
+ @com.lynx.tasm.behavior.LynxUIMethod <methods>;
29
+ }
30
+ -keep class com.lynx.jsbridge.LynxModule { *; }
31
+ -keep class * extends com.lynx.tasm.behavior.ui.LynxBaseUI
32
+ -keep class * extends com.lynx.tasm.behavior.shadow.ShadowNode
33
+ -keep class * extends com.lynx.jsbridge.LynxModule { *; }
34
+ -keep class * extends com.lynx.jsbridge.LynxContextModule
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,30 @@
1
+ package com.lynxship.expo
2
+
3
+ import expo.modules.kotlin.modules.Module
4
+ import expo.modules.kotlin.modules.ModuleDefinition
5
+
6
+ class LynxShipExpoModule : Module() {
7
+ override fun definition() = ModuleDefinition {
8
+ Name("LynxShip")
9
+
10
+ View(LynxShipExpoView::class) {
11
+ Events("onReady", "onError", "onUpdate")
12
+
13
+ Prop("bundle") { view: LynxShipExpoView, value: String? ->
14
+ view.bundleName = value ?: "main.lynx.bundle"
15
+ }
16
+
17
+ Prop("initialData") { view: LynxShipExpoView, value: String? ->
18
+ view.initialData = value ?: ""
19
+ }
20
+
21
+ Prop("reloadOnUpdate") { view: LynxShipExpoView, value: Boolean? ->
22
+ view.reloadOnUpdate = value ?: true
23
+ }
24
+
25
+ AsyncFunction("reload") { view: LynxShipExpoView ->
26
+ view.reload()
27
+ }
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,196 @@
1
+ package com.lynxship.expo
2
+
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+ import android.os.Bundle
6
+ import android.provider.Settings
7
+ import com.facebook.drawee.backends.pipeline.Fresco
8
+ import com.facebook.imagepipeline.core.ImagePipelineConfig
9
+ import com.facebook.imagepipeline.memory.PoolConfig
10
+ import com.facebook.imagepipeline.memory.PoolFactory
11
+ import com.lynx.service.http.LynxHttpService
12
+ import com.lynx.service.image.LynxImageService
13
+ import com.lynx.service.log.LynxLogService
14
+ import com.lynx.tasm.LynxEnv
15
+ import com.lynx.tasm.LynxView
16
+ import com.lynx.tasm.LynxViewClient
17
+ import com.lynx.tasm.LynxViewBuilder
18
+ import com.lynx.tasm.provider.AbsTemplateProvider
19
+ import com.lynx.tasm.service.LynxServiceCenter
20
+ import com.lynxship.sdk.android.LynxShipOtaClient
21
+ import expo.modules.kotlin.AppContext
22
+ import expo.modules.kotlin.events.EventDispatcher
23
+ import expo.modules.kotlin.views.ExpoView
24
+ import java.io.ByteArrayOutputStream
25
+ import java.io.File
26
+ import java.io.IOException
27
+ import java.util.UUID
28
+
29
+ class LynxShipExpoView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
30
+ private val applicationContext = context.applicationContext
31
+ private val onReady by EventDispatcher()
32
+ private val onError by EventDispatcher()
33
+ private val onUpdate by EventDispatcher()
34
+ private val preferences: SharedPreferences = applicationContext.getSharedPreferences("lynxship-expo", Context.MODE_PRIVATE)
35
+ private val metadata: Bundle = applicationContext.applicationInfo.metaData ?: Bundle()
36
+ private val otaClient = createOtaClient(applicationContext)
37
+ private val templateProvider = object : AbsTemplateProvider() {
38
+ override fun loadTemplate(uri: String, callback: Callback) {
39
+ Thread {
40
+ try {
41
+ val bytes = otaClient?.openActiveAsset(uri) ?: readEmbeddedAsset(uri)
42
+ callback.onSuccess(bytes)
43
+ otaClient?.markLaunchSuccess()
44
+ } catch (error: Exception) {
45
+ callback.onFailed(error.message ?: "Unable to load Lynx bundle")
46
+ }
47
+ }.start()
48
+ }
49
+ }
50
+ private val lynxView: LynxView
51
+ var bundleName: String = metadata.getString("com.lynxship.expo.embeddedBundle", "main.lynx.bundle")
52
+ var initialData: String = ""
53
+ var reloadOnUpdate: Boolean = true
54
+
55
+ init {
56
+ initializeLynx(context)
57
+ lynxView = LynxViewBuilder().setTemplateProvider(templateProvider).build(applicationContext)
58
+ lynxView.addLynxViewClient(object : LynxViewClient() {
59
+ override fun onFirstScreen() {
60
+ try {
61
+ otaClient?.markLaunchSuccess()
62
+ } catch (error: IOException) {
63
+ onError(mapOf("message" to (error.message ?: "Could not record Lynx launch"), "recoverable" to true))
64
+ }
65
+ onReady(mapOf("bundle" to bundleName, "sequence" to (otaClient?.activeSequence ?: 0)))
66
+ }
67
+ })
68
+ addView(lynxView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
69
+ }
70
+
71
+ override fun onAttachedToWindow() {
72
+ super.onAttachedToWindow()
73
+ try {
74
+ otaClient?.beginLaunch()
75
+ installUpdateInBackground()
76
+ render()
77
+ } catch (error: Exception) {
78
+ onError(mapOf("message" to (error.message ?: "Lynx startup failed")))
79
+ }
80
+ }
81
+
82
+ fun reload() {
83
+ render()
84
+ }
85
+
86
+ private fun render() {
87
+ try {
88
+ lynxView.renderTemplateUrl(bundleName, initialData)
89
+ } catch (error: Exception) {
90
+ onError(mapOf("message" to (error.message ?: "Lynx render failed")))
91
+ }
92
+ }
93
+
94
+ private fun installUpdateInBackground() {
95
+ otaClient?.checkAndInstallAsync(object : LynxShipOtaClient.Listener {
96
+ override fun onSuccess(updateAvailable: Boolean) {
97
+ if (!updateAvailable) return
98
+ try {
99
+ otaClient.activateCandidate()
100
+ post {
101
+ onUpdate(mapOf("sequence" to otaClient.activeSequence))
102
+ if (reloadOnUpdate) render()
103
+ }
104
+ } catch (error: Exception) {
105
+ emitError(error, "OTA activation failed", recoverable = true)
106
+ }
107
+ }
108
+
109
+ override fun onFailure(error: Exception) {
110
+ emitError(error, "OTA check failed", recoverable = true)
111
+ }
112
+ })
113
+ }
114
+
115
+ private fun emitError(error: Exception, fallback: String, recoverable: Boolean = false) {
116
+ post {
117
+ onError(
118
+ mapOf(
119
+ "message" to (error.message ?: fallback),
120
+ "recoverable" to recoverable,
121
+ ),
122
+ )
123
+ }
124
+ }
125
+
126
+ private fun readEmbeddedAsset(path: String): ByteArray {
127
+ if (path.contains("..") || path.startsWith("/") || path.contains("\\")) throw IOException("Unsafe Lynx asset path")
128
+ applicationContext.assets.open(path).use { input ->
129
+ ByteArrayOutputStream().use { output ->
130
+ input.copyTo(output)
131
+ return output.toByteArray()
132
+ }
133
+ }
134
+ }
135
+
136
+ private fun createOtaClient(context: Context): LynxShipOtaClient? {
137
+ val endpoint = metadata.getString("com.lynxship.expo.endpoint", "")
138
+ val projectId = metadata.getString("com.lynxship.expo.projectId", "")
139
+ val runtimeVersion = metadata.getString("com.lynxship.expo.runtimeVersion", "")
140
+ val keyJson = metadata.getString("com.lynxship.expo.publicKeys", "{}")
141
+ if (endpoint.isBlank() || projectId.isBlank() || runtimeVersion.isBlank()) return null
142
+ return try {
143
+ val keys = org.json.JSONObject(keyJson).let { json ->
144
+ buildMap {
145
+ json.keys().forEach { key -> put(key, json.getString(key)) }
146
+ }
147
+ }
148
+ val installationId = preferences.getString("installationId", null) ?: Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: UUID.randomUUID().toString()
149
+ preferences.edit().putString("installationId", installationId).apply()
150
+ LynxShipOtaClient(
151
+ LynxShipOtaClient.Config(
152
+ File(context.filesDir, "lynxship-ota"), endpoint, projectId,
153
+ metadata.getString("com.lynxship.expo.channel", "production"), "android",
154
+ runtimeVersion, installationId, keys, { path -> readEmbeddedAsset(path) },
155
+ 3,
156
+ metadata.getString("com.lynxship.expo.maxReleaseBytes", "104857600").toLong(),
157
+ 10_000,
158
+ 30_000,
159
+ ),
160
+ )
161
+ } catch (error: Exception) {
162
+ onError(mapOf("message" to (error.message ?: "Invalid LynxShip OTA configuration")))
163
+ null
164
+ }
165
+ }
166
+
167
+ private fun initializeLynx(context: Context) {
168
+ synchronized(LynxShipExpoView::class.java) {
169
+ if (!initialized) {
170
+ val applicationContext = context.applicationContext
171
+ // Lynx's official Android integration requires these services
172
+ // before a LynxView is created. Expo does not give this package
173
+ // an application subclass, so initialize them once at the
174
+ // first view creation. If the host already initialized Fresco,
175
+ // keep that host-owned instance and continue registering Lynx's
176
+ // services against it.
177
+ val factory = PoolFactory(PoolConfig.newBuilder().build())
178
+ val builder = ImagePipelineConfig.newBuilder(applicationContext).setPoolFactory(factory)
179
+ try {
180
+ Fresco.initialize(applicationContext, builder.build())
181
+ } catch (_: IllegalStateException) {
182
+ // Fresco was initialized by the host application.
183
+ }
184
+ LynxServiceCenter.inst().registerService(LynxImageService.getInstance())
185
+ LynxServiceCenter.inst().registerService(LynxLogService)
186
+ LynxServiceCenter.inst().registerService(LynxHttpService)
187
+ LynxEnv.inst().init(applicationContext, null, null, null)
188
+ initialized = true
189
+ }
190
+ }
191
+ }
192
+
193
+ companion object {
194
+ @Volatile private var initialized = false
195
+ }
196
+ }
package/app.plugin.cjs ADDED
@@ -0,0 +1,204 @@
1
+ const {
2
+ withAndroidManifest,
3
+ withGradleProperties,
4
+ withInfoPlist,
5
+ withPodfile,
6
+ withSettingsGradle,
7
+ } = require("@expo/config-plugins");
8
+
9
+ const MARKER = "# @lynxship/expo managed";
10
+ const DEFAULT_LYNX_VERSION = "auto";
11
+
12
+ function optionsFromPluginEntry(entry) {
13
+ if (Array.isArray(entry) && entry[0] === "@lynxship/expo")
14
+ return entry[1] || {};
15
+ return {};
16
+ }
17
+
18
+ function getOptions(config) {
19
+ return (
20
+ (config.plugins || [])
21
+ .map(optionsFromPluginEntry)
22
+ .find((value) => Object.keys(value).length > 0) || {}
23
+ );
24
+ }
25
+
26
+ function assertOptions(options) {
27
+ if (options.endpoint) {
28
+ const url = new URL(options.endpoint);
29
+ if (
30
+ url.protocol !== "https:" &&
31
+ !["localhost", "127.0.0.1"].includes(url.hostname)
32
+ ) {
33
+ throw new Error(
34
+ "@lynxship/expo endpoint must use HTTPS outside localhost",
35
+ );
36
+ }
37
+ }
38
+ if (options.publicKeys) {
39
+ for (const [key, value] of Object.entries(options.publicKeys)) {
40
+ if (
41
+ !key ||
42
+ typeof value !== "string" ||
43
+ !value.includes("BEGIN PUBLIC KEY")
44
+ ) {
45
+ throw new Error(
46
+ "@lynxship/expo publicKeys must contain PEM public keys",
47
+ );
48
+ }
49
+ }
50
+ }
51
+ if (
52
+ options.lynxVersion !== undefined &&
53
+ options.lynxVersion !== "auto" &&
54
+ options.lynxVersion !== "latest" &&
55
+ !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(options.lynxVersion)
56
+ ) {
57
+ throw new Error(
58
+ "@lynxship/expo lynxVersion must be auto, latest, or an exact semver",
59
+ );
60
+ }
61
+ return options;
62
+ }
63
+
64
+ function lynxVersion(options) {
65
+ return options.lynxVersion || DEFAULT_LYNX_VERSION;
66
+ }
67
+
68
+ function gradleLynxVersion(options) {
69
+ const value = lynxVersion(options);
70
+ return value === "auto" || value === "latest" ? "latest.release" : value;
71
+ }
72
+
73
+ function addAndroidMetaData(application, name, value) {
74
+ application["meta-data"] = application["meta-data"] || [];
75
+ const existing = application["meta-data"].find(
76
+ (item) => item.$ && item.$["android:name"] === name,
77
+ );
78
+ if (existing) existing.$["android:value"] = value;
79
+ else
80
+ application["meta-data"].push({
81
+ $: { "android:name": name, "android:value": value },
82
+ });
83
+ }
84
+
85
+ function withLynxShipAndroidManifest(config, options) {
86
+ return withAndroidManifest(config, (value) => {
87
+ const manifest = value.modResults.manifest;
88
+ manifest["uses-permission"] = manifest["uses-permission"] || [];
89
+ if (
90
+ !manifest["uses-permission"].some(
91
+ (item) =>
92
+ item.$ && item.$["android:name"] === "android.permission.INTERNET",
93
+ )
94
+ ) {
95
+ manifest["uses-permission"].push({
96
+ $: { "android:name": "android.permission.INTERNET" },
97
+ });
98
+ }
99
+ const application = manifest.application && manifest.application[0];
100
+ if (!application)
101
+ throw new Error("@lynxship/expo requires an Android application node");
102
+ const metadata = {
103
+ endpoint: options.endpoint || "",
104
+ projectId: options.projectId || "",
105
+ channel: options.channel || "production",
106
+ runtimeVersion: options.runtimeVersion || "",
107
+ embeddedBundle: options.embeddedBundle || "main.lynx.bundle",
108
+ publicKeys: JSON.stringify(options.publicKeys || {}),
109
+ maxReleaseBytes: String(options.maxReleaseBytes || 104857600),
110
+ };
111
+ for (const [key, item] of Object.entries(metadata))
112
+ addAndroidMetaData(application, `com.lynxship.expo.${key}`, item);
113
+ return value;
114
+ });
115
+ }
116
+
117
+ function withLynxShipInfoPlist(config, options) {
118
+ return withInfoPlist(config, (value) => {
119
+ value.modResults.LynxShipExpo = {
120
+ endpoint: options.endpoint || "",
121
+ projectId: options.projectId || "",
122
+ channel: options.channel || "production",
123
+ runtimeVersion: options.runtimeVersion || "",
124
+ embeddedBundle: options.embeddedBundle || "main.lynx.bundle",
125
+ publicKeys: options.publicKeys || {},
126
+ maxReleaseBytes: options.maxReleaseBytes || 104857600,
127
+ };
128
+ return value;
129
+ });
130
+ }
131
+
132
+ function withLynxShipGradle(config, options) {
133
+ return withGradleProperties(config, (value) => {
134
+ const properties = value.modResults;
135
+ const key = "LYNXSHIP_LYNX_VERSION";
136
+ const current = properties.find((item) => item.key === key);
137
+ if (current) current.value = gradleLynxVersion(options);
138
+ else
139
+ properties.push({
140
+ type: "property",
141
+ key,
142
+ value: gradleLynxVersion(options),
143
+ });
144
+ const androidX = properties.find(
145
+ (item) => item.key === "android.useAndroidX",
146
+ );
147
+ if (androidX) androidX.value = "true";
148
+ else
149
+ properties.push({
150
+ type: "property",
151
+ key: "android.useAndroidX",
152
+ value: "true",
153
+ });
154
+ return value;
155
+ });
156
+ }
157
+
158
+ function withLynxShipAndroidSdk(config) {
159
+ return withSettingsGradle(config, (value) => {
160
+ const settings = value.modResults.contents;
161
+ if (settings.includes("@lynxship/expo managed sdk")) return value;
162
+ value.modResults.contents = `${settings.trimEnd()}\n\n// @lynxship/expo managed sdk\ninclude ':lynxship-sdk-android'\nproject(':lynxship-sdk-android').projectDir = new File(rootDir, '../node_modules/@lynxship/sdk-android')\n`;
163
+ return value;
164
+ });
165
+ }
166
+
167
+ function withLynxShipPodfile(config, options) {
168
+ return withPodfile(config, (value) => {
169
+ const podfile = value.modResults.contents;
170
+ if (podfile.includes(MARKER)) return value;
171
+ const additions = [
172
+ ` ${MARKER}`,
173
+ ...(lynxVersion(options) === "auto" || lynxVersion(options) === "latest"
174
+ ? []
175
+ : [` ENV['LYNXSHIP_LYNX_VERSION'] = '${lynxVersion(options)}'`]),
176
+ " pod 'LynxShipOta', :path => '../node_modules/@lynxship/sdk-ios'",
177
+ ` ${MARKER} end`,
178
+ ].join("\n");
179
+ const target = podfile.search(/^target\s+['\"][^'\"]+['\"]\s+do\s*$/m);
180
+ if (target < 0)
181
+ throw new Error("@lynxship/expo could not find an iOS Podfile target");
182
+ const lineEnd = podfile.indexOf("\n", target);
183
+ value.modResults.contents = `${podfile.slice(0, lineEnd + 1)}${additions}\n${podfile.slice(lineEnd + 1)}`;
184
+ return value;
185
+ });
186
+ }
187
+
188
+ function withLynxShipExpo(config, props = {}) {
189
+ const options = assertOptions({ ...getOptions(config), ...props });
190
+ return withLynxShipPodfile(
191
+ withLynxShipAndroidSdk(
192
+ withLynxShipGradle(
193
+ withLynxShipInfoPlist(
194
+ withLynxShipAndroidManifest(config, options),
195
+ options,
196
+ ),
197
+ options,
198
+ ),
199
+ ),
200
+ options,
201
+ );
202
+ }
203
+
204
+ module.exports = withLynxShipExpo;
package/app.plugin.js ADDED
@@ -0,0 +1,4 @@
1
+ // Expo resolves package config plugins through this conventional entry point.
2
+ // Keep the implementation CommonJS so it can be evaluated by Expo's Node.js
3
+ // config phase regardless of the consuming app's module configuration.
4
+ module.exports = require("./app.plugin.cjs");
@@ -0,0 +1,12 @@
1
+ export interface LynxShipExpoConfig {
2
+ endpoint?: string;
3
+ projectId?: string;
4
+ channel?: string;
5
+ runtimeVersion?: string;
6
+ publicKeys?: Record<string, string>;
7
+ embeddedBundle?: string;
8
+ lynxVersion?: string;
9
+ maxReleaseBytes?: number;
10
+ }
11
+ export declare function validateLynxShipExpoConfig(value?: LynxShipExpoConfig): LynxShipExpoConfig;
12
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAKD,wBAAgB,0BAA0B,CACxC,KAAK,GAAE,kBAAuB,GAC7B,kBAAkB,CAkCpB"}
package/dist/config.js ADDED
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateLynxShipExpoConfig = validateLynxShipExpoConfig;
4
+ /** Let the native package manager resolve the current Lynx SDK by default. */
5
+ const DEFAULT_LYNX_VERSION = "auto";
6
+ function validateLynxShipExpoConfig(value = {}) {
7
+ if (value.endpoint !== undefined) {
8
+ const url = new URL(value.endpoint);
9
+ if (url.protocol !== "https:" && !isLocalDevelopment(url.hostname))
10
+ throw new Error("LynxShip Expo endpoint must use HTTPS outside localhost");
11
+ }
12
+ for (const [key, publicKey] of Object.entries(value.publicKeys ?? {})) {
13
+ if (!key.trim() || !publicKey.includes("BEGIN PUBLIC KEY"))
14
+ throw new Error("LynxShip Expo publicKeys must contain PEM public keys");
15
+ }
16
+ if (value.maxReleaseBytes !== undefined) {
17
+ if (!Number.isInteger(value.maxReleaseBytes) || value.maxReleaseBytes < 1)
18
+ throw new Error("LynxShip Expo maxReleaseBytes must be a positive integer");
19
+ }
20
+ if (value.lynxVersion !== undefined &&
21
+ value.lynxVersion !== "auto" &&
22
+ value.lynxVersion !== "latest" &&
23
+ !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(value.lynxVersion)) {
24
+ throw new Error("LynxShip Expo lynxVersion must be auto, latest, or an exact semver");
25
+ }
26
+ return {
27
+ ...value,
28
+ channel: value.channel ?? "production",
29
+ embeddedBundle: value.embeddedBundle ?? "main.lynx.bundle",
30
+ lynxVersion: value.lynxVersion ?? DEFAULT_LYNX_VERSION,
31
+ };
32
+ }
33
+ function isLocalDevelopment(hostname) {
34
+ return hostname === "localhost" || hostname === "127.0.0.1";
35
+ }
@@ -0,0 +1,21 @@
1
+ import { type ComponentType } from "react";
2
+ import type { ViewProps } from "react-native";
3
+ import { type LynxShipExpoConfig } from "./config.js";
4
+ export type { LynxShipExpoConfig } from "./config.js";
5
+ export { validateLynxShipExpoConfig } from "./config.js";
6
+ export interface LynxViewProps extends ViewProps {
7
+ /** Bundle name resolved by the native Lynx template provider. */
8
+ bundle?: string;
9
+ /** Optional initial data passed to Lynx when the view is rendered. */
10
+ initialData?: string;
11
+ /** Requests a fresh render after an OTA candidate has been activated. */
12
+ reloadOnUpdate?: boolean;
13
+ }
14
+ /**
15
+ * A LynxView that can be placed anywhere in an Expo/React Native view tree.
16
+ * Native OTA configuration is supplied by the LynxShip config plugin.
17
+ */
18
+ export declare function LynxView(props: LynxViewProps): unknown;
19
+ export declare const LynxShipView: ComponentType<LynxViewProps>;
20
+ export declare function defineLynxShipExpoConfig(config: LynxShipExpoConfig): LynxShipExpoConfig;
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAE1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAEzD,MAAM,WAAW,aAAc,SAAQ,SAAS;IAC9C,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAID;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAOtD;AAED,eAAO,MAAM,YAAY,EAAE,aAAa,CAAC,aAAa,CAAY,CAAC;AAEnE,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,kBAAkB,GACzB,kBAAkB,CAEpB"}
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LynxShipView = exports.validateLynxShipExpoConfig = void 0;
4
+ exports.LynxView = LynxView;
5
+ exports.defineLynxShipExpoConfig = defineLynxShipExpoConfig;
6
+ const react_1 = require("react");
7
+ const expo_modules_core_1 = require("expo-modules-core");
8
+ const config_js_1 = require("./config.js");
9
+ var config_js_2 = require("./config.js");
10
+ Object.defineProperty(exports, "validateLynxShipExpoConfig", { enumerable: true, get: function () { return config_js_2.validateLynxShipExpoConfig; } });
11
+ const NativeLynxView = (0, expo_modules_core_1.requireNativeViewManager)("LynxShip");
12
+ /**
13
+ * A LynxView that can be placed anywhere in an Expo/React Native view tree.
14
+ * Native OTA configuration is supplied by the LynxShip config plugin.
15
+ */
16
+ function LynxView(props) {
17
+ return (0, react_1.createElement)(NativeLynxView, {
18
+ bundle: props.bundle ?? "main.lynx.bundle",
19
+ initialData: props.initialData ?? "",
20
+ reloadOnUpdate: props.reloadOnUpdate ?? true,
21
+ ...props,
22
+ });
23
+ }
24
+ exports.LynxShipView = LynxView;
25
+ function defineLynxShipExpoConfig(config) {
26
+ return (0, config_js_1.validateLynxShipExpoConfig)(config);
27
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["android", "ios"],
3
+ "android": {
4
+ "modules": ["com.lynxship.expo.LynxShipExpoModule"]
5
+ },
6
+ "ios": {
7
+ "modules": ["LynxShipExpoModule"]
8
+ }
9
+ }
@@ -0,0 +1,22 @@
1
+ import ExpoModulesCore
2
+
3
+ public class LynxShipExpoModule: Module {
4
+ public func definition() -> ModuleDefinition {
5
+ Name("LynxShip")
6
+ View(LynxShipExpoView.self) {
7
+ Events("onReady", "onError", "onUpdate")
8
+ Prop("bundle") { (view: LynxShipExpoView, value: String?) in
9
+ view.bundleName = value ?? "main.lynx.bundle"
10
+ }
11
+ Prop("initialData") { (view: LynxShipExpoView, value: String?) in
12
+ view.initialData = value ?? ""
13
+ }
14
+ Prop("reloadOnUpdate") { (view: LynxShipExpoView, value: Bool?) in
15
+ view.reloadOnUpdate = value ?? true
16
+ }
17
+ AsyncFunction("reload") { (view: LynxShipExpoView) in
18
+ try view.reload()
19
+ }
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,150 @@
1
+ import ExpoModulesCore
2
+ import Foundation
3
+ import Lynx
4
+ import LynxShipOta
5
+ import UIKit
6
+
7
+ private final class LynxShipTemplateProvider: NSObject, LynxTemplateProvider {
8
+ private let load: (String) throws -> Data
9
+ private let onLoaded: () -> Void
10
+
11
+ init(load: @escaping (String) throws -> Data, onLoaded: @escaping () -> Void) {
12
+ self.load = load
13
+ self.onLoaded = onLoaded
14
+ }
15
+
16
+ func loadTemplate(withUrl url: String!, onComplete callback: LynxTemplateLoadBlock!) {
17
+ do {
18
+ callback(load(url), nil)
19
+ onLoaded()
20
+ } catch {
21
+ callback(nil, error)
22
+ }
23
+ }
24
+ }
25
+
26
+ public final class LynxShipExpoView: ExpoView {
27
+ let onReady = EventDispatcher()
28
+ let onError = EventDispatcher()
29
+ let onUpdate = EventDispatcher()
30
+
31
+ private lazy var templateProvider = LynxShipTemplateProvider(
32
+ load: { [weak self] path in
33
+ guard let self else { throw NSError(domain: "LynxShip", code: 1, userInfo: [NSLocalizedDescriptionKey: "Lynx view was released"]) }
34
+ return try self.otaClient?.openActiveAsset(path) ?? self.readEmbeddedAsset(path)
35
+ },
36
+ onLoaded: { [weak self] in
37
+ guard let self else { return }
38
+ try? self.otaClient?.markLaunchSuccess()
39
+ DispatchQueue.main.async {
40
+ self.onReady(["bundle": self.bundleName, "sequence": self.otaClient?.activeSequence ?? 0])
41
+ }
42
+ }
43
+ )
44
+
45
+ private lazy var lynxView: LynxView = {
46
+ LynxView { builder in
47
+ builder.config = LynxConfig(provider: self.templateProvider)
48
+ builder.fontScale = 1.0
49
+ }
50
+ }()
51
+
52
+ private var otaClient: LynxShipOtaClient?
53
+ private var hasRendered = false
54
+ var bundleName = "main.lynx.bundle"
55
+ var initialData = ""
56
+ var reloadOnUpdate = true
57
+
58
+ public required init(appContext: AppContext? = nil) {
59
+ super.init(appContext: appContext)
60
+ LynxEnv.sharedInstance()
61
+ otaClient = makeOtaClient()
62
+ addSubview(lynxView)
63
+ }
64
+
65
+ public override func layoutSubviews() {
66
+ super.layoutSubviews()
67
+ lynxView.frame = bounds
68
+ lynxView.preferredLayoutWidth = bounds.width
69
+ lynxView.preferredLayoutHeight = bounds.height
70
+ }
71
+
72
+ public override func didMoveToWindow() {
73
+ super.didMoveToWindow()
74
+ guard window != nil, !hasRendered else { return }
75
+ do {
76
+ try otaClient?.beginLaunch()
77
+ render()
78
+ checkForUpdate()
79
+ } catch {
80
+ onError(["message": error.localizedDescription])
81
+ }
82
+ }
83
+
84
+ func reload() throws {
85
+ render()
86
+ }
87
+
88
+ private func render() {
89
+ lynxView.loadTemplate(fromURL: bundleName, initData: initialData)
90
+ hasRendered = true
91
+ }
92
+
93
+ private func checkForUpdate() {
94
+ Task { [weak self] in
95
+ guard let self else { return }
96
+ do {
97
+ guard let otaClient else { return }
98
+ guard try await otaClient.checkAndInstall() else { return }
99
+ try otaClient.activateCandidate()
100
+ await MainActor.run {
101
+ self.onUpdate(["sequence": otaClient.activeSequence])
102
+ if self.reloadOnUpdate { self.render() }
103
+ }
104
+ } catch {
105
+ await MainActor.run {
106
+ self.onError(["message": error.localizedDescription, "recoverable": true])
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ private func readEmbeddedAsset(_ path: String) throws -> Data {
113
+ guard !path.isEmpty, !path.hasPrefix("/"), !path.contains(".."), !path.contains("\\") else {
114
+ throw NSError(domain: "LynxShip", code: 2, userInfo: [NSLocalizedDescriptionKey: "Unsafe Lynx asset path"])
115
+ }
116
+ guard let url = Bundle.main.url(forResource: path, withExtension: nil) ?? Bundle.main.url(forResource: path, withExtension: "bundle") else {
117
+ throw NSError(domain: "LynxShip", code: 3, userInfo: [NSLocalizedDescriptionKey: "Embedded Lynx bundle not found: \(path)"])
118
+ }
119
+ return try Data(contentsOf: url)
120
+ }
121
+
122
+ private func makeOtaClient() -> LynxShipOtaClient? {
123
+ guard let value = Bundle.main.object(forInfoDictionaryKey: "LynxShipExpo") as? [String: Any],
124
+ let endpointString = value["endpoint"] as? String, !endpointString.isEmpty,
125
+ let endpoint = URL(string: endpointString),
126
+ let projectId = value["projectId"] as? String, !projectId.isEmpty,
127
+ let runtimeVersion = value["runtimeVersion"] as? String, !runtimeVersion.isEmpty else { return nil }
128
+ do {
129
+ let keys = value["publicKeys"] as? [String: String] ?? [:]
130
+ let channel = value["channel"] as? String ?? "production"
131
+ let storage = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0].appendingPathComponent("lynxship-ota", isDirectory: true)
132
+ let installationId = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
133
+ let configuration = try LynxShipOtaClient.Configuration(
134
+ storageDirectory: storage,
135
+ endpoint: endpoint,
136
+ projectID: projectId,
137
+ channel: channel,
138
+ runtimeVersion: runtimeVersion,
139
+ installationID: installationId,
140
+ publicKeys: keys,
141
+ embeddedAssets: readEmbeddedAsset,
142
+ maxReleaseBytes: value["maxReleaseBytes"] as? Int ?? 100 * 1024 * 1024
143
+ )
144
+ return try LynxShipOtaClient(configuration: configuration)
145
+ } catch {
146
+ onError(["message": error.localizedDescription])
147
+ return nil
148
+ }
149
+ }
150
+ }
@@ -0,0 +1,25 @@
1
+ Pod::Spec.new do |s|
2
+ s.name = 'LynxShipExpo'
3
+ s.version = '0.1.0'
4
+ s.summary = 'Expo native LynxView integration for LynxShip.'
5
+ s.license = { :type => 'MIT' }
6
+ s.author = { 'LynxShip' => 'opensource@lynxship.dev' }
7
+ s.source = { :path => '.' }
8
+ s.source_files = 'ios/**/*.{h,m,mm,swift}'
9
+ s.platform = :ios, '15.0'
10
+ s.swift_version = '5.9'
11
+ s.dependency 'ExpoModulesCore'
12
+ s.dependency 'LynxShipOta'
13
+ lynx_version = ENV['LYNXSHIP_LYNX_VERSION']
14
+ if lynx_version && !lynx_version.empty? && lynx_version != 'auto' && lynx_version != 'latest'
15
+ s.dependency 'Lynx', lynx_version, :subspecs => ['Framework']
16
+ s.dependency 'PrimJS', lynx_version, :subspecs => ['quickjs', 'napi']
17
+ s.dependency 'LynxService', lynx_version, :subspecs => ['Image', 'Log', 'Http']
18
+ else
19
+ s.dependency 'Lynx', :subspecs => ['Framework']
20
+ s.dependency 'PrimJS', :subspecs => ['quickjs', 'napi']
21
+ s.dependency 'LynxService', :subspecs => ['Image', 'Log', 'Http']
22
+ end
23
+ s.dependency 'SDWebImage', '5.15.5'
24
+ s.dependency 'SDWebImageWebPCoder', '0.11.0'
25
+ end
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@lynxship/expo",
3
+ "version": "0.1.0",
4
+ "description": "Expo native LynxView with LynxShip OTA delivery and cache support.",
5
+ "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "android",
11
+ "ios",
12
+ "src",
13
+ "app.plugin.js",
14
+ "app.plugin.cjs",
15
+ "expo-module.config.json",
16
+ "lynxship-expo.podspec",
17
+ "README.md"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/p0ulsh3n/lynx-ship.git",
22
+ "directory": "packages/expo"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/p0ulsh3n/lynx-ship/issues"
26
+ },
27
+ "homepage": "https://github.com/p0ulsh3n/lynx-ship/tree/main/packages/expo#readme",
28
+ "keywords": [
29
+ "lynxjs",
30
+ "lynx",
31
+ "expo",
32
+ "react-native",
33
+ "ota"
34
+ ],
35
+ "engines": {
36
+ "node": ">=20.19.0"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "peerDependencies": {
42
+ "@expo/config-plugins": ">=9",
43
+ "expo": ">=52",
44
+ "expo-modules-core": ">=1",
45
+ "react": ">=18",
46
+ "react-native": ">=0.76"
47
+ },
48
+ "dependencies": {
49
+ "@lynxship/sdk-android": "0.1.0",
50
+ "@lynxship/sdk-ios": "0.1.0"
51
+ },
52
+ "scripts": {
53
+ "build": "tsc -b tsconfig.json",
54
+ "typecheck": "tsc -b tsconfig.json --pretty false"
55
+ }
56
+ }
package/src/config.ts ADDED
@@ -0,0 +1,55 @@
1
+ export interface LynxShipExpoConfig {
2
+ endpoint?: string;
3
+ projectId?: string;
4
+ channel?: string;
5
+ runtimeVersion?: string;
6
+ publicKeys?: Record<string, string>;
7
+ embeddedBundle?: string;
8
+ lynxVersion?: string;
9
+ maxReleaseBytes?: number;
10
+ }
11
+
12
+ /** Let the native package manager resolve the current Lynx SDK by default. */
13
+ const DEFAULT_LYNX_VERSION = "auto";
14
+
15
+ export function validateLynxShipExpoConfig(
16
+ value: LynxShipExpoConfig = {},
17
+ ): LynxShipExpoConfig {
18
+ if (value.endpoint !== undefined) {
19
+ const url = new URL(value.endpoint);
20
+ if (url.protocol !== "https:" && !isLocalDevelopment(url.hostname))
21
+ throw new Error(
22
+ "LynxShip Expo endpoint must use HTTPS outside localhost",
23
+ );
24
+ }
25
+ for (const [key, publicKey] of Object.entries(value.publicKeys ?? {})) {
26
+ if (!key.trim() || !publicKey.includes("BEGIN PUBLIC KEY"))
27
+ throw new Error("LynxShip Expo publicKeys must contain PEM public keys");
28
+ }
29
+ if (value.maxReleaseBytes !== undefined) {
30
+ if (!Number.isInteger(value.maxReleaseBytes) || value.maxReleaseBytes < 1)
31
+ throw new Error(
32
+ "LynxShip Expo maxReleaseBytes must be a positive integer",
33
+ );
34
+ }
35
+ if (
36
+ value.lynxVersion !== undefined &&
37
+ value.lynxVersion !== "auto" &&
38
+ value.lynxVersion !== "latest" &&
39
+ !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(value.lynxVersion)
40
+ ) {
41
+ throw new Error(
42
+ "LynxShip Expo lynxVersion must be auto, latest, or an exact semver",
43
+ );
44
+ }
45
+ return {
46
+ ...value,
47
+ channel: value.channel ?? "production",
48
+ embeddedBundle: value.embeddedBundle ?? "main.lynx.bundle",
49
+ lynxVersion: value.lynxVersion ?? DEFAULT_LYNX_VERSION,
50
+ };
51
+ }
52
+
53
+ function isLocalDevelopment(hostname: string): boolean {
54
+ return hostname === "localhost" || hostname === "127.0.0.1";
55
+ }
@@ -0,0 +1,23 @@
1
+ declare module "react" {
2
+ export type ComponentType<Props = Record<string, unknown>> = (
3
+ props: Props,
4
+ ) => unknown;
5
+ export function createElement(
6
+ type: unknown,
7
+ props: Record<string, unknown> | null,
8
+ ): unknown;
9
+ }
10
+
11
+ declare module "react-native" {
12
+ export interface ViewProps {
13
+ accessibilityLabel?: string;
14
+ style?: unknown;
15
+ testID?: string;
16
+ }
17
+ }
18
+
19
+ declare module "expo-modules-core" {
20
+ export function requireNativeViewManager<Props = Record<string, unknown>>(
21
+ moduleName: string,
22
+ ): (props: Props) => unknown;
23
+ }
package/src/index.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { createElement, type ComponentType } from "react";
2
+ import { requireNativeViewManager } from "expo-modules-core";
3
+ import type { ViewProps } from "react-native";
4
+ import {
5
+ validateLynxShipExpoConfig,
6
+ type LynxShipExpoConfig,
7
+ } from "./config.js";
8
+
9
+ export type { LynxShipExpoConfig } from "./config.js";
10
+
11
+ export { validateLynxShipExpoConfig } from "./config.js";
12
+
13
+ export interface LynxViewProps extends ViewProps {
14
+ /** Bundle name resolved by the native Lynx template provider. */
15
+ bundle?: string;
16
+ /** Optional initial data passed to Lynx when the view is rendered. */
17
+ initialData?: string;
18
+ /** Requests a fresh render after an OTA candidate has been activated. */
19
+ reloadOnUpdate?: boolean;
20
+ }
21
+
22
+ const NativeLynxView = requireNativeViewManager<LynxViewProps>("LynxShip");
23
+
24
+ /**
25
+ * A LynxView that can be placed anywhere in an Expo/React Native view tree.
26
+ * Native OTA configuration is supplied by the LynxShip config plugin.
27
+ */
28
+ export function LynxView(props: LynxViewProps): unknown {
29
+ return createElement(NativeLynxView, {
30
+ bundle: props.bundle ?? "main.lynx.bundle",
31
+ initialData: props.initialData ?? "",
32
+ reloadOnUpdate: props.reloadOnUpdate ?? true,
33
+ ...props,
34
+ });
35
+ }
36
+
37
+ export const LynxShipView: ComponentType<LynxViewProps> = LynxView;
38
+
39
+ export function defineLynxShipExpoConfig(
40
+ config: LynxShipExpoConfig,
41
+ ): LynxShipExpoConfig {
42
+ return validateLynxShipExpoConfig(config);
43
+ }