peachsim 0.1.570 → 0.1.571

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 (79) hide show
  1. package/README.md +2 -2
  2. package/cli/bridge-flow-runner.ts +15 -7
  3. package/cli/cloud-dispatch.ts +1 -0
  4. package/cli/cloud-test-result-files.ts +97 -0
  5. package/cli/commands/maestro.ts +123 -31
  6. package/cli/commands/test.ts +161 -3
  7. package/cli/maestro-report.ts +3 -1
  8. package/cli/outbound-endpoints.ts +2 -0
  9. package/cli/run-registry.ts +2 -1
  10. package/detox/jest-preset.cjs +2 -2
  11. package/dist-lib/agent-daemon-client.cjs +1 -1
  12. package/dist-lib/agent-events.cjs +1 -1
  13. package/dist-lib/agent-identity.cjs +1 -1
  14. package/dist-lib/agent-sessions.cjs +1 -1
  15. package/dist-lib/attached-projects.cjs +1 -1
  16. package/dist-lib/auth/shared-session.cjs +1 -1
  17. package/dist-lib/backend-origin.cjs +1 -1
  18. package/dist-lib/beta.cjs +1 -1
  19. package/dist-lib/beta.mjs +1 -1
  20. package/dist-lib/bridge-constants.cjs +1 -1
  21. package/dist-lib/bridge-contract-input.cjs +1 -1
  22. package/dist-lib/bridge-contract-input.mjs +1 -1
  23. package/dist-lib/bridge-contract.cjs +1 -1
  24. package/dist-lib/bridge-contract.mjs +1 -1
  25. package/dist-lib/capture-contract.cjs +1 -1
  26. package/dist-lib/capture-contract.mjs +1 -1
  27. package/dist-lib/cli-constants.cjs +1 -1
  28. package/dist-lib/cloud-contract.cjs +1 -1
  29. package/dist-lib/cloud-contract.mjs +1 -1
  30. package/dist-lib/cloud-test-args.cjs +116 -0
  31. package/dist-lib/cloud-test-args.mjs +86 -0
  32. package/dist-lib/cloud.cjs +1 -1
  33. package/dist-lib/cloud.mjs +1 -1
  34. package/dist-lib/config.cjs +1 -1
  35. package/dist-lib/detox/index.cjs +1 -1
  36. package/dist-lib/detox/jest-preset.cjs +2 -2
  37. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  38. package/dist-lib/home-paths.cjs +1 -1
  39. package/dist-lib/host/bridge-host.cjs +16 -7
  40. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  41. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  42. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  43. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  44. package/dist-lib/host/websocket-proxy.cjs +1 -1
  45. package/dist-lib/index.cjs +16 -26
  46. package/dist-lib/jump-to-source-babel.cjs +1 -1
  47. package/dist-lib/jump-to-source-native.cjs +1 -1
  48. package/dist-lib/menu.cjs +1 -1
  49. package/dist-lib/menu.mjs +1 -1
  50. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  51. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  52. package/dist-lib/metro-production-bundle.cjs +1 -1
  53. package/dist-lib/metro-production-bundle.mjs +1 -1
  54. package/dist-lib/metro.cjs +1 -1
  55. package/dist-lib/profiles.cjs +1 -1
  56. package/dist-lib/public-brand.cjs +1 -1
  57. package/dist-lib/react-native-host-modules.cjs +1 -1
  58. package/dist-lib/react-native-host-modules.mjs +1 -1
  59. package/dist-lib/render-mode.cjs +1 -1
  60. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  61. package/dist-lib/sdk.cjs +1 -1
  62. package/dist-lib/sdk.mjs +1 -1
  63. package/dist-lib/skills.cjs +119 -106
  64. package/dist-lib/swift-compile.cjs +372 -0
  65. package/dist-lib/swift-compile.mjs +339 -0
  66. package/dist-lib/swift-package.cjs +78 -0
  67. package/dist-lib/swift-package.mjs +52 -0
  68. package/dist-lib/swift-submit.cjs +1 -1
  69. package/dist-lib/swift-submit.mjs +1 -1
  70. package/dist-lib/swift.cjs +1 -1
  71. package/dist-lib/vite.cjs +456 -138
  72. package/package.json +22 -1
  73. package/skills/rnx-test/SKILL.md +3 -3
  74. package/src/cloud-test-args.ts +100 -0
  75. package/src/rolldown-plugin-swift.ts +8 -7
  76. package/src/swift-compile.ts +359 -0
  77. package/src/swift-package.ts +69 -0
  78. package/src/swift-submit.ts +3 -2
  79. package/src/vite-plugin-swift.ts +103 -107
@@ -1,42 +1,30 @@
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.
1
+ // `.swift` handling for a vite dev server: send the project source set through
2
+ // the same project-unit compiler as `/api/swift-compile`, serve the resulting
3
+ // artifact at a content-hashed url, and answer every `.swift` import with the
4
+ // module that mounts it.
4
5
  //
5
6
  // two hosts use this. the bundler that builds the app's native bundle runs
6
7
  // resolveId/load, and the dev server runs the middleware that serves the
7
8
  // artifact. both share the build below, so the artifact is compiled once.
8
9
  //
9
- // the hash is the hash of the built wasm, so a new hash means a genuinely
10
- // different app: createSwiftEntry moves the @State store across it instead of
11
- // remounting.
10
+ // the hash covers the same source/module/toolchain inputs as the remote
11
+ // service, so a new hash means a genuinely different app: createSwiftEntry
12
+ // moves the @State store across it instead of remounting.
12
13
 
13
- import { execFile } from 'node:child_process'
14
14
  import { createHash } from 'node:crypto'
15
15
  import fs from 'node:fs'
16
16
  import os from 'node:os'
17
17
  import path from 'node:path'
18
- import { promisify } from 'node:util'
19
18
  import {
20
19
  enumerateBundleImages,
21
20
  enumerateBundleJson,
22
21
  } from '../../sootsim-swift/src/bundleAssets'
23
22
  import { swiftEntryModuleEsm } from '../../sootsim-swift/src/entryModule'
24
- import toolchain from '../../sootsim-swift/toolchain.json'
23
+ import { compileSwiftProjectUnit, swiftModuleInputFiles } from './swift-compile'
24
+ import type { SwiftCompileInput } from './swift-compile'
25
+ import type { SwiftImageUpload } from './swift-submit'
25
26
  import type { Plugin, ViteDevServer } from 'vite'
26
27
 
27
- const run = promisify(execFile)
28
-
29
- // the swift.org release scripts/build.sh builds with, from the one place it is
30
- // named: the toolchain is <release>.xctoolchain and its matching wasm sdk is
31
- // <release>_wasm. xcode's swift has no wasm backend, so this toolchain is the
32
- // only one that can build the target.
33
- const TOOLCHAIN_BIN = path.join(
34
- os.homedir(),
35
- `Library/Developer/Toolchains/${toolchain.release}.xctoolchain/usr/bin`,
36
- )
37
- const WASM_SDK = `${toolchain.release}_wasm`
38
- const WASM_TRIPLE = 'wasm32-unknown-wasip1'
39
-
40
28
  const ARTIFACT_PATH = '/__rnx/swift/'
41
29
  const RESOURCE_PATH = '/__rnx/swift-res/'
42
30
  const VIRTUAL_PREFIX = '\0rnx-swift:'
@@ -141,6 +129,8 @@ const RESOURCE_CONTENT_TYPES: Record<string, string> = {
141
129
  export interface SwiftResourceTable {
142
130
  /** bundle asset name to absolute serving url. */
143
131
  table: Record<string, string>
132
+ /** the same rasters as compile-address inputs. */
133
+ images: Record<string, SwiftImageUpload>
144
134
  /** project-relative image files this load's table covers (for watching). */
145
135
  watched: string[]
146
136
  }
@@ -168,6 +158,7 @@ export function buildSwiftResourceTable(
168
158
  }
169
159
  })
170
160
  const table: Record<string, string> = {}
161
+ const images: Record<string, SwiftImageUpload> = {}
171
162
  const watched: string[] = []
172
163
  const registered = new Map<string, string>()
173
164
  for (const { name, file } of found) {
@@ -182,6 +173,10 @@ export function buildSwiftResourceTable(
182
173
  const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 16)
183
174
  const ext = path.posix.extname(file).toLowerCase()
184
175
  table[name] = `${baseUrl}${RESOURCE_PATH}${hash}${ext}`
176
+ images[name] = {
177
+ file: path.posix.basename(file),
178
+ data: bytes.toString('base64'),
179
+ }
185
180
  watched.push(file)
186
181
  // a Contents.json edit (scale swap, file rename) re-resolves the same
187
182
  // way a raster edit does, so the catalog rides the watcher too.
@@ -190,7 +185,7 @@ export function buildSwiftResourceTable(
190
185
  registered.set(`${hash}${ext}`, disk)
191
186
  }
192
187
  project.resourceFiles = registered
193
- return { table, watched }
188
+ return { table, images, watched }
194
189
  }
195
190
 
196
191
  // the /res pack: every Resources/ json in the package, basename-keyed and
@@ -293,22 +288,41 @@ export function linkedModuleRoots(packagePath: string): string[] {
293
288
  return [...roots].sort()
294
289
  }
295
290
 
296
- // the stamp covers the project plus the modules it links: a running dev
297
- // server rebuilds only when this moves, so leaving the linked sources out
298
- // would keep serving the artifact built before a SwiftUI module edit for as
299
- // long as the project's own sources stayed the same. it names the same
300
- // closure the compile service's artifact hash names, but it is not the same
301
- // function: the stamp hashes mtimes, which is the cheap rebuild signal for a
302
- // watcher, while the artifact address hashes content, which is what stays
303
- // stable when a checkout restores byte-identical files. a restore rebuilds
304
- // here and reuses the stored artifact there, by design.
291
+ // the stamp covers every file that contributes to compileInput plus the
292
+ // linked module and toolchain release that the shared artifact hash names. a
293
+ // running dev server rebuilds only when this moves, so its input closure must
294
+ // stay identical to the artifact address's even though this cheap watcher
295
+ // signal hashes mtimes while that stable address hashes content. a checkout
296
+ // restoring byte-identical files rebuilds here and reuses the same address
297
+ // there, by design.
305
298
  export function sourceStamp(packagePath: string): string {
306
299
  const hash = createHash('sha256')
307
- for (const root of [packagePath, ...linkedModuleRoots(packagePath)]) {
308
- hash.update(root)
309
- for (const file of sourceFiles(root).sort()) {
310
- hash.update(path.relative(root, file))
311
- hash.update(String(fs.statSync(file).mtimeMs))
300
+ hash.update(packagePath)
301
+ const projectInputs = new Set([
302
+ path.join(packagePath, 'Package.swift'),
303
+ ...sourceFiles(packagePath),
304
+ ...buildSwiftBundleFiles(packagePath).watched.map((file) =>
305
+ path.join(packagePath, ...file.split(path.posix.sep)),
306
+ ),
307
+ ...buildSwiftResourceTable(projectFor(packagePath), '').watched.map((file) =>
308
+ path.join(packagePath, ...file.split(path.posix.sep)),
309
+ ),
310
+ ])
311
+ for (const file of [...projectInputs].sort()) {
312
+ hash.update(path.relative(packagePath, file))
313
+ hash.update(String(fs.statSync(file).mtimeMs))
314
+ }
315
+ const moduleRoot = linkedModuleRoots(packagePath).find((root) =>
316
+ fs.existsSync(path.join(root, 'toolchain.json')),
317
+ )
318
+ if (moduleRoot) {
319
+ hash.update(moduleRoot)
320
+ for (const relative of [
321
+ ...swiftModuleInputFiles(moduleRoot),
322
+ 'toolchain.json',
323
+ ].sort()) {
324
+ hash.update(relative)
325
+ hash.update(String(fs.statSync(path.join(moduleRoot, relative)).mtimeMs))
312
326
  }
313
327
  }
314
328
  return hash.digest('hex').slice(0, 16)
@@ -339,82 +353,62 @@ function artifactUrl(project: SwiftProject, artifact: SwiftArtifact): string {
339
353
  return `${serverOrigin()}${ARTIFACT_PATH}${artifact.hash}.wasm`
340
354
  }
341
355
 
342
- // one executable is the app. a package with several is ambiguous: the host
343
- // cannot know which one the page is, so it says so instead of guessing.
344
- function builtArtifact(packagePath: string): string {
345
- const binDir = path.join(packagePath, '.build', WASM_TRIPLE, 'release')
346
- const artifacts = fs.existsSync(binDir)
347
- ? fs
348
- .readdirSync(binDir)
349
- .filter((name) => name.endsWith('.wasm'))
350
- .sort()
351
- : []
352
- if (artifacts.length !== 1) {
353
- throw new Error(
354
- `swift project ${packagePath} built ${artifacts.length} wasm artifacts; exactly one executable is the app`,
355
- )
356
+ function compileInput(packagePath: string, entry: string): SwiftCompileInput {
357
+ const files: Record<string, string> = {}
358
+ for (const file of sourceFiles(packagePath)) {
359
+ const relative = path.relative(packagePath, file).split(path.sep).join(path.posix.sep)
360
+ files[relative] = fs.readFileSync(file, 'utf8')
361
+ }
362
+ const manifest = path.join(packagePath, 'Package.swift')
363
+ if (fs.existsSync(manifest)) files['Package.swift'] = fs.readFileSync(manifest, 'utf8')
364
+ const resources = buildSwiftBundleFiles(packagePath).files
365
+ const images = buildSwiftResourceTable(projectFor(packagePath), '').images
366
+ return {
367
+ files,
368
+ resources,
369
+ images,
370
+ entry,
356
371
  }
357
- return path.join(binDir, artifacts[0])
358
- }
359
-
360
- function streamText(value: unknown): string {
361
- if (typeof value === 'string') return value.trim()
362
- if (Buffer.isBuffer(value)) return value.toString('utf8').trim()
363
- return ''
364
372
  }
365
373
 
366
- async function compile(packagePath: string, stamp: string): Promise<SwiftArtifact> {
367
- const swift = path.join(TOOLCHAIN_BIN, 'swift')
368
- if (!fs.existsSync(swift)) {
374
+ async function compile(
375
+ packagePath: string,
376
+ entry: string,
377
+ stamp: string,
378
+ ): Promise<SwiftArtifact> {
379
+ const moduleRoot = linkedModuleRoots(packagePath).find((root) =>
380
+ fs.existsSync(path.join(root, 'toolchain.json')),
381
+ )
382
+ if (!moduleRoot) {
369
383
  throw new Error(
370
- `no swift.org toolchain at ${TOOLCHAIN_BIN}. install it and the ${WASM_SDK} sdk; see packages/sootsim-swift/README.md`,
384
+ `swift project ${packagePath} has no linked rnx SwiftUI package with toolchain.json`,
371
385
  )
372
386
  }
373
- try {
374
- await run(
375
- swift,
376
- [
377
- 'build',
378
- '--package-path',
379
- packagePath,
380
- '--swift-sdk',
381
- WASM_SDK,
382
- '-c',
383
- 'release',
384
- '-Xswiftc',
385
- '-Osize',
386
- ],
387
- { maxBuffer: 64 * 1024 * 1024 },
388
- )
389
- } catch (error) {
390
- // `swift build` prints the compiler's diagnostics on stdout and swiftpm's
391
- // own on stderr; the failure's message holds only the command line, so
392
- // both streams are what names the line that broke. stdout leads because
393
- // compiler diagnostics are what the editor needs to show.
394
- const streams =
395
- error && typeof error === 'object'
396
- ? [
397
- streamText(Reflect.get(error, 'stdout')),
398
- streamText(Reflect.get(error, 'stderr')),
399
- ].filter(Boolean)
400
- : []
401
- const detail =
402
- streams.length > 0
403
- ? streams.join('\n')
404
- : error instanceof Error
405
- ? error.message
406
- : String(error)
407
- throw new Error(`swift build failed for ${packagePath}\n${detail}`)
408
- }
409
- const bytes = fs.readFileSync(builtArtifact(packagePath))
410
- return {
411
- hash: createHash('sha256').update(bytes).digest('hex').slice(0, 16),
412
- bytes,
413
- stamp,
387
+ const toolchain: unknown = JSON.parse(
388
+ fs.readFileSync(path.join(moduleRoot, 'toolchain.json'), 'utf8'),
389
+ )
390
+ const release =
391
+ toolchain && typeof toolchain === 'object'
392
+ ? Reflect.get(toolchain, 'release')
393
+ : undefined
394
+ if (typeof release !== 'string') {
395
+ throw new Error(`swift toolchain manifest at ${moduleRoot} has no release`)
414
396
  }
397
+ const artifact = await compileSwiftProjectUnit(compileInput(packagePath, entry), {
398
+ buildRoot:
399
+ process.env.CONTRAST_SWIFT_BUILD_ROOT ??
400
+ path.join(os.tmpdir(), 'contrast-swift-build'),
401
+ moduleRoot,
402
+ release,
403
+ toolchainBin: path.join(
404
+ os.homedir(),
405
+ `Library/Developer/Toolchains/${release}.xctoolchain/usr/bin`,
406
+ ),
407
+ })
408
+ return { ...artifact, stamp }
415
409
  }
416
410
 
417
- function artifactFor(project: SwiftProject): Promise<SwiftArtifact> {
411
+ function artifactFor(project: SwiftProject, entry: string): Promise<SwiftArtifact> {
418
412
  const stamp = sourceStamp(project.packagePath)
419
413
  if (project.artifact?.stamp === stamp) return Promise.resolve(project.artifact)
420
414
  if (project.building) {
@@ -424,10 +418,10 @@ function artifactFor(project: SwiftProject): Promise<SwiftArtifact> {
424
418
  // would emit the older artifact and nothing would trigger another load.
425
419
  const running = project.building
426
420
  if (running.stamp === stamp) return running.promise
427
- const again = () => artifactFor(project)
421
+ const again = () => artifactFor(project, entry)
428
422
  return running.promise.then(again, again)
429
423
  }
430
- const promise = compile(project.packagePath, stamp).then(
424
+ const promise = compile(project.packagePath, entry, stamp).then(
431
425
  (artifact) => {
432
426
  project.artifact = artifact
433
427
  project.building = null
@@ -499,11 +493,13 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
499
493
  if (!fs.existsSync(file)) throw new Error(`swift source ${file} no longer exists`)
500
494
  const packagePath = options.packagePath ?? packageFor(file)
501
495
  const project = projectFor(packagePath)
496
+ const entry = path.relative(packagePath, file).split(path.sep).join(path.posix.sep)
502
497
  // every source in the package is a dependency of this module, so a
503
498
  // bundler watching this module learns about a `.swift` edit and re-runs
504
499
  // load. that is the whole hot reload: the new hash in the emitted module
505
500
  // is what tells the mount it is looking at a different app.
506
501
  for (const source of sourceFiles(packagePath)) this.addWatchFile(source)
502
+ this.addWatchFile(path.join(packagePath, 'Package.swift'))
507
503
  // an edit to the linked SwiftUI module rebuilds the artifact without
508
504
  // touching the project's own sources, so those files gate the reload
509
505
  // too: the new hash in the emitted module is what tells the mount it
@@ -513,7 +509,7 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
513
509
  }
514
510
  let artifact: SwiftArtifact
515
511
  try {
516
- artifact = await artifactFor(project)
512
+ artifact = await artifactFor(project, entry)
517
513
  } catch (error) {
518
514
  // a compile error must not fail this module. a module whose load threw
519
515
  // leaves the graph unwatched, so the edit that fixes the error would