@lynxship/expo 0.1.0 → 0.1.2
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 +35 -1
- package/app.plugin.cjs +108 -12
- package/asset-sync.cjs +264 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/ios/LynxShipExpoView.swift +4 -1
- package/package.json +2 -1
- package/src/config.ts +18 -0
- package/src/externals.d.ts +4 -3
- package/src/index.ts +2 -2
package/README.md
CHANGED
|
@@ -7,7 +7,12 @@ embedded `main.lynx.bundle` fallback.
|
|
|
7
7
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
|
+
Build the Lynx output before the native prebuild. The plugin then copies the
|
|
11
|
+
whole Rspeedy output directory, including `static/` assets, into the generated
|
|
12
|
+
Android and iOS hosts and records hashes in a managed manifest.
|
|
13
|
+
|
|
10
14
|
```bash
|
|
15
|
+
lynxship build --local
|
|
11
16
|
npx expo install @lynxship/expo
|
|
12
17
|
npx expo prebuild
|
|
13
18
|
npx pod-install
|
|
@@ -37,6 +42,8 @@ options are needed, add the config plugin manually:
|
|
|
37
42
|
"release-key-1": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
|
|
38
43
|
},
|
|
39
44
|
"embeddedBundle": "main.lynx.bundle",
|
|
45
|
+
"bundlePath": "../lynx-app/dist/main.lynx.bundle",
|
|
46
|
+
"syncBundle": true,
|
|
40
47
|
"lynxVersion": "auto"
|
|
41
48
|
}
|
|
42
49
|
]
|
|
@@ -49,6 +56,19 @@ For a dynamic config, keep the same `plugins` entry in the returned Expo
|
|
|
49
56
|
configuration. Expo cannot safely rewrite JavaScript or TypeScript config
|
|
50
57
|
files automatically.
|
|
51
58
|
|
|
59
|
+
`bundlePath` points to the generated `.lynx.bundle` and is resolved relative
|
|
60
|
+
to the Expo project. All files beside that bundle are copied so Rspeedy's
|
|
61
|
+
`static/` resources remain available at runtime. The default is
|
|
62
|
+
`dist/main.lynx.bundle`. `syncBundle` defaults to `true`; set it to `false`
|
|
63
|
+
only when a different native asset pipeline owns the bundle. Prebuild fails
|
|
64
|
+
with a repair command if the configured bundle is missing, rather than
|
|
65
|
+
producing a native app that opens to a blank Lynx view.
|
|
66
|
+
|
|
67
|
+
The sync is deterministic and idempotent. LynxShip updates files previously
|
|
68
|
+
owned by its manifest, removes only stale files from that manifest, and refuses
|
|
69
|
+
to overwrite a developer-owned native asset. The iOS output is added as a
|
|
70
|
+
folder resource so nested asset paths are preserved.
|
|
71
|
+
|
|
52
72
|
The endpoint must be HTTPS outside localhost. The public verification key is
|
|
53
73
|
safe to ship in the application; private signing keys must remain in the
|
|
54
74
|
LynxShip CLI or CI secret store.
|
|
@@ -88,7 +108,7 @@ modules and Lynx runtime changes still require a new binary.
|
|
|
88
108
|
|
|
89
109
|
## Build workflow
|
|
90
110
|
|
|
91
|
-
From the
|
|
111
|
+
From the Lynx project, after the bundle has been generated:
|
|
92
112
|
|
|
93
113
|
```bash
|
|
94
114
|
lynxship init
|
|
@@ -100,6 +120,20 @@ lynxship ota doctor --platform android
|
|
|
100
120
|
lynxship ota doctor --platform ios
|
|
101
121
|
```
|
|
102
122
|
|
|
123
|
+
For a separate Lynx and Expo checkout, keep the bundle path in the Expo plugin
|
|
124
|
+
configuration and run the two build stages in this order:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
lynxship --project-dir ../lynx-app build --local
|
|
128
|
+
npx expo prebuild --clean
|
|
129
|
+
npx eas-cli@latest build --platform android --profile preview
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
EAS receives the Expo project directory, so the Lynx bundle must be generated
|
|
133
|
+
and embedded before the cloud upload (or by an explicitly configured CI build
|
|
134
|
+
step). Config plugins do not run arbitrary network downloads or compilers
|
|
135
|
+
during `prebuild`.
|
|
136
|
+
|
|
103
137
|
The package does not silently replace an existing native host. `expo prebuild`
|
|
104
138
|
and the official Lynx dependencies remain the source of truth for native
|
|
105
139
|
project generation. iOS builds still require macOS/Xcode; Android builds use
|
package/app.plugin.cjs
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
const {
|
|
2
2
|
withAndroidManifest,
|
|
3
|
+
withDangerousMod,
|
|
3
4
|
withGradleProperties,
|
|
4
5
|
withInfoPlist,
|
|
5
6
|
withPodfile,
|
|
6
7
|
withSettingsGradle,
|
|
8
|
+
withXcodeProject,
|
|
9
|
+
IOSConfig,
|
|
7
10
|
} = require("@expo/config-plugins");
|
|
11
|
+
const path = require("node:path");
|
|
12
|
+
const { syncLynxAssets } = require("./asset-sync.cjs");
|
|
8
13
|
|
|
9
14
|
const MARKER = "# @lynxship/expo managed";
|
|
10
15
|
const DEFAULT_LYNX_VERSION = "auto";
|
|
@@ -58,6 +63,33 @@ function assertOptions(options) {
|
|
|
58
63
|
"@lynxship/expo lynxVersion must be auto, latest, or an exact semver",
|
|
59
64
|
);
|
|
60
65
|
}
|
|
66
|
+
if (
|
|
67
|
+
options.bundlePath !== undefined &&
|
|
68
|
+
(typeof options.bundlePath !== "string" ||
|
|
69
|
+
options.bundlePath.trim() === "" ||
|
|
70
|
+
options.bundlePath.includes("\0"))
|
|
71
|
+
) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"@lynxship/expo bundlePath must be a non-empty path without null bytes",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (
|
|
77
|
+
options.syncBundle !== undefined &&
|
|
78
|
+
typeof options.syncBundle !== "boolean"
|
|
79
|
+
)
|
|
80
|
+
throw new Error("@lynxship/expo syncBundle must be a boolean");
|
|
81
|
+
if (
|
|
82
|
+
options.embeddedBundle !== undefined &&
|
|
83
|
+
(typeof options.embeddedBundle !== "string" ||
|
|
84
|
+
options.embeddedBundle.trim() === "" ||
|
|
85
|
+
options.embeddedBundle.startsWith("/") ||
|
|
86
|
+
options.embeddedBundle.includes("\\") ||
|
|
87
|
+
options.embeddedBundle.split("/").includes(".."))
|
|
88
|
+
) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"@lynxship/expo embeddedBundle must be a portable relative path without '..'",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
61
93
|
return options;
|
|
62
94
|
}
|
|
63
95
|
|
|
@@ -185,20 +217,84 @@ function withLynxShipPodfile(config, options) {
|
|
|
185
217
|
});
|
|
186
218
|
}
|
|
187
219
|
|
|
220
|
+
function withLynxShipAndroidAssets(config, options) {
|
|
221
|
+
if (options.syncBundle === false) return config;
|
|
222
|
+
return withDangerousMod(config, [
|
|
223
|
+
"android",
|
|
224
|
+
async (value) => {
|
|
225
|
+
await syncLynxAssets(value.modRequest.projectRoot, {
|
|
226
|
+
...options,
|
|
227
|
+
platform: "android",
|
|
228
|
+
});
|
|
229
|
+
return value;
|
|
230
|
+
},
|
|
231
|
+
]);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function addIosFolderResource(project, folderPath, groupName) {
|
|
235
|
+
const group = project.pbxGroupByName(groupName);
|
|
236
|
+
if (!group)
|
|
237
|
+
throw new Error(`@lynxship/expo could not find iOS group ${groupName}`);
|
|
238
|
+
const groups = project.hash.project.objects.PBXGroup || {};
|
|
239
|
+
const groupKey = Object.keys(groups).find(
|
|
240
|
+
(key) => !key.endsWith("_comment") && groups[key] === group,
|
|
241
|
+
);
|
|
242
|
+
if (!groupKey)
|
|
243
|
+
throw new Error(`@lynxship/expo could not resolve iOS group ${groupName}`);
|
|
244
|
+
const existing = group.children?.find(
|
|
245
|
+
(child) => child.comment === path.basename(folderPath),
|
|
246
|
+
);
|
|
247
|
+
if (existing) return;
|
|
248
|
+
const file = project.addFile(folderPath, groupKey, {
|
|
249
|
+
lastKnownFileType: "folder",
|
|
250
|
+
sourceTree: "<group>",
|
|
251
|
+
});
|
|
252
|
+
if (!file) return;
|
|
253
|
+
file.uuid = project.generateUuid();
|
|
254
|
+
const target = project.getTarget("com.apple.product-type.application");
|
|
255
|
+
if (!target)
|
|
256
|
+
throw new Error("@lynxship/expo could not find the iOS application target");
|
|
257
|
+
file.target = target.uuid;
|
|
258
|
+
project.addToPbxBuildFileSection(file);
|
|
259
|
+
project.addToPbxResourcesBuildPhase(file);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function withLynxShipIosAssets(config, options) {
|
|
263
|
+
if (options.syncBundle === false) return config;
|
|
264
|
+
const withCopiedAssets = withDangerousMod(config, [
|
|
265
|
+
"ios",
|
|
266
|
+
async (value) => {
|
|
267
|
+
const sourceRoot = IOSConfig.Paths.getSourceRoot(
|
|
268
|
+
value.modRequest.projectRoot,
|
|
269
|
+
);
|
|
270
|
+
await syncLynxAssets(value.modRequest.projectRoot, {
|
|
271
|
+
...options,
|
|
272
|
+
platform: "ios",
|
|
273
|
+
iosSourceRoot: sourceRoot,
|
|
274
|
+
});
|
|
275
|
+
return value;
|
|
276
|
+
},
|
|
277
|
+
]);
|
|
278
|
+
return withXcodeProject(withCopiedAssets, (value) => {
|
|
279
|
+
const sourceRoot = IOSConfig.Paths.getSourceRoot(
|
|
280
|
+
value.modRequest.projectRoot,
|
|
281
|
+
);
|
|
282
|
+
const projectName = path.basename(sourceRoot);
|
|
283
|
+
const folderPath = `${projectName}/LynxShipAssets`;
|
|
284
|
+
addIosFolderResource(value.modResults, folderPath, projectName);
|
|
285
|
+
return value;
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
188
289
|
function withLynxShipExpo(config, props = {}) {
|
|
189
290
|
const options = assertOptions({ ...getOptions(config), ...props });
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
options,
|
|
198
|
-
),
|
|
199
|
-
),
|
|
200
|
-
options,
|
|
201
|
-
);
|
|
291
|
+
let result = withLynxShipAndroidManifest(config, options);
|
|
292
|
+
result = withLynxShipInfoPlist(result, options);
|
|
293
|
+
result = withLynxShipGradle(result, options);
|
|
294
|
+
result = withLynxShipAndroidSdk(result);
|
|
295
|
+
result = withLynxShipAndroidAssets(result, options);
|
|
296
|
+
result = withLynxShipIosAssets(result, options);
|
|
297
|
+
return withLynxShipPodfile(result, options);
|
|
202
298
|
}
|
|
203
299
|
|
|
204
300
|
module.exports = withLynxShipExpo;
|
package/asset-sync.cjs
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
const crypto = require("node:crypto");
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const fsp = require("node:fs/promises");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
|
|
6
|
+
const MANIFEST_VERSION = 1;
|
|
7
|
+
|
|
8
|
+
function isAbsoluteAny(value) {
|
|
9
|
+
return path.isAbsolute(value) || path.win32.isAbsolute(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function safeRelative(value, label) {
|
|
13
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
14
|
+
throw new Error(`@lynxship/expo ${label} must be a non-empty path`);
|
|
15
|
+
}
|
|
16
|
+
const normalized = value.replaceAll("\\", "/");
|
|
17
|
+
if (
|
|
18
|
+
isAbsoluteAny(value) ||
|
|
19
|
+
normalized.startsWith("/") ||
|
|
20
|
+
normalized.split("/").some((part) => part === "..") ||
|
|
21
|
+
normalized.includes("\0")
|
|
22
|
+
) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`@lynxship/expo ${label} must be a portable relative path without '..'`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return normalized.replace(/^\.\//, "");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function destinationPath(root, relativePath) {
|
|
31
|
+
const normalized = safeRelative(relativePath, "embeddedBundle");
|
|
32
|
+
const destination = path.resolve(root, ...normalized.split("/"));
|
|
33
|
+
const relative = path.relative(root, destination);
|
|
34
|
+
if (
|
|
35
|
+
relative === "" ||
|
|
36
|
+
relative.startsWith(".." + path.sep) ||
|
|
37
|
+
path.isAbsolute(relative)
|
|
38
|
+
) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"@lynxship/expo embeddedBundle must stay in the asset root",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return { normalized, destination };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function collectFiles(directory, prefix = "") {
|
|
47
|
+
const entries = await fsp.readdir(directory, { withFileTypes: true });
|
|
48
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
49
|
+
const files = [];
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
52
|
+
const absolute = path.join(directory, entry.name);
|
|
53
|
+
if (entry.isSymbolicLink()) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`@lynxship/expo refuses symbolic links in the Lynx output: ${relative}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
files.push(...(await collectFiles(absolute, relative)));
|
|
60
|
+
} else if (entry.isFile()) {
|
|
61
|
+
files.push({ absolute, relative: relative.replaceAll("\\", "/") });
|
|
62
|
+
} else {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`@lynxship/expo found an unsupported filesystem entry in the Lynx output: ${relative}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return files;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function createBundlePlan(projectRoot, options = {}) {
|
|
72
|
+
const bundlePath = options.bundlePath || "dist/main.lynx.bundle";
|
|
73
|
+
if (typeof bundlePath !== "string" || bundlePath.trim() === "") {
|
|
74
|
+
throw new Error("@lynxship/expo bundlePath must be a non-empty path");
|
|
75
|
+
}
|
|
76
|
+
const sourceBundle = path.resolve(projectRoot, bundlePath);
|
|
77
|
+
const sourceDirectory = path.dirname(sourceBundle);
|
|
78
|
+
const sourceStat = await fsp.stat(sourceBundle).catch(() => undefined);
|
|
79
|
+
if (!sourceStat || !sourceStat.isFile() || sourceStat.size === 0) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Lynx bundle not found or empty: ${sourceBundle}. Build Rspeedy first (for example: lynxship build --local), or set expo.plugins[@lynxship/expo].bundlePath to the generated .lynx.bundle.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const embeddedBundle = safeRelative(
|
|
85
|
+
options.embeddedBundle || "main.lynx.bundle",
|
|
86
|
+
"embeddedBundle",
|
|
87
|
+
);
|
|
88
|
+
const files = await collectFiles(sourceDirectory);
|
|
89
|
+
const plan = files.map((file) => ({
|
|
90
|
+
...file,
|
|
91
|
+
destination:
|
|
92
|
+
path.resolve(sourceDirectory, file.relative) === sourceBundle
|
|
93
|
+
? embeddedBundle
|
|
94
|
+
: safeRelative(file.relative, "bundle output file"),
|
|
95
|
+
}));
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
for (const file of plan) {
|
|
98
|
+
if (seen.has(file.destination)) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`@lynxship/expo found duplicate embedded asset path: ${file.destination}`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
seen.add(file.destination);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
sourceBundle,
|
|
107
|
+
sourceDirectory,
|
|
108
|
+
bundlePath,
|
|
109
|
+
embeddedBundle,
|
|
110
|
+
files: plan,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function sha256File(file) {
|
|
115
|
+
const hash = crypto.createHash("sha256");
|
|
116
|
+
hash.update(await fsp.readFile(file));
|
|
117
|
+
return hash.digest("hex");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function readManifest(file) {
|
|
121
|
+
try {
|
|
122
|
+
const value = JSON.parse(await fsp.readFile(file, "utf8"));
|
|
123
|
+
if (value.version !== MANIFEST_VERSION || !Array.isArray(value.files)) {
|
|
124
|
+
return { files: [] };
|
|
125
|
+
}
|
|
126
|
+
return value;
|
|
127
|
+
} catch {
|
|
128
|
+
return { files: [] };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function manifestPaths(manifest) {
|
|
133
|
+
return new Set(
|
|
134
|
+
manifest.files
|
|
135
|
+
.map((file) => file && file.path)
|
|
136
|
+
.filter((file) => typeof file === "string")
|
|
137
|
+
.map((file) => safeRelative(file, "managed asset path")),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function syncBundleDirectory({
|
|
142
|
+
projectRoot,
|
|
143
|
+
plan,
|
|
144
|
+
destinationRoot,
|
|
145
|
+
manifestPath,
|
|
146
|
+
platform,
|
|
147
|
+
}) {
|
|
148
|
+
await fsp.mkdir(destinationRoot, { recursive: true });
|
|
149
|
+
const previous = await readManifest(manifestPath);
|
|
150
|
+
const previouslyManaged = manifestPaths(previous);
|
|
151
|
+
const desired = new Set(plan.files.map((file) => file.destination));
|
|
152
|
+
|
|
153
|
+
for (const file of plan.files) {
|
|
154
|
+
const { destination } = destinationPath(destinationRoot, file.destination);
|
|
155
|
+
const existing = await fsp.lstat(destination).catch(() => undefined);
|
|
156
|
+
if (existing && !previouslyManaged.has(file.destination)) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`@lynxship/expo will not overwrite an unmanaged ${platform} asset: ${destination}. Choose another embeddedBundle path or remove the conflicting file yourself.`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (existing && !existing.isFile()) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`@lynxship/expo cannot replace a non-file ${platform} asset: ${destination}`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const stale of previouslyManaged) {
|
|
169
|
+
if (desired.has(stale)) continue;
|
|
170
|
+
const { destination } = destinationPath(destinationRoot, stale);
|
|
171
|
+
const existing = await fsp.lstat(destination).catch(() => undefined);
|
|
172
|
+
if (existing?.isFile()) await fsp.rm(destination);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const manifestFiles = [];
|
|
176
|
+
for (const file of plan.files) {
|
|
177
|
+
const { destination } = destinationPath(destinationRoot, file.destination);
|
|
178
|
+
await fsp.mkdir(path.dirname(destination), { recursive: true });
|
|
179
|
+
await fsp.copyFile(file.absolute, destination);
|
|
180
|
+
const stat = await fsp.stat(destination);
|
|
181
|
+
manifestFiles.push({
|
|
182
|
+
path: file.destination,
|
|
183
|
+
size: stat.size,
|
|
184
|
+
sha256: await sha256File(destination),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
await fsp.mkdir(path.dirname(manifestPath), { recursive: true });
|
|
189
|
+
const source = path
|
|
190
|
+
.relative(projectRoot, plan.sourceDirectory)
|
|
191
|
+
.replaceAll("\\", "/");
|
|
192
|
+
await fsp.writeFile(
|
|
193
|
+
manifestPath,
|
|
194
|
+
JSON.stringify(
|
|
195
|
+
{
|
|
196
|
+
version: MANIFEST_VERSION,
|
|
197
|
+
platform,
|
|
198
|
+
source: source || ".",
|
|
199
|
+
bundlePath: plan.bundlePath,
|
|
200
|
+
embeddedBundle: plan.embeddedBundle,
|
|
201
|
+
files: manifestFiles,
|
|
202
|
+
},
|
|
203
|
+
null,
|
|
204
|
+
2,
|
|
205
|
+
) + "\n",
|
|
206
|
+
"utf8",
|
|
207
|
+
);
|
|
208
|
+
return {
|
|
209
|
+
platform,
|
|
210
|
+
sourceBundle: plan.sourceBundle,
|
|
211
|
+
destinationRoot,
|
|
212
|
+
manifestPath,
|
|
213
|
+
files: manifestFiles,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function syncLynxAssets(projectRoot, options = {}) {
|
|
218
|
+
const plan = await createBundlePlan(projectRoot, options);
|
|
219
|
+
const platform = options.platform;
|
|
220
|
+
if (platform === "android") {
|
|
221
|
+
const destinationRoot = path.join(
|
|
222
|
+
projectRoot,
|
|
223
|
+
"android",
|
|
224
|
+
"app",
|
|
225
|
+
"src",
|
|
226
|
+
"main",
|
|
227
|
+
"assets",
|
|
228
|
+
);
|
|
229
|
+
if (!fs.existsSync(path.join(projectRoot, "android"))) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
"Android native project is missing. Run `npx expo prebuild --platform android` before syncing the Lynx bundle.",
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
return syncBundleDirectory({
|
|
235
|
+
projectRoot,
|
|
236
|
+
plan,
|
|
237
|
+
destinationRoot,
|
|
238
|
+
manifestPath: path.join(projectRoot, "android", ".lynxship-assets.json"),
|
|
239
|
+
platform,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (platform === "ios") {
|
|
243
|
+
const iosSourceRoot = options.iosSourceRoot;
|
|
244
|
+
if (!iosSourceRoot) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
"iOS native source root is unavailable. Run `npx expo prebuild --platform ios` before syncing the Lynx bundle.",
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return syncBundleDirectory({
|
|
250
|
+
projectRoot,
|
|
251
|
+
plan,
|
|
252
|
+
destinationRoot: path.join(iosSourceRoot, "LynxShipAssets"),
|
|
253
|
+
manifestPath: path.join(projectRoot, "ios", ".lynxship-assets.json"),
|
|
254
|
+
platform,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
throw new Error(`@lynxship/expo does not support asset sync for ${platform}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = {
|
|
261
|
+
createBundlePlan,
|
|
262
|
+
syncBundleDirectory,
|
|
263
|
+
syncLynxAssets,
|
|
264
|
+
};
|
package/dist/config.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ export interface LynxShipExpoConfig {
|
|
|
5
5
|
runtimeVersion?: string;
|
|
6
6
|
publicKeys?: Record<string, string>;
|
|
7
7
|
embeddedBundle?: string;
|
|
8
|
+
/** Relative path to the generated .lynx.bundle, normally dist/main.lynx.bundle. */
|
|
9
|
+
bundlePath?: string;
|
|
10
|
+
/** Copy the Lynx output into generated Android/iOS hosts during prebuild. */
|
|
11
|
+
syncBundle?: boolean;
|
|
8
12
|
lynxVersion?: string;
|
|
9
13
|
maxReleaseBytes?: number;
|
|
10
14
|
}
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +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,
|
|
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,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAKD,wBAAgB,0BAA0B,CACxC,KAAK,GAAE,kBAAuB,GAC7B,kBAAkB,CAgDpB"}
|
package/dist/config.js
CHANGED
|
@@ -17,6 +17,14 @@ function validateLynxShipExpoConfig(value = {}) {
|
|
|
17
17
|
if (!Number.isInteger(value.maxReleaseBytes) || value.maxReleaseBytes < 1)
|
|
18
18
|
throw new Error("LynxShip Expo maxReleaseBytes must be a positive integer");
|
|
19
19
|
}
|
|
20
|
+
if (value.bundlePath !== undefined) {
|
|
21
|
+
if (typeof value.bundlePath !== "string" ||
|
|
22
|
+
value.bundlePath.trim().length === 0 ||
|
|
23
|
+
value.bundlePath.includes("\0"))
|
|
24
|
+
throw new Error("LynxShip Expo bundlePath must be a non-empty path without null bytes");
|
|
25
|
+
}
|
|
26
|
+
if (value.syncBundle !== undefined && typeof value.syncBundle !== "boolean")
|
|
27
|
+
throw new Error("LynxShip Expo syncBundle must be a boolean");
|
|
20
28
|
if (value.lynxVersion !== undefined &&
|
|
21
29
|
value.lynxVersion !== "auto" &&
|
|
22
30
|
value.lynxVersion !== "latest" &&
|
|
@@ -27,6 +35,8 @@ function validateLynxShipExpoConfig(value = {}) {
|
|
|
27
35
|
...value,
|
|
28
36
|
channel: value.channel ?? "production",
|
|
29
37
|
embeddedBundle: value.embeddedBundle ?? "main.lynx.bundle",
|
|
38
|
+
bundlePath: value.bundlePath ?? "dist/main.lynx.bundle",
|
|
39
|
+
syncBundle: value.syncBundle ?? true,
|
|
30
40
|
lynxVersion: value.lynxVersion ?? DEFAULT_LYNX_VERSION,
|
|
31
41
|
};
|
|
32
42
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ComponentType } from "react";
|
|
1
|
+
import { type ComponentType, type ReactNode } from "react";
|
|
2
2
|
import type { ViewProps } from "react-native";
|
|
3
3
|
import { type LynxShipExpoConfig } from "./config.js";
|
|
4
4
|
export type { LynxShipExpoConfig } from "./config.js";
|
|
@@ -15,7 +15,7 @@ export interface LynxViewProps extends ViewProps {
|
|
|
15
15
|
* A LynxView that can be placed anywhere in an Expo/React Native view tree.
|
|
16
16
|
* Native OTA configuration is supplied by the LynxShip config plugin.
|
|
17
17
|
*/
|
|
18
|
-
export declare function LynxView(props: LynxViewProps):
|
|
18
|
+
export declare function LynxView(props: LynxViewProps): ReactNode;
|
|
19
19
|
export declare const LynxShipView: ComponentType<LynxViewProps>;
|
|
20
20
|
export declare function defineLynxShipExpoConfig(config: LynxShipExpoConfig): LynxShipExpoConfig;
|
|
21
21
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE1E,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,SAAS,CAOxD;AAED,eAAO,MAAM,YAAY,EAAE,aAAa,CAAC,aAAa,CAAY,CAAC;AAEnE,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,kBAAkB,GACzB,kBAAkB,CAEpB"}
|
|
@@ -113,7 +113,10 @@ public final class LynxShipExpoView: ExpoView {
|
|
|
113
113
|
guard !path.isEmpty, !path.hasPrefix("/"), !path.contains(".."), !path.contains("\\") else {
|
|
114
114
|
throw NSError(domain: "LynxShip", code: 2, userInfo: [NSLocalizedDescriptionKey: "Unsafe Lynx asset path"])
|
|
115
115
|
}
|
|
116
|
-
guard let url = Bundle.main.url(forResource: path, withExtension: nil
|
|
116
|
+
guard let url = Bundle.main.url(forResource: path, withExtension: nil, subdirectory: "LynxShipAssets")
|
|
117
|
+
?? Bundle.main.url(forResource: path, withExtension: "bundle", subdirectory: "LynxShipAssets")
|
|
118
|
+
?? Bundle.main.url(forResource: path, withExtension: nil)
|
|
119
|
+
?? Bundle.main.url(forResource: path, withExtension: "bundle") else {
|
|
117
120
|
throw NSError(domain: "LynxShip", code: 3, userInfo: [NSLocalizedDescriptionKey: "Embedded Lynx bundle not found: \(path)"])
|
|
118
121
|
}
|
|
119
122
|
return try Data(contentsOf: url)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lynxship/expo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Expo native LynxView with LynxShip OTA delivery and cache support.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"src",
|
|
13
13
|
"app.plugin.js",
|
|
14
14
|
"app.plugin.cjs",
|
|
15
|
+
"asset-sync.cjs",
|
|
15
16
|
"expo-module.config.json",
|
|
16
17
|
"lynxship-expo.podspec",
|
|
17
18
|
"README.md"
|
package/src/config.ts
CHANGED
|
@@ -5,6 +5,10 @@ export interface LynxShipExpoConfig {
|
|
|
5
5
|
runtimeVersion?: string;
|
|
6
6
|
publicKeys?: Record<string, string>;
|
|
7
7
|
embeddedBundle?: string;
|
|
8
|
+
/** Relative path to the generated .lynx.bundle, normally dist/main.lynx.bundle. */
|
|
9
|
+
bundlePath?: string;
|
|
10
|
+
/** Copy the Lynx output into generated Android/iOS hosts during prebuild. */
|
|
11
|
+
syncBundle?: boolean;
|
|
8
12
|
lynxVersion?: string;
|
|
9
13
|
maxReleaseBytes?: number;
|
|
10
14
|
}
|
|
@@ -32,6 +36,18 @@ export function validateLynxShipExpoConfig(
|
|
|
32
36
|
"LynxShip Expo maxReleaseBytes must be a positive integer",
|
|
33
37
|
);
|
|
34
38
|
}
|
|
39
|
+
if (value.bundlePath !== undefined) {
|
|
40
|
+
if (
|
|
41
|
+
typeof value.bundlePath !== "string" ||
|
|
42
|
+
value.bundlePath.trim().length === 0 ||
|
|
43
|
+
value.bundlePath.includes("\0")
|
|
44
|
+
)
|
|
45
|
+
throw new Error(
|
|
46
|
+
"LynxShip Expo bundlePath must be a non-empty path without null bytes",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (value.syncBundle !== undefined && typeof value.syncBundle !== "boolean")
|
|
50
|
+
throw new Error("LynxShip Expo syncBundle must be a boolean");
|
|
35
51
|
if (
|
|
36
52
|
value.lynxVersion !== undefined &&
|
|
37
53
|
value.lynxVersion !== "auto" &&
|
|
@@ -46,6 +62,8 @@ export function validateLynxShipExpoConfig(
|
|
|
46
62
|
...value,
|
|
47
63
|
channel: value.channel ?? "production",
|
|
48
64
|
embeddedBundle: value.embeddedBundle ?? "main.lynx.bundle",
|
|
65
|
+
bundlePath: value.bundlePath ?? "dist/main.lynx.bundle",
|
|
66
|
+
syncBundle: value.syncBundle ?? true,
|
|
49
67
|
lynxVersion: value.lynxVersion ?? DEFAULT_LYNX_VERSION,
|
|
50
68
|
};
|
|
51
69
|
}
|
package/src/externals.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
declare module "react" {
|
|
2
|
+
export type ReactNode = unknown;
|
|
2
3
|
export type ComponentType<Props = Record<string, unknown>> = (
|
|
3
4
|
props: Props,
|
|
4
|
-
) =>
|
|
5
|
+
) => ReactNode;
|
|
5
6
|
export function createElement(
|
|
6
7
|
type: unknown,
|
|
7
8
|
props: Record<string, unknown> | null,
|
|
8
|
-
):
|
|
9
|
+
): ReactNode;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
declare module "react-native" {
|
|
@@ -19,5 +20,5 @@ declare module "react-native" {
|
|
|
19
20
|
declare module "expo-modules-core" {
|
|
20
21
|
export function requireNativeViewManager<Props = Record<string, unknown>>(
|
|
21
22
|
moduleName: string,
|
|
22
|
-
): (props: Props) =>
|
|
23
|
+
): (props: Props) => import("react").ReactNode;
|
|
23
24
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createElement, type ComponentType } from "react";
|
|
1
|
+
import { createElement, type ComponentType, type ReactNode } from "react";
|
|
2
2
|
import { requireNativeViewManager } from "expo-modules-core";
|
|
3
3
|
import type { ViewProps } from "react-native";
|
|
4
4
|
import {
|
|
@@ -25,7 +25,7 @@ const NativeLynxView = requireNativeViewManager<LynxViewProps>("LynxShip");
|
|
|
25
25
|
* A LynxView that can be placed anywhere in an Expo/React Native view tree.
|
|
26
26
|
* Native OTA configuration is supplied by the LynxShip config plugin.
|
|
27
27
|
*/
|
|
28
|
-
export function LynxView(props: LynxViewProps):
|
|
28
|
+
export function LynxView(props: LynxViewProps): ReactNode {
|
|
29
29
|
return createElement(NativeLynxView, {
|
|
30
30
|
bundle: props.bundle ?? "main.lynx.bundle",
|
|
31
31
|
initialData: props.initialData ?? "",
|