rnxsim 0.1.507 → 0.1.508
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 +19 -8
- 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 +507 -0
- package/dist-lib/vite.cjs +185 -5
- package/package.json +6 -1
- package/src/swift.ts +6 -0
- package/src/vite-plugin-one.ts +8 -0
- package/src/vite-plugin-swift.ts +254 -0
package/dist-lib/vite.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.508 | (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;
|
|
@@ -34,11 +34,12 @@ var vite_plugin_one_exports = {};
|
|
|
34
34
|
__export(vite_plugin_one_exports, {
|
|
35
35
|
default: () => vite_plugin_one_default,
|
|
36
36
|
rnxPlugin: () => rnxPlugin,
|
|
37
|
-
sootsimPlugin: () => sootsimPlugin
|
|
37
|
+
sootsimPlugin: () => sootsimPlugin,
|
|
38
|
+
swiftPlugin: () => swiftPlugin
|
|
38
39
|
});
|
|
39
40
|
module.exports = __toCommonJS(vite_plugin_one_exports);
|
|
40
41
|
var import_fs2 = __toESM(require("fs"), 1);
|
|
41
|
-
var
|
|
42
|
+
var import_node_crypto3 = require("node:crypto");
|
|
42
43
|
var import_path2 = __toESM(require("path"), 1);
|
|
43
44
|
|
|
44
45
|
// src/bridge-constants.ts
|
|
@@ -748,6 +749,184 @@ function serveRuntimeFile(req, res, fullPath) {
|
|
|
748
749
|
import_fs.default.createReadStream(fullPath).pipe(res);
|
|
749
750
|
}
|
|
750
751
|
|
|
752
|
+
// src/vite-plugin-swift.ts
|
|
753
|
+
var import_node_child_process2 = require("node:child_process");
|
|
754
|
+
var import_node_crypto2 = require("node:crypto");
|
|
755
|
+
var import_node_fs3 = __toESM(require("node:fs"), 1);
|
|
756
|
+
var import_node_os2 = __toESM(require("node:os"), 1);
|
|
757
|
+
var import_node_path4 = __toESM(require("node:path"), 1);
|
|
758
|
+
var import_node_util = require("node:util");
|
|
759
|
+
var run = (0, import_node_util.promisify)(import_node_child_process2.execFile);
|
|
760
|
+
var TOOLCHAIN_BIN = import_node_path4.default.join(
|
|
761
|
+
import_node_os2.default.homedir(),
|
|
762
|
+
"Library/Developer/Toolchains/swift-6.2-RELEASE.xctoolchain/usr/bin"
|
|
763
|
+
);
|
|
764
|
+
var WASM_SDK = "swift-6.2-RELEASE_wasm";
|
|
765
|
+
var WASM_TRIPLE = "wasm32-unknown-wasip1";
|
|
766
|
+
var ARTIFACT_PATH = "/__rnx/swift/";
|
|
767
|
+
var VIRTUAL_PREFIX = "\0rnx-swift:";
|
|
768
|
+
var projects = /* @__PURE__ */ new Map();
|
|
769
|
+
var devServer;
|
|
770
|
+
function projectFor(packagePath) {
|
|
771
|
+
const existing = projects.get(packagePath);
|
|
772
|
+
if (existing) return existing;
|
|
773
|
+
const created = { packagePath, artifact: null, building: null };
|
|
774
|
+
projects.set(packagePath, created);
|
|
775
|
+
return created;
|
|
776
|
+
}
|
|
777
|
+
function rootFor(file) {
|
|
778
|
+
return devServer?.config.root ?? import_node_path4.default.dirname(file);
|
|
779
|
+
}
|
|
780
|
+
function sourceFiles(packagePath) {
|
|
781
|
+
const found = [];
|
|
782
|
+
const walk = (dir) => {
|
|
783
|
+
for (const entry of import_node_fs3.default.readdirSync(dir, { withFileTypes: true })) {
|
|
784
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
785
|
+
const full = import_node_path4.default.join(dir, entry.name);
|
|
786
|
+
if (entry.isDirectory()) walk(full);
|
|
787
|
+
else if (entry.name.endsWith(".swift")) found.push(full);
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
walk(packagePath);
|
|
791
|
+
return found;
|
|
792
|
+
}
|
|
793
|
+
function sourceStamp(packagePath) {
|
|
794
|
+
const hash = (0, import_node_crypto2.createHash)("sha256");
|
|
795
|
+
for (const file of sourceFiles(packagePath).sort()) {
|
|
796
|
+
hash.update(import_node_path4.default.relative(packagePath, file));
|
|
797
|
+
hash.update(String(import_node_fs3.default.statSync(file).mtimeMs));
|
|
798
|
+
}
|
|
799
|
+
return hash.digest("hex").slice(0, 16);
|
|
800
|
+
}
|
|
801
|
+
function artifactUrl(project, artifact) {
|
|
802
|
+
const address = devServer?.httpServer?.address();
|
|
803
|
+
const port = address && typeof address === "object" ? address.port : devServer?.config.server.port;
|
|
804
|
+
if (!port) throw new Error("the swift artifact url needs the dev server port");
|
|
805
|
+
return `http://localhost:${port}${ARTIFACT_PATH}${artifact.hash}.wasm`;
|
|
806
|
+
}
|
|
807
|
+
function builtArtifact(packagePath) {
|
|
808
|
+
const binDir = import_node_path4.default.join(packagePath, ".build", WASM_TRIPLE, "release");
|
|
809
|
+
const artifacts = import_node_fs3.default.existsSync(binDir) ? import_node_fs3.default.readdirSync(binDir).filter((name) => name.endsWith(".wasm")).sort() : [];
|
|
810
|
+
if (artifacts.length !== 1) {
|
|
811
|
+
throw new Error(
|
|
812
|
+
`swift project ${packagePath} built ${artifacts.length} wasm artifacts; exactly one executable is the app`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
return import_node_path4.default.join(binDir, artifacts[0]);
|
|
816
|
+
}
|
|
817
|
+
async function compile(packagePath, stamp) {
|
|
818
|
+
const swift = import_node_path4.default.join(TOOLCHAIN_BIN, "swift");
|
|
819
|
+
if (!import_node_fs3.default.existsSync(swift)) {
|
|
820
|
+
throw new Error(
|
|
821
|
+
`no swift.org toolchain at ${TOOLCHAIN_BIN}. install it and the ${WASM_SDK} sdk; see packages/sootsim-swift/README.md`
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
try {
|
|
825
|
+
await run(
|
|
826
|
+
swift,
|
|
827
|
+
[
|
|
828
|
+
"build",
|
|
829
|
+
"--package-path",
|
|
830
|
+
packagePath,
|
|
831
|
+
"--swift-sdk",
|
|
832
|
+
WASM_SDK,
|
|
833
|
+
"-c",
|
|
834
|
+
"release",
|
|
835
|
+
"-Xswiftc",
|
|
836
|
+
"-Osize"
|
|
837
|
+
],
|
|
838
|
+
{ maxBuffer: 64 * 1024 * 1024 }
|
|
839
|
+
);
|
|
840
|
+
} catch (error) {
|
|
841
|
+
throw new Error(
|
|
842
|
+
`swift build failed for ${packagePath}
|
|
843
|
+
${error instanceof Error ? error.message : String(error)}`
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
const bytes = import_node_fs3.default.readFileSync(builtArtifact(packagePath));
|
|
847
|
+
return {
|
|
848
|
+
hash: (0, import_node_crypto2.createHash)("sha256").update(bytes).digest("hex").slice(0, 16),
|
|
849
|
+
bytes,
|
|
850
|
+
stamp
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
function artifactFor(project) {
|
|
854
|
+
const stamp = sourceStamp(project.packagePath);
|
|
855
|
+
if (project.artifact?.stamp === stamp) return Promise.resolve(project.artifact);
|
|
856
|
+
project.building ??= compile(project.packagePath, stamp).then(
|
|
857
|
+
(artifact) => {
|
|
858
|
+
project.artifact = artifact;
|
|
859
|
+
project.building = null;
|
|
860
|
+
return artifact;
|
|
861
|
+
},
|
|
862
|
+
(error) => {
|
|
863
|
+
project.building = null;
|
|
864
|
+
throw error;
|
|
865
|
+
}
|
|
866
|
+
);
|
|
867
|
+
return project.building;
|
|
868
|
+
}
|
|
869
|
+
function swiftPlugin(options = {}) {
|
|
870
|
+
const packagePathFor = (file) => options.packagePath ?? rootFor(file);
|
|
871
|
+
return {
|
|
872
|
+
name: "rnx-swift",
|
|
873
|
+
enforce: "pre",
|
|
874
|
+
async resolveId(source, importer) {
|
|
875
|
+
if (!source.endsWith(".swift")) return null;
|
|
876
|
+
const resolved = await this.resolve(source, importer, { skipSelf: true });
|
|
877
|
+
if (!resolved) throw new Error(`cannot resolve swift source ${source}`);
|
|
878
|
+
return VIRTUAL_PREFIX + resolved.id;
|
|
879
|
+
},
|
|
880
|
+
async load(id) {
|
|
881
|
+
if (!id.startsWith(VIRTUAL_PREFIX)) return null;
|
|
882
|
+
const packagePath = packagePathFor(id.slice(VIRTUAL_PREFIX.length));
|
|
883
|
+
const project = projectFor(packagePath);
|
|
884
|
+
for (const source of sourceFiles(packagePath)) this.addWatchFile(source);
|
|
885
|
+
let artifact;
|
|
886
|
+
try {
|
|
887
|
+
artifact = await artifactFor(project);
|
|
888
|
+
} catch (error) {
|
|
889
|
+
if (!project.artifact) throw error;
|
|
890
|
+
this.warn(error instanceof Error ? error.message : String(error));
|
|
891
|
+
artifact = project.artifact;
|
|
892
|
+
}
|
|
893
|
+
return [
|
|
894
|
+
`import { createSwiftModule } from 'rnxsim/swift'`,
|
|
895
|
+
`export default createSwiftModule({ url: ${JSON.stringify(artifactUrl(project, artifact))}, hash: ${JSON.stringify(artifact.hash)} })`,
|
|
896
|
+
""
|
|
897
|
+
].join("\n");
|
|
898
|
+
},
|
|
899
|
+
configureServer(server) {
|
|
900
|
+
devServer = server;
|
|
901
|
+
const packagePath = packagePathFor("");
|
|
902
|
+
server.middlewares.use((req, res, next) => {
|
|
903
|
+
const url = (req.url ?? "").split("?")[0];
|
|
904
|
+
if (!url.startsWith(ARTIFACT_PATH)) {
|
|
905
|
+
next();
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
const project = projectFor(packagePath);
|
|
909
|
+
void artifactFor(project).then(
|
|
910
|
+
(artifact) => {
|
|
911
|
+
if (url !== `${ARTIFACT_PATH}${artifact.hash}.wasm`) {
|
|
912
|
+
res.statusCode = 404;
|
|
913
|
+
res.end("swift artifact is stale");
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
res.setHeader("access-control-allow-origin", "*");
|
|
917
|
+
res.setHeader("content-type", "application/wasm");
|
|
918
|
+
res.end(artifact.bytes);
|
|
919
|
+
},
|
|
920
|
+
(error) => {
|
|
921
|
+
res.statusCode = 500;
|
|
922
|
+
res.end(error instanceof Error ? error.message : String(error));
|
|
923
|
+
}
|
|
924
|
+
);
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
751
930
|
// src/vite-plugin-one.ts
|
|
752
931
|
function rnxPlugin(options = {}) {
|
|
753
932
|
const prefix = options.prefix || "/__soot";
|
|
@@ -757,7 +936,7 @@ function rnxPlugin(options = {}) {
|
|
|
757
936
|
if (options.enabled === false) return;
|
|
758
937
|
const bundleUrl = options.bundleUrl || "/node_modules/one/metro-entry.bundle?platform=ios&dev=true&minify=false";
|
|
759
938
|
const appName = resolveRnxDesktopAppName(options.open, server.config.root);
|
|
760
|
-
const replacementToken = (0,
|
|
939
|
+
const replacementToken = (0, import_node_crypto3.randomUUID)();
|
|
761
940
|
const configuredPort = server.config.server.port || 8081;
|
|
762
941
|
const listeningPort = () => {
|
|
763
942
|
const address = server.httpServer?.address();
|
|
@@ -883,5 +1062,6 @@ var vite_plugin_one_default = rnxPlugin;
|
|
|
883
1062
|
// Annotate the CommonJS export names for ESM import in node:
|
|
884
1063
|
0 && (module.exports = {
|
|
885
1064
|
rnxPlugin,
|
|
886
|
-
sootsimPlugin
|
|
1065
|
+
sootsimPlugin,
|
|
1066
|
+
swiftPlugin
|
|
887
1067
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rnxsim",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.508",
|
|
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",
|
|
@@ -34,6 +34,11 @@
|
|
|
34
34
|
"source": "./src/vite-plugin-one.ts",
|
|
35
35
|
"default": "./dist-lib/vite.cjs"
|
|
36
36
|
},
|
|
37
|
+
"./swift": {
|
|
38
|
+
"types": "./src/swift.ts",
|
|
39
|
+
"source": "./src/swift.ts",
|
|
40
|
+
"default": "./dist-lib/swift.cjs"
|
|
41
|
+
},
|
|
37
42
|
"./metro": {
|
|
38
43
|
"source": "./src/metro-plugin.ts",
|
|
39
44
|
"default": "./dist-lib/metro.cjs"
|
package/src/swift.ts
ADDED
package/src/vite-plugin-one.ts
CHANGED
|
@@ -235,3 +235,11 @@ export function rnxPlugin(options: RnxPluginOptions = {}): Plugin {
|
|
|
235
235
|
|
|
236
236
|
export const sootsimPlugin = rnxPlugin
|
|
237
237
|
export default rnxPlugin
|
|
238
|
+
|
|
239
|
+
// the `.swift` transform is opt-in per project: an app that never imports a
|
|
240
|
+
// `.swift` file has no reason to watch for one, and an app that ships a native
|
|
241
|
+
// ios/ tree (every swift file in it is a pod or an xcode target, not app
|
|
242
|
+
// source) would otherwise rebuild the wrong package on every edit. a swift app
|
|
243
|
+
// registers it in both the bundler that builds its native bundle and the main
|
|
244
|
+
// dev server that serves the artifact.
|
|
245
|
+
export { swiftPlugin, type SwiftPluginOptions } from './vite-plugin-swift.ts'
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// `.swift` handling for a vite dev server: compile the project's swift sources
|
|
2
|
+
// with the swift.org toolchain, serve the artifact at a content-hashed url,
|
|
3
|
+
// and answer every `.swift` import with the module that mounts it.
|
|
4
|
+
//
|
|
5
|
+
// two hosts use this. the bundler that builds the app's native bundle runs
|
|
6
|
+
// resolveId/load, and the dev server runs the middleware that serves the
|
|
7
|
+
// artifact. both share the build below, so the artifact is compiled once.
|
|
8
|
+
//
|
|
9
|
+
// the hash is the hash of the built wasm, so a new hash means a genuinely
|
|
10
|
+
// different app: createSwiftModule moves the @State store across it instead of
|
|
11
|
+
// remounting.
|
|
12
|
+
|
|
13
|
+
import { execFile } from 'node:child_process'
|
|
14
|
+
import { createHash } from 'node:crypto'
|
|
15
|
+
import fs from 'node:fs'
|
|
16
|
+
import os from 'node:os'
|
|
17
|
+
import path from 'node:path'
|
|
18
|
+
import { promisify } from 'node:util'
|
|
19
|
+
import type { Plugin, ViteDevServer } from 'vite'
|
|
20
|
+
|
|
21
|
+
const run = promisify(execFile)
|
|
22
|
+
|
|
23
|
+
// the toolchain layout scripts/build.sh pins. xcode's swift has no wasm
|
|
24
|
+
// backend, so the swift.org toolchain and its matching wasm sdk are the only
|
|
25
|
+
// ones that can build this target.
|
|
26
|
+
const TOOLCHAIN_BIN = path.join(
|
|
27
|
+
os.homedir(),
|
|
28
|
+
'Library/Developer/Toolchains/swift-6.2-RELEASE.xctoolchain/usr/bin',
|
|
29
|
+
)
|
|
30
|
+
const WASM_SDK = 'swift-6.2-RELEASE_wasm'
|
|
31
|
+
const WASM_TRIPLE = 'wasm32-unknown-wasip1'
|
|
32
|
+
|
|
33
|
+
const ARTIFACT_PATH = '/__rnx/swift/'
|
|
34
|
+
const VIRTUAL_PREFIX = '\0rnx-swift:'
|
|
35
|
+
|
|
36
|
+
interface SwiftArtifact {
|
|
37
|
+
hash: string
|
|
38
|
+
bytes: Buffer
|
|
39
|
+
// the sources this artifact was built from. a watcher event and a `load`
|
|
40
|
+
// reach the project in either order, so freshness is read from the sources
|
|
41
|
+
// rather than from whichever of them ran first.
|
|
42
|
+
stamp: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface SwiftProject {
|
|
46
|
+
packagePath: string
|
|
47
|
+
artifact: SwiftArtifact | null
|
|
48
|
+
building: Promise<SwiftArtifact> | null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// a project is its swiftpm package: the build, the artifact, and the dev
|
|
52
|
+
// server that serves it are shared by every plugin instance in this process.
|
|
53
|
+
const projects = new Map<string, SwiftProject>()
|
|
54
|
+
let devServer: ViteDevServer | undefined
|
|
55
|
+
|
|
56
|
+
function projectFor(packagePath: string): SwiftProject {
|
|
57
|
+
const existing = projects.get(packagePath)
|
|
58
|
+
if (existing) return existing
|
|
59
|
+
const created: SwiftProject = { packagePath, artifact: null, building: null }
|
|
60
|
+
projects.set(packagePath, created)
|
|
61
|
+
return created
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function rootFor(file: string): string {
|
|
65
|
+
return devServer?.config.root ?? path.dirname(file)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// the compile unit is the whole package: `swift build` compiles every source
|
|
69
|
+
// in it, so one file's edit can change the artifact for all of them.
|
|
70
|
+
function sourceFiles(packagePath: string): string[] {
|
|
71
|
+
const found: string[] = []
|
|
72
|
+
const walk = (dir: string): void => {
|
|
73
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
74
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue
|
|
75
|
+
const full = path.join(dir, entry.name)
|
|
76
|
+
if (entry.isDirectory()) walk(full)
|
|
77
|
+
else if (entry.name.endsWith('.swift')) found.push(full)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
walk(packagePath)
|
|
81
|
+
return found
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sourceStamp(packagePath: string): string {
|
|
85
|
+
const hash = createHash('sha256')
|
|
86
|
+
for (const file of sourceFiles(packagePath).sort()) {
|
|
87
|
+
hash.update(path.relative(packagePath, file))
|
|
88
|
+
hash.update(String(fs.statSync(file).mtimeMs))
|
|
89
|
+
}
|
|
90
|
+
return hash.digest('hex').slice(0, 16)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// the app fetches the artifact from a worker whose origin is the shell, so the
|
|
94
|
+
// url has to name this dev server rather than resolve relative to the shell.
|
|
95
|
+
function artifactUrl(project: SwiftProject, artifact: SwiftArtifact): string {
|
|
96
|
+
const address = devServer?.httpServer?.address()
|
|
97
|
+
const port =
|
|
98
|
+
address && typeof address === 'object' ? address.port : devServer?.config.server.port
|
|
99
|
+
if (!port) throw new Error('the swift artifact url needs the dev server port')
|
|
100
|
+
return `http://localhost:${port}${ARTIFACT_PATH}${artifact.hash}.wasm`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// one executable is the app. a package with several is ambiguous: the host
|
|
104
|
+
// cannot know which one the page is, so it says so instead of guessing.
|
|
105
|
+
function builtArtifact(packagePath: string): string {
|
|
106
|
+
const binDir = path.join(packagePath, '.build', WASM_TRIPLE, 'release')
|
|
107
|
+
const artifacts = fs.existsSync(binDir)
|
|
108
|
+
? fs
|
|
109
|
+
.readdirSync(binDir)
|
|
110
|
+
.filter((name) => name.endsWith('.wasm'))
|
|
111
|
+
.sort()
|
|
112
|
+
: []
|
|
113
|
+
if (artifacts.length !== 1) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`swift project ${packagePath} built ${artifacts.length} wasm artifacts; exactly one executable is the app`,
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
return path.join(binDir, artifacts[0])
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function compile(packagePath: string, stamp: string): Promise<SwiftArtifact> {
|
|
122
|
+
const swift = path.join(TOOLCHAIN_BIN, 'swift')
|
|
123
|
+
if (!fs.existsSync(swift)) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`no swift.org toolchain at ${TOOLCHAIN_BIN}. install it and the ${WASM_SDK} sdk; see packages/sootsim-swift/README.md`,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
await run(
|
|
130
|
+
swift,
|
|
131
|
+
[
|
|
132
|
+
'build',
|
|
133
|
+
'--package-path',
|
|
134
|
+
packagePath,
|
|
135
|
+
'--swift-sdk',
|
|
136
|
+
WASM_SDK,
|
|
137
|
+
'-c',
|
|
138
|
+
'release',
|
|
139
|
+
'-Xswiftc',
|
|
140
|
+
'-Osize',
|
|
141
|
+
],
|
|
142
|
+
{ maxBuffer: 64 * 1024 * 1024 },
|
|
143
|
+
)
|
|
144
|
+
} catch (error) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`swift build failed for ${packagePath}\n${error instanceof Error ? error.message : String(error)}`,
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
const bytes = fs.readFileSync(builtArtifact(packagePath))
|
|
150
|
+
return {
|
|
151
|
+
hash: createHash('sha256').update(bytes).digest('hex').slice(0, 16),
|
|
152
|
+
bytes,
|
|
153
|
+
stamp,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function artifactFor(project: SwiftProject): Promise<SwiftArtifact> {
|
|
158
|
+
const stamp = sourceStamp(project.packagePath)
|
|
159
|
+
if (project.artifact?.stamp === stamp) return Promise.resolve(project.artifact)
|
|
160
|
+
project.building ??= compile(project.packagePath, stamp).then(
|
|
161
|
+
(artifact) => {
|
|
162
|
+
project.artifact = artifact
|
|
163
|
+
project.building = null
|
|
164
|
+
return artifact
|
|
165
|
+
},
|
|
166
|
+
(error: unknown) => {
|
|
167
|
+
project.building = null
|
|
168
|
+
throw error
|
|
169
|
+
},
|
|
170
|
+
)
|
|
171
|
+
return project.building
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface SwiftPluginOptions {
|
|
175
|
+
// swiftpm package that owns the project's .swift sources. defaults to the
|
|
176
|
+
// vite root, which is the layout a swift-only project uses.
|
|
177
|
+
packagePath?: string
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
|
|
181
|
+
const packagePathFor = (file: string): string => options.packagePath ?? rootFor(file)
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
name: 'rnx-swift',
|
|
185
|
+
enforce: 'pre',
|
|
186
|
+
|
|
187
|
+
async resolveId(source, importer) {
|
|
188
|
+
if (!source.endsWith('.swift')) return null
|
|
189
|
+
const resolved = await this.resolve(source, importer, { skipSelf: true })
|
|
190
|
+
if (!resolved) throw new Error(`cannot resolve swift source ${source}`)
|
|
191
|
+
return VIRTUAL_PREFIX + resolved.id
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
async load(id) {
|
|
195
|
+
if (!id.startsWith(VIRTUAL_PREFIX)) return null
|
|
196
|
+
const packagePath = packagePathFor(id.slice(VIRTUAL_PREFIX.length))
|
|
197
|
+
const project = projectFor(packagePath)
|
|
198
|
+
// every source in the package is a dependency of this module, so a
|
|
199
|
+
// bundler watching this module learns about a `.swift` edit and re-runs
|
|
200
|
+
// load. that is the whole hot reload: the new hash in the emitted module
|
|
201
|
+
// is what tells the mount it is looking at a different app.
|
|
202
|
+
for (const source of sourceFiles(packagePath)) this.addWatchFile(source)
|
|
203
|
+
let artifact: SwiftArtifact
|
|
204
|
+
try {
|
|
205
|
+
artifact = await artifactFor(project)
|
|
206
|
+
} catch (error) {
|
|
207
|
+
// a compile error must not fail this module. a module whose load threw
|
|
208
|
+
// leaves the graph unwatched, so the edit that fixes the error would
|
|
209
|
+
// never rebuild it and the app would sit on a stale frame until the
|
|
210
|
+
// dev server restarted. the previous artifact keeps rendering, and the
|
|
211
|
+
// error is reported here instead.
|
|
212
|
+
if (!project.artifact) throw error
|
|
213
|
+
this.warn(error instanceof Error ? error.message : String(error))
|
|
214
|
+
artifact = project.artifact
|
|
215
|
+
}
|
|
216
|
+
return [
|
|
217
|
+
`import { createSwiftModule } from 'rnxsim/swift'`,
|
|
218
|
+
`export default createSwiftModule({ url: ${JSON.stringify(artifactUrl(project, artifact))}, hash: ${JSON.stringify(artifact.hash)} })`,
|
|
219
|
+
'',
|
|
220
|
+
].join('\n')
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
configureServer(server) {
|
|
224
|
+
devServer = server
|
|
225
|
+
const packagePath = packagePathFor('')
|
|
226
|
+
|
|
227
|
+
server.middlewares.use((req, res, next) => {
|
|
228
|
+
const url = (req.url ?? '').split('?')[0]
|
|
229
|
+
if (!url.startsWith(ARTIFACT_PATH)) {
|
|
230
|
+
next()
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
const project = projectFor(packagePath)
|
|
234
|
+
void artifactFor(project).then(
|
|
235
|
+
(artifact) => {
|
|
236
|
+
if (url !== `${ARTIFACT_PATH}${artifact.hash}.wasm`) {
|
|
237
|
+
res.statusCode = 404
|
|
238
|
+
res.end('swift artifact is stale')
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
// the tenant worker's origin is the shell, never this dev server
|
|
242
|
+
res.setHeader('access-control-allow-origin', '*')
|
|
243
|
+
res.setHeader('content-type', 'application/wasm')
|
|
244
|
+
res.end(artifact.bytes)
|
|
245
|
+
},
|
|
246
|
+
(error: unknown) => {
|
|
247
|
+
res.statusCode = 500
|
|
248
|
+
res.end(error instanceof Error ? error.message : String(error))
|
|
249
|
+
},
|
|
250
|
+
)
|
|
251
|
+
})
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
}
|