peachsim 0.1.637 → 0.1.639

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 (94) hide show
  1. package/README.md +8 -5
  2. package/cli/bridge-flow-runner.ts +442 -14
  3. package/cli/command-registry.ts +1 -0
  4. package/cli/commands/control.ts +42 -5
  5. package/cli/commands/inspect/core.ts +93 -24
  6. package/cli/commands/maestro.ts +2 -2
  7. package/cli/commands/serve.ts +13 -1
  8. package/cli/commands/swift.ts +326 -0
  9. package/cli/drivers/playwright-provisioning.ts +8 -16
  10. package/cli/drivers/playwright-sim-host.ts +3 -0
  11. package/cli/drivers/playwright.ts +1 -0
  12. package/cli/internal-child.ts +51 -8
  13. package/cli/main.ts +11 -0
  14. package/cli/outbound-endpoints.ts +5 -1
  15. package/dist-lib/agent-daemon-client.cjs +1 -1
  16. package/dist-lib/agent-events.cjs +1 -1
  17. package/dist-lib/agent-identity.cjs +1 -1
  18. package/dist-lib/agent-sessions.cjs +1 -1
  19. package/dist-lib/attached-projects.cjs +1 -1
  20. package/dist-lib/auth/shared-session.cjs +1 -1
  21. package/dist-lib/backend-origin.cjs +1 -1
  22. package/dist-lib/beta.cjs +1 -1
  23. package/dist-lib/beta.mjs +1 -1
  24. package/dist-lib/bridge-constants.cjs +1 -1
  25. package/dist-lib/bridge-contract-input.cjs +3 -2
  26. package/dist-lib/bridge-contract-input.mjs +3 -2
  27. package/dist-lib/bridge-contract.cjs +1 -1
  28. package/dist-lib/bridge-contract.mjs +1 -1
  29. package/dist-lib/capture-contract.cjs +1 -1
  30. package/dist-lib/capture-contract.mjs +1 -1
  31. package/dist-lib/cli-constants.cjs +1 -1
  32. package/dist-lib/cloud-contract.cjs +1 -1
  33. package/dist-lib/cloud-contract.mjs +1 -1
  34. package/dist-lib/cloud-test-args.cjs +1 -1
  35. package/dist-lib/cloud-test-args.mjs +1 -1
  36. package/dist-lib/cloud.cjs +1 -1
  37. package/dist-lib/cloud.mjs +1 -1
  38. package/dist-lib/config.cjs +1 -1
  39. package/dist-lib/detox/index.cjs +1 -1
  40. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  41. package/dist-lib/home-paths.cjs +1 -1
  42. package/dist-lib/host/bridge-host.cjs +4 -2
  43. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  44. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  45. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  46. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  47. package/dist-lib/host/websocket-proxy.cjs +1 -1
  48. package/dist-lib/index.cjs +88 -23
  49. package/dist-lib/jump-to-source-babel.cjs +1 -1
  50. package/dist-lib/jump-to-source-native.cjs +1 -1
  51. package/dist-lib/kotlin-host.cjs +1 -1
  52. package/dist-lib/menu.cjs +1 -1
  53. package/dist-lib/menu.mjs +1 -1
  54. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  55. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  56. package/dist-lib/metro-production-bundle.cjs +1 -1
  57. package/dist-lib/metro-production-bundle.mjs +1 -1
  58. package/dist-lib/metro.cjs +1 -1
  59. package/dist-lib/native-app-identity.cjs +1 -1
  60. package/dist-lib/native-app-identity.mjs +1 -1
  61. package/dist-lib/profiles.cjs +1 -1
  62. package/dist-lib/public-brand.cjs +1 -1
  63. package/dist-lib/react-native-host-modules.cjs +1 -1
  64. package/dist-lib/react-native-host-modules.mjs +1 -1
  65. package/dist-lib/render-mode.cjs +1 -1
  66. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  67. package/dist-lib/sdk.cjs +78 -20
  68. package/dist-lib/sdk.mjs +77 -20
  69. package/dist-lib/skills.cjs +441 -94
  70. package/dist-lib/swift-compile.cjs +89 -7
  71. package/dist-lib/swift-compile.mjs +89 -7
  72. package/dist-lib/swift-package.cjs +1 -1
  73. package/dist-lib/swift-package.mjs +1 -1
  74. package/dist-lib/swift-submit.cjs +89 -8
  75. package/dist-lib/swift-submit.mjs +78 -8
  76. package/dist-lib/swift.cjs +16 -33220
  77. package/dist-lib/vite-swift.cjs +510 -253
  78. package/dist-lib/vite.cjs +13 -4
  79. package/package.json +1 -1
  80. package/skills/contrast/SKILL.md +1 -0
  81. package/src/assetCatalogContract.ts +16 -0
  82. package/src/bridge-contract-input.ts +3 -2
  83. package/src/bridge-contract.ts +8 -0
  84. package/src/host/bridge-host.ts +5 -5
  85. package/src/metro-runtime-source.ts +15 -0
  86. package/src/native-seam-manifest.ts +4 -6
  87. package/src/swift-compile.ts +105 -9
  88. package/src/swift-guest.ts +51 -0
  89. package/src/swift-host.ts +900 -0
  90. package/src/swift-submit.ts +97 -6
  91. package/src/swift.ts +36 -8
  92. package/src/swiftContract.ts +52 -0
  93. package/src/vite-plugin-one.ts +4 -3
  94. package/src/vite-plugin-swift.ts +43 -755
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.637 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.639 | (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;
@@ -801,8 +801,16 @@ function streamFile(res, stream) {
801
801
  stream.pipe(res);
802
802
  }
803
803
 
804
- // src/vite-plugin-one.ts
804
+ // src/swift-submit.ts
805
+ var SWIFT_PACKAGE_PATH = "/__peach/swift-package/";
805
806
  var SWIFT_HMR_PATH = "/__peach/swift-hmr";
807
+ var MAX_SWIFT_COMPILE_SOURCE_BYTES = 2 * 1024 * 1024;
808
+ var MAX_SWIFT_COMPILE_RESOURCE_BYTES = 2 * 1024 * 1024;
809
+ var MAX_SWIFT_COMPILE_RESOURCE_TOTAL_BYTES = 8 * 1024 * 1024;
810
+ var MAX_SWIFT_COMPILE_IMAGE_BYTES = 4 * 1024 * 1024;
811
+ var MAX_SWIFT_COMPILE_IMAGE_TOTAL_BYTES = 16 * 1024 * 1024;
812
+
813
+ // src/vite-plugin-one.ts
806
814
  var SWIFT_ROUTE_PREFIX = "/__peach/swift";
807
815
  var swiftHost;
808
816
  var loadSwiftHost = () => swiftHost ??= import("peachsim/vite-swift");
@@ -904,7 +912,8 @@ function rnxPlugin(options = {}) {
904
912
  if (!served) next();
905
913
  }).catch(next);
906
914
  });
907
- server.watcher.on("change", (file) => {
915
+ server.watcher.on("all", (event, file) => {
916
+ if (event !== "add" && event !== "unlink" && event !== "change") return;
908
917
  if (activeSwiftPackages.size === 0) return;
909
918
  const moduleRoot = moduleRootFor();
910
919
  let packages;
@@ -952,7 +961,7 @@ function rnxPlugin(options = {}) {
952
961
  if (!pathname.startsWith(SWIFT_ROUTE_PREFIX)) return next();
953
962
  loadSwiftHost().then((host) => {
954
963
  if (host.serveSwiftPackageAsset(pathname, res)) return;
955
- const prefix2 = "/__peach/swift-package/";
964
+ const prefix2 = SWIFT_PACKAGE_PATH;
956
965
  if (!pathname.startsWith(prefix2)) return next();
957
966
  if (!swiftSockets) {
958
967
  res.statusCode = 501;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peachsim",
3
- "version": "0.1.637",
3
+ "version": "0.1.639",
4
4
  "description": "Vite and Metro plugins, testing drivers, and SDK exports for peach.",
5
5
  "author": "Tamagui LLC",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -19,6 +19,7 @@ automation, inspecting the render tree, and managing sims.
19
19
 
20
20
  | command | description |
21
21
  | --- | --- |
22
+ | `peach swift` | preview a SwiftPM app with hosted compilation and live Swift edits |
22
23
  | `peach maestro` | author, generate, and run Maestro YAML flows against peach |
23
24
  | `peach detox` | run existing Detox suites against peach on headless Chromium |
24
25
  | `peach cleanup` | inspect and reclaim local peach disk usage |
@@ -0,0 +1,16 @@
1
+ // the asset data exchanged by project hosts and the private renderer.
2
+ export interface SwiftImageSource {
3
+ uri: string
4
+ scale: number
5
+ }
6
+
7
+ // an app catalog color resolves by appearance or names a system color.
8
+ export type AppCatalogColor = { light: string; dark: string } | { system: string }
9
+
10
+ export interface SfSymbolVector {
11
+ viewBox: '0 0 24 24'
12
+ advance: number
13
+ box?: readonly [number, number, number, number]
14
+ controlBoxes?: Record<string, readonly [number, number, number]>
15
+ layers: readonly { d: string; color?: string }[]
16
+ }
@@ -1,4 +1,5 @@
1
1
  import { SIM_LONG_PRESS_MAX_MS } from './bridge-contract'
2
+ import { isRecord } from './is-record'
2
3
  // untrusted JSON -> bridge-contract argument. one owner for that conversion, so
3
4
  // every transport in front of a sim (the Peach Cloud sim service's HTTP and
4
5
  // WebSocket routes) validates its request bodies here rather than growing a
@@ -13,7 +14,6 @@ import type {
13
14
  } from './bridge-contract'
14
15
  import type { SimPerformStep, SimScreenshotOptions } from './sim-client'
15
16
  import type { SootSimReplayTarget } from '@peach/globals'
16
- import { isRecord } from './is-record'
17
17
 
18
18
  /** the caller sent a value this bridge argument cannot be read from. */
19
19
  export class BridgeInputError extends Error {
@@ -371,6 +371,7 @@ export function parseSemanticQuery(value: unknown): SimSemanticQuery {
371
371
  const query = recordValue(value, 'query')
372
372
  const kind = stringValue(query.kind, 'query.kind')
373
373
  if (kind === 'count') return { kind }
374
+ if (kind === 'accessibility') return { kind }
374
375
  if (kind === 'tree') {
375
376
  const depth = optionalNumber(query, 'depth', 'query')
376
377
  return { kind, ...(depth === undefined ? {} : { depth }) }
@@ -378,7 +379,7 @@ export function parseSemanticQuery(value: unknown): SimSemanticQuery {
378
379
  if (kind === 'find') {
379
380
  return { kind, selector: parseSemanticSelector(query.selector, 'query.selector') }
380
381
  }
381
- return validationFailure('query.kind', 'must be tree, find, or count')
382
+ return validationFailure('query.kind', 'must be tree, find, count, or accessibility')
382
383
  }
383
384
 
384
385
  function semanticResolveSelector(
@@ -9,6 +9,7 @@
9
9
  // this is the direction that ships.
10
10
 
11
11
  import type {
12
+ PeachAccessibilityNode,
12
13
  SootSimAgentActionDetail,
13
14
  SootSimCaptureFilter,
14
15
  SootSimExternalAppLifecycleOptions,
@@ -489,10 +490,17 @@ export type SimSemanticQuery =
489
490
  | { kind: 'tree'; depth?: number }
490
491
  | { kind: 'find'; selector: SimSemanticSelector }
491
492
  | { kind: 'count' }
493
+ // the XCTest accessibility projection. it is not a SimSemanticNode tree, so
494
+ // the snapshot rides in `accessibility` and `nodes` stays empty.
495
+ | { kind: 'accessibility' }
492
496
 
493
497
  export interface SimSemanticQueryResult {
494
498
  nodes: SimSemanticNode[]
495
499
  total: number
500
+ // present only for { kind: 'accessibility' }: the engine's recursive
501
+ // accessibility snapshot (PeachAccessibilityNode), the same tree the
502
+ // Maestro structural selectors resolve against.
503
+ accessibility?: PeachAccessibilityNode[]
496
504
  }
497
505
 
498
506
  export type SimSemanticResolveSelector = { testID: string } | { text: string }
@@ -229,6 +229,8 @@ interface BridgeForwardedCommand {
229
229
  export interface BridgeHostOptions {
230
230
  /** use 0 for an OS-assigned port; startAsync returns the bound address. */
231
231
  port?: number
232
+ /** listener address; defaults to 127.0.0.1. */
233
+ host?: string
232
234
  openUrl?: (url: string, options?: OpenUrlOptions) => Promise<void> | void
233
235
  /** Contrast-specific excludes for the agent host's dev-server scan. passed
234
236
  * through to AgentHost; see packages/peach-engine/src/dev-scan-excludes.ts
@@ -404,6 +406,7 @@ export class SootSimBridgeHost {
404
406
  // of reconnecting the zombie straight back in.
405
407
  private static SIM_IDLE_REAP_TTL_MS = 30 * 60_000
406
408
 
409
+ private bindHost: string
407
410
  private preferredPort: number
408
411
  private portFallbackCount: number
409
412
  private shouldWriteLockfile: boolean
@@ -438,6 +441,7 @@ export class SootSimBridgeHost {
438
441
  private static SCAN_FRESH_MS = 2000
439
442
 
440
443
  constructor(opts: BridgeHostOptions = {}) {
444
+ this.bindHost = opts.host ?? '127.0.0.1'
441
445
  this.preferredPort = opts.port ?? DEFAULT_SOOTSIM_BRIDGE_PORT
442
446
  this.port = this.preferredPort
443
447
  // default off — callers that want the lockfile (peach serve) opt in.
@@ -533,11 +537,7 @@ export class SootSimBridgeHost {
533
537
  }
534
538
  server.once('error', onError)
535
539
 
536
- // loopback-only bind. we listen on 127.0.0.1 explicitly because the
537
- // daemon serves the active runtime's dist as plain static files —
538
- // never expose that to LAN peers. tests + the electron renderer use
539
- // 127.0.0.1 explicitly to avoid the localhost-vs-::1 DNS coinflip.
540
- server.listen(port, '127.0.0.1', () => {
540
+ server.listen(port, this.bindHost, () => {
541
541
  if (settled) return
542
542
  const address = server.address()
543
543
  if (!address || typeof address === 'string') {
@@ -0,0 +1,15 @@
1
+ // GENERATED by scripts/generate-runtime-source.ts — do not edit.
2
+ // verbatim metro-runtime@0.84.4 sources, inlined so the worker never touches a filesystem.
3
+
4
+ export const METRO_RUNTIME_VERSION = '0.84.4'
5
+
6
+ // metro-runtime/src/polyfills/require.js — defines global.__d / __r / __c / __accept
7
+ export const METRO_REQUIRE_POLYFILL =
8
+ '"use strict";\n\nglobal.__r = metroRequire;\nglobal[`${__METRO_GLOBAL_PREFIX__}__d`] = define;\nglobal.__c = clear;\nglobal.__registerSegment = registerSegment;\nvar modules = clear();\nconst EMPTY = {};\nconst CYCLE_DETECTED = {};\nconst { hasOwnProperty } = {};\nif (__DEV__) {\n global.$RefreshReg$ = global.$RefreshReg$ ?? (() => {});\n global.$RefreshSig$ = global.$RefreshSig$ ?? (() => (type) => type);\n}\nfunction clear() {\n modules = new Map();\n return modules;\n}\nif (__DEV__) {\n var verboseNamesToModuleIds = new Map();\n var getModuleIdForVerboseName = (verboseName) => {\n const moduleId = verboseNamesToModuleIds.get(verboseName);\n if (moduleId == null) {\n throw new Error(`Unknown named module: "${verboseName}"`);\n }\n return moduleId;\n };\n var initializingModuleIds = [];\n}\nfunction define(factory, moduleId, dependencyMap) {\n if (modules.has(moduleId)) {\n if (__DEV__) {\n const inverseDependencies = arguments[4];\n if (inverseDependencies) {\n global.__accept(moduleId, factory, dependencyMap, inverseDependencies);\n }\n }\n return;\n }\n const mod = {\n dependencyMap,\n factory,\n hasError: false,\n importedAll: EMPTY,\n importedDefault: EMPTY,\n isInitialized: false,\n publicModule: {\n exports: {},\n },\n };\n modules.set(moduleId, mod);\n if (__DEV__) {\n mod.hot = createHotReloadingObject();\n const verboseName = arguments[3];\n if (verboseName) {\n mod.verboseName = verboseName;\n verboseNamesToModuleIds.set(verboseName, moduleId);\n }\n }\n}\nfunction metroRequire(moduleId, maybeNameForDev) {\n if (moduleId === null) {\n if (__DEV__ && typeof maybeNameForDev === "string") {\n throw new Error("Cannot find module \'" + maybeNameForDev + "\'");\n }\n throw new Error("Cannot find module");\n }\n if (__DEV__ && typeof moduleId === "string") {\n const verboseName = moduleId;\n moduleId = getModuleIdForVerboseName(verboseName);\n console.warn(\n `Requiring module "${verboseName}" by name is only supported for ` +\n "debugging purposes and will BREAK IN PRODUCTION!",\n );\n }\n const moduleIdReallyIsNumber = moduleId;\n if (__DEV__) {\n const initializingIndex = initializingModuleIds.indexOf(\n moduleIdReallyIsNumber,\n );\n if (initializingIndex !== -1) {\n const cycle = initializingModuleIds\n .slice(initializingIndex)\n .map((id) => modules.get(id)?.verboseName ?? "[unknown]");\n if (shouldPrintRequireCycle(cycle)) {\n cycle.push(cycle[0]);\n console.warn(\n `Require cycle: ${cycle.join(" -> ")}\\n\\n` +\n "Require cycles are allowed, but can result in uninitialized values. " +\n "Consider refactoring to remove the need for a cycle.",\n );\n }\n }\n }\n const module = modules.get(moduleIdReallyIsNumber);\n return module && module.isInitialized\n ? module.publicModule.exports\n : guardedLoadModule(moduleIdReallyIsNumber, module);\n}\nfunction shouldPrintRequireCycle(modules) {\n const regExps =\n global[__METRO_GLOBAL_PREFIX__ + "__requireCycleIgnorePatterns"];\n if (!Array.isArray(regExps)) {\n return true;\n }\n const isIgnored = (module) =>\n module != null && regExps.some((regExp) => regExp.test(module));\n return modules.every((module) => !isIgnored(module));\n}\nfunction metroImportDefault(moduleId) {\n if (__DEV__ && typeof moduleId === "string") {\n const verboseName = moduleId;\n moduleId = getModuleIdForVerboseName(verboseName);\n }\n const moduleIdReallyIsNumber = moduleId;\n const maybeInitializedModule = modules.get(moduleIdReallyIsNumber);\n if (\n maybeInitializedModule &&\n maybeInitializedModule.importedDefault !== EMPTY\n ) {\n return maybeInitializedModule.importedDefault;\n }\n const exports = metroRequire(moduleIdReallyIsNumber);\n const importedDefault =\n exports && exports.__esModule ? exports.default : exports;\n const initializedModule = modules.get(moduleIdReallyIsNumber);\n return (initializedModule.importedDefault = importedDefault);\n}\nmetroRequire.importDefault = metroImportDefault;\nfunction metroImportAll(moduleId) {\n if (__DEV__ && typeof moduleId === "string") {\n const verboseName = moduleId;\n moduleId = getModuleIdForVerboseName(verboseName);\n }\n const moduleIdReallyIsNumber = moduleId;\n const maybeInitializedModule = modules.get(moduleIdReallyIsNumber);\n if (maybeInitializedModule && maybeInitializedModule.importedAll !== EMPTY) {\n return maybeInitializedModule.importedAll;\n }\n const exports = metroRequire(moduleIdReallyIsNumber);\n let importedAll;\n if (exports && exports.__esModule) {\n importedAll = exports;\n } else {\n importedAll = {};\n if (exports) {\n for (const key in exports) {\n if (hasOwnProperty.call(exports, key)) {\n importedAll[key] = exports[key];\n }\n }\n }\n importedAll.default = exports;\n }\n const initializedModule = modules.get(moduleIdReallyIsNumber);\n return (initializedModule.importedAll = importedAll);\n}\nmetroRequire.importAll = metroImportAll;\nmetroRequire.context = function fallbackRequireContext() {\n if (__DEV__) {\n throw new Error(\n "The experimental Metro feature `require.context` is not enabled in your project.\\nThis can be enabled by setting the `transformer.unstable_allowRequireContext` property to `true` in your Metro configuration.",\n );\n }\n throw new Error(\n "The experimental Metro feature `require.context` is not enabled in your project.",\n );\n};\nmetroRequire.resolveWeak = function fallbackRequireResolveWeak() {\n if (__DEV__) {\n throw new Error(\n "require.resolveWeak cannot be called dynamically. Ensure you are using the same version of `metro` and `metro-runtime`.",\n );\n }\n throw new Error("require.resolveWeak cannot be called dynamically.");\n};\nlet inGuard = false;\nfunction guardedLoadModule(moduleId, module) {\n if (!inGuard && global.ErrorUtils) {\n inGuard = true;\n let returnValue;\n try {\n returnValue = loadModuleImplementation(moduleId, module);\n } catch (e) {\n global.ErrorUtils.reportFatalError(e);\n }\n inGuard = false;\n return returnValue;\n } else {\n return loadModuleImplementation(moduleId, module);\n }\n}\nconst ID_MASK_SHIFT = 16;\nconst LOCAL_ID_MASK = ~0 >>> ID_MASK_SHIFT;\nfunction unpackModuleId(moduleId) {\n const segmentId = moduleId >>> ID_MASK_SHIFT;\n const localId = moduleId & LOCAL_ID_MASK;\n return {\n segmentId,\n localId,\n };\n}\nmetroRequire.unpackModuleId = unpackModuleId;\nfunction packModuleId(value) {\n return (value.segmentId << ID_MASK_SHIFT) + value.localId;\n}\nmetroRequire.packModuleId = packModuleId;\nconst moduleDefinersBySegmentID = [];\nconst definingSegmentByModuleID = new Map();\nfunction registerSegment(segmentId, moduleDefiner, moduleIds) {\n moduleDefinersBySegmentID[segmentId] = moduleDefiner;\n if (__DEV__) {\n if (segmentId === 0 && moduleIds) {\n throw new Error(\n "registerSegment: Expected moduleIds to be null for main segment",\n );\n }\n if (segmentId !== 0 && !moduleIds) {\n throw new Error(\n "registerSegment: Expected moduleIds to be passed for segment #" +\n segmentId,\n );\n }\n }\n if (moduleIds) {\n moduleIds.forEach((moduleId) => {\n if (!modules.has(moduleId) && !definingSegmentByModuleID.has(moduleId)) {\n definingSegmentByModuleID.set(moduleId, segmentId);\n }\n });\n }\n}\nfunction loadModuleImplementation(moduleId, module) {\n if (!module && moduleDefinersBySegmentID.length > 0) {\n const segmentId = definingSegmentByModuleID.get(moduleId) ?? 0;\n const definer = moduleDefinersBySegmentID[segmentId];\n if (definer != null) {\n definer(moduleId);\n module = modules.get(moduleId);\n definingSegmentByModuleID.delete(moduleId);\n }\n }\n const nativeRequire = global.nativeRequire;\n if (!module && nativeRequire) {\n const { segmentId, localId } = unpackModuleId(moduleId);\n nativeRequire(localId, segmentId);\n module = modules.get(moduleId);\n }\n if (!module) {\n throw unknownModuleError(moduleId);\n }\n if (module.hasError) {\n throw module.error;\n }\n if (__DEV__) {\n var Systrace = requireSystrace();\n var Refresh = requireRefresh();\n }\n module.isInitialized = true;\n const { factory, dependencyMap } = module;\n if (__DEV__) {\n initializingModuleIds.push(moduleId);\n }\n try {\n if (__DEV__) {\n Systrace.beginEvent("JS_require_" + (module.verboseName || moduleId));\n }\n const moduleObject = module.publicModule;\n if (__DEV__) {\n moduleObject.hot = module.hot;\n var prevRefreshReg = global.$RefreshReg$;\n var prevRefreshSig = global.$RefreshSig$;\n if (Refresh != null) {\n const RefreshRuntime = Refresh;\n global.$RefreshReg$ = (type, id) => {\n const prefixedModuleId =\n __METRO_GLOBAL_PREFIX__ + " " + moduleId + " " + id;\n RefreshRuntime.register(type, prefixedModuleId);\n };\n global.$RefreshSig$ =\n RefreshRuntime.createSignatureFunctionForTransform;\n }\n }\n moduleObject.id = moduleId;\n factory(\n global,\n metroRequire,\n metroImportDefault,\n metroImportAll,\n moduleObject,\n moduleObject.exports,\n dependencyMap,\n );\n if (!__DEV__) {\n module.factory = undefined;\n module.dependencyMap = undefined;\n }\n if (__DEV__) {\n Systrace.endEvent();\n if (Refresh != null) {\n const prefixedModuleId = __METRO_GLOBAL_PREFIX__ + " " + moduleId;\n registerExportsForReactRefresh(\n Refresh,\n moduleObject.exports,\n prefixedModuleId,\n );\n }\n }\n return moduleObject.exports;\n } catch (e) {\n module.hasError = true;\n module.error = e;\n module.isInitialized = false;\n module.publicModule.exports = undefined;\n throw e;\n } finally {\n if (__DEV__) {\n if (initializingModuleIds.pop() !== moduleId) {\n throw new Error(\n "initializingModuleIds is corrupt; something is terribly wrong",\n );\n }\n global.$RefreshReg$ = prevRefreshReg;\n global.$RefreshSig$ = prevRefreshSig;\n }\n }\n}\nfunction unknownModuleError(id) {\n let message = \'Requiring unknown module "\' + id + \'".\';\n if (__DEV__) {\n message +=\n " If you are sure the module exists, try restarting Metro. " +\n "You may also want to run `yarn` or `npm install`.";\n }\n return Error(message);\n}\nif (__DEV__) {\n metroRequire.Systrace = {\n beginEvent: () => {},\n endEvent: () => {},\n };\n metroRequire.getModules = () => {\n return modules;\n };\n var createHotReloadingObject = function () {\n const hot = {\n _acceptCallback: null,\n _disposeCallback: null,\n _didAccept: false,\n accept: (callback) => {\n hot._didAccept = true;\n hot._acceptCallback = callback;\n },\n dispose: (callback) => {\n hot._disposeCallback = callback;\n },\n };\n return hot;\n };\n let reactRefreshTimeout = null;\n const metroHotUpdateModule = function (\n id,\n factory,\n dependencyMap,\n inverseDependencies,\n ) {\n const mod = modules.get(id);\n if (!mod) {\n if (factory) {\n return;\n }\n throw unknownModuleError(id);\n }\n const forceFullRefreshPatterns =\n global[__METRO_GLOBAL_PREFIX__ + "__unstable_forceFullRefreshPatterns"];\n if (\n forceFullRefreshPatterns != null &&\n Array.isArray(forceFullRefreshPatterns) &&\n mod.verboseName != null &&\n forceFullRefreshPatterns.some((pattern) =>\n pattern.test(mod.verboseName ?? ""),\n )\n ) {\n performFullRefresh("Module matched unstable_forceFullRefreshPatterns", {\n source: mod,\n });\n return;\n }\n if (!mod.hasError && !mod.isInitialized) {\n mod.factory = factory;\n mod.dependencyMap = dependencyMap;\n return;\n }\n const Refresh = requireRefresh();\n const refreshBoundaryIDs = new Set();\n let didBailOut = false;\n let updatedModuleIDs;\n try {\n updatedModuleIDs = topologicalSort(\n [id],\n (pendingID) => {\n const pendingModule = modules.get(pendingID);\n if (pendingModule == null) {\n return [];\n }\n const pendingHot = pendingModule.hot;\n if (pendingHot == null) {\n throw new Error(\n "[Refresh] Expected module.hot to always exist in DEV.",\n );\n }\n let canAccept = pendingHot._didAccept;\n if (!canAccept && Refresh != null) {\n const isBoundary = isReactRefreshBoundary(\n Refresh,\n pendingModule.publicModule.exports,\n );\n if (isBoundary) {\n canAccept = true;\n refreshBoundaryIDs.add(pendingID);\n }\n }\n if (canAccept) {\n return [];\n }\n const parentIDs = inverseDependencies[pendingID];\n if (parentIDs.length === 0) {\n performFullRefresh("No root boundary", {\n source: mod,\n failed: pendingModule,\n });\n didBailOut = true;\n return [];\n }\n return parentIDs;\n },\n () => didBailOut,\n ).reverse();\n } catch (e) {\n if (e === CYCLE_DETECTED) {\n performFullRefresh("Dependency cycle", {\n source: mod,\n });\n return;\n }\n throw e;\n }\n if (didBailOut) {\n return;\n }\n const seenModuleIDs = new Set();\n for (let i = 0; i < updatedModuleIDs.length; i++) {\n const updatedID = updatedModuleIDs[i];\n if (seenModuleIDs.has(updatedID)) {\n continue;\n }\n seenModuleIDs.add(updatedID);\n const updatedMod = modules.get(updatedID);\n if (updatedMod == null) {\n throw new Error("[Refresh] Expected to find the updated module.");\n }\n const prevExports = updatedMod.publicModule.exports;\n const didError = runUpdatedModule(\n updatedID,\n updatedID === id ? factory : undefined,\n updatedID === id ? dependencyMap : undefined,\n );\n const nextExports = updatedMod.publicModule.exports;\n if (didError) {\n return;\n }\n if (refreshBoundaryIDs.has(updatedID)) {\n const isNoLongerABoundary = !isReactRefreshBoundary(\n Refresh,\n nextExports,\n );\n const didInvalidate = shouldInvalidateReactRefreshBoundary(\n Refresh,\n prevExports,\n nextExports,\n );\n if (isNoLongerABoundary || didInvalidate) {\n const parentIDs = inverseDependencies[updatedID];\n if (parentIDs.length === 0) {\n performFullRefresh(\n isNoLongerABoundary\n ? "No longer a boundary"\n : "Invalidated boundary",\n {\n source: mod,\n failed: updatedMod,\n },\n );\n return;\n }\n for (let j = 0; j < parentIDs.length; j++) {\n const parentID = parentIDs[j];\n const parentMod = modules.get(parentID);\n if (parentMod == null) {\n throw new Error("[Refresh] Expected to find parent module.");\n }\n const canAcceptParent = isReactRefreshBoundary(\n Refresh,\n parentMod.publicModule.exports,\n );\n if (canAcceptParent) {\n refreshBoundaryIDs.add(parentID);\n updatedModuleIDs.push(parentID);\n } else {\n performFullRefresh("Invalidated boundary", {\n source: mod,\n failed: parentMod,\n });\n return;\n }\n }\n }\n }\n }\n if (Refresh != null) {\n if (reactRefreshTimeout == null) {\n reactRefreshTimeout = setTimeout(() => {\n reactRefreshTimeout = null;\n Refresh.performReactRefresh();\n }, 30);\n }\n }\n };\n const topologicalSort = function (roots, getEdges, earlyStop) {\n const result = [];\n const visited = new Set();\n const stack = new Set();\n function traverseDependentNodes(node) {\n if (stack.has(node)) {\n throw CYCLE_DETECTED;\n }\n if (visited.has(node)) {\n return;\n }\n visited.add(node);\n stack.add(node);\n const dependentNodes = getEdges(node);\n if (earlyStop(node)) {\n stack.delete(node);\n return;\n }\n dependentNodes.forEach((dependent) => {\n traverseDependentNodes(dependent);\n });\n stack.delete(node);\n result.push(node);\n }\n roots.forEach((root) => {\n traverseDependentNodes(root);\n });\n return result;\n };\n const runUpdatedModule = function (id, factory, dependencyMap) {\n const mod = modules.get(id);\n if (mod == null) {\n throw new Error("[Refresh] Expected to find the module.");\n }\n const { hot } = mod;\n if (!hot) {\n throw new Error("[Refresh] Expected module.hot to always exist in DEV.");\n }\n if (hot._disposeCallback) {\n try {\n hot._disposeCallback();\n } catch (error) {\n console.error(\n `Error while calling dispose handler for module ${id}: `,\n error,\n );\n }\n }\n if (factory) {\n mod.factory = factory;\n }\n if (dependencyMap) {\n mod.dependencyMap = dependencyMap;\n }\n mod.hasError = false;\n mod.error = undefined;\n mod.importedAll = EMPTY;\n mod.importedDefault = EMPTY;\n mod.isInitialized = false;\n const prevExports = mod.publicModule.exports;\n mod.publicModule.exports = {};\n hot._didAccept = false;\n hot._acceptCallback = null;\n hot._disposeCallback = null;\n metroRequire(id);\n if (mod.hasError) {\n mod.hasError = false;\n mod.isInitialized = true;\n mod.error = null;\n mod.publicModule.exports = prevExports;\n return true;\n }\n if (hot._acceptCallback) {\n try {\n hot._acceptCallback();\n } catch (error) {\n console.error(\n `Error while calling accept handler for module ${id}: `,\n error,\n );\n }\n }\n return false;\n };\n const performFullRefresh = (reason, modules) => {\n if (\n typeof window !== "undefined" &&\n window.location != null &&\n typeof window.location.reload === "function"\n ) {\n window.location.reload();\n } else {\n const Refresh = requireRefresh();\n if (Refresh != null) {\n const sourceName = modules.source?.verboseName ?? "unknown";\n const failedName = modules.failed?.verboseName ?? "unknown";\n Refresh.performFullRefresh(\n `Fast Refresh - ${reason} <${sourceName}> <${failedName}>`,\n );\n } else {\n console.warn("Could not reload the application after an edit.");\n }\n }\n };\n const isExportSafeToAccess = (moduleExports, key) => {\n return (\n moduleExports?.__esModule ||\n Object.getOwnPropertyDescriptor(moduleExports, key)?.get == null\n );\n };\n var isReactRefreshBoundary = function (Refresh, moduleExports) {\n if (Refresh.isLikelyComponentType(moduleExports)) {\n return true;\n }\n if (moduleExports == null || typeof moduleExports !== "object") {\n return false;\n }\n let hasExports = false;\n let areAllExportsComponents = true;\n for (const key in moduleExports) {\n hasExports = true;\n if (key === "__esModule") {\n continue;\n } else if (!isExportSafeToAccess(moduleExports, key)) {\n return false;\n }\n const exportValue = moduleExports[key];\n if (!Refresh.isLikelyComponentType(exportValue)) {\n areAllExportsComponents = false;\n }\n }\n return hasExports && areAllExportsComponents;\n };\n var shouldInvalidateReactRefreshBoundary = (\n Refresh,\n prevExports,\n nextExports,\n ) => {\n const prevSignature = getRefreshBoundarySignature(Refresh, prevExports);\n const nextSignature = getRefreshBoundarySignature(Refresh, nextExports);\n if (prevSignature.length !== nextSignature.length) {\n return true;\n }\n for (let i = 0; i < nextSignature.length; i++) {\n if (prevSignature[i] !== nextSignature[i]) {\n return true;\n }\n }\n return false;\n };\n var getRefreshBoundarySignature = (Refresh, moduleExports) => {\n const signature = [];\n signature.push(Refresh.getFamilyByType(moduleExports));\n if (moduleExports == null || typeof moduleExports !== "object") {\n return signature;\n }\n for (const key in moduleExports) {\n if (key === "__esModule") {\n continue;\n } else if (!isExportSafeToAccess(moduleExports, key)) {\n continue;\n }\n const exportValue = moduleExports[key];\n signature.push(key);\n signature.push(Refresh.getFamilyByType(exportValue));\n }\n return signature;\n };\n var registerExportsForReactRefresh = (Refresh, moduleExports, moduleID) => {\n Refresh.register(moduleExports, moduleID + " %exports%");\n if (moduleExports == null || typeof moduleExports !== "object") {\n return;\n }\n for (const key in moduleExports) {\n if (!isExportSafeToAccess(moduleExports, key)) {\n continue;\n }\n const exportValue = moduleExports[key];\n const typeID = moduleID + " %exports% " + key;\n Refresh.register(exportValue, typeID);\n }\n };\n global.__accept = metroHotUpdateModule;\n}\nif (__DEV__) {\n var requireSystrace = function requireSystrace() {\n return (\n global[__METRO_GLOBAL_PREFIX__ + "__SYSTRACE"] || metroRequire.Systrace\n );\n };\n var requireRefresh = function requireRefresh() {\n return (\n global[__METRO_GLOBAL_PREFIX__ + "__ReactRefresh"] ||\n global[global.__METRO_GLOBAL_PREFIX__ + "__ReactRefresh"] ||\n metroRequire.Refresh\n );\n };\n}\n'
9
+
10
+ // metro-runtime/src/modules/asyncRequire.js — the module import() compiles down to
11
+ export const METRO_ASYNC_REQUIRE =
12
+ '"use strict";\n\nfunction maybeLoadBundle(moduleID, paths) {\n const loadBundle = global[`${__METRO_GLOBAL_PREFIX__}__loadBundleAsync`];\n if (loadBundle != null) {\n const stringModuleID = String(moduleID);\n if (paths != null) {\n const bundlePath = paths[stringModuleID];\n if (bundlePath != null) {\n return loadBundle(bundlePath);\n }\n }\n }\n return undefined;\n}\nfunction asyncRequireImpl(moduleID, paths) {\n const maybeLoadBundlePromise = maybeLoadBundle(moduleID, paths);\n const importAll = () => require.importAll(moduleID);\n if (maybeLoadBundlePromise != null) {\n return maybeLoadBundlePromise.then(importAll);\n }\n return importAll();\n}\nasync function asyncRequire(moduleID, paths, moduleName) {\n return asyncRequireImpl(moduleID, paths);\n}\nasyncRequire.unstable_importMaybeSync = function unstable_importMaybeSync(\n moduleID,\n paths,\n) {\n return asyncRequireImpl(moduleID, paths);\n};\nasyncRequire.prefetch = function (moduleID, paths, moduleName) {\n maybeLoadBundle(moduleID, paths)?.then(\n () => {},\n () => {},\n );\n};\nmodule.exports = asyncRequire;\n'
13
+
14
+ // metro-runtime/src/modules/empty-module.js — what a browser-field `false` redirect resolves to
15
+ export const METRO_EMPTY_MODULE = '"use strict";\n'
@@ -153,15 +153,13 @@ export const NATIVE_SEAM_MANIFEST: NativeSeamEntry[] = [
153
153
  },
154
154
  {
155
155
  pkg: '@bam.tech/react-native-image-resizer',
156
- // upstream is not shipped to most guest bundles via node_modules — it
157
- // arrives through the bundle-loader stub-registry which routes the
158
- // package entry to `stubs/bam-react-native-image-resizer.ts`. listed here
159
- // for documentation parity; vite-path redirect would key on
160
- // `NativeImageResizer`.
156
+ // a Metro bundle reaches the same seam through the ImageResizer
157
+ // TurboModule that `stubs/bam-react-native-image-resizer-register.ts`
158
+ // registers.
161
159
  seamBasenames: ['NativeImageResizer'],
162
160
  target: seamPath('bam-react-native-image-resizer'),
163
161
  notes:
164
- 'ImageResizer TurboModule — browser-side canvas resize implementing the iOS contain/cover/stretch + onlyScaleDown semantics from ios/ImageResizer.mm',
162
+ 'ImageResizer TurboModule following ios/ImageResizer.mm: loader decode, rotation, scaling, and the file written to the app container',
165
163
  },
166
164
  {
167
165
  pkg: 'react-native-adjust',
@@ -224,8 +224,8 @@ export function swiftArtifactHash(
224
224
  moduleRoot: string,
225
225
  ): string {
226
226
  const hash = createHash('sha256')
227
- // v2: sources compile with os(iOS) true (swiftSourceAsIOS)
228
- hash.update(`swift-compile/v2\n${release}\n`)
227
+ // v3: sources compile with os(iOS) true on code lines (swiftSourceAsIOS)
228
+ hash.update(`swift-compile/v3\n${release}\n`)
229
229
  if (input.oneModule) hash.update('one-module\n')
230
230
  hash.update(linkedModuleDigest(moduleRoot))
231
231
  for (const file of Object.keys(input.files).sort()) {
@@ -347,15 +347,111 @@ function syncSwiftTree(base: string, files: Map<string, string>): void {
347
347
  // every iOS branch of the app would compile out. a condition line names iOS
348
348
  // as `true` padded to the same length, so diagnostics keep their columns;
349
349
  // os(WASI) still holds for code written for wasm. only `#if` and `#elseif`
350
- // lines change, and the artifact hash stays over the original text.
351
- const CONDITION_LINE = /^([ \t]*#(?:if|elseif)\b)(.*)$/gm
350
+ // lines that begin in code change: a line inside a string literal or a block
351
+ // comment is content. the artifact hash stays over the original text.
352
+ const DIRECTIVE = /^[ \t]*#(?:if|elseif)\b/
352
353
  const OS_IOS = /\bos\(\s*iOS\s*\)/g
353
354
  export function swiftSourceAsIOS(source: string): string {
354
- return source.replace(
355
- CONDITION_LINE,
356
- (_, directive: string, condition: string) =>
357
- directive + condition.replace(OS_IOS, (match) => 'true'.padEnd(match.length)),
358
- )
355
+ const code = swiftCodeLineStarts(source)
356
+ let offset = 0
357
+ return source
358
+ .split('\n')
359
+ .map((line) => {
360
+ const start = offset
361
+ offset += line.length + 1
362
+ if (!code.has(start) || !DIRECTIVE.test(line)) return line
363
+ return line.replace(OS_IOS, (match) => 'true'.padEnd(match.length))
364
+ })
365
+ .join('\n')
366
+ }
367
+
368
+ // the offsets of the lines that begin in code, from a scan of Swift's lexical
369
+ // contexts: nested block comments, line comments, string literals (single and
370
+ // multiline, raw with any number of #, interpolations holding code and
371
+ // strings of their own) and #/ /# regex literals. a bare /regex/ literal
372
+ // reads as code.
373
+ type Lexical =
374
+ | { kind: 'code'; parens: number }
375
+ | { kind: 'block'; depth: number }
376
+ | { kind: 'string'; hashes: number; multiline: boolean }
377
+ | { kind: 'regex'; hashes: number }
378
+ function swiftCodeLineStarts(source: string): Set<number> {
379
+ const starts = new Set<number>()
380
+ const stack: Lexical[] = [{ kind: 'code', parens: 0 }]
381
+ const hashesAt = (at: number) => {
382
+ let end = at
383
+ while (source[end] === '#') end++
384
+ return end - at
385
+ }
386
+ const closes = (at: number, hashes: number) => source.startsWith('#'.repeat(hashes), at)
387
+ let i = 0
388
+ while (i < source.length) {
389
+ const top = stack[stack.length - 1]
390
+ if ((i === 0 || source[i - 1] === '\n') && top.kind === 'code' && stack.length === 1)
391
+ starts.add(i)
392
+ const char = source[i]
393
+ if (top.kind === 'block') {
394
+ if (source.startsWith('/*', i)) {
395
+ top.depth++
396
+ i += 2
397
+ } else if (source.startsWith('*/', i)) {
398
+ if (--top.depth === 0) stack.pop()
399
+ i += 2
400
+ } else i++
401
+ continue
402
+ }
403
+ if (top.kind === 'string') {
404
+ if (char === '\\' && closes(i + 1, top.hashes)) {
405
+ const after = i + 1 + top.hashes
406
+ if (source[after] === '(') {
407
+ stack.push({ kind: 'code', parens: 0 })
408
+ i = after + 1
409
+ } else i = after + 1
410
+ continue
411
+ }
412
+ const quote = top.multiline ? '"""' : '"'
413
+ if (source.startsWith(quote, i) && closes(i + quote.length, top.hashes)) {
414
+ stack.pop()
415
+ i += quote.length + top.hashes
416
+ } else i++
417
+ continue
418
+ }
419
+ if (top.kind === 'regex') {
420
+ if (char === '/' && closes(i + 1, top.hashes)) {
421
+ stack.pop()
422
+ i += 1 + top.hashes
423
+ } else i++
424
+ continue
425
+ }
426
+ if (source.startsWith('//', i)) {
427
+ const end = source.indexOf('\n', i)
428
+ i = end === -1 ? source.length : end
429
+ } else if (source.startsWith('/*', i)) {
430
+ stack.push({ kind: 'block', depth: 1 })
431
+ i += 2
432
+ } else if (char === '"' || char === '#') {
433
+ const hashes = hashesAt(i)
434
+ const at = i + hashes
435
+ if (source.startsWith('"""', at)) {
436
+ stack.push({ kind: 'string', hashes, multiline: true })
437
+ i = at + 3
438
+ } else if (source[at] === '"') {
439
+ stack.push({ kind: 'string', hashes, multiline: false })
440
+ i = at + 1
441
+ } else if (hashes > 0 && source[at] === '/') {
442
+ stack.push({ kind: 'regex', hashes })
443
+ i = at + 1
444
+ } else i = Math.max(at, i + 1)
445
+ } else if (char === '(') {
446
+ top.parens++
447
+ i++
448
+ } else if (char === ')') {
449
+ if (top.parens === 0 && stack.length > 1) stack.pop()
450
+ else top.parens--
451
+ i++
452
+ } else i++
453
+ }
454
+ return starts
359
455
  }
360
456
 
361
457
  const compiledSource = (file: string, contents: string) =>
@@ -0,0 +1,51 @@
1
+ // the built-in guest uses the existing Metro runtime and host module seam.
2
+ // the author supplies Swift; this entry carries no engine or compiler code.
3
+ import {
4
+ appendRNXMetroModuleIdentityFooter,
5
+ RNX_METRO_MODULE_IDENTITY_VERSION,
6
+ } from './metro-production-bundle'
7
+ import { METRO_REQUIRE_POLYFILL } from './metro-runtime-source'
8
+
9
+ export function swiftGuestBundle(packageName: string): string {
10
+ const modules = [
11
+ {
12
+ name: 'peach-swift-entry.js',
13
+ code: `const { AppRegistry } = require(1)
14
+ const { createSwiftPreviewEntry } = require(2)
15
+ AppRegistry.registerComponent('PeachSwift', () => createSwiftPreviewEntry(${JSON.stringify(packageName)}))`,
16
+ deps: [1, 2],
17
+ },
18
+ {
19
+ name: 'react-native',
20
+ code: `module.exports = global.__contrastHostModules['react-native']`,
21
+ deps: [],
22
+ },
23
+ {
24
+ name: 'peachsim/swift',
25
+ code: `module.exports = global.__contrastHostModules['peachsim/swift']`,
26
+ deps: [],
27
+ },
28
+ {
29
+ name: 'react',
30
+ code: `module.exports = global.__contrastHostModules['react']`,
31
+ deps: [],
32
+ },
33
+ ]
34
+ const bundle = [
35
+ `var __METRO_GLOBAL_PREFIX__='';`,
36
+ `(function(global){${METRO_REQUIRE_POLYFILL}})(globalThis);`,
37
+ ...modules.map(
38
+ (module, id) =>
39
+ `__d(function(global,require,_importDefault,_importAll,module,exports,_dependencyMap){\n${module.code}\n},${id},${JSON.stringify(module.deps)},${JSON.stringify(module.name)});`,
40
+ ),
41
+ '__r(0);',
42
+ ].join('\n')
43
+ return appendRNXMetroModuleIdentityFooter(bundle, {
44
+ version: RNX_METRO_MODULE_IDENTITY_VERSION,
45
+ identitySource: 'contrast-bundler',
46
+ modulePaths: Object.fromEntries(modules.map((module, id) => [id, module.name])),
47
+ logicalSpecifiers: Object.fromEntries(
48
+ modules.flatMap((module, id) => (id === 0 ? [] : [[id, [module.name]]])),
49
+ ),
50
+ })
51
+ }