rnxsim 0.1.524 → 0.1.526

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.
Files changed (60) hide show
  1. package/cli/outbound-endpoints.ts +15 -0
  2. package/dist-lib/agent-daemon-client.cjs +1 -1
  3. package/dist-lib/agent-events.cjs +1 -1
  4. package/dist-lib/agent-identity.cjs +1 -1
  5. package/dist-lib/agent-sessions.cjs +1 -1
  6. package/dist-lib/attached-projects.cjs +1 -1
  7. package/dist-lib/auth/shared-session.cjs +1 -1
  8. package/dist-lib/backend-origin.cjs +1 -1
  9. package/dist-lib/beta.cjs +1 -1
  10. package/dist-lib/beta.mjs +1 -1
  11. package/dist-lib/bridge-constants.cjs +1 -1
  12. package/dist-lib/bridge-contract-input.cjs +1 -1
  13. package/dist-lib/bridge-contract-input.mjs +1 -1
  14. package/dist-lib/bridge-contract.cjs +1 -1
  15. package/dist-lib/bridge-contract.mjs +1 -1
  16. package/dist-lib/capture-contract.cjs +1 -1
  17. package/dist-lib/capture-contract.mjs +1 -1
  18. package/dist-lib/cli-constants.cjs +1 -1
  19. package/dist-lib/cloud-contract.cjs +1 -1
  20. package/dist-lib/cloud-contract.mjs +1 -1
  21. package/dist-lib/cloud.cjs +1 -1
  22. package/dist-lib/cloud.mjs +1 -1
  23. package/dist-lib/config.cjs +1 -1
  24. package/dist-lib/detox/index.cjs +1 -1
  25. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  26. package/dist-lib/home-paths.cjs +1 -1
  27. package/dist-lib/host/bridge-host.cjs +33 -4
  28. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  29. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  30. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  31. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  32. package/dist-lib/host/websocket-proxy.cjs +1 -1
  33. package/dist-lib/index.cjs +3 -1
  34. package/dist-lib/jump-to-source-babel.cjs +1 -1
  35. package/dist-lib/jump-to-source-native.cjs +1 -1
  36. package/dist-lib/menu.cjs +1 -1
  37. package/dist-lib/menu.mjs +1 -1
  38. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  39. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  40. package/dist-lib/metro-production-bundle.cjs +1 -1
  41. package/dist-lib/metro-production-bundle.mjs +1 -1
  42. package/dist-lib/metro.cjs +1 -1
  43. package/dist-lib/profiles.cjs +1 -1
  44. package/dist-lib/public-brand.cjs +1 -1
  45. package/dist-lib/react-native-host-modules.cjs +1 -1
  46. package/dist-lib/react-native-host-modules.mjs +1 -1
  47. package/dist-lib/render-mode.cjs +1 -1
  48. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  49. package/dist-lib/sdk.cjs +1 -1
  50. package/dist-lib/sdk.mjs +1 -1
  51. package/dist-lib/skills.cjs +37 -8
  52. package/dist-lib/swift-submit.cjs +92 -0
  53. package/dist-lib/swift-submit.mjs +65 -0
  54. package/dist-lib/swift.cjs +316 -23
  55. package/dist-lib/vite.cjs +280 -11
  56. package/package.json +8 -1
  57. package/src/rolldown-plugin-swift.ts +240 -0
  58. package/src/swift-submit.ts +185 -0
  59. package/src/vite-plugin-one.ts +10 -3
  60. package/src/vite-plugin-swift.ts +86 -2
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.524 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.526 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -35,7 +35,8 @@ __export(vite_plugin_one_exports, {
35
35
  default: () => vite_plugin_one_default,
36
36
  rnxPlugin: () => rnxPlugin,
37
37
  sootsimPlugin: () => sootsimPlugin,
38
- swiftPlugin: () => swiftPlugin
38
+ swiftPlugin: () => swiftPlugin,
39
+ swiftRemotePlugin: () => swiftRemotePlugin
39
40
  });
40
41
  module.exports = __toCommonJS(vite_plugin_one_exports);
41
42
  var import_fs2 = __toESM(require("fs"), 1);
@@ -796,13 +797,13 @@ function imagesetName(contentsPath) {
796
797
  if (!leaf.endsWith(".imageset")) return null;
797
798
  return leaf.slice(0, -".imageset".length) || null;
798
799
  }
799
- function isFlatResourceImage(path7) {
800
- return path7.split("/").includes("Resources") && RASTER_EXTENSIONS.has(extOf(path7));
800
+ function isFlatResourceImage(path8) {
801
+ return path8.split("/").includes("Resources") && RASTER_EXTENSIONS.has(extOf(path8));
801
802
  }
802
803
  function enumerateBundleImages(paths, readText) {
803
804
  const table = [];
804
805
  const seen = /* @__PURE__ */ new Set();
805
- const catalogs = paths.filter((path7) => baseOf(path7) === "Contents.json");
806
+ const catalogs = paths.filter((path8) => baseOf(path8) === "Contents.json");
806
807
  for (const contentsPath of catalogs) {
807
808
  const name = imagesetName(contentsPath);
808
809
  if (name === null || seen.has(name)) continue;
@@ -822,15 +823,33 @@ function enumerateBundleImages(paths, readText) {
822
823
  seen.add(name);
823
824
  table.push({ name, file: `${dir}/${file}` });
824
825
  }
825
- for (const path7 of paths) {
826
- if (!isFlatResourceImage(path7)) continue;
827
- const name = baseOf(path7).slice(0, -extOf(path7).length);
826
+ for (const path8 of paths) {
827
+ if (!isFlatResourceImage(path8)) continue;
828
+ const name = baseOf(path8).slice(0, -extOf(path8).length);
828
829
  if (name === "" || seen.has(name)) continue;
829
830
  seen.add(name);
830
- table.push({ name, file: path7 });
831
+ table.push({ name, file: path8 });
831
832
  }
832
833
  return table;
833
834
  }
835
+ function enumerateBundleJson(paths) {
836
+ const found = [];
837
+ const seen = /* @__PURE__ */ new Set();
838
+ const ordered = [...paths].filter(
839
+ (path8) => path8.split("/").includes("Resources") && path8.split("/").every((segment) => segment !== "" && segment !== "..") && extOf(path8) === ".json"
840
+ ).sort((a, b) => {
841
+ const aDepth = a.split("/").length;
842
+ const bDepth = b.split("/").length;
843
+ return aDepth - bDepth || (a < b ? -1 : a > b ? 1 : 0);
844
+ });
845
+ for (const file of ordered) {
846
+ const base = baseOf(file);
847
+ if (seen.has(base)) continue;
848
+ seen.add(base);
849
+ found.push({ base, file });
850
+ }
851
+ return found;
852
+ }
834
853
 
835
854
  // ../sootsim-swift/toolchain.json
836
855
  var toolchain_default = {
@@ -944,6 +963,40 @@ function buildSwiftResourceTable(project, baseUrl) {
944
963
  project.resourceFiles = registered;
945
964
  return { table, watched };
946
965
  }
966
+ var BUNDLE_MAX_FILES = 128;
967
+ var BUNDLE_MAX_FILE_BYTES = 2 * 1024 * 1024;
968
+ var BUNDLE_MAX_TOTAL_BYTES = 8 * 1024 * 1024;
969
+ function buildSwiftBundleFiles(packagePath) {
970
+ const relative2 = resourceCandidateFiles(packagePath).map(
971
+ (file) => import_node_path4.default.relative(packagePath, file).split(import_node_path4.default.sep).join(import_node_path4.default.posix.sep)
972
+ );
973
+ const files = {};
974
+ const watched = [];
975
+ const skipped = [];
976
+ let total = 0;
977
+ for (const { base, file } of enumerateBundleJson(relative2)) {
978
+ watched.push(file);
979
+ let bytes;
980
+ try {
981
+ bytes = import_node_fs3.default.readFileSync(import_node_path4.default.join(packagePath, ...file.split("/")));
982
+ } catch {
983
+ continue;
984
+ }
985
+ if (bytes.length > BUNDLE_MAX_FILE_BYTES) {
986
+ skipped.push(`${file} (${bytes.length} bytes past the per-file cap)`);
987
+ continue;
988
+ }
989
+ if (Object.keys(files).length >= BUNDLE_MAX_FILES || total + bytes.length > BUNDLE_MAX_TOTAL_BYTES) {
990
+ skipped.push(
991
+ `${file} (pack caps: ${BUNDLE_MAX_FILES} files / ${BUNDLE_MAX_TOTAL_BYTES} bytes)`
992
+ );
993
+ continue;
994
+ }
995
+ files[base] = bytes.toString("base64");
996
+ total += bytes.length;
997
+ }
998
+ return { files, watched, skipped };
999
+ }
947
1000
  function serveSwiftResource(project, key) {
948
1001
  const disk = project.resourceFiles.get(key);
949
1002
  if (!disk) return null;
@@ -1127,9 +1180,26 @@ function swiftPlugin(options = {}) {
1127
1180
  `swift resources for ${packagePath}: ${error instanceof Error ? error.message : String(error)}`
1128
1181
  );
1129
1182
  }
1183
+ let bundleFiles = {};
1184
+ try {
1185
+ const packed = buildSwiftBundleFiles(packagePath);
1186
+ bundleFiles = packed.files;
1187
+ for (const watched of packed.watched) {
1188
+ this.addWatchFile(import_node_path4.default.join(packagePath, ...watched.split(import_node_path4.default.posix.sep)));
1189
+ }
1190
+ if (packed.skipped.length > 0) {
1191
+ this.warn(
1192
+ `swift bundle files skipped for ${packagePath}: ${packed.skipped.join("; ")}`
1193
+ );
1194
+ }
1195
+ } catch (error) {
1196
+ this.warn(
1197
+ `swift bundle files for ${packagePath}: ${error instanceof Error ? error.message : String(error)}`
1198
+ );
1199
+ }
1130
1200
  return [
1131
1201
  `import { createSwiftEntry } from 'rnxsim/swift'`,
1132
- `export default createSwiftEntry({ url: ${JSON.stringify(artifactUrl(project, artifact))}, hash: ${JSON.stringify(artifact.hash)}, rootId: ${JSON.stringify(entryRootId(packagePath, file))}, resources: ${JSON.stringify(resources)} })`,
1202
+ `export default createSwiftEntry({ url: ${JSON.stringify(artifactUrl(project, artifact))}, hash: ${JSON.stringify(artifact.hash)}, rootId: ${JSON.stringify(entryRootId(packagePath, file))}, resources: ${JSON.stringify(resources)}, bundleFiles: ${JSON.stringify(bundleFiles)} })`,
1133
1203
  ""
1134
1204
  ].join("\n");
1135
1205
  },
@@ -1171,6 +1241,204 @@ function swiftPlugin(options = {}) {
1171
1241
  };
1172
1242
  }
1173
1243
 
1244
+ // src/rolldown-plugin-swift.ts
1245
+ var import_node_fs4 = __toESM(require("node:fs"), 1);
1246
+ var import_node_path5 = __toESM(require("node:path"), 1);
1247
+
1248
+ // src/swift-submit.ts
1249
+ var SWIFT_COMPILE_PATH = "/api/swift-compile";
1250
+ var POLL_INTERVAL_MS = 1500;
1251
+ function httpSwiftCompileSubmit(context) {
1252
+ const { assetReadToken, endpoint } = context;
1253
+ if (!assetReadToken) return void 0;
1254
+ const url = endpoint ? `${endpoint.replace(/\/+$/, "")}${SWIFT_COMPILE_PATH}` : SWIFT_COMPILE_PATH;
1255
+ return async (work) => {
1256
+ let response;
1257
+ try {
1258
+ response = await fetch(url, {
1259
+ method: "POST",
1260
+ headers: { "content-type": "application/json" },
1261
+ body: JSON.stringify({ ...work, token: assetReadToken })
1262
+ });
1263
+ } catch (error) {
1264
+ throw new Error(
1265
+ `contrast-bundler: fetch failed for ${url} while compiling ${work.entry}: ${error instanceof Error ? error.message : String(error)}`,
1266
+ { cause: error }
1267
+ );
1268
+ }
1269
+ if (!response.ok) {
1270
+ const detail = (await response.text()).trim();
1271
+ throw new Error(
1272
+ `contrast-bundler: the swift compile for ${work.entry} answered ${response.status}${detail ? `: ${detail}` : ""}`
1273
+ );
1274
+ }
1275
+ return await response.json();
1276
+ };
1277
+ }
1278
+ function resolveImageUrls(urls, origin) {
1279
+ const resolved = {};
1280
+ for (const [name, url] of Object.entries(urls)) {
1281
+ resolved[name] = new URL(url, origin).href;
1282
+ }
1283
+ return resolved;
1284
+ }
1285
+ async function compileSwiftProject(submit, work, origin) {
1286
+ while (true) {
1287
+ const outcome = await submit(work);
1288
+ switch (outcome.status) {
1289
+ case "published":
1290
+ return {
1291
+ url: new URL(outcome.artifactUrl, origin).href,
1292
+ hash: outcome.hash,
1293
+ ...outcome.imageUrls === void 0 ? {} : { imageUrls: resolveImageUrls(outcome.imageUrls, origin) }
1294
+ };
1295
+ case "compiling":
1296
+ await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
1297
+ continue;
1298
+ case "refused":
1299
+ case "failed":
1300
+ throw new Error(
1301
+ `contrast-bundler: the swift sources for ${work.entry} cannot be compiled: ${outcome.detail}`
1302
+ );
1303
+ }
1304
+ }
1305
+ }
1306
+
1307
+ // src/rolldown-plugin-swift.ts
1308
+ var VIRTUAL_PREFIX2 = "\0rnx-swift-remote:";
1309
+ var projects2 = /* @__PURE__ */ new Map();
1310
+ function projectFor2(packagePath) {
1311
+ const existing = projects2.get(packagePath);
1312
+ if (existing) return existing;
1313
+ const created = { packagePath, address: null };
1314
+ projects2.set(packagePath, created);
1315
+ return created;
1316
+ }
1317
+ function packageFiles(root) {
1318
+ const found = [];
1319
+ const walk = (dir) => {
1320
+ for (const entry of import_node_fs4.default.readdirSync(dir, { withFileTypes: true })) {
1321
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === ".build")
1322
+ continue;
1323
+ const full = import_node_path5.default.join(dir, entry.name);
1324
+ if (entry.isDirectory()) {
1325
+ if (full !== root && import_node_fs4.default.existsSync(import_node_path5.default.join(full, "Package.swift"))) continue;
1326
+ walk(full);
1327
+ } else {
1328
+ found.push(import_node_path5.default.relative(root, full).split(import_node_path5.default.sep).join(import_node_path5.default.posix.sep));
1329
+ }
1330
+ }
1331
+ };
1332
+ walk(root);
1333
+ return found;
1334
+ }
1335
+ function sourceFiles2(packagePath) {
1336
+ const files = {};
1337
+ for (const relative2 of packageFiles(packagePath)) {
1338
+ if (!relative2.endsWith(".swift")) continue;
1339
+ files[relative2] = import_node_fs4.default.readFileSync(
1340
+ import_node_path5.default.join(packagePath, ...relative2.split("/")),
1341
+ "utf8"
1342
+ );
1343
+ }
1344
+ return files;
1345
+ }
1346
+ function bundlePack(packagePath) {
1347
+ const relative2 = packageFiles(packagePath);
1348
+ const disk = (file) => import_node_path5.default.join(packagePath, ...file.split("/"));
1349
+ const resources = {};
1350
+ const watched = [];
1351
+ for (const { base, file } of enumerateBundleJson(relative2)) {
1352
+ resources[base] = import_node_fs4.default.readFileSync(disk(file)).toString("base64");
1353
+ watched.push(file);
1354
+ }
1355
+ const images = {};
1356
+ const byRelative = new Map(relative2.map((file) => [file, disk(file)]));
1357
+ for (const { name, file } of enumerateBundleImages(relative2, (candidate) => {
1358
+ const absolute = byRelative.get(candidate);
1359
+ if (!absolute || import_node_path5.default.basename(absolute) !== "Contents.json") return null;
1360
+ try {
1361
+ return import_node_fs4.default.readFileSync(absolute, "utf8");
1362
+ } catch {
1363
+ return null;
1364
+ }
1365
+ })) {
1366
+ const absolute = byRelative.get(file);
1367
+ if (!absolute) continue;
1368
+ images[name] = {
1369
+ file: import_node_path5.default.posix.basename(file),
1370
+ data: import_node_fs4.default.readFileSync(absolute).toString("base64")
1371
+ };
1372
+ watched.push(file);
1373
+ }
1374
+ return { resources, images, watched };
1375
+ }
1376
+ function packageFor2(file, stop) {
1377
+ let dir = import_node_path5.default.dirname(file);
1378
+ for (; ; ) {
1379
+ if (import_node_fs4.default.existsSync(import_node_path5.default.join(dir, "Package.swift"))) return dir;
1380
+ if (stop !== void 0 && dir === stop || import_node_path5.default.dirname(dir) === dir) break;
1381
+ dir = import_node_path5.default.dirname(dir);
1382
+ }
1383
+ throw new Error(
1384
+ `no Package.swift above ${file}; a .swift import needs a swiftpm package like the one in packages/sootsim-swift/example`
1385
+ );
1386
+ }
1387
+ function swiftRemotePlugin(options) {
1388
+ const submit = httpSwiftCompileSubmit({
1389
+ assetReadToken: options.assetReadToken,
1390
+ endpoint: options.endpoint
1391
+ });
1392
+ if (!submit) {
1393
+ throw new Error(
1394
+ "swiftRemotePlugin needs an assetReadToken: a build without a compile credential cannot compile .swift sources"
1395
+ );
1396
+ }
1397
+ return {
1398
+ name: "rnx-swift-remote",
1399
+ enforce: "pre",
1400
+ async resolveId(source, importer) {
1401
+ if (source.startsWith(VIRTUAL_PREFIX2)) return source;
1402
+ if (!source.endsWith(".swift")) return null;
1403
+ const resolved = await this.resolve(source, importer, { skipSelf: true });
1404
+ if (!resolved) throw new Error(`cannot resolve swift source ${source}`);
1405
+ return VIRTUAL_PREFIX2 + resolved.id;
1406
+ },
1407
+ async load(id) {
1408
+ if (!id.startsWith(VIRTUAL_PREFIX2)) return null;
1409
+ const file = id.slice(VIRTUAL_PREFIX2.length);
1410
+ if (!import_node_fs4.default.existsSync(file)) throw new Error(`swift source ${file} no longer exists`);
1411
+ const packagePath = options.packagePath ?? packageFor2(file);
1412
+ const project = projectFor2(packagePath);
1413
+ const files = sourceFiles2(packagePath);
1414
+ const entry = import_node_path5.default.relative(packagePath, file).split(import_node_path5.default.sep).join(import_node_path5.default.posix.sep);
1415
+ for (const source of Object.keys(files)) {
1416
+ this.addWatchFile(import_node_path5.default.join(packagePath, ...source.split("/")));
1417
+ }
1418
+ const pack = bundlePack(packagePath);
1419
+ for (const watched of pack.watched) {
1420
+ this.addWatchFile(import_node_path5.default.join(packagePath, ...watched.split("/")));
1421
+ }
1422
+ try {
1423
+ project.address = await compileSwiftProject(
1424
+ submit,
1425
+ { files, resources: pack.resources, images: pack.images, entry },
1426
+ options.endpoint
1427
+ );
1428
+ } catch (error) {
1429
+ if (!project.address) throw error;
1430
+ this.warn(error instanceof Error ? error.message : String(error));
1431
+ }
1432
+ const address = project.address;
1433
+ return [
1434
+ `import { createSwiftEntry } from 'rnxsim/swift'`,
1435
+ `export default createSwiftEntry({ url: ${JSON.stringify(address.url)}, hash: ${JSON.stringify(address.hash)}, rootId: ${JSON.stringify(entryRootId(packagePath, file))}, resources: ${JSON.stringify(address.imageUrls ?? {})}, bundleFiles: ${JSON.stringify(pack.resources)} })`,
1436
+ ""
1437
+ ].join("\n");
1438
+ }
1439
+ };
1440
+ }
1441
+
1174
1442
  // src/vite-plugin-one.ts
1175
1443
  function rnxPlugin(options = {}) {
1176
1444
  const prefix = options.prefix || "/__soot";
@@ -1318,5 +1586,6 @@ var vite_plugin_one_default = rnxPlugin;
1318
1586
  0 && (module.exports = {
1319
1587
  rnxPlugin,
1320
1588
  sootsimPlugin,
1321
- swiftPlugin
1589
+ swiftPlugin,
1590
+ swiftRemotePlugin
1322
1591
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rnxsim",
3
- "version": "0.1.524",
3
+ "version": "0.1.526",
4
4
  "description": "Vite and Metro plugins, testing drivers, and SDK exports for rnx.",
5
5
  "author": "Tamagui LLC",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -39,6 +39,13 @@
39
39
  "source": "./src/swift.ts",
40
40
  "default": "./dist-lib/swift.cjs"
41
41
  },
42
+ "./swift-submit": {
43
+ "types": "./src/swift-submit.ts",
44
+ "source": "./src/swift-submit.ts",
45
+ "bun": "./src/swift-submit.ts",
46
+ "import": "./dist-lib/swift-submit.mjs",
47
+ "default": "./dist-lib/swift-submit.cjs"
48
+ },
42
49
  "./metro": {
43
50
  "source": "./src/metro-plugin.ts",
44
51
  "default": "./dist-lib/metro.cjs"
@@ -0,0 +1,240 @@
1
+ // `.swift` handling for One's native rolldown engine: submit the owning
2
+ // swiftpm package to `/api/swift-compile` and answer every `.swift` import
3
+ // with the module that mounts the published artifact.
4
+ //
5
+ // this is the remote twin of `vite-plugin-swift.ts`. that plugin runs the
6
+ // local toolchain and serves the artifact from dev-server middleware; the
7
+ // native engine is its own rolldown instance with no middleware seam, so
8
+ // this plugin compiles nothing in-process and serves nothing. the artifact
9
+ // url the service answers is absolute (compile-cdn), so there is nothing to
10
+ // serve. same compiler, same store, same hash, same credential as the
11
+ // browser worker — one path, no second toolchain to install for `one dev`.
12
+ //
13
+ // an app registers it through One's native plugin seam, not `config.plugins`
14
+ // (which never reach the native engine):
15
+ //
16
+ // // vite.config.ts
17
+ // export default {
18
+ // plugins: [one({
19
+ // native: {
20
+ // bundler: 'vite',
21
+ // bundlerOptions: {
22
+ // plugins: [swiftRemotePlugin({
23
+ // endpoint: 'https://contrast.dev',
24
+ // assetReadToken: process.env.CONTRAST_CCDN_KEY,
25
+ // })],
26
+ // },
27
+ // },
28
+ // })],
29
+ // }
30
+ //
31
+ // no babel anywhere in this path: resolveId and load only, emitting an ESM
32
+ // module. offline `one dev` fails loud at the submit, the same as a browser
33
+ // build without a credential; the standalone `rnx` CLI keeps the local
34
+ // toolchain plugin for that case.
35
+
36
+ import fs from 'node:fs'
37
+ import path from 'node:path'
38
+ import {
39
+ enumerateBundleImages,
40
+ enumerateBundleJson,
41
+ } from '../../sootsim-swift/src/bundleAssets'
42
+ import { compileSwiftProject, httpSwiftCompileSubmit } from './swift-submit'
43
+ import { entryRootId } from './vite-plugin-swift'
44
+ import type { SwiftModuleAddress } from './swift-submit'
45
+ import type { Plugin } from 'vite'
46
+
47
+ const VIRTUAL_PREFIX = '\0rnx-swift-remote:'
48
+
49
+ export interface SwiftRemotePluginOptions {
50
+ /** origin serving `/api/swift-compile` (the Contrast app). */
51
+ endpoint: string
52
+ /** a `ccdn_` API key or dev session token the compile verifier accepts. */
53
+ assetReadToken: string
54
+ /**
55
+ * directory uploaded `.swift` imports resolve under. defaults to the
56
+ * nearest `Package.swift` above each imported file — the swiftpm package
57
+ * that owns it — which is the compile unit, exactly like the local host.
58
+ * an app that keeps sources somewhere else names that package explicitly.
59
+ */
60
+ packagePath?: string
61
+ }
62
+
63
+ interface RemoteProject {
64
+ packagePath: string
65
+ address: SwiftModuleAddress | null
66
+ }
67
+
68
+ // one address per package per process: a failed submit keeps serving the
69
+ // last published artifact (and warns) rather than failing the module, so
70
+ // the edit that fixes the error still has a graph to rebuild.
71
+ const projects = new Map<string, RemoteProject>()
72
+
73
+ function projectFor(packagePath: string): RemoteProject {
74
+ const existing = projects.get(packagePath)
75
+ if (existing) return existing
76
+ const created: RemoteProject = { packagePath, address: null }
77
+ projects.set(packagePath, created)
78
+ return created
79
+ }
80
+
81
+ // every file under a directory, posix-relative, skipping what can never be
82
+ // a source, a manifest, or bundle content. a nested Package.swift is a
83
+ // separate package with its own compile: its files belong to that unit.
84
+ // `.build` holds tens of thousands of intermediates and no bundle content;
85
+ // walking it per import would tax every reload.
86
+ function packageFiles(root: string): string[] {
87
+ const found: string[] = []
88
+ const walk = (dir: string): void => {
89
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
90
+ if (
91
+ entry.name.startsWith('.') ||
92
+ entry.name === 'node_modules' ||
93
+ entry.name === '.build'
94
+ )
95
+ continue
96
+ const full = path.join(dir, entry.name)
97
+ if (entry.isDirectory()) {
98
+ if (full !== root && fs.existsSync(path.join(full, 'Package.swift'))) continue
99
+ walk(full)
100
+ } else {
101
+ found.push(path.relative(root, full).split(path.sep).join(path.posix.sep))
102
+ }
103
+ }
104
+ }
105
+ walk(root)
106
+ return found
107
+ }
108
+
109
+ // the file map the service compiles: the package's `.swift` sources plus
110
+ // its root manifest, keyed package-relative. the manifest is partitioning
111
+ // input only — the service emits its own manifest that links every target
112
+ // against its SwiftUI module — so a standard local package travels as is.
113
+ function sourceFiles(packagePath: string): Record<string, string> {
114
+ const files: Record<string, string> = {}
115
+ for (const relative of packageFiles(packagePath)) {
116
+ if (!relative.endsWith('.swift')) continue
117
+ files[relative] = fs.readFileSync(
118
+ path.join(packagePath, ...relative.split('/')),
119
+ 'utf8',
120
+ )
121
+ }
122
+ return files
123
+ }
124
+
125
+ // the bundle pack and the photo uploads, through the shared enumeration
126
+ // both hosts pack with, so the collision winner here is the winner on the
127
+ // local host too. binary bytes straight from disk: this host is node.
128
+ function bundlePack(packagePath: string): {
129
+ resources: Record<string, string>
130
+ images: Record<string, { file: string; data: string }>
131
+ watched: string[]
132
+ } {
133
+ const relative = packageFiles(packagePath)
134
+ const disk = (file: string): string => path.join(packagePath, ...file.split('/'))
135
+ const resources: Record<string, string> = {}
136
+ const watched: string[] = []
137
+ for (const { base, file } of enumerateBundleJson(relative)) {
138
+ resources[base] = fs.readFileSync(disk(file)).toString('base64')
139
+ watched.push(file)
140
+ }
141
+ const images: Record<string, { file: string; data: string }> = {}
142
+ const byRelative = new Map(relative.map((file) => [file, disk(file)]))
143
+ for (const { name, file } of enumerateBundleImages(relative, (candidate) => {
144
+ const absolute = byRelative.get(candidate)
145
+ if (!absolute || path.basename(absolute) !== 'Contents.json') return null
146
+ try {
147
+ return fs.readFileSync(absolute, 'utf8')
148
+ } catch {
149
+ return null
150
+ }
151
+ })) {
152
+ const absolute = byRelative.get(file)
153
+ if (!absolute) continue
154
+ images[name] = {
155
+ file: path.posix.basename(file),
156
+ data: fs.readFileSync(absolute).toString('base64'),
157
+ }
158
+ watched.push(file)
159
+ }
160
+ return { resources, images, watched }
161
+ }
162
+
163
+ // the swiftpm package that owns an imported file: the nearest directory at
164
+ // or above the file that holds a Package.swift. a `.swift` file with no
165
+ // package above it (a pod, an xcode target) is refused loud rather than
166
+ // compiled as the wrong unit.
167
+ function packageFor(file: string, stop?: string): string {
168
+ let dir = path.dirname(file)
169
+ for (;;) {
170
+ if (fs.existsSync(path.join(dir, 'Package.swift'))) return dir
171
+ if ((stop !== undefined && dir === stop) || path.dirname(dir) === dir) break
172
+ dir = path.dirname(dir)
173
+ }
174
+ throw new Error(
175
+ `no Package.swift above ${file}; a .swift import needs a swiftpm package like the one in packages/sootsim-swift/example`,
176
+ )
177
+ }
178
+
179
+ export function swiftRemotePlugin(options: SwiftRemotePluginOptions): Plugin {
180
+ const submit = httpSwiftCompileSubmit({
181
+ assetReadToken: options.assetReadToken,
182
+ endpoint: options.endpoint,
183
+ })
184
+ if (!submit) {
185
+ throw new Error(
186
+ 'swiftRemotePlugin needs an assetReadToken: a build without a compile credential cannot compile .swift sources',
187
+ )
188
+ }
189
+ return {
190
+ name: 'rnx-swift-remote',
191
+ enforce: 'pre',
192
+
193
+ async resolveId(source, importer) {
194
+ if (source.startsWith(VIRTUAL_PREFIX)) return source
195
+ if (!source.endsWith('.swift')) return null
196
+ const resolved = await this.resolve(source, importer, { skipSelf: true })
197
+ if (!resolved) throw new Error(`cannot resolve swift source ${source}`)
198
+ return VIRTUAL_PREFIX + resolved.id
199
+ },
200
+
201
+ async load(id) {
202
+ if (!id.startsWith(VIRTUAL_PREFIX)) return null
203
+ const file = id.slice(VIRTUAL_PREFIX.length)
204
+ if (!fs.existsSync(file)) throw new Error(`swift source ${file} no longer exists`)
205
+ const packagePath = options.packagePath ?? packageFor(file)
206
+ const project = projectFor(packagePath)
207
+ const files = sourceFiles(packagePath)
208
+ const entry = path.relative(packagePath, file).split(path.sep).join(path.posix.sep)
209
+ for (const source of Object.keys(files)) {
210
+ this.addWatchFile(path.join(packagePath, ...source.split('/')))
211
+ }
212
+ const pack = bundlePack(packagePath)
213
+ for (const watched of pack.watched) {
214
+ this.addWatchFile(path.join(packagePath, ...watched.split('/')))
215
+ }
216
+ try {
217
+ project.address = await compileSwiftProject(
218
+ submit,
219
+ { files, resources: pack.resources, images: pack.images, entry },
220
+ options.endpoint,
221
+ )
222
+ } catch (error) {
223
+ // a compile error must not fail this module: the graph would stop
224
+ // watching it and the fix would never rebuild. the last published
225
+ // artifact keeps rendering while the diagnostic is reported here.
226
+ if (!project.address) throw error
227
+ this.warn(error instanceof Error ? error.message : String(error))
228
+ }
229
+ const address = project.address
230
+ // every import mounts through the one entry: the artifact answers
231
+ // whether it is a view or an app root at runtime, so the transform
232
+ // never re-derives the conformance from source.
233
+ return [
234
+ `import { createSwiftEntry } from 'rnxsim/swift'`,
235
+ `export default createSwiftEntry({ url: ${JSON.stringify(address.url)}, hash: ${JSON.stringify(address.hash)}, rootId: ${JSON.stringify(entryRootId(packagePath, file))}, resources: ${JSON.stringify(address.imageUrls ?? {})}, bundleFiles: ${JSON.stringify(pack.resources)} })`,
236
+ '',
237
+ ].join('\n')
238
+ },
239
+ }
240
+ }