rnxsim 0.1.519 → 0.1.521
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/cli/cloud-client.ts +419 -287
- package/cli/cloud-dispatch.ts +2 -21
- package/cli/cloud-session.ts +31 -85
- package/cli/commands/box.ts +15 -11
- package/cli/commands/inspect.ts +34 -1
- package/cli/commands/platform.ts +184 -232
- package/cli/main.ts +10 -2
- package/cli/outbound-endpoints.ts +21 -6
- 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 +7 -2
- package/dist-lib/bridge-contract-input.mjs +7 -2
- 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 +18 -2
- package/dist-lib/cloud.mjs +18 -2
- 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 +112 -39
- 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 +44 -117
- 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 +30 -1
- package/dist-lib/menu.mjs +27 -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 +41 -4
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +138 -164
- package/dist-lib/swift.cjs +766 -24
- package/dist-lib/vite.cjs +103 -30
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +72 -5
- package/src/bridge-contract-input.ts +10 -0
- package/src/bridge-contract.ts +5 -0
- package/src/cloud-contract.ts +7 -1
- package/src/cloud.ts +45 -6
- package/src/connect.ts +1 -1
- package/src/menu.ts +44 -0
- package/src/swift.ts +2 -0
- package/src/vite-plugin-one.ts +12 -0
- package/src/vite-plugin-swift.ts +141 -39
package/dist-lib/vite.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.521 | (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;
|
|
@@ -762,12 +762,20 @@ var import_node_fs3 = __toESM(require("node:fs"), 1);
|
|
|
762
762
|
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
|
+
|
|
766
|
+
// ../sootsim-swift/toolchain.json
|
|
767
|
+
var toolchain_default = {
|
|
768
|
+
release: "swift-6.3-RELEASE",
|
|
769
|
+
wasmSdkChecksum: "9fa4016ee632c7e9e906608ec3b55cf13dfc4dff44e47574c5af58064dc33fd9"
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
// src/vite-plugin-swift.ts
|
|
765
773
|
var run = (0, import_node_util.promisify)(import_node_child_process2.execFile);
|
|
766
774
|
var TOOLCHAIN_BIN = import_node_path4.default.join(
|
|
767
775
|
import_node_os2.default.homedir(),
|
|
768
|
-
|
|
776
|
+
`Library/Developer/Toolchains/${toolchain_default.release}.xctoolchain/usr/bin`
|
|
769
777
|
);
|
|
770
|
-
var WASM_SDK =
|
|
778
|
+
var WASM_SDK = `${toolchain_default.release}_wasm`;
|
|
771
779
|
var WASM_TRIPLE = "wasm32-unknown-wasip1";
|
|
772
780
|
var ARTIFACT_PATH = "/__rnx/swift/";
|
|
773
781
|
var VIRTUAL_PREFIX = "\0rnx-swift:";
|
|
@@ -780,30 +788,50 @@ function projectFor(packagePath) {
|
|
|
780
788
|
projects.set(packagePath, created);
|
|
781
789
|
return created;
|
|
782
790
|
}
|
|
783
|
-
function
|
|
784
|
-
return devServer?.config.root ?? import_node_path4.default.dirname(file);
|
|
785
|
-
}
|
|
786
|
-
function sourceFiles(packagePath) {
|
|
791
|
+
function sourceFiles(root) {
|
|
787
792
|
const found = [];
|
|
788
793
|
const walk = (dir) => {
|
|
789
794
|
for (const entry of import_node_fs3.default.readdirSync(dir, { withFileTypes: true })) {
|
|
790
795
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
791
796
|
const full = import_node_path4.default.join(dir, entry.name);
|
|
792
|
-
if (entry.isDirectory())
|
|
793
|
-
|
|
797
|
+
if (entry.isDirectory()) {
|
|
798
|
+
if (full !== root && import_node_fs3.default.existsSync(import_node_path4.default.join(full, "Package.swift"))) continue;
|
|
799
|
+
walk(full);
|
|
800
|
+
} else if (entry.name.endsWith(".swift")) found.push(full);
|
|
794
801
|
}
|
|
795
802
|
};
|
|
796
|
-
walk(
|
|
803
|
+
walk(root);
|
|
797
804
|
return found;
|
|
798
805
|
}
|
|
806
|
+
function linkedModuleRoots(packagePath) {
|
|
807
|
+
let manifest;
|
|
808
|
+
try {
|
|
809
|
+
manifest = import_node_fs3.default.readFileSync(import_node_path4.default.join(packagePath, "Package.swift"), "utf8");
|
|
810
|
+
} catch {
|
|
811
|
+
return [];
|
|
812
|
+
}
|
|
813
|
+
const roots = /* @__PURE__ */ new Set();
|
|
814
|
+
const dependency = /\.package\(\s*path:\s*"([^"]+)"\s*\)/g;
|
|
815
|
+
for (const match of manifest.replace(/^\s*\/\/.*$/gm, "").matchAll(dependency)) {
|
|
816
|
+
const root = import_node_path4.default.resolve(packagePath, match[1]);
|
|
817
|
+
if (import_node_fs3.default.existsSync(import_node_path4.default.join(root, "Package.swift"))) roots.add(root);
|
|
818
|
+
}
|
|
819
|
+
return [...roots].sort();
|
|
820
|
+
}
|
|
799
821
|
function sourceStamp(packagePath) {
|
|
800
822
|
const hash = (0, import_node_crypto2.createHash)("sha256");
|
|
801
|
-
for (const
|
|
802
|
-
hash.update(
|
|
803
|
-
|
|
823
|
+
for (const root of [packagePath, ...linkedModuleRoots(packagePath)]) {
|
|
824
|
+
hash.update(root);
|
|
825
|
+
for (const file of sourceFiles(root).sort()) {
|
|
826
|
+
hash.update(import_node_path4.default.relative(root, file));
|
|
827
|
+
hash.update(String(import_node_fs3.default.statSync(file).mtimeMs));
|
|
828
|
+
}
|
|
804
829
|
}
|
|
805
830
|
return hash.digest("hex").slice(0, 16);
|
|
806
831
|
}
|
|
832
|
+
function viewRootId(packagePath, file) {
|
|
833
|
+
return import_node_path4.default.join(packagePath, import_node_path4.default.relative(packagePath, file));
|
|
834
|
+
}
|
|
807
835
|
function artifactUrl(project, artifact) {
|
|
808
836
|
const address = devServer?.httpServer?.address();
|
|
809
837
|
const port = address && typeof address === "object" ? address.port : devServer?.config.server.port;
|
|
@@ -872,12 +900,36 @@ function artifactFor(project) {
|
|
|
872
900
|
);
|
|
873
901
|
return project.building;
|
|
874
902
|
}
|
|
903
|
+
function packageFor(file) {
|
|
904
|
+
let dir = import_node_path4.default.dirname(file);
|
|
905
|
+
const stop = devServer?.config.root;
|
|
906
|
+
for (; ; ) {
|
|
907
|
+
if (import_node_fs3.default.existsSync(import_node_path4.default.join(dir, "Package.swift"))) return dir;
|
|
908
|
+
if (stop !== void 0 && dir === stop || import_node_path4.default.dirname(dir) === dir) break;
|
|
909
|
+
dir = import_node_path4.default.dirname(dir);
|
|
910
|
+
}
|
|
911
|
+
throw new Error(
|
|
912
|
+
`no Package.swift above ${file}${stop === void 0 ? "" : ` (searched up to ${stop})`}; a .swift import needs a swiftpm package like the one in packages/sootsim-swift/example`
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
function isViewPackage(packagePath) {
|
|
916
|
+
const main = /@main\s+(?:\w+\s+)*(?:struct|class|enum|actor)\s+\w+[^{]*:\s*[^{]*\bRNXPackage\b/;
|
|
917
|
+
for (const file of sourceFiles(packagePath)) {
|
|
918
|
+
const text = import_node_fs3.default.readFileSync(file, "utf8").replace(/^\s*\/\/.*$/gm, "");
|
|
919
|
+
if (main.test(text)) return true;
|
|
920
|
+
}
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
875
923
|
function swiftPlugin(options = {}) {
|
|
876
|
-
const packagePathFor = (file) => options.packagePath ?? rootFor(file);
|
|
877
924
|
return {
|
|
878
925
|
name: "rnx-swift",
|
|
879
926
|
enforce: "pre",
|
|
880
927
|
async resolveId(source, importer) {
|
|
928
|
+
if (source.startsWith(VIRTUAL_PREFIX)) return source;
|
|
929
|
+
if (source.startsWith("/@id/")) {
|
|
930
|
+
const decoded = source.slice("/@id/".length).replace(/^__x00__/, "\0");
|
|
931
|
+
return decoded.startsWith(VIRTUAL_PREFIX) ? decoded : null;
|
|
932
|
+
}
|
|
881
933
|
if (!source.endsWith(".swift")) return null;
|
|
882
934
|
const resolved = await this.resolve(source, importer, { skipSelf: true });
|
|
883
935
|
if (!resolved) throw new Error(`cannot resolve swift source ${source}`);
|
|
@@ -885,9 +937,13 @@ function swiftPlugin(options = {}) {
|
|
|
885
937
|
},
|
|
886
938
|
async load(id) {
|
|
887
939
|
if (!id.startsWith(VIRTUAL_PREFIX)) return null;
|
|
888
|
-
const
|
|
940
|
+
const file = id.slice(VIRTUAL_PREFIX.length);
|
|
941
|
+
const packagePath = options.packagePath ?? packageFor(file);
|
|
889
942
|
const project = projectFor(packagePath);
|
|
890
943
|
for (const source of sourceFiles(packagePath)) this.addWatchFile(source);
|
|
944
|
+
for (const root of linkedModuleRoots(packagePath)) {
|
|
945
|
+
for (const source of sourceFiles(root)) this.addWatchFile(source);
|
|
946
|
+
}
|
|
891
947
|
let artifact;
|
|
892
948
|
try {
|
|
893
949
|
artifact = await artifactFor(project);
|
|
@@ -896,6 +952,14 @@ function swiftPlugin(options = {}) {
|
|
|
896
952
|
this.warn(error instanceof Error ? error.message : String(error));
|
|
897
953
|
artifact = project.artifact;
|
|
898
954
|
}
|
|
955
|
+
if (isViewPackage(packagePath)) {
|
|
956
|
+
const rootId = viewRootId(packagePath, file);
|
|
957
|
+
return [
|
|
958
|
+
`import { createSwiftView } from 'rnxsim/swift'`,
|
|
959
|
+
`export default createSwiftView({ url: ${JSON.stringify(artifactUrl(project, artifact))}, hash: ${JSON.stringify(artifact.hash)}, rootId: ${JSON.stringify(rootId)} })`,
|
|
960
|
+
""
|
|
961
|
+
].join("\n");
|
|
962
|
+
}
|
|
899
963
|
return [
|
|
900
964
|
`import { createSwiftModule } from 'rnxsim/swift'`,
|
|
901
965
|
`export default createSwiftModule({ url: ${JSON.stringify(artifactUrl(project, artifact))}, hash: ${JSON.stringify(artifact.hash)} })`,
|
|
@@ -904,30 +968,28 @@ function swiftPlugin(options = {}) {
|
|
|
904
968
|
},
|
|
905
969
|
configureServer(server) {
|
|
906
970
|
devServer = server;
|
|
907
|
-
const packagePath = packagePathFor("");
|
|
908
971
|
server.middlewares.use((req, res, next) => {
|
|
909
972
|
const url = (req.url ?? "").split("?")[0];
|
|
910
973
|
if (!url.startsWith(ARTIFACT_PATH)) {
|
|
911
974
|
next();
|
|
912
975
|
return;
|
|
913
976
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
if (url
|
|
918
|
-
res.
|
|
919
|
-
res.
|
|
977
|
+
void (async () => {
|
|
978
|
+
for (const project of projects.values()) {
|
|
979
|
+
const artifact = await artifactFor(project);
|
|
980
|
+
if (url === `${ARTIFACT_PATH}${artifact.hash}.wasm`) {
|
|
981
|
+
res.setHeader("access-control-allow-origin", "*");
|
|
982
|
+
res.setHeader("content-type", "application/wasm");
|
|
983
|
+
res.end(artifact.bytes);
|
|
920
984
|
return;
|
|
921
985
|
}
|
|
922
|
-
res.setHeader("access-control-allow-origin", "*");
|
|
923
|
-
res.setHeader("content-type", "application/wasm");
|
|
924
|
-
res.end(artifact.bytes);
|
|
925
|
-
},
|
|
926
|
-
(error) => {
|
|
927
|
-
res.statusCode = 500;
|
|
928
|
-
res.end(error instanceof Error ? error.message : String(error));
|
|
929
986
|
}
|
|
930
|
-
|
|
987
|
+
res.statusCode = 404;
|
|
988
|
+
res.end("swift artifact is stale");
|
|
989
|
+
})().catch((error) => {
|
|
990
|
+
res.statusCode = 500;
|
|
991
|
+
res.end(error instanceof Error ? error.message : String(error));
|
|
992
|
+
});
|
|
931
993
|
});
|
|
932
994
|
}
|
|
933
995
|
};
|
|
@@ -938,6 +1000,17 @@ function rnxPlugin(options = {}) {
|
|
|
938
1000
|
const prefix = options.prefix || "/__soot";
|
|
939
1001
|
return {
|
|
940
1002
|
name: "sootsim-one",
|
|
1003
|
+
// use-latest-callback ships a CJS main (module.exports, no default) with
|
|
1004
|
+
// an esm.mjs shim that re-imports it. react-native-bottom-tabs
|
|
1005
|
+
// default-imports it from source served live, so vite hands the browser
|
|
1006
|
+
// the raw CJS file and the import fails with 'does not provide an export
|
|
1007
|
+
// named default' before anything mounts. forcing it through dep
|
|
1008
|
+
// optimization lets esbuild's CJS interop answer the default import, and
|
|
1009
|
+
// every bare import of it (including the nested copy's) resolves to the
|
|
1010
|
+
// one pre-bundled instance.
|
|
1011
|
+
config() {
|
|
1012
|
+
return { optimizeDeps: { include: ["use-latest-callback"] } };
|
|
1013
|
+
},
|
|
941
1014
|
configureServer(server) {
|
|
942
1015
|
if (options.enabled === false) return;
|
|
943
1016
|
const bundleUrl = options.bundleUrl || "/node_modules/one/metro-entry.bundle?platform=ios&dev=true&minify=false";
|
package/package.json
CHANGED
|
@@ -94,6 +94,10 @@ export interface DiscoveredServer {
|
|
|
94
94
|
lastSeen: number
|
|
95
95
|
iconUrl?: string
|
|
96
96
|
iconPath?: string
|
|
97
|
+
/** the app's own brand colour from its manifest, used as the home tile's
|
|
98
|
+
* background so the tile reads as that app before its icon decodes and
|
|
99
|
+
* when the icon is missing entirely. */
|
|
100
|
+
primaryColor?: string
|
|
97
101
|
bundleId?: string
|
|
98
102
|
settingsBundle?: readonly AppSettingsSpecifier[]
|
|
99
103
|
settingsStorageScopeUrl?: string
|
|
@@ -594,6 +598,43 @@ function safeParseManifest(body: string): Record<string, unknown> | null {
|
|
|
594
598
|
}
|
|
595
599
|
}
|
|
596
600
|
|
|
601
|
+
// a manifest icon path ("./assets/app-icons/x.png") is relative to the project
|
|
602
|
+
// root. metro's asset route prefixes the whole relative path with /assets/; a
|
|
603
|
+
// statically-served project answers at the literal path. the manifest says
|
|
604
|
+
// nothing about which, so the two candidates are probed once per port and the
|
|
605
|
+
// winner cached. keyed by port and cleared with the rest of that port's state
|
|
606
|
+
// when the process disappears.
|
|
607
|
+
const localIconPathCache = new Map<number, string>()
|
|
608
|
+
const localIconProbeInFlight = new Set<number>()
|
|
609
|
+
|
|
610
|
+
function metroAssetPath(rawIcon: string): string {
|
|
611
|
+
return `/assets/${rawIcon.replace(/^\.?\//, '')}`
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function staticAssetPath(rawIcon: string): string {
|
|
615
|
+
return `/${rawIcon.replace(/^\.?\//, '')}`
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// probe both shapes and cache whichever the server actually serves. resolves
|
|
619
|
+
// nothing when neither answers — the tile falls back to the app's brand colour.
|
|
620
|
+
async function resolveLocalIconPath(
|
|
621
|
+
hostname: string,
|
|
622
|
+
port: number,
|
|
623
|
+
rawIcon: string,
|
|
624
|
+
): Promise<void> {
|
|
625
|
+
const candidates = [metroAssetPath(rawIcon), staticAssetPath(rawIcon)]
|
|
626
|
+
for (const candidate of candidates) {
|
|
627
|
+
const res = await httpGet(hostname, port, candidate, 'HEAD', ENRICH_TIMEOUT_MS)
|
|
628
|
+
if (res?.statusCode === 200) {
|
|
629
|
+
localIconPathCache.set(port, candidate)
|
|
630
|
+
// the identified result caches as a strong entry and won't re-probe on
|
|
631
|
+
// its own; drop it so the next scan republishes with the working url.
|
|
632
|
+
portCache.delete(port)
|
|
633
|
+
return
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
597
638
|
// fold expo manifest fields (name, icon, bundleId, launchAsset) into a result.
|
|
598
639
|
// the manifest body is always the GET / response we already have from probePort,
|
|
599
640
|
// so this never issues another http request.
|
|
@@ -607,6 +648,7 @@ function applyManifest(
|
|
|
607
648
|
const manifest = JSON.parse(manifestRes.body)
|
|
608
649
|
const client = manifest?.extra?.expoClient || manifest?.extra || {}
|
|
609
650
|
if (client.name) result.projectName = client.name
|
|
651
|
+
if (typeof client.primaryColor === 'string') result.primaryColor = client.primaryColor
|
|
610
652
|
if (client.ios?.bundleIdentifier) result.bundleId = client.ios.bundleIdentifier
|
|
611
653
|
if (result.framework === 'metro' && client.sdkVersion) result.framework = 'expo'
|
|
612
654
|
// preserve a confirmed direct one/vxrn metro-entry bundle. expo manifests
|
|
@@ -645,11 +687,16 @@ function applyManifest(
|
|
|
645
687
|
resolvedIconUrl = rawIcon
|
|
646
688
|
}
|
|
647
689
|
} else {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
690
|
+
// a manifest icon path is relative to the project root, and the two
|
|
691
|
+
// server shapes we meet serve it at different URLs: metro's asset route
|
|
692
|
+
// is `/assets/` + that whole relative path (so `assets/x.png` lands at
|
|
693
|
+
// `/assets/assets/x.png`), while a statically-served project answers at
|
|
694
|
+
// the literal path. neither shape can be derived from the manifest, so
|
|
695
|
+
// resolveLocalIconPath probes the server once and caches the answer;
|
|
696
|
+
// until it lands we publish the metro form, which is the common case.
|
|
697
|
+
const base = buildLoopbackProbeBaseUrl('http:', manifestRes.hostname, result.port)
|
|
698
|
+
const assetPath = localIconPathCache.get(result.port) ?? metroAssetPath(rawIcon)
|
|
699
|
+
const localUrl = `${base}${assetPath}`
|
|
653
700
|
resolvedIconUrl = buildIconProxyUrl ? buildIconProxyUrl(localUrl) : localUrl
|
|
654
701
|
}
|
|
655
702
|
if (resolvedIconUrl) {
|
|
@@ -700,6 +747,21 @@ interface ManifestEnrichment {
|
|
|
700
747
|
const enrichmentCache = new Map<number, ManifestEnrichment>()
|
|
701
748
|
const enrichmentInFlight = new Set<number>()
|
|
702
749
|
|
|
750
|
+
// a result that carries a relative icon path has not had its serving shape
|
|
751
|
+
// confirmed yet. probe once per port; applyManifest reads the cached answer.
|
|
752
|
+
function ensureLocalIconPathResolved(result: DiscoveredServer): void {
|
|
753
|
+
const rawIcon = result.iconPath
|
|
754
|
+
if (!rawIcon) return
|
|
755
|
+
if (rawIcon.startsWith('http://') || rawIcon.startsWith('https://')) return
|
|
756
|
+
if (localIconPathCache.has(result.port)) return
|
|
757
|
+
if (localIconProbeInFlight.has(result.port)) return
|
|
758
|
+
localIconProbeInFlight.add(result.port)
|
|
759
|
+
const hostname = normalizeHostname(new URL(result.bundleUrl).hostname)
|
|
760
|
+
void resolveLocalIconPath(hostname, result.port, rawIcon).finally(() =>
|
|
761
|
+
localIconProbeInFlight.delete(result.port),
|
|
762
|
+
)
|
|
763
|
+
}
|
|
764
|
+
|
|
703
765
|
// apply already-captured name/icon, or kick off a single background manifest
|
|
704
766
|
// fetch to capture them. a no-op once the in-race manifest already named the
|
|
705
767
|
// server, so it's safe to wrap every identified return.
|
|
@@ -707,6 +769,7 @@ function enrichWithManifest(
|
|
|
707
769
|
result: DiscoveredServer,
|
|
708
770
|
buildIconProxyUrl?: (externalUrl: string) => string,
|
|
709
771
|
): DiscoveredServer {
|
|
772
|
+
ensureLocalIconPathResolved(result)
|
|
710
773
|
if (result.projectName) return result
|
|
711
774
|
|
|
712
775
|
const cached = enrichmentCache.get(result.port)
|
|
@@ -1157,6 +1220,9 @@ export async function scanDevServers(
|
|
|
1157
1220
|
for (const p of [...enrichmentCache.keys()]) {
|
|
1158
1221
|
if (!currentPorts.has(p)) enrichmentCache.delete(p)
|
|
1159
1222
|
}
|
|
1223
|
+
for (const p of [...localIconPathCache.keys()]) {
|
|
1224
|
+
if (!currentPorts.has(p)) localIconPathCache.delete(p)
|
|
1225
|
+
}
|
|
1160
1226
|
|
|
1161
1227
|
const results: DiscoveredServer[] = []
|
|
1162
1228
|
const toProbe: ListeningProcess[] = []
|
|
@@ -1179,6 +1245,7 @@ export async function scanDevServers(
|
|
|
1179
1245
|
knownOne.delete(port)
|
|
1180
1246
|
// a restarted process may be a different app — drop its captured name/icon.
|
|
1181
1247
|
enrichmentCache.delete(port)
|
|
1248
|
+
localIconPathCache.delete(port)
|
|
1182
1249
|
}
|
|
1183
1250
|
toProbe.push({ port, pid })
|
|
1184
1251
|
}
|
|
@@ -435,9 +435,19 @@ export function parseResetOptions(value: unknown): ResetOptions {
|
|
|
435
435
|
if (strategy !== 'data' && strategy !== 'full') {
|
|
436
436
|
return validationFailure('reset.strategy', 'must be data or full')
|
|
437
437
|
}
|
|
438
|
+
// the shape check only; the simulator service re-validates the id against
|
|
439
|
+
// its own strict pattern and the caller's account before touching storage.
|
|
440
|
+
const from = input.from
|
|
441
|
+
if (
|
|
442
|
+
from !== undefined &&
|
|
443
|
+
(typeof from !== 'string' || from.length === 0 || from.length > 64)
|
|
444
|
+
) {
|
|
445
|
+
return validationFailure('reset.from', 'must name a simulator')
|
|
446
|
+
}
|
|
438
447
|
return {
|
|
439
448
|
strategy,
|
|
440
449
|
relaunch: optionalBoolean(input, 'relaunch', 'reset') ?? true,
|
|
450
|
+
...(from === undefined ? {} : { from }),
|
|
441
451
|
}
|
|
442
452
|
}
|
|
443
453
|
|
package/src/bridge-contract.ts
CHANGED
|
@@ -502,6 +502,11 @@ export interface ResetOptions extends Pick<
|
|
|
502
502
|
strategy?: ResetStrategy
|
|
503
503
|
// relaunch the app after wiping. default true.
|
|
504
504
|
relaunch?: boolean
|
|
505
|
+
// boot the wiped simulator on a live copy of another simulator's storage
|
|
506
|
+
// instead of empty stores: the cheap clean-but-not-cold seam. the id names
|
|
507
|
+
// a simulator on the same account; runtimes that cannot reach another
|
|
508
|
+
// simulator's storage refuse it by name rather than wiping without forking.
|
|
509
|
+
from?: string
|
|
505
510
|
}
|
|
506
511
|
|
|
507
512
|
export interface ResetResult {
|
package/src/cloud-contract.ts
CHANGED
|
@@ -6,7 +6,13 @@ export const RNX_CLOUD_DESIGN_MAX_STORAGE_BYTES = 1024 * 1024
|
|
|
6
6
|
export const RNX_CLOUD_TEST_FILES_MAX_COUNT = 256
|
|
7
7
|
export const RNX_CLOUD_TEST_FILES_MAX_BYTES = 500_000
|
|
8
8
|
|
|
9
|
-
export type RnxCloudCommandScope =
|
|
9
|
+
export type RnxCloudCommandScope =
|
|
10
|
+
| 'read'
|
|
11
|
+
| 'drive'
|
|
12
|
+
| 'reset'
|
|
13
|
+
| 'media'
|
|
14
|
+
| 'debug'
|
|
15
|
+
| 'watch'
|
|
10
16
|
|
|
11
17
|
const RNX_CLOUD_COMMAND_SCOPE_SET = new Set<string>([
|
|
12
18
|
'read',
|
package/src/cloud.ts
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
// public surface.
|
|
10
10
|
|
|
11
11
|
import { isLoopbackHost } from './backend-origin'
|
|
12
|
-
import type { SimStateOptions } from './bridge-contract'
|
|
13
12
|
import { isRnxScreenCapture, type RnxScreenCapture } from './capture-contract'
|
|
14
13
|
import {
|
|
15
14
|
isRnxCloudCommandScope,
|
|
@@ -27,6 +26,7 @@ import {
|
|
|
27
26
|
type RnxCloudScopedToken,
|
|
28
27
|
} from './cloud-contract'
|
|
29
28
|
import { rnxPublicBrand } from './public-brand'
|
|
29
|
+
import type { SimStateOptions } from './bridge-contract'
|
|
30
30
|
|
|
31
31
|
export {
|
|
32
32
|
isRnxCloudCommandScope,
|
|
@@ -93,7 +93,12 @@ export interface RnxCommands {
|
|
|
93
93
|
// a `data:image/png;base64,` url, assembled by the service from the frame
|
|
94
94
|
screenshot(options?: { layers?: 'full' | 'tenant' | 'shell' }): Promise<string>
|
|
95
95
|
capture(): Promise<RnxScreenCapture>
|
|
96
|
-
|
|
96
|
+
// data wipes the app's stores and cold-remounts, keeping install-scoped
|
|
97
|
+
// credentials; full clears those too. from boots the wiped simulator on a
|
|
98
|
+
// wholesale live copy of another simulator's storage instead of empty
|
|
99
|
+
// stores. the bundle itself is never reinstalled: artifacts are immutable
|
|
100
|
+
// and sha-addressed, so there is no install to redo.
|
|
101
|
+
reset(options?: { strategy?: 'data' | 'full'; from?: string }): Promise<unknown>
|
|
97
102
|
// the one-call read: screenshot, tree, route, and recent errors together.
|
|
98
103
|
state(options?: SimStateOptions): Promise<unknown>
|
|
99
104
|
}
|
|
@@ -433,10 +438,39 @@ export class RemoteSimulator {
|
|
|
433
438
|
}
|
|
434
439
|
return result
|
|
435
440
|
},
|
|
436
|
-
reset: () =>
|
|
441
|
+
reset: (options) => {
|
|
442
|
+
if (
|
|
443
|
+
options?.strategy !== undefined &&
|
|
444
|
+
options.strategy !== 'data' &&
|
|
445
|
+
options.strategy !== 'full'
|
|
446
|
+
) {
|
|
447
|
+
throw new Error('RNX Cloud reset strategy must be data or full')
|
|
448
|
+
}
|
|
449
|
+
if (
|
|
450
|
+
options?.from !== undefined &&
|
|
451
|
+
(typeof options.from !== 'string' || options.from.length === 0)
|
|
452
|
+
) {
|
|
453
|
+
throw new Error('RNX Cloud reset from must name a simulator')
|
|
454
|
+
}
|
|
455
|
+
return this.sendCommand(
|
|
456
|
+
options === undefined
|
|
457
|
+
? { type: 'reset' }
|
|
458
|
+
: {
|
|
459
|
+
type: 'reset',
|
|
460
|
+
resetOptions: {
|
|
461
|
+
...(options.strategy === undefined
|
|
462
|
+
? {}
|
|
463
|
+
: { strategy: options.strategy }),
|
|
464
|
+
...(options.from === undefined ? {} : { from: options.from }),
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
)
|
|
468
|
+
},
|
|
437
469
|
state: (options) =>
|
|
438
470
|
this.sendCommand(
|
|
439
|
-
options === undefined
|
|
471
|
+
options === undefined
|
|
472
|
+
? { type: 'state' }
|
|
473
|
+
: { type: 'state', stateOptions: options },
|
|
440
474
|
),
|
|
441
475
|
}
|
|
442
476
|
}
|
|
@@ -653,7 +687,10 @@ function readConnectClaim(value: unknown): RnxCloudClaim | null {
|
|
|
653
687
|
return readClaim(value)
|
|
654
688
|
}
|
|
655
689
|
|
|
656
|
-
function readConnectScopes(
|
|
690
|
+
function readConnectScopes(
|
|
691
|
+
value: unknown,
|
|
692
|
+
token: string,
|
|
693
|
+
): readonly RnxCloudCommandScope[] {
|
|
657
694
|
// the full per-simulator token carries every scope except debug, which the
|
|
658
695
|
// service already guarantees. a scoped token learns its verbs from the get
|
|
659
696
|
// it just made, so a verb outside them is refused before any request.
|
|
@@ -678,7 +715,9 @@ function readConnectScopes(value: unknown, token: string): readonly RnxCloudComm
|
|
|
678
715
|
// attach to a simulator another process created. one get proves the token,
|
|
679
716
|
// and the simulator it returns drives with that token's scopes, takes no
|
|
680
717
|
// claim, and never steals one.
|
|
681
|
-
export async function connect(
|
|
718
|
+
export async function connect(
|
|
719
|
+
input: ConnectRemoteSimulatorInput,
|
|
720
|
+
): Promise<RemoteSimulator> {
|
|
682
721
|
if (!input.id) throw new Error('RNX Cloud connect needs a simulator id')
|
|
683
722
|
if (!input.token) throw new Error('RNX Cloud connect needs a token')
|
|
684
723
|
const origin = resolveOrigin(input.origin)
|
package/src/connect.ts
CHANGED
|
@@ -67,7 +67,7 @@ export async function connect(options: ConnectOptions = {}): Promise<SimClient>
|
|
|
67
67
|
const bridge = createCloudBridgeForParsed(target)
|
|
68
68
|
if (!bridge)
|
|
69
69
|
throw new SimConnectError('remote simulator session disappeared while connecting')
|
|
70
|
-
return new SimClient(bridge, cloud.
|
|
70
|
+
return new SimClient(bridge, cloud.simId, {
|
|
71
71
|
commandTimeoutMs: options.timeoutMs ?? 15_000,
|
|
72
72
|
})
|
|
73
73
|
}
|
package/src/menu.ts
CHANGED
|
@@ -29,6 +29,28 @@ export const SOOTSIM_AMBIENT_GLOW_LABELS: Record<SootSimAmbientGlowLevel, string
|
|
|
29
29
|
full: 'Full',
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// which ios home screen the simulator portrays. the home screen is authored
|
|
33
|
+
// twice (the flat ios 26 home and the ios 27 home measured off apple's art),
|
|
34
|
+
// so the era is one value every host carries: the menus, the persisted
|
|
35
|
+
// setting, the ?homeScreen= boot seed, the live set-home-screen push and
|
|
36
|
+
// electron's shared config.
|
|
37
|
+
export type SootSimHomeScreenVersion = 'ios26' | 'ios27'
|
|
38
|
+
export const SOOTSIM_HOME_SCREEN_VERSIONS = ['ios26', 'ios27'] as const
|
|
39
|
+
|
|
40
|
+
export function isSootSimHomeScreenVersion(
|
|
41
|
+
value: unknown,
|
|
42
|
+
): value is SootSimHomeScreenVersion {
|
|
43
|
+
return value === 'ios26' || value === 'ios27'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const SOOTSIM_HOME_SCREEN_VERSION_LABELS: Record<
|
|
47
|
+
SootSimHomeScreenVersion,
|
|
48
|
+
string
|
|
49
|
+
> = {
|
|
50
|
+
ios26: 'iOS 26',
|
|
51
|
+
ios27: 'iOS 27',
|
|
52
|
+
}
|
|
53
|
+
|
|
32
54
|
export type SootSimMenuItemDef =
|
|
33
55
|
| { type: 'separator' }
|
|
34
56
|
| {
|
|
@@ -56,6 +78,11 @@ export type SootSimMenuDef = {
|
|
|
56
78
|
|
|
57
79
|
export interface SootSimMenuState {
|
|
58
80
|
currentDevice: string
|
|
81
|
+
homeScreenVersion: SootSimHomeScreenVersion
|
|
82
|
+
// whether this surface is a contributor development surface. unreleased
|
|
83
|
+
// simulator surfaces (the iPhone Duo and its home-screen era switch) are
|
|
84
|
+
// offered here and nowhere a user can reach.
|
|
85
|
+
developmentMode: boolean
|
|
59
86
|
showBezels: boolean
|
|
60
87
|
showMenuBar: boolean
|
|
61
88
|
showRail: boolean
|
|
@@ -455,6 +482,23 @@ function windowMenu(state: SootSimMenuState): SootSimMenuDef {
|
|
|
455
482
|
actionPayload: level,
|
|
456
483
|
})),
|
|
457
484
|
},
|
|
485
|
+
// both eras stay selectable while the ios 27 home surfaces land one at a
|
|
486
|
+
// time, and the device's own os picks the starting value. the switch is
|
|
487
|
+
// the iPhone Duo's, so it is offered exactly where the Duo is.
|
|
488
|
+
...(state.developmentMode
|
|
489
|
+
? [
|
|
490
|
+
{
|
|
491
|
+
label: 'Home Screen',
|
|
492
|
+
items: SOOTSIM_HOME_SCREEN_VERSIONS.map((version) => ({
|
|
493
|
+
label: SOOTSIM_HOME_SCREEN_VERSION_LABELS[version],
|
|
494
|
+
type: 'radio' as const,
|
|
495
|
+
checked: state.homeScreenVersion === version,
|
|
496
|
+
action: 'set-home-screen',
|
|
497
|
+
actionPayload: version,
|
|
498
|
+
})),
|
|
499
|
+
},
|
|
500
|
+
]
|
|
501
|
+
: []),
|
|
458
502
|
{
|
|
459
503
|
label: 'Wireframe',
|
|
460
504
|
type: 'checkbox',
|
package/src/swift.ts
CHANGED
package/src/vite-plugin-one.ts
CHANGED
|
@@ -46,6 +46,18 @@ export function rnxPlugin(options: RnxPluginOptions = {}): Plugin {
|
|
|
46
46
|
return {
|
|
47
47
|
name: 'sootsim-one',
|
|
48
48
|
|
|
49
|
+
// use-latest-callback ships a CJS main (module.exports, no default) with
|
|
50
|
+
// an esm.mjs shim that re-imports it. react-native-bottom-tabs
|
|
51
|
+
// default-imports it from source served live, so vite hands the browser
|
|
52
|
+
// the raw CJS file and the import fails with 'does not provide an export
|
|
53
|
+
// named default' before anything mounts. forcing it through dep
|
|
54
|
+
// optimization lets esbuild's CJS interop answer the default import, and
|
|
55
|
+
// every bare import of it (including the nested copy's) resolves to the
|
|
56
|
+
// one pre-bundled instance.
|
|
57
|
+
config() {
|
|
58
|
+
return { optimizeDeps: { include: ['use-latest-callback'] } }
|
|
59
|
+
},
|
|
60
|
+
|
|
49
61
|
configureServer(server) {
|
|
50
62
|
if (options.enabled === false) return
|
|
51
63
|
const bundleUrl =
|