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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peachsim",
3
- "version": "0.1.570",
3
+ "version": "0.1.571",
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",
@@ -46,6 +46,20 @@
46
46
  "import": "./dist-lib/swift-submit.mjs",
47
47
  "default": "./dist-lib/swift-submit.cjs"
48
48
  },
49
+ "./swift-package": {
50
+ "types": "./src/swift-package.ts",
51
+ "source": "./src/swift-package.ts",
52
+ "bun": "./src/swift-package.ts",
53
+ "import": "./dist-lib/swift-package.mjs",
54
+ "default": "./dist-lib/swift-package.cjs"
55
+ },
56
+ "./swift-compile": {
57
+ "types": "./src/swift-compile.ts",
58
+ "source": "./src/swift-compile.ts",
59
+ "bun": "./src/swift-compile.ts",
60
+ "import": "./dist-lib/swift-compile.mjs",
61
+ "default": "./dist-lib/swift-compile.cjs"
62
+ },
49
63
  "./metro": {
50
64
  "source": "./src/metro-plugin.ts",
51
65
  "default": "./dist-lib/metro.cjs"
@@ -150,6 +164,13 @@
150
164
  "import": "./dist-lib/cloud-contract.mjs",
151
165
  "default": "./dist-lib/cloud-contract.cjs"
152
166
  },
167
+ "./cloud-test-args": {
168
+ "types": "./src/cloud-test-args.ts",
169
+ "source": "./src/cloud-test-args.ts",
170
+ "bun": "./src/cloud-test-args.ts",
171
+ "import": "./dist-lib/cloud-test-args.mjs",
172
+ "default": "./dist-lib/cloud-test-args.cjs"
173
+ },
153
174
  "./cloud": {
154
175
  "types": "./src/cloud.ts",
155
176
  "source": "./src/cloud.ts",
@@ -238,7 +238,7 @@ upstream detox surface.
238
238
  ```js
239
239
  // rnx-detox.config.cjs
240
240
  module.exports = {
241
- preset: 'rnxsim/detox',
241
+ preset: 'peachsim/detox',
242
242
  rootDir: __dirname,
243
243
  testMatch: ['<rootDir>/e2e/**/*.test.ts'],
244
244
  }
@@ -248,7 +248,7 @@ or, if you already have a jest config, pull in just the module mapper:
248
248
 
249
249
  ```js
250
250
  moduleNameMapper: {
251
- '^detox$': require.resolve('rnxsim/detox'),
251
+ '^detox$': require.resolve('peachsim/detox'),
252
252
  }
253
253
  ```
254
254
 
@@ -294,7 +294,7 @@ rnx detox init # scaffold config + e2e/example.test.ts
294
294
  - **"no Peach shell reachable"** — start one yourself
295
295
  (`rnx open <port>`) or pass `--port <n>`.
296
296
  - **"jest cannot find module 'detox'"** — the preset isn't wired. confirm
297
- `preset: 'rnxsim/detox'` is in the jest config jest
297
+ `preset: 'peachsim/detox'` is in the jest config jest
298
298
  actually loaded (`--config` overrides package.json).
299
299
  - **matchers fail on elements that exist** — canvas has no DOM. use
300
300
  `by.id` (testID); `rnx describe` shows what the shell sees.
@@ -0,0 +1,100 @@
1
+ // hosted `rnx test` selection args: the argv slice a cloud submitter
2
+ // forwards and the recorder appends verbatim to its executor invocation.
3
+ //
4
+ // selection only. the flags below choose WHICH tests run; everything
5
+ // about HOW they run (workers, timeouts, shards, devices, environment)
6
+ // stays with the recorder that owns the execution environment, and
7
+ // everything about WHERE they run (app, origin, tunnel) is consumed by
8
+ // the submitter. a flag outside this list never reaches the executor:
9
+ // the submitter warns and the server refuses, so a filter is never
10
+ // silently dropped. pure: the gpu recorder daemon imports this too, so
11
+ // no ~/ aliases, no server-only, no node builtins.
12
+ export const CLOUD_TEST_ARGS_MAX_COUNT = 32
13
+ export const CLOUD_TEST_ARG_MAX_LENGTH = 1000
14
+ export const CLOUD_TEST_ARGS_MAX_BYTES = 16 * 1024
15
+
16
+ // every flag a hosted job honors from the submitting command line. each
17
+ // takes exactly one value, as a separate token or --flag=value.
18
+ export const CLOUD_TEST_VALUE_FLAGS: readonly string[] = [
19
+ '-t',
20
+ '--testNamePattern',
21
+ '--testPathPattern',
22
+ '--include-tags',
23
+ '--exclude-tags',
24
+ '--grep',
25
+ ]
26
+
27
+ const CLOUD_TEST_VALUE_FLAG_SET = new Set<string>(CLOUD_TEST_VALUE_FLAGS)
28
+
29
+ export function isCloudTestValueFlag(name: string): boolean {
30
+ return CLOUD_TEST_VALUE_FLAG_SET.has(name)
31
+ }
32
+
33
+ export type CloudTestArgsVerdict =
34
+ | { ok: true; args: string[] }
35
+ | { ok: false; message: string }
36
+
37
+ // the submit payload's testArgs field, or null when the submission
38
+ // carries no selection. every violation names the offending arg.
39
+ export function parseCloudTestArgs(value: unknown): CloudTestArgsVerdict {
40
+ if (value === undefined || value === null) return { ok: true, args: [] }
41
+ if (!Array.isArray(value)) {
42
+ return { ok: false, message: 'testArgs must be an array of strings' }
43
+ }
44
+ if (value.length > CLOUD_TEST_ARGS_MAX_COUNT) {
45
+ return {
46
+ ok: false,
47
+ message: `testArgs may contain at most ${CLOUD_TEST_ARGS_MAX_COUNT} entries`,
48
+ }
49
+ }
50
+ // utf-8 bytes, not utf-16 code units: the bound names bytes, and a
51
+ // multibyte pattern must not slip twice its size past the gate.
52
+ const encoder = new TextEncoder()
53
+ let bytes = 0
54
+ const entries: string[] = []
55
+ for (const entry of value) {
56
+ if (typeof entry !== 'string' || entry.length === 0) {
57
+ return { ok: false, message: 'testArgs entries must be non-empty strings' }
58
+ }
59
+ if (entry.length > CLOUD_TEST_ARG_MAX_LENGTH || entry.includes('\0')) {
60
+ return { ok: false, message: `testArgs entry is too long or carries a NUL byte: ${entry.slice(0, 60)}` }
61
+ }
62
+ bytes += encoder.encode(entry).length
63
+ if (bytes > CLOUD_TEST_ARGS_MAX_BYTES) {
64
+ return {
65
+ ok: false,
66
+ message: `testArgs may contain at most ${CLOUD_TEST_ARGS_MAX_BYTES} bytes`,
67
+ }
68
+ }
69
+ entries.push(entry)
70
+ }
71
+ const args: string[] = []
72
+ for (let i = 0; i < entries.length; i++) {
73
+ const entry = entries[i]
74
+ if (!entry.startsWith('-')) {
75
+ return {
76
+ ok: false,
77
+ message: `testArgs carries a positional argument (selection flags only): ${entry.slice(0, 60)}`,
78
+ }
79
+ }
80
+ const eq = entry.indexOf('=')
81
+ const head = eq > 0 ? entry.slice(0, eq) : entry
82
+ if (!isCloudTestValueFlag(head)) {
83
+ return { ok: false, message: `testArgs carries an unknown flag: ${head.slice(0, 60)}` }
84
+ }
85
+ if (eq > 0) {
86
+ if (!entry.slice(eq + 1)) {
87
+ return { ok: false, message: `${head} expects a value` }
88
+ }
89
+ args.push(entry)
90
+ continue
91
+ }
92
+ const val = entries[i + 1]
93
+ if (val === undefined || val.startsWith('-')) {
94
+ return { ok: false, message: `${head} expects a value` }
95
+ }
96
+ args.push(entry, val)
97
+ i += 1
98
+ }
99
+ return { ok: true, args }
100
+ }
@@ -2,13 +2,14 @@
2
2
  // swiftpm package to `/api/swift-compile` and answer every `.swift` import
3
3
  // with the module that mounts the published artifact.
4
4
  //
5
- // this is the remote twin of `vite-plugin-swift.ts`. that plugin runs the
6
- // local toolchain and serves the artifact from dev-server middleware; the
7
- // native engine is its own rolldown instance with no middleware seam, so
8
- // this plugin compiles nothing in-process and serves nothing. the artifact
9
- // url the service answers is absolute (compile-cdn), so there is nothing to
10
- // serve. same compiler, same store, same hash, same credential as the
11
- // browser worker — one path, no second toolchain to install for `one dev`.
5
+ // this is the remote twin of `vite-plugin-swift.ts`. that plugin calls the
6
+ // shared project-unit compiler and serves the artifact from dev-server
7
+ // middleware. the native engine is its own rolldown instance with no
8
+ // middleware seam, so this plugin compiles nothing in-process and serves
9
+ // nothing. the artifact url the service answers is absolute (compile-cdn), so
10
+ // there is nothing to serve. same compiler, same store, same hash, same
11
+ // credential as the browser worker — one path, no second toolchain to install
12
+ // for `one dev`.
12
13
  //
13
14
  // an app registers it through One's native plugin seam, not `config.plugins`
14
15
  // (which never reach the native engine):
@@ -0,0 +1,359 @@
1
+ import { execFile } from 'node:child_process'
2
+ import { createHash } from 'node:crypto'
3
+ import {
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ readdirSync,
8
+ realpathSync,
9
+ rmSync,
10
+ statSync,
11
+ writeFileSync,
12
+ } from 'node:fs'
13
+ import path from 'node:path'
14
+ import { promisify } from 'node:util'
15
+ import { swiftManifestTargets, type SwiftPackageTarget } from './swift-package'
16
+ import type { SwiftImageUpload } from './swift-submit'
17
+
18
+ const run = promisify(execFile)
19
+ const WASM_TRIPLE = 'wasm32-unknown-wasip1'
20
+
21
+ export interface SwiftCompileInput {
22
+ files: Record<string, string>
23
+ resources?: Record<string, string>
24
+ images?: Record<string, SwiftImageUpload>
25
+ entry: string
26
+ }
27
+
28
+ export interface SwiftCompileOptions {
29
+ buildRoot: string
30
+ moduleRoot: string
31
+ release: string
32
+ toolchainBin: string
33
+ timeoutMs?: number
34
+ }
35
+
36
+ export interface SwiftCompileArtifact {
37
+ hash: string
38
+ bytes: Buffer
39
+ }
40
+
41
+ export interface SwiftProjectLayout {
42
+ root: string
43
+ sources: string
44
+ multi: boolean
45
+ }
46
+
47
+ export function swiftModuleInputFiles(root: string): string[] {
48
+ const files: string[] = ['Package.swift', 'scripts/contrast-app-Package.swift']
49
+ const walk = (dir: string) => {
50
+ for (const entry of readdirSync(path.join(root, dir), { withFileTypes: true })) {
51
+ const relative = `${dir}/${entry.name}`
52
+ if (entry.isDirectory()) walk(relative)
53
+ else if (entry.name.endsWith('.swift')) files.push(relative)
54
+ }
55
+ }
56
+ walk('Sources')
57
+ return files.sort()
58
+ }
59
+
60
+ const linkedModuleDigests = new Map<string, { stamp: string; digest: Buffer }>()
61
+
62
+ export function clearLinkedModuleDigests(root?: string): void {
63
+ if (root === undefined) linkedModuleDigests.clear()
64
+ else linkedModuleDigests.delete(root)
65
+ }
66
+
67
+ function linkedModuleDigest(root: string): Buffer {
68
+ const files = swiftModuleInputFiles(root)
69
+ const stampHash = createHash('sha256')
70
+ for (const file of files) {
71
+ const stat = statSync(path.join(root, file), { bigint: true })
72
+ stampHash.update(file)
73
+ stampHash.update(
74
+ `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}\n`,
75
+ )
76
+ }
77
+ const stamp = stampHash.digest('hex')
78
+ const cached = linkedModuleDigests.get(root)
79
+ if (cached?.stamp === stamp) return cached.digest
80
+ const hash = createHash('sha256')
81
+ for (const file of files) {
82
+ hash.update(createHash('sha256').update(file).digest())
83
+ hash.update(
84
+ createHash('sha256')
85
+ .update(readFileSync(path.join(root, file)))
86
+ .digest(),
87
+ )
88
+ }
89
+ const digest = hash.digest()
90
+ linkedModuleDigests.set(root, { stamp, digest })
91
+ return digest
92
+ }
93
+
94
+ export function compileTargets(input: SwiftCompileInput): SwiftPackageTarget[] {
95
+ const manifest = input.files['Package.swift']
96
+ return manifest === undefined ? [] : swiftManifestTargets(manifest)
97
+ }
98
+
99
+ function swiftSiblingDependencies(
100
+ files: Record<string, string>,
101
+ targets: SwiftPackageTarget[],
102
+ ): Map<string, string[]> {
103
+ const names = new Set(targets.map((target) => target.name))
104
+ const siblings = new Map<string, string[]>()
105
+ for (const target of targets) {
106
+ const prefix = `${target.path}/`
107
+ const deps = new Set<string>()
108
+ for (const [file, contents] of Object.entries(files)) {
109
+ if (file === 'Package.swift' || !file.startsWith(prefix)) continue
110
+ for (const match of contents.matchAll(/^\s*import\s+([A-Za-z_][A-Za-z0-9_]*)/gm)) {
111
+ if (names.has(match[1]) && match[1] !== target.name) deps.add(match[1])
112
+ }
113
+ }
114
+ siblings.set(target.name, [...deps].sort())
115
+ }
116
+ return siblings
117
+ }
118
+
119
+ /** bump with any change to the emitted multi-target manifest text. */
120
+ export const MULTI_MANIFEST_VERSION = 1
121
+
122
+ export function swiftArtifactHash(
123
+ input: SwiftCompileInput,
124
+ release: string,
125
+ moduleRoot: string,
126
+ ): string {
127
+ const hash = createHash('sha256')
128
+ hash.update(`swift-compile/v1\n${release}\n`)
129
+ hash.update(linkedModuleDigest(moduleRoot))
130
+ for (const file of Object.keys(input.files).sort()) {
131
+ hash.update(createHash('sha256').update(file).digest())
132
+ hash.update(createHash('sha256').update(input.files[file]).digest())
133
+ }
134
+ for (const base of Object.keys(input.resources ?? {}).sort()) {
135
+ hash.update(createHash('sha256').update(base).digest())
136
+ hash.update(
137
+ createHash('sha256')
138
+ .update(input.resources?.[base] ?? '')
139
+ .digest(),
140
+ )
141
+ }
142
+ for (const name of Object.keys(input.images ?? {}).sort()) {
143
+ const image = input.images?.[name]
144
+ if (!image) continue
145
+ hash.update(createHash('sha256').update(name).digest())
146
+ hash.update(createHash('sha256').update(image.file).digest())
147
+ hash.update(createHash('sha256').update(image.data).digest())
148
+ }
149
+ const targets = compileTargets(input)
150
+ if (targets.length > 0) {
151
+ hash.update(`swift-multi/v${MULTI_MANIFEST_VERSION}\n`)
152
+ hash.update(
153
+ JSON.stringify({
154
+ targets,
155
+ siblings: [...swiftSiblingDependencies(input.files, targets)].sort((a, b) =>
156
+ String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0,
157
+ ),
158
+ }),
159
+ )
160
+ }
161
+ return hash.digest('hex')
162
+ }
163
+
164
+ export function emitSwiftPackageManifest(args: {
165
+ targets: SwiftPackageTarget[]
166
+ siblings: Map<string, string[]>
167
+ moduleRoot: string
168
+ }): string {
169
+ const { targets, siblings, moduleRoot } = args
170
+ const executable = targets.find((target) => target.executable)
171
+ if (!executable) throw new Error('swift-compile: no executable target to emit')
172
+ const swiftSettings =
173
+ 'swiftSettings: [.swiftLanguageMode(.v5), .unsafeFlags(["-Osize"])]'
174
+ const stanza = (target: SwiftPackageTarget): string => {
175
+ const deps = [
176
+ '.product(name: "SwiftUI", package: "sootsim-swift")',
177
+ ...(siblings.get(target.name) ?? []).map((sibling) => `"${sibling}"`),
178
+ ].join(', ')
179
+ const kind = target.executable ? '.executableTarget' : '.target'
180
+ const link = target.executable
181
+ ? `,
182
+ linkerSettings: [
183
+ .unsafeFlags([
184
+ "-Xclang-linker", "-mexec-model=reactor",
185
+ "-Xlinker", "--export-if-defined=__main_argc_argv",
186
+ ]),
187
+ .unsafeFlags(["-Xlinker", "--strip-all"], .when(configuration: .release)),
188
+ ]`
189
+ : ''
190
+ return [
191
+ ` ${kind}(`,
192
+ ` name: "${target.name}",`,
193
+ ` dependencies: [${deps}],`,
194
+ ` path: "${target.path}",`,
195
+ ` ${swiftSettings}${link}`,
196
+ ` ),`,
197
+ ].join('\n')
198
+ }
199
+ return [
200
+ '// swift-tools-version: 6.0',
201
+ 'import PackageDescription',
202
+ '',
203
+ '// generated by swift-compile from the project package partition.',
204
+ 'let package = Package(',
205
+ ' name: "app",',
206
+ ` products: [.executable(name: "${executable.name}", targets: ["${executable.name}"])],`,
207
+ ` dependencies: [.package(path: "${moduleRoot}")],`,
208
+ ' targets: [',
209
+ ...targets.map(stanza),
210
+ ' ]',
211
+ ')',
212
+ '',
213
+ ].join('\n')
214
+ }
215
+
216
+ export function writeSwiftProject(
217
+ layout: SwiftProjectLayout,
218
+ input: SwiftCompileInput,
219
+ moduleRoot: string,
220
+ ): void {
221
+ const targets = compileTargets(input)
222
+ if (targets.length === 0) {
223
+ rmSync(layout.sources, { recursive: true, force: true })
224
+ for (const [file, contents] of Object.entries(input.files)) {
225
+ const target = path.join(layout.sources, file)
226
+ mkdirSync(path.dirname(target), { recursive: true })
227
+ writeFileSync(target, contents, 'utf8')
228
+ }
229
+ const manifest = readFileSync(
230
+ path.join(moduleRoot, 'scripts/contrast-app-Package.swift'),
231
+ 'utf8',
232
+ ).replaceAll('__SOOTSIM_SWIFT_PACKAGE__', moduleRoot)
233
+ writeFileSync(path.join(layout.root, 'Package.swift'), manifest, 'utf8')
234
+ return
235
+ }
236
+ mkdirSync(layout.root, { recursive: true })
237
+ for (const child of readdirSync(layout.root)) {
238
+ if (child === '.build') continue
239
+ rmSync(path.join(layout.root, child), { recursive: true, force: true })
240
+ }
241
+ for (const [file, contents] of Object.entries(input.files)) {
242
+ if (file === 'Package.swift') continue
243
+ const target = path.join(layout.root, file)
244
+ mkdirSync(path.dirname(target), { recursive: true })
245
+ writeFileSync(target, contents, 'utf8')
246
+ }
247
+ writeFileSync(
248
+ path.join(layout.root, 'Package.swift'),
249
+ emitSwiftPackageManifest({
250
+ targets,
251
+ siblings: swiftSiblingDependencies(input.files, targets),
252
+ moduleRoot,
253
+ }),
254
+ 'utf8',
255
+ )
256
+ }
257
+
258
+ function streamText(value: unknown): string {
259
+ if (typeof value === 'string') return value.trim()
260
+ if (Buffer.isBuffer(value)) return value.toString('utf8').trim()
261
+ return ''
262
+ }
263
+
264
+ function outputOf(error: unknown): string {
265
+ if (error && typeof error === 'object') {
266
+ const streams = [
267
+ streamText(Reflect.get(error, 'stdout')),
268
+ streamText(Reflect.get(error, 'stderr')),
269
+ ].filter(Boolean)
270
+ if (streams.length > 0) return streams.join('\n')
271
+ }
272
+ return error instanceof Error ? error.message : String(error)
273
+ }
274
+
275
+ function builtArtifact(layout: SwiftProjectLayout): string {
276
+ const binDir = path.join(layout.root, '.build', WASM_TRIPLE, 'release')
277
+ const artifacts = existsSync(binDir)
278
+ ? readdirSync(binDir)
279
+ .filter((name) => name.endsWith('.wasm'))
280
+ .sort()
281
+ : []
282
+ if (artifacts.length !== 1) {
283
+ throw new Error(
284
+ `swift-compile: the build produced ${artifacts.length} wasm artifacts; exactly one executable is the app`,
285
+ )
286
+ }
287
+ return path.join(binDir, artifacts[0])
288
+ }
289
+
290
+ function projectDiagnostic(output: string, layout: SwiftProjectLayout): string {
291
+ const base = layout.multi ? layout.root : layout.sources
292
+ const prefix = `${base}${path.sep}`
293
+ return output
294
+ .split('\n')
295
+ .map((line) => (line.startsWith(prefix) ? line.slice(prefix.length) : line))
296
+ .join('\n')
297
+ .trim()
298
+ }
299
+
300
+ const ROOT_LOCKS = Symbol.for('peachsim.swiftCompile.rootLocks')
301
+ const host = globalThis as typeof globalThis & {
302
+ [ROOT_LOCKS]?: Map<string, Promise<unknown>>
303
+ }
304
+ const rootLocks = (host[ROOT_LOCKS] ??= new Map())
305
+
306
+ function serialize<T>(root: string, work: () => Promise<T>): Promise<T> {
307
+ const current = rootLocks.get(root) ?? Promise.resolve()
308
+ const next = current.then(work, work)
309
+ rootLocks.set(
310
+ root,
311
+ next.catch(() => {}),
312
+ )
313
+ return next
314
+ }
315
+
316
+ /** the one local compiler used by the route host and Vite host. */
317
+ export function compileSwiftProjectUnit(
318
+ input: SwiftCompileInput,
319
+ options: SwiftCompileOptions,
320
+ ): Promise<SwiftCompileArtifact> {
321
+ mkdirSync(options.buildRoot, { recursive: true })
322
+ const root = realpathSync(options.buildRoot)
323
+ const moduleRoot = path.resolve(options.moduleRoot)
324
+ const layout: SwiftProjectLayout = {
325
+ root,
326
+ sources: path.join(root, 'Sources', 'App'),
327
+ multi: compileTargets(input).length > 0,
328
+ }
329
+ const hash = swiftArtifactHash(input, options.release, moduleRoot)
330
+ return serialize(root, async () => {
331
+ writeSwiftProject(layout, input, moduleRoot)
332
+ const swift = path.join(options.toolchainBin, 'swift')
333
+ if (!existsSync(swift)) {
334
+ throw new Error(
335
+ `swift-compile: no swift.org toolchain at ${options.toolchainBin}; install ${options.release} and its matching wasm sdk`,
336
+ )
337
+ }
338
+ try {
339
+ await run(
340
+ swift,
341
+ [
342
+ 'build',
343
+ '--package-path',
344
+ root,
345
+ '--swift-sdk',
346
+ `${options.release}_wasm`,
347
+ '-c',
348
+ 'release',
349
+ '-Xswiftc',
350
+ '-Osize',
351
+ ],
352
+ { maxBuffer: 64 * 1024 * 1024, timeout: options.timeoutMs },
353
+ )
354
+ } catch (error) {
355
+ throw new Error(projectDiagnostic(outputOf(error), layout), { cause: error })
356
+ }
357
+ return { hash, bytes: readFileSync(builtArtifact(layout)) }
358
+ })
359
+ }
@@ -0,0 +1,69 @@
1
+ /** one SwiftPM target a sent root manifest declares. */
2
+ export interface SwiftPackageTarget {
3
+ /** the target name, as `import <name>` spells it. */
4
+ name: string
5
+ /** project-relative directory holding the target's sources. */
6
+ path: string
7
+ /** true for `.executableTarget`: the one that links the wasm artifact. */
8
+ executable: boolean
9
+ }
10
+
11
+ /** whether a project-relative key is a SwiftPM manifest, not a source. */
12
+ export function isSwiftManifestFile(file: string): boolean {
13
+ const segments = file.split('/')
14
+ return segments[segments.length - 1] === 'Package.swift'
15
+ }
16
+
17
+ /**
18
+ * the library and executable targets a sent root manifest declares, in order.
19
+ *
20
+ * A manifest is Swift code, not data, so this is a scanner, not a parser:
21
+ * each `.target(` / `.executableTarget(` call is matched to its balanced
22
+ * close paren (string-aware, so a paren inside a literal cannot end it
23
+ * early), and `name:` / `path:` are read from the call's own depth only, so
24
+ * a nested `.product(name: "SwiftUI", ...)` inside dependencies never wins.
25
+ */
26
+ export function swiftManifestTargets(manifest: string): SwiftPackageTarget[] {
27
+ const code = manifest.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '')
28
+ const targets: SwiftPackageTarget[] = []
29
+ const pattern = /\.(executableTarget|target)\s*\(/g
30
+ let match: RegExpExecArray | null
31
+ while ((match = pattern.exec(code)) !== null) {
32
+ const executable = match[1] === 'executableTarget'
33
+ let depth = 1
34
+ let i = pattern.lastIndex
35
+ let inString = false
36
+ let escaped = false
37
+ let flat = ''
38
+ while (i < code.length && depth > 0) {
39
+ const ch = code[i]
40
+ if (inString) {
41
+ if (escaped) escaped = false
42
+ else if (ch === '\\') escaped = true
43
+ else if (ch === '"') inString = false
44
+ if (depth === 1) flat += ch
45
+ } else if (ch === '"') {
46
+ inString = true
47
+ if (depth === 1) flat += ch
48
+ } else if (ch === '(') {
49
+ depth++
50
+ if (depth === 1) flat += ch
51
+ } else if (ch === ')') {
52
+ depth--
53
+ if (depth >= 1) flat += ch
54
+ } else if (depth === 1) {
55
+ flat += ch
56
+ }
57
+ i++
58
+ }
59
+ const name = /\bname\s*:\s*"([^"]+)"/.exec(flat)?.[1]
60
+ if (!name) continue
61
+ const rawPath = /\bpath\s*:\s*"([^"]*)"/.exec(flat)?.[1]
62
+ const targetPath =
63
+ rawPath == null || rawPath === ''
64
+ ? `Sources/${name}`
65
+ : rawPath.replace(/^\.\//, '').replace(/\/+$/, '')
66
+ targets.push({ name, path: targetPath, executable })
67
+ }
68
+ return targets
69
+ }
@@ -7,8 +7,9 @@
7
7
  * its own origin's `/api/swift-compile`), the node bundle builder used by
8
8
  * the test-drive CLI and the deploy publisher (posts an absolute endpoint),
9
9
  * and the One/rolldown `.swift` transform (same, with a `ccdn_` credential).
10
- * A caller never compiles in-process: the container is the one compiler, and
11
- * no bundler is that instance.
10
+ * Remote callers never compile in-process: the container is the compiler for
11
+ * this path. The local Vite host uses the same project-unit compiler directly
12
+ * and does not pass through this HTTP adapter.
12
13
  *
13
14
  * It lives here (rather than in the app's `src/bundler/`) because the One
14
15
  * transform ships in this package and cannot import app code; the app's own