rnxsim 0.1.523 → 0.1.525
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/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +1 -1
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +3 -1
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +1 -1
- package/dist-lib/swift.cjs +342 -28
- package/dist-lib/vite.cjs +255 -6
- package/package.json +1 -1
- package/src/vite-plugin-swift.ts +249 -7
package/dist-lib/vite.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.525 | (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;
|
|
@@ -763,6 +763,93 @@ var import_node_os2 = __toESM(require("node:os"), 1);
|
|
|
763
763
|
var import_node_path4 = __toESM(require("node:path"), 1);
|
|
764
764
|
var import_node_util = require("node:util");
|
|
765
765
|
|
|
766
|
+
// ../sootsim-swift/src/bundleAssets.ts
|
|
767
|
+
var RASTER_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
768
|
+
".avif",
|
|
769
|
+
".bmp",
|
|
770
|
+
".gif",
|
|
771
|
+
".ico",
|
|
772
|
+
".jpeg",
|
|
773
|
+
".jpg",
|
|
774
|
+
".png",
|
|
775
|
+
".webp"
|
|
776
|
+
]);
|
|
777
|
+
function extOf(file) {
|
|
778
|
+
const dot = file.lastIndexOf(".");
|
|
779
|
+
return dot < 0 ? "" : file.slice(dot).toLowerCase();
|
|
780
|
+
}
|
|
781
|
+
function baseOf(file) {
|
|
782
|
+
const slash = file.lastIndexOf("/");
|
|
783
|
+
return slash < 0 ? file : file.slice(slash + 1);
|
|
784
|
+
}
|
|
785
|
+
function pickImagesetFile(images) {
|
|
786
|
+
const rasters = images.filter(
|
|
787
|
+
(image) => typeof image.filename === "string" && RASTER_EXTENSIONS.has(extOf(image.filename))
|
|
788
|
+
);
|
|
789
|
+
if (rasters.length === 0) return null;
|
|
790
|
+
const rank = (scale) => scale === "2x" ? 0 : scale === "3x" ? 1 : scale === "1x" ? 2 : 3;
|
|
791
|
+
return [...rasters].sort((a, b) => rank(a.scale) - rank(b.scale))[0].filename;
|
|
792
|
+
}
|
|
793
|
+
function imagesetName(contentsPath) {
|
|
794
|
+
const dir = contentsPath.split("/").slice(0, -1).join("/");
|
|
795
|
+
const leaf = baseOf(dir);
|
|
796
|
+
if (!leaf.endsWith(".imageset")) return null;
|
|
797
|
+
return leaf.slice(0, -".imageset".length) || null;
|
|
798
|
+
}
|
|
799
|
+
function isFlatResourceImage(path7) {
|
|
800
|
+
return path7.split("/").includes("Resources") && RASTER_EXTENSIONS.has(extOf(path7));
|
|
801
|
+
}
|
|
802
|
+
function enumerateBundleImages(paths, readText) {
|
|
803
|
+
const table = [];
|
|
804
|
+
const seen = /* @__PURE__ */ new Set();
|
|
805
|
+
const catalogs = paths.filter((path7) => baseOf(path7) === "Contents.json");
|
|
806
|
+
for (const contentsPath of catalogs) {
|
|
807
|
+
const name = imagesetName(contentsPath);
|
|
808
|
+
if (name === null || seen.has(name)) continue;
|
|
809
|
+
let parsed;
|
|
810
|
+
try {
|
|
811
|
+
const text = readText(contentsPath);
|
|
812
|
+
if (text === null) continue;
|
|
813
|
+
parsed = JSON.parse(text);
|
|
814
|
+
} catch {
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
const images = parsed?.images;
|
|
818
|
+
if (!Array.isArray(images)) continue;
|
|
819
|
+
const dir = contentsPath.split("/").slice(0, -1).join("/");
|
|
820
|
+
const file = pickImagesetFile(images);
|
|
821
|
+
if (file === null || file.includes("/") || file.includes("\\")) continue;
|
|
822
|
+
seen.add(name);
|
|
823
|
+
table.push({ name, file: `${dir}/${file}` });
|
|
824
|
+
}
|
|
825
|
+
for (const path7 of paths) {
|
|
826
|
+
if (!isFlatResourceImage(path7)) continue;
|
|
827
|
+
const name = baseOf(path7).slice(0, -extOf(path7).length);
|
|
828
|
+
if (name === "" || seen.has(name)) continue;
|
|
829
|
+
seen.add(name);
|
|
830
|
+
table.push({ name, file: path7 });
|
|
831
|
+
}
|
|
832
|
+
return table;
|
|
833
|
+
}
|
|
834
|
+
function enumerateBundleJson(paths) {
|
|
835
|
+
const found = [];
|
|
836
|
+
const seen = /* @__PURE__ */ new Set();
|
|
837
|
+
const ordered = [...paths].filter(
|
|
838
|
+
(path7) => path7.split("/").includes("Resources") && path7.split("/").every((segment) => segment !== "" && segment !== "..") && extOf(path7) === ".json"
|
|
839
|
+
).sort((a, b) => {
|
|
840
|
+
const aDepth = a.split("/").length;
|
|
841
|
+
const bDepth = b.split("/").length;
|
|
842
|
+
return aDepth - bDepth || (a < b ? -1 : a > b ? 1 : 0);
|
|
843
|
+
});
|
|
844
|
+
for (const file of ordered) {
|
|
845
|
+
const base = baseOf(file);
|
|
846
|
+
if (seen.has(base)) continue;
|
|
847
|
+
seen.add(base);
|
|
848
|
+
found.push({ base, file });
|
|
849
|
+
}
|
|
850
|
+
return found;
|
|
851
|
+
}
|
|
852
|
+
|
|
766
853
|
// ../sootsim-swift/toolchain.json
|
|
767
854
|
var toolchain_default = {
|
|
768
855
|
release: "swift-6.3-RELEASE",
|
|
@@ -778,13 +865,19 @@ var TOOLCHAIN_BIN = import_node_path4.default.join(
|
|
|
778
865
|
var WASM_SDK = `${toolchain_default.release}_wasm`;
|
|
779
866
|
var WASM_TRIPLE = "wasm32-unknown-wasip1";
|
|
780
867
|
var ARTIFACT_PATH = "/__rnx/swift/";
|
|
868
|
+
var RESOURCE_PATH = "/__rnx/swift-res/";
|
|
781
869
|
var VIRTUAL_PREFIX = "\0rnx-swift:";
|
|
782
870
|
var projects = /* @__PURE__ */ new Map();
|
|
783
871
|
var devServer;
|
|
784
872
|
function projectFor(packagePath) {
|
|
785
873
|
const existing = projects.get(packagePath);
|
|
786
874
|
if (existing) return existing;
|
|
787
|
-
const created = {
|
|
875
|
+
const created = {
|
|
876
|
+
packagePath,
|
|
877
|
+
artifact: null,
|
|
878
|
+
building: null,
|
|
879
|
+
resourceFiles: /* @__PURE__ */ new Map()
|
|
880
|
+
};
|
|
788
881
|
projects.set(packagePath, created);
|
|
789
882
|
return created;
|
|
790
883
|
}
|
|
@@ -803,6 +896,118 @@ function sourceFiles(root) {
|
|
|
803
896
|
walk(root);
|
|
804
897
|
return found;
|
|
805
898
|
}
|
|
899
|
+
function resourceCandidateFiles(root) {
|
|
900
|
+
const found = [];
|
|
901
|
+
const walk = (dir) => {
|
|
902
|
+
for (const entry of import_node_fs3.default.readdirSync(dir, { withFileTypes: true })) {
|
|
903
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === ".build")
|
|
904
|
+
continue;
|
|
905
|
+
const full = import_node_path4.default.join(dir, entry.name);
|
|
906
|
+
if (entry.isDirectory()) {
|
|
907
|
+
if (full !== root && import_node_fs3.default.existsSync(import_node_path4.default.join(full, "Package.swift"))) continue;
|
|
908
|
+
walk(full);
|
|
909
|
+
} else {
|
|
910
|
+
found.push(full);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
walk(root);
|
|
915
|
+
return found;
|
|
916
|
+
}
|
|
917
|
+
var RESOURCE_CONTENT_TYPES = {
|
|
918
|
+
".avif": "image/avif",
|
|
919
|
+
".bmp": "image/bmp",
|
|
920
|
+
".gif": "image/gif",
|
|
921
|
+
".ico": "image/x-icon",
|
|
922
|
+
".jpeg": "image/jpeg",
|
|
923
|
+
".jpg": "image/jpeg",
|
|
924
|
+
".png": "image/png",
|
|
925
|
+
".webp": "image/webp"
|
|
926
|
+
};
|
|
927
|
+
function buildSwiftResourceTable(project, baseUrl) {
|
|
928
|
+
const absolute = resourceCandidateFiles(project.packagePath);
|
|
929
|
+
const relative2 = absolute.map(
|
|
930
|
+
(file) => import_node_path4.default.relative(project.packagePath, file).split(import_node_path4.default.sep).join(import_node_path4.default.posix.sep)
|
|
931
|
+
);
|
|
932
|
+
const byRelative = new Map(absolute.map((file, index) => [relative2[index], file]));
|
|
933
|
+
const found = enumerateBundleImages(relative2, (candidate) => {
|
|
934
|
+
const file = byRelative.get(candidate);
|
|
935
|
+
if (!file || import_node_path4.default.basename(file) !== "Contents.json") return null;
|
|
936
|
+
try {
|
|
937
|
+
return import_node_fs3.default.readFileSync(file, "utf8");
|
|
938
|
+
} catch {
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
const table = {};
|
|
943
|
+
const watched = [];
|
|
944
|
+
const registered = /* @__PURE__ */ new Map();
|
|
945
|
+
for (const { name, file } of found) {
|
|
946
|
+
const disk = byRelative.get(file);
|
|
947
|
+
if (!disk) continue;
|
|
948
|
+
let bytes;
|
|
949
|
+
try {
|
|
950
|
+
bytes = import_node_fs3.default.readFileSync(disk);
|
|
951
|
+
} catch {
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
const hash = (0, import_node_crypto2.createHash)("sha256").update(bytes).digest("hex").slice(0, 16);
|
|
955
|
+
const ext = import_node_path4.default.posix.extname(file).toLowerCase();
|
|
956
|
+
table[name] = `${baseUrl}${RESOURCE_PATH}${hash}${ext}`;
|
|
957
|
+
watched.push(file);
|
|
958
|
+
const contents = file.replace(/\/[^/]+$/, "/Contents.json");
|
|
959
|
+
if (byRelative.has(contents)) watched.push(contents);
|
|
960
|
+
registered.set(`${hash}${ext}`, disk);
|
|
961
|
+
}
|
|
962
|
+
project.resourceFiles = registered;
|
|
963
|
+
return { table, watched };
|
|
964
|
+
}
|
|
965
|
+
var BUNDLE_MAX_FILES = 128;
|
|
966
|
+
var BUNDLE_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
967
|
+
var BUNDLE_MAX_TOTAL_BYTES = 8 * 1024 * 1024;
|
|
968
|
+
function buildSwiftBundleFiles(packagePath) {
|
|
969
|
+
const relative2 = resourceCandidateFiles(packagePath).map(
|
|
970
|
+
(file) => import_node_path4.default.relative(packagePath, file).split(import_node_path4.default.sep).join(import_node_path4.default.posix.sep)
|
|
971
|
+
);
|
|
972
|
+
const files = {};
|
|
973
|
+
const watched = [];
|
|
974
|
+
const skipped = [];
|
|
975
|
+
let total = 0;
|
|
976
|
+
for (const { base, file } of enumerateBundleJson(relative2)) {
|
|
977
|
+
watched.push(file);
|
|
978
|
+
let bytes;
|
|
979
|
+
try {
|
|
980
|
+
bytes = import_node_fs3.default.readFileSync(import_node_path4.default.join(packagePath, ...file.split("/")));
|
|
981
|
+
} catch {
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
if (bytes.length > BUNDLE_MAX_FILE_BYTES) {
|
|
985
|
+
skipped.push(`${file} (${bytes.length} bytes past the per-file cap)`);
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
if (Object.keys(files).length >= BUNDLE_MAX_FILES || total + bytes.length > BUNDLE_MAX_TOTAL_BYTES) {
|
|
989
|
+
skipped.push(
|
|
990
|
+
`${file} (pack caps: ${BUNDLE_MAX_FILES} files / ${BUNDLE_MAX_TOTAL_BYTES} bytes)`
|
|
991
|
+
);
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
files[base] = bytes.toString("base64");
|
|
995
|
+
total += bytes.length;
|
|
996
|
+
}
|
|
997
|
+
return { files, watched, skipped };
|
|
998
|
+
}
|
|
999
|
+
function serveSwiftResource(project, key) {
|
|
1000
|
+
const disk = project.resourceFiles.get(key);
|
|
1001
|
+
if (!disk) return null;
|
|
1002
|
+
try {
|
|
1003
|
+
return {
|
|
1004
|
+
bytes: import_node_fs3.default.readFileSync(disk),
|
|
1005
|
+
contentType: RESOURCE_CONTENT_TYPES[import_node_path4.default.extname(disk).toLowerCase()] ?? "application/octet-stream"
|
|
1006
|
+
};
|
|
1007
|
+
} catch {
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
806
1011
|
function linkedModuleRoots(packagePath) {
|
|
807
1012
|
let manifest;
|
|
808
1013
|
try {
|
|
@@ -833,11 +1038,14 @@ function entryRootId(packagePath, file) {
|
|
|
833
1038
|
const relative2 = import_node_path4.default.relative(packagePath, file).split(import_node_path4.default.sep).join(import_node_path4.default.posix.sep);
|
|
834
1039
|
return `${import_node_path4.default.basename(packagePath)}${import_node_path4.default.posix.sep}${relative2}`;
|
|
835
1040
|
}
|
|
836
|
-
function
|
|
1041
|
+
function serverOrigin() {
|
|
837
1042
|
const address = devServer?.httpServer?.address();
|
|
838
1043
|
const port = address && typeof address === "object" ? address.port : devServer?.config.server.port;
|
|
839
|
-
if (!port) throw new Error("the swift
|
|
840
|
-
return `http://localhost:${port}
|
|
1044
|
+
if (!port) throw new Error("the swift urls need the dev server port");
|
|
1045
|
+
return `http://localhost:${port}`;
|
|
1046
|
+
}
|
|
1047
|
+
function artifactUrl(project, artifact) {
|
|
1048
|
+
return `${serverOrigin()}${ARTIFACT_PATH}${artifact.hash}.wasm`;
|
|
841
1049
|
}
|
|
842
1050
|
function builtArtifact(packagePath) {
|
|
843
1051
|
const binDir = import_node_path4.default.join(packagePath, ".build", WASM_TRIPLE, "release");
|
|
@@ -961,9 +1169,36 @@ function swiftPlugin(options = {}) {
|
|
|
961
1169
|
this.warn(error instanceof Error ? error.message : String(error));
|
|
962
1170
|
artifact = project.artifact;
|
|
963
1171
|
}
|
|
1172
|
+
let resources = {};
|
|
1173
|
+
try {
|
|
1174
|
+
const built = buildSwiftResourceTable(project, serverOrigin());
|
|
1175
|
+
resources = built.table;
|
|
1176
|
+
for (const watched of built.watched) this.addWatchFile(watched);
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
this.warn(
|
|
1179
|
+
`swift resources for ${packagePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
let bundleFiles = {};
|
|
1183
|
+
try {
|
|
1184
|
+
const packed = buildSwiftBundleFiles(packagePath);
|
|
1185
|
+
bundleFiles = packed.files;
|
|
1186
|
+
for (const watched of packed.watched) {
|
|
1187
|
+
this.addWatchFile(import_node_path4.default.join(packagePath, ...watched.split(import_node_path4.default.posix.sep)));
|
|
1188
|
+
}
|
|
1189
|
+
if (packed.skipped.length > 0) {
|
|
1190
|
+
this.warn(
|
|
1191
|
+
`swift bundle files skipped for ${packagePath}: ${packed.skipped.join("; ")}`
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
this.warn(
|
|
1196
|
+
`swift bundle files for ${packagePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
964
1199
|
return [
|
|
965
1200
|
`import { createSwiftEntry } from 'rnxsim/swift'`,
|
|
966
|
-
`export default createSwiftEntry({ url: ${JSON.stringify(artifactUrl(project, artifact))}, hash: ${JSON.stringify(artifact.hash)}, rootId: ${JSON.stringify(entryRootId(packagePath, file))} })`,
|
|
1201
|
+
`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)} })`,
|
|
967
1202
|
""
|
|
968
1203
|
].join("\n");
|
|
969
1204
|
},
|
|
@@ -971,6 +1206,20 @@ function swiftPlugin(options = {}) {
|
|
|
971
1206
|
devServer = server;
|
|
972
1207
|
server.middlewares.use((req, res, next) => {
|
|
973
1208
|
const url = (req.url ?? "").split("?")[0];
|
|
1209
|
+
if (url.startsWith(RESOURCE_PATH)) {
|
|
1210
|
+
for (const project of projects.values()) {
|
|
1211
|
+
const served = serveSwiftResource(project, url.slice(RESOURCE_PATH.length));
|
|
1212
|
+
if (served) {
|
|
1213
|
+
res.setHeader("access-control-allow-origin", "*");
|
|
1214
|
+
res.setHeader("content-type", served.contentType);
|
|
1215
|
+
res.end(served.bytes);
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
res.statusCode = 404;
|
|
1220
|
+
res.end("swift resource is stale");
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
974
1223
|
if (!url.startsWith(ARTIFACT_PATH)) {
|
|
975
1224
|
next();
|
|
976
1225
|
return;
|
package/package.json
CHANGED
package/src/vite-plugin-swift.ts
CHANGED
|
@@ -16,6 +16,10 @@ import fs from 'node:fs'
|
|
|
16
16
|
import os from 'node:os'
|
|
17
17
|
import path from 'node:path'
|
|
18
18
|
import { promisify } from 'node:util'
|
|
19
|
+
import {
|
|
20
|
+
enumerateBundleImages,
|
|
21
|
+
enumerateBundleJson,
|
|
22
|
+
} from '../../sootsim-swift/src/bundleAssets'
|
|
19
23
|
import toolchain from '../../sootsim-swift/toolchain.json'
|
|
20
24
|
import type { Plugin, ViteDevServer } from 'vite'
|
|
21
25
|
|
|
@@ -33,6 +37,7 @@ const WASM_SDK = `${toolchain.release}_wasm`
|
|
|
33
37
|
const WASM_TRIPLE = 'wasm32-unknown-wasip1'
|
|
34
38
|
|
|
35
39
|
const ARTIFACT_PATH = '/__rnx/swift/'
|
|
40
|
+
const RESOURCE_PATH = '/__rnx/swift-res/'
|
|
36
41
|
const VIRTUAL_PREFIX = '\0rnx-swift:'
|
|
37
42
|
|
|
38
43
|
interface SwiftArtifact {
|
|
@@ -49,6 +54,9 @@ interface SwiftProject {
|
|
|
49
54
|
artifact: SwiftArtifact | null
|
|
50
55
|
// the build in flight and the sources it was started from
|
|
51
56
|
building: { stamp: string; promise: Promise<SwiftArtifact> } | null
|
|
57
|
+
// content hash to absolute path for this load's bundle images: the served
|
|
58
|
+
// urls name a hash, never a path, so there is nothing to traverse.
|
|
59
|
+
resourceFiles: Map<string, string>
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
// a project is its swiftpm package: the build, the artifact, and the dev
|
|
@@ -59,7 +67,12 @@ let devServer: ViteDevServer | undefined
|
|
|
59
67
|
function projectFor(packagePath: string): SwiftProject {
|
|
60
68
|
const existing = projects.get(packagePath)
|
|
61
69
|
if (existing) return existing
|
|
62
|
-
const created: SwiftProject = {
|
|
70
|
+
const created: SwiftProject = {
|
|
71
|
+
packagePath,
|
|
72
|
+
artifact: null,
|
|
73
|
+
building: null,
|
|
74
|
+
resourceFiles: new Map(),
|
|
75
|
+
}
|
|
63
76
|
projects.set(packagePath, created)
|
|
64
77
|
return created
|
|
65
78
|
}
|
|
@@ -86,6 +99,180 @@ function sourceFiles(root: string): string[] {
|
|
|
86
99
|
return found
|
|
87
100
|
}
|
|
88
101
|
|
|
102
|
+
// every file that can carry bundle content: catalogs and flat Resources
|
|
103
|
+
// rasters anywhere under the package. `.build` is skipped where sourceFiles
|
|
104
|
+
// does not need to: a release tree holds tens of thousands of files and no
|
|
105
|
+
// bundle content, and walking it per import would tax every hot reload.
|
|
106
|
+
function resourceCandidateFiles(root: string): string[] {
|
|
107
|
+
const found: string[] = []
|
|
108
|
+
const walk = (dir: string): void => {
|
|
109
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
110
|
+
if (
|
|
111
|
+
entry.name.startsWith('.') ||
|
|
112
|
+
entry.name === 'node_modules' ||
|
|
113
|
+
entry.name === '.build'
|
|
114
|
+
)
|
|
115
|
+
continue
|
|
116
|
+
const full = path.join(dir, entry.name)
|
|
117
|
+
if (entry.isDirectory()) {
|
|
118
|
+
if (full !== root && fs.existsSync(path.join(full, 'Package.swift'))) continue
|
|
119
|
+
walk(full)
|
|
120
|
+
} else {
|
|
121
|
+
found.push(full)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
walk(root)
|
|
126
|
+
return found
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const RESOURCE_CONTENT_TYPES: Record<string, string> = {
|
|
130
|
+
'.avif': 'image/avif',
|
|
131
|
+
'.bmp': 'image/bmp',
|
|
132
|
+
'.gif': 'image/gif',
|
|
133
|
+
'.ico': 'image/x-icon',
|
|
134
|
+
'.jpeg': 'image/jpeg',
|
|
135
|
+
'.jpg': 'image/jpeg',
|
|
136
|
+
'.png': 'image/png',
|
|
137
|
+
'.webp': 'image/webp',
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface SwiftResourceTable {
|
|
141
|
+
/** bundle asset name to absolute serving url. */
|
|
142
|
+
table: Record<string, string>
|
|
143
|
+
/** project-relative image files this load's table covers (for watching). */
|
|
144
|
+
watched: string[]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// the producer for the local vehicle: enumerate the package's bundle images
|
|
148
|
+
// through the shared rule both hosts use, hash each raster's bytes into its
|
|
149
|
+
// url (an edited photo moves its url, so no stale paint survives a reload),
|
|
150
|
+
// and register hash to path for the serving middleware below.
|
|
151
|
+
export function buildSwiftResourceTable(
|
|
152
|
+
project: SwiftProject,
|
|
153
|
+
baseUrl: string,
|
|
154
|
+
): SwiftResourceTable {
|
|
155
|
+
const absolute = resourceCandidateFiles(project.packagePath)
|
|
156
|
+
const relative = absolute.map((file) =>
|
|
157
|
+
path.relative(project.packagePath, file).split(path.sep).join(path.posix.sep),
|
|
158
|
+
)
|
|
159
|
+
const byRelative = new Map(absolute.map((file, index) => [relative[index], file]))
|
|
160
|
+
const found = enumerateBundleImages(relative, (candidate) => {
|
|
161
|
+
const file = byRelative.get(candidate)
|
|
162
|
+
if (!file || path.basename(file) !== 'Contents.json') return null
|
|
163
|
+
try {
|
|
164
|
+
return fs.readFileSync(file, 'utf8')
|
|
165
|
+
} catch {
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
const table: Record<string, string> = {}
|
|
170
|
+
const watched: string[] = []
|
|
171
|
+
const registered = new Map<string, string>()
|
|
172
|
+
for (const { name, file } of found) {
|
|
173
|
+
const disk = byRelative.get(file)
|
|
174
|
+
if (!disk) continue
|
|
175
|
+
let bytes: Buffer
|
|
176
|
+
try {
|
|
177
|
+
bytes = fs.readFileSync(disk)
|
|
178
|
+
} catch {
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 16)
|
|
182
|
+
const ext = path.posix.extname(file).toLowerCase()
|
|
183
|
+
table[name] = `${baseUrl}${RESOURCE_PATH}${hash}${ext}`
|
|
184
|
+
watched.push(file)
|
|
185
|
+
// a Contents.json edit (scale swap, file rename) re-resolves the same
|
|
186
|
+
// way a raster edit does, so the catalog rides the watcher too.
|
|
187
|
+
const contents = file.replace(/\/[^/]+$/, '/Contents.json')
|
|
188
|
+
if (byRelative.has(contents)) watched.push(contents)
|
|
189
|
+
registered.set(`${hash}${ext}`, disk)
|
|
190
|
+
}
|
|
191
|
+
project.resourceFiles = registered
|
|
192
|
+
return { table, watched }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// the /res pack: every Resources/ json in the package, basename-keyed and
|
|
196
|
+
// base64, riding the entry module so the mount serves it without a fetch.
|
|
197
|
+
// the namespace is flat like the bundle the wasm side reads: shallower
|
|
198
|
+
// paths pack first (the Resources root is the bundle root) with a lexical
|
|
199
|
+
// tiebreak, and the first spelling of a basename wins a collision. caps are
|
|
200
|
+
// INFERRED (no app in either rung is near them; landmarkData-scale json is
|
|
201
|
+
// tens of KB): a file past them is skipped and named in `skipped` so the
|
|
202
|
+
// load warns instead of silently serving a partial bundle. binary bytes
|
|
203
|
+
// travel the same map when a port reaches past json; the worker half packs
|
|
204
|
+
// text only until then (see the gaps ledger).
|
|
205
|
+
const BUNDLE_MAX_FILES = 128
|
|
206
|
+
const BUNDLE_MAX_FILE_BYTES = 2 * 1024 * 1024
|
|
207
|
+
const BUNDLE_MAX_TOTAL_BYTES = 8 * 1024 * 1024
|
|
208
|
+
|
|
209
|
+
export interface SwiftBundlePack {
|
|
210
|
+
/** basename to base64 bytes for the mount's /res. */
|
|
211
|
+
files: Record<string, string>
|
|
212
|
+
/** project-relative json files this load's pack covers (for watching). */
|
|
213
|
+
watched: string[]
|
|
214
|
+
/** project-relative files skipped by a cap, for the load warning. */
|
|
215
|
+
skipped: string[]
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function buildSwiftBundleFiles(packagePath: string): SwiftBundlePack {
|
|
219
|
+
// resourceCandidateFiles walks every non-ignored file with the nested
|
|
220
|
+
// package exclusion; the shared enumeration decides namespace, order and
|
|
221
|
+
// collisions, identically for every host that packs.
|
|
222
|
+
const relative = resourceCandidateFiles(packagePath).map((file) =>
|
|
223
|
+
path.relative(packagePath, file).split(path.sep).join(path.posix.sep),
|
|
224
|
+
)
|
|
225
|
+
const files: Record<string, string> = {}
|
|
226
|
+
const watched: string[] = []
|
|
227
|
+
const skipped: string[] = []
|
|
228
|
+
let total = 0
|
|
229
|
+
for (const { base, file } of enumerateBundleJson(relative)) {
|
|
230
|
+
watched.push(file)
|
|
231
|
+
let bytes: Buffer
|
|
232
|
+
try {
|
|
233
|
+
bytes = fs.readFileSync(path.join(packagePath, ...file.split('/')))
|
|
234
|
+
} catch {
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
if (bytes.length > BUNDLE_MAX_FILE_BYTES) {
|
|
238
|
+
skipped.push(`${file} (${bytes.length} bytes past the per-file cap)`)
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
if (
|
|
242
|
+
Object.keys(files).length >= BUNDLE_MAX_FILES ||
|
|
243
|
+
total + bytes.length > BUNDLE_MAX_TOTAL_BYTES
|
|
244
|
+
) {
|
|
245
|
+
skipped.push(
|
|
246
|
+
`${file} (pack caps: ${BUNDLE_MAX_FILES} files / ${BUNDLE_MAX_TOTAL_BYTES} bytes)`,
|
|
247
|
+
)
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
files[base] = bytes.toString('base64')
|
|
251
|
+
total += bytes.length
|
|
252
|
+
}
|
|
253
|
+
return { files, watched, skipped }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// the middleware's lookup, exported for the producer's own proof: a hit is
|
|
257
|
+
// known bytes for a known hash, anything else is null, never a file.
|
|
258
|
+
export function serveSwiftResource(
|
|
259
|
+
project: SwiftProject,
|
|
260
|
+
key: string,
|
|
261
|
+
): { bytes: Buffer; contentType: string } | null {
|
|
262
|
+
const disk = project.resourceFiles.get(key)
|
|
263
|
+
if (!disk) return null
|
|
264
|
+
try {
|
|
265
|
+
return {
|
|
266
|
+
bytes: fs.readFileSync(disk),
|
|
267
|
+
contentType:
|
|
268
|
+
RESOURCE_CONTENT_TYPES[path.extname(disk).toLowerCase()] ??
|
|
269
|
+
'application/octet-stream',
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
return null
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
89
276
|
// the SwiftUI module the project links: its manifest names it as a path
|
|
90
277
|
// dependency, and `swift build` compiles it with the project, so an edit
|
|
91
278
|
// there changes the artifact without moving any of the project's own mtimes.
|
|
@@ -137,14 +324,18 @@ export function entryRootId(packagePath: string, file: string): string {
|
|
|
137
324
|
return `${path.basename(packagePath)}${path.posix.sep}${relative}`
|
|
138
325
|
}
|
|
139
326
|
|
|
140
|
-
// the app fetches
|
|
141
|
-
//
|
|
142
|
-
function
|
|
327
|
+
// the app fetches from a worker whose origin is the shell, so urls have to
|
|
328
|
+
// name this dev server rather than resolve relative to the shell.
|
|
329
|
+
function serverOrigin(): string {
|
|
143
330
|
const address = devServer?.httpServer?.address()
|
|
144
331
|
const port =
|
|
145
332
|
address && typeof address === 'object' ? address.port : devServer?.config.server.port
|
|
146
|
-
if (!port) throw new Error('the swift
|
|
147
|
-
return `http://localhost:${port}
|
|
333
|
+
if (!port) throw new Error('the swift urls need the dev server port')
|
|
334
|
+
return `http://localhost:${port}`
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function artifactUrl(project: SwiftProject, artifact: SwiftArtifact): string {
|
|
338
|
+
return `${serverOrigin()}${ARTIFACT_PATH}${artifact.hash}.wasm`
|
|
148
339
|
}
|
|
149
340
|
|
|
150
341
|
// one executable is the app. a package with several is ambiguous: the host
|
|
@@ -332,13 +523,46 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
|
|
|
332
523
|
this.warn(error instanceof Error ? error.message : String(error))
|
|
333
524
|
artifact = project.artifact
|
|
334
525
|
}
|
|
526
|
+
// the bundle image table rides the same module: a photo edit moves
|
|
527
|
+
// its urls without rebuilding the artifact, and the watched files
|
|
528
|
+
// re-run this load so the mount sees the new table on hot reload.
|
|
529
|
+
let resources: Record<string, string> = {}
|
|
530
|
+
try {
|
|
531
|
+
const built = buildSwiftResourceTable(project, serverOrigin())
|
|
532
|
+
resources = built.table
|
|
533
|
+
for (const watched of built.watched) this.addWatchFile(watched)
|
|
534
|
+
} catch (error) {
|
|
535
|
+
this.warn(
|
|
536
|
+
`swift resources for ${packagePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
// the /res pack rides the same module as bytes: a json edit moves the
|
|
540
|
+
// pack, the watched files re-run this load, and the mount's
|
|
541
|
+
// bundleFiles compare treats it as a new app and remounts through it.
|
|
542
|
+
let bundleFiles: Record<string, string> = {}
|
|
543
|
+
try {
|
|
544
|
+
const packed = buildSwiftBundleFiles(packagePath)
|
|
545
|
+
bundleFiles = packed.files
|
|
546
|
+
for (const watched of packed.watched) {
|
|
547
|
+
this.addWatchFile(path.join(packagePath, ...watched.split(path.posix.sep)))
|
|
548
|
+
}
|
|
549
|
+
if (packed.skipped.length > 0) {
|
|
550
|
+
this.warn(
|
|
551
|
+
`swift bundle files skipped for ${packagePath}: ${packed.skipped.join('; ')}`,
|
|
552
|
+
)
|
|
553
|
+
}
|
|
554
|
+
} catch (error) {
|
|
555
|
+
this.warn(
|
|
556
|
+
`swift bundle files for ${packagePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
557
|
+
)
|
|
558
|
+
}
|
|
335
559
|
// every import mounts through the one entry: the artifact answers
|
|
336
560
|
// whether it is a view or an app root at runtime, so the transform
|
|
337
561
|
// never re-derives the conformance from source. the entry is rooted by
|
|
338
562
|
// its package and source path so a hot reload finds the same instance.
|
|
339
563
|
return [
|
|
340
564
|
`import { createSwiftEntry } from 'rnxsim/swift'`,
|
|
341
|
-
`export default createSwiftEntry({ url: ${JSON.stringify(artifactUrl(project, artifact))}, hash: ${JSON.stringify(artifact.hash)}, rootId: ${JSON.stringify(entryRootId(packagePath, file))} })`,
|
|
565
|
+
`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)} })`,
|
|
342
566
|
'',
|
|
343
567
|
].join('\n')
|
|
344
568
|
},
|
|
@@ -348,6 +572,24 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
|
|
|
348
572
|
|
|
349
573
|
server.middlewares.use((req, res, next) => {
|
|
350
574
|
const url = (req.url ?? '').split('?')[0]
|
|
575
|
+
if (url.startsWith(RESOURCE_PATH)) {
|
|
576
|
+
// hash-addressed: the table this load emitted names these hashes,
|
|
577
|
+
// so a hit serves known bytes and anything else is a 404, never a
|
|
578
|
+
// neighboring file. the tenant worker is cross-origin like the
|
|
579
|
+
// artifact fetch, hence the same cors header.
|
|
580
|
+
for (const project of projects.values()) {
|
|
581
|
+
const served = serveSwiftResource(project, url.slice(RESOURCE_PATH.length))
|
|
582
|
+
if (served) {
|
|
583
|
+
res.setHeader('access-control-allow-origin', '*')
|
|
584
|
+
res.setHeader('content-type', served.contentType)
|
|
585
|
+
res.end(served.bytes)
|
|
586
|
+
return
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
res.statusCode = 404
|
|
590
|
+
res.end('swift resource is stale')
|
|
591
|
+
return
|
|
592
|
+
}
|
|
351
593
|
if (!url.startsWith(ARTIFACT_PATH)) {
|
|
352
594
|
next()
|
|
353
595
|
return
|