peachsim 0.1.638 → 0.1.640

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 (81) hide show
  1. package/cli/command-registry.ts +1 -0
  2. package/cli/commands/control.ts +75 -6
  3. package/cli/commands/swift.ts +326 -0
  4. package/cli/main.ts +11 -0
  5. package/cli/outbound-endpoints.ts +5 -1
  6. package/dist-lib/agent-daemon-client.cjs +1 -1
  7. package/dist-lib/agent-events.cjs +1 -1
  8. package/dist-lib/agent-identity.cjs +1 -1
  9. package/dist-lib/agent-sessions.cjs +1 -1
  10. package/dist-lib/attached-projects.cjs +1 -1
  11. package/dist-lib/auth/shared-session.cjs +1 -1
  12. package/dist-lib/backend-origin.cjs +1 -1
  13. package/dist-lib/beta.cjs +1 -1
  14. package/dist-lib/beta.mjs +1 -1
  15. package/dist-lib/bridge-constants.cjs +1 -1
  16. package/dist-lib/bridge-contract-input.cjs +1 -1
  17. package/dist-lib/bridge-contract-input.mjs +1 -1
  18. package/dist-lib/bridge-contract.cjs +1 -1
  19. package/dist-lib/bridge-contract.mjs +1 -1
  20. package/dist-lib/capture-contract.cjs +1 -1
  21. package/dist-lib/capture-contract.mjs +1 -1
  22. package/dist-lib/cli-constants.cjs +1 -1
  23. package/dist-lib/cloud-contract.cjs +1 -1
  24. package/dist-lib/cloud-contract.mjs +1 -1
  25. package/dist-lib/cloud-test-args.cjs +1 -1
  26. package/dist-lib/cloud-test-args.mjs +1 -1
  27. package/dist-lib/cloud.cjs +1 -1
  28. package/dist-lib/cloud.mjs +1 -1
  29. package/dist-lib/config.cjs +1 -1
  30. package/dist-lib/detox/index.cjs +1 -1
  31. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  32. package/dist-lib/home-paths.cjs +1 -1
  33. package/dist-lib/host/bridge-host.cjs +1 -1
  34. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  35. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  36. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  37. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  38. package/dist-lib/host/websocket-proxy.cjs +1 -1
  39. package/dist-lib/index.cjs +13 -4
  40. package/dist-lib/jump-to-source-babel.cjs +1 -1
  41. package/dist-lib/jump-to-source-native.cjs +1 -1
  42. package/dist-lib/kotlin-host.cjs +1 -1
  43. package/dist-lib/menu.cjs +1 -1
  44. package/dist-lib/menu.mjs +1 -1
  45. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  46. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  47. package/dist-lib/metro-production-bundle.cjs +1 -1
  48. package/dist-lib/metro-production-bundle.mjs +1 -1
  49. package/dist-lib/metro.cjs +1 -1
  50. package/dist-lib/native-app-identity.cjs +1 -1
  51. package/dist-lib/native-app-identity.mjs +1 -1
  52. package/dist-lib/profiles.cjs +1 -1
  53. package/dist-lib/public-brand.cjs +1 -1
  54. package/dist-lib/react-native-host-modules.cjs +1 -1
  55. package/dist-lib/react-native-host-modules.mjs +1 -1
  56. package/dist-lib/render-mode.cjs +1 -1
  57. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  58. package/dist-lib/sdk.cjs +1 -1
  59. package/dist-lib/sdk.mjs +1 -1
  60. package/dist-lib/skills.cjs +389 -232
  61. package/dist-lib/swift-compile.cjs +1 -1
  62. package/dist-lib/swift-compile.mjs +1 -1
  63. package/dist-lib/swift-package.cjs +1 -1
  64. package/dist-lib/swift-package.mjs +1 -1
  65. package/dist-lib/swift-submit.cjs +89 -8
  66. package/dist-lib/swift-submit.mjs +78 -8
  67. package/dist-lib/swift.cjs +16 -33308
  68. package/dist-lib/vite-swift.cjs +422 -247
  69. package/dist-lib/vite.cjs +13 -4
  70. package/package.json +1 -1
  71. package/skills/contrast/SKILL.md +1 -0
  72. package/src/assetCatalogContract.ts +16 -0
  73. package/src/dot-env.ts +148 -0
  74. package/src/metro-runtime-source.ts +15 -0
  75. package/src/swift-guest.ts +51 -0
  76. package/src/swift-host.ts +900 -0
  77. package/src/swift-submit.ts +97 -6
  78. package/src/swift.ts +36 -8
  79. package/src/swiftContract.ts +52 -0
  80. package/src/vite-plugin-one.ts +4 -3
  81. package/src/vite-plugin-swift.ts +43 -755
@@ -1,459 +1,42 @@
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.
5
- //
6
- // two hosts use this. the bundler that builds the app's native bundle runs
7
- // resolveId/load, and the dev server runs the middleware that serves the
8
- // artifact. both share the build below, so the artifact is compiled once.
9
- //
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.
13
-
14
- import { createHash } from 'node:crypto'
1
+ // vite adapts the common Swift package host to native source imports.
15
2
  import fs from 'node:fs'
16
- import os from 'node:os'
17
3
  import path from 'node:path'
18
- import {
19
- enumerateBundleColors,
20
- enumerateBundleSymbols,
21
- enumerateBundleImages,
22
- enumerateBundleJson,
23
- type SwiftImageSource,
24
- } from '../../sootsim-swift/src/bundleAssets'
25
4
  import { swiftEntryModuleEsm } from '../../sootsim-swift/src/entryModule'
26
- import { authHeaderValue, resolveCliAuth } from '../cli/auth'
27
- import { resolveContrastBackendOrigin } from './backend-origin'
28
5
  import {
29
- compileSwiftProjectUnit,
30
- withNativeSourceGlue,
31
- swiftModuleInputFiles,
32
- } from './swift-compile'
33
- import {
34
- compileSwiftProject,
35
- httpSwiftCompileSubmit,
36
- swiftNativeEntry,
37
- type SwiftImageUpload,
38
- type SwiftNativeModules,
39
- } from './swift-submit'
40
- import type { SwiftEntryModuleArgs } from '../../sootsim-swift/src/entryModule'
41
- import type { SwiftCompileInput, SwiftIcuAsset } from './swift-compile'
42
- import type { ServerResponse } from 'node:http'
43
- import type { SfSymbolVector } from 'peach-engine/ios/sf-symbol-catalog'
44
- import type { AppCatalogColor } from 'peach-engine/react-native/platform-colors'
6
+ artifactFor,
7
+ entryRootId,
8
+ icuUrl,
9
+ linkedModuleRoots,
10
+ nativeSourceDescriptor,
11
+ nativeSourceFiles,
12
+ projectFor,
13
+ serveSwiftPackageAsset,
14
+ sourceFiles,
15
+ type SwiftArtifact,
16
+ type SwiftProject,
17
+ } from './swift-host'
18
+ import { swiftNativeEntry } from './swift-submit'
45
19
  import type { Plugin, ViteDevServer } from 'vite'
46
20
 
21
+ export {
22
+ buildSwiftBundleFiles,
23
+ buildSwiftResourceTable,
24
+ compileInput,
25
+ entryRootId,
26
+ linkedModuleRoots,
27
+ serveSwiftPackageAsset,
28
+ serveSwiftResource,
29
+ sourceStamp,
30
+ swiftPackageMetadata,
31
+ swiftPackageWatchFiles,
32
+ type SwiftBundlePack,
33
+ type SwiftResourceTable,
34
+ } from './swift-host'
35
+
47
36
  const ARTIFACT_PATH = '/__peach/swift/'
48
- const RESOURCE_PATH = '/__peach/swift-res/'
49
- const ICU_PATH = '/__peach/swift-icu/'
50
37
  const VIRTUAL_PREFIX = '\0peach-swift:'
51
- const SKIP_SOURCE_DIRS = new Set([
52
- 'node_modules',
53
- 'ios',
54
- 'android',
55
- 'dist',
56
- 'types',
57
- 'build',
58
- ])
59
- const NATIVE_DESCRIPTOR = '.one-native-source.json'
60
- const NATIVE_GLUE = '.one-native-source.swift.txt'
61
-
62
- // vxrn's `swiftPackageId`: the id One's device registry and typegen name a
63
- // package by.
64
- function swiftPackageId(packagePath: string): string {
65
- return path
66
- .basename(packagePath)
67
- .replace(/[^A-Za-z0-9_]/g, '_')
68
- .replace(/^(\d)/, '_$1')
69
- }
70
-
71
- function nativeSourceDescriptor(packagePath: string): SwiftNativeModules | null {
72
- const file = path.join(packagePath, NATIVE_DESCRIPTOR)
73
- if (!fs.existsSync(file)) return null
74
- const value: unknown = JSON.parse(fs.readFileSync(file, 'utf8'))
75
- const invalid = () =>
76
- new Error(`${file}: invalid One native source descriptor; run one typegen`)
77
- if (
78
- !value ||
79
- typeof value !== 'object' ||
80
- Reflect.get(value, 'generatedBy') !== 'one typegen' ||
81
- !fs.existsSync(path.join(packagePath, NATIVE_GLUE))
82
- ) {
83
- throw invalid()
84
- }
85
- const packageId = Reflect.get(value, 'packageId')
86
- const hash = Reflect.get(value, 'hash')
87
- const rawFiles: unknown = Reflect.get(value, 'files')
88
- const rawModules: unknown = Reflect.get(value, 'modules')
89
- const rawDefaults: unknown = Reflect.get(value, 'defaultViewFiles')
90
- if (
91
- typeof packageId !== 'string' ||
92
- typeof hash !== 'string' ||
93
- !rawFiles ||
94
- typeof rawFiles !== 'object' ||
95
- !rawModules ||
96
- typeof rawModules !== 'object' ||
97
- !Array.isArray(rawDefaults) ||
98
- !rawDefaults.every((name) => typeof name === 'string')
99
- ) {
100
- throw invalid()
101
- }
102
- const files: Record<string, string[]> = {}
103
- const modules: Record<string, string[]> = {}
104
- for (const [key, names] of Object.entries(rawFiles)) {
105
- if (!Array.isArray(names) || !names.every((name) => typeof name === 'string'))
106
- throw invalid()
107
- files[key] = names
108
- }
109
- for (const [key, names] of Object.entries(rawModules)) {
110
- if (!Array.isArray(names) || !names.every((name) => typeof name === 'string'))
111
- throw invalid()
112
- modules[key] = names
113
- }
114
- if (packageId !== swiftPackageId(packagePath)) {
115
- throw new Error(`${file}: invalid One native source descriptor; run one typegen`)
116
- }
117
- return { packageId, hash, files, modules, defaultViewFiles: rawDefaults }
118
- }
119
-
120
- function nativeSourceFiles(packagePath: string): string[] {
121
- return [NATIVE_DESCRIPTOR, NATIVE_GLUE]
122
- .map((name) => path.join(packagePath, name))
123
- .filter((file) => fs.existsSync(file))
124
- }
125
-
126
- interface SwiftArtifact {
127
- hash: string
128
- bytes: Buffer
129
- // the icu common data the artifact reserves: built here and served from
130
- // this server, or already published by the Contrast compile that built it
131
- icu: SwiftIcuAsset | { url: string }
132
- // the sources this artifact was built from. a watcher event and a `load`
133
- // reach the project in either order, so freshness is read from the sources
134
- // rather than from whichever of them ran first.
135
- stamp: string
136
- }
137
-
138
- interface SwiftProject {
139
- packagePath: string
140
- artifact: SwiftArtifact | null
141
- // the build in flight and the sources it was started from
142
- building: { stamp: string; promise: Promise<SwiftArtifact> } | null
143
- // content hash to absolute path for this load's bundle images: the served
144
- // urls name a hash, never a path, so there is nothing to traverse.
145
- resourceFiles: Map<string, string>
146
- }
147
-
148
- // a project is its swiftpm package: the build, the artifact, and the dev
149
- // server that serves it are shared by every plugin instance in this process.
150
- const projects = new Map<string, SwiftProject>()
151
38
  let devServer: ViteDevServer | undefined
152
39
 
153
- function projectFor(packagePath: string): SwiftProject {
154
- const existing = projects.get(packagePath)
155
- if (existing) return existing
156
- const created: SwiftProject = {
157
- packagePath,
158
- artifact: null,
159
- building: null,
160
- resourceFiles: new Map(),
161
- }
162
- projects.set(packagePath, created)
163
- return created
164
- }
165
-
166
- // the compile unit is the whole package: `swift build` compiles every source
167
- // in it, so one file's edit can change the artifact for all of them. also
168
- // used for a linked module below, whose own nested packages are separate
169
- // units by the same rule.
170
- function sourceFiles(root: string): string[] {
171
- const found: string[] = []
172
- const walk = (dir: string): void => {
173
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
174
- if (entry.name.startsWith('.') || SKIP_SOURCE_DIRS.has(entry.name)) continue
175
- const full = path.join(dir, entry.name)
176
- // a nested Package.swift is a separate package with its own compile:
177
- // its sources belong to that unit's stamp, not this one's
178
- if (entry.isDirectory()) {
179
- if (full !== root && fs.existsSync(path.join(full, 'Package.swift'))) continue
180
- walk(full)
181
- } else if (
182
- entry.name.endsWith('.swift') &&
183
- (entry.isFile() || (entry.isSymbolicLink() && fs.statSync(full).isFile()))
184
- )
185
- found.push(full)
186
- }
187
- }
188
- walk(root)
189
- return found
190
- }
191
-
192
- // every file that can carry bundle content: catalogs and flat Resources
193
- // rasters anywhere under the package. `.build` is skipped where sourceFiles
194
- // does not need to: a release tree holds tens of thousands of files and no
195
- // bundle content, and walking it per import would tax every hot reload.
196
- function resourceCandidateFiles(root: string): string[] {
197
- const found: string[] = []
198
- const walk = (dir: string): void => {
199
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
200
- if (
201
- entry.name.startsWith('.') ||
202
- SKIP_SOURCE_DIRS.has(entry.name) ||
203
- entry.name === '.build'
204
- )
205
- continue
206
- const full = path.join(dir, entry.name)
207
- if (entry.isDirectory()) {
208
- if (full !== root && fs.existsSync(path.join(full, 'Package.swift'))) continue
209
- walk(full)
210
- } else {
211
- found.push(full)
212
- }
213
- }
214
- }
215
- walk(root)
216
- return found
217
- }
218
-
219
- const RESOURCE_CONTENT_TYPES: Record<string, string> = {
220
- '.avif': 'image/avif',
221
- '.bmp': 'image/bmp',
222
- '.gif': 'image/gif',
223
- '.ico': 'image/x-icon',
224
- '.jpeg': 'image/jpeg',
225
- '.jpg': 'image/jpeg',
226
- '.png': 'image/png',
227
- '.webp': 'image/webp',
228
- }
229
-
230
- export interface SwiftResourceTable {
231
- /** bundle asset name to its absolute serving url and scale. */
232
- table: Record<string, SwiftImageSource>
233
- /** the same rasters as compile-address inputs. */
234
- images: Record<string, SwiftImageUpload>
235
- /** asset-catalog color name to its value. */
236
- colors: Record<string, AppCatalogColor>
237
- symbols: Record<string, SfSymbolVector>
238
- /** project-relative image and color files this load's table covers (for watching). */
239
- watched: string[]
240
- }
241
-
242
- // the producer for the local vehicle: enumerate the package's bundle images
243
- // through the shared rule both hosts use, hash each raster's bytes into its
244
- // url (an edited photo moves its url, so no stale paint survives a reload),
245
- // and register hash to path for the serving middleware below.
246
- export function buildSwiftResourceTable(
247
- project: SwiftProject,
248
- baseUrl: string,
249
- ): SwiftResourceTable {
250
- const absolute = resourceCandidateFiles(project.packagePath)
251
- const relative = absolute.map((file) =>
252
- path.relative(project.packagePath, file).split(path.sep).join(path.posix.sep),
253
- )
254
- const byRelative = new Map(absolute.map((file, index) => [relative[index], file]))
255
- const readContents = (candidate: string): string | null => {
256
- const file = byRelative.get(candidate)
257
- // catalog json, and the glyph template a symbolset names
258
- if (!file || !/(^|\/)Contents\.json$|\.symbolset\/[^/]+\.svg$/.test(candidate))
259
- return null
260
- try {
261
- return fs.readFileSync(file, 'utf8')
262
- } catch {
263
- return null
264
- }
265
- }
266
- const found = enumerateBundleImages(relative, readContents)
267
- const colors = enumerateBundleColors(relative, readContents)
268
- const symbols = enumerateBundleSymbols(relative, readContents)
269
- const table: Record<string, SwiftImageSource> = {}
270
- const images: Record<string, SwiftImageUpload> = {}
271
- const watched: string[] = []
272
- const registered = new Map<string, string>()
273
- for (const { name, file, scale } of found) {
274
- const disk = byRelative.get(file)
275
- if (!disk) continue
276
- let bytes: Buffer
277
- try {
278
- bytes = fs.readFileSync(disk)
279
- } catch {
280
- continue
281
- }
282
- const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 16)
283
- const ext = path.posix.extname(file).toLowerCase()
284
- table[name] = { uri: `${baseUrl}${RESOURCE_PATH}${hash}${ext}`, scale }
285
- images[name] = {
286
- file: path.posix.basename(file),
287
- data: bytes.toString('base64'),
288
- }
289
- watched.push(file)
290
- // a Contents.json edit (scale swap, file rename) re-resolves the same
291
- // way a raster edit does, so the catalog rides the watcher too.
292
- const contents = file.replace(/\/[^/]+$/, '/Contents.json')
293
- if (byRelative.has(contents)) watched.push(contents)
294
- registered.set(`${hash}${ext}`, disk)
295
- }
296
- // a colorset or symbolset edit re-resolves like an imageset edit.
297
- for (const file of relative) {
298
- if (file.endsWith('.colorset/Contents.json') || file.includes('.symbolset/'))
299
- watched.push(file)
300
- }
301
- project.resourceFiles = registered
302
- return { table, images, colors, symbols, watched }
303
- }
304
-
305
- // the /res pack: every Resources/ json in the package, basename-keyed and
306
- // base64, riding the entry module so the mount serves it without a fetch.
307
- // the namespace is flat like the bundle the wasm side reads: shallower
308
- // paths pack first (the Resources root is the bundle root) with a lexical
309
- // tiebreak, and the first spelling of a basename wins a collision. data json
310
- // runs tens of KB, but a string catalog carries every language it ships (a
311
- // 19-language app catalog measured 2.2MB), which sets the per-file cap: a
312
- // file past a cap is skipped and named in `skipped` so the load warns
313
- // instead of silently serving a partial bundle. binary bytes travel the same
314
- // map when a port reaches past json; the worker half packs text only until
315
- // then (see the gaps ledger).
316
- const BUNDLE_MAX_FILES = 128
317
- const BUNDLE_MAX_FILE_BYTES = 4 * 1024 * 1024
318
- const BUNDLE_MAX_TOTAL_BYTES = 8 * 1024 * 1024
319
-
320
- export interface SwiftBundlePack {
321
- /** basename to base64 bytes for the mount's /res. */
322
- files: Record<string, string>
323
- /** project-relative json files this load's pack covers (for watching). */
324
- watched: string[]
325
- /** project-relative files skipped by a cap, for the load warning. */
326
- skipped: string[]
327
- }
328
-
329
- export function buildSwiftBundleFiles(packagePath: string): SwiftBundlePack {
330
- // resourceCandidateFiles walks every non-ignored file with the nested
331
- // package exclusion; the shared enumeration decides namespace, order and
332
- // collisions, identically for every host that packs.
333
- const relative = resourceCandidateFiles(packagePath).map((file) =>
334
- path.relative(packagePath, file).split(path.sep).join(path.posix.sep),
335
- )
336
- const files: Record<string, string> = {}
337
- const watched: string[] = []
338
- const skipped: string[] = []
339
- let total = 0
340
- for (const { base, file } of enumerateBundleJson(relative)) {
341
- watched.push(file)
342
- let bytes: Buffer
343
- try {
344
- bytes = fs.readFileSync(path.join(packagePath, ...file.split('/')))
345
- } catch {
346
- continue
347
- }
348
- if (bytes.length > BUNDLE_MAX_FILE_BYTES) {
349
- skipped.push(`${file} (${bytes.length} bytes past the per-file cap)`)
350
- continue
351
- }
352
- if (
353
- Object.keys(files).length >= BUNDLE_MAX_FILES ||
354
- total + bytes.length > BUNDLE_MAX_TOTAL_BYTES
355
- ) {
356
- skipped.push(
357
- `${file} (pack caps: ${BUNDLE_MAX_FILES} files / ${BUNDLE_MAX_TOTAL_BYTES} bytes)`,
358
- )
359
- continue
360
- }
361
- files[base] = bytes.toString('base64')
362
- total += bytes.length
363
- }
364
- return { files, watched, skipped }
365
- }
366
-
367
- // the middleware's lookup, exported for the producer's own proof: a hit is
368
- // known bytes for a known hash, anything else is null, never a file.
369
- export function serveSwiftResource(
370
- project: SwiftProject,
371
- key: string,
372
- ): { bytes: Buffer; contentType: string } | null {
373
- const disk = project.resourceFiles.get(key)
374
- if (!disk) return null
375
- try {
376
- return {
377
- bytes: fs.readFileSync(disk),
378
- contentType:
379
- RESOURCE_CONTENT_TYPES[path.extname(disk).toLowerCase()] ??
380
- 'application/octet-stream',
381
- }
382
- } catch {
383
- return null
384
- }
385
- }
386
-
387
- // the SwiftUI module the project links: its manifest names it as a path
388
- // dependency, and `swift build` compiles it with the project, so an edit
389
- // there changes the artifact without moving any of the project's own mtimes.
390
- export function linkedModuleRoots(packagePath: string): string[] {
391
- let manifest: string
392
- try {
393
- manifest = fs.readFileSync(path.join(packagePath, 'Package.swift'), 'utf8')
394
- } catch {
395
- return []
396
- }
397
- const roots = new Set<string>()
398
- const dependency = /\.package\(\s*path:\s*"([^"]+)"\s*\)/g
399
- for (const match of manifest.replace(/^\s*\/\/.*$/gm, '').matchAll(dependency)) {
400
- const root = path.resolve(packagePath, match[1])
401
- if (fs.existsSync(path.join(root, 'Package.swift'))) roots.add(root)
402
- }
403
- return [...roots].sort()
404
- }
405
-
406
- // the stamp covers every file that contributes to compileInput plus the
407
- // linked module and toolchain release that the shared artifact hash names. a
408
- // running dev server rebuilds only when this moves, so its input closure must
409
- // stay identical to the artifact address's even though this cheap watcher
410
- // signal hashes mtimes while that stable address hashes content. a checkout
411
- // restoring byte-identical files rebuilds here and reuses the same address
412
- // there, by design.
413
- export function sourceStamp(packagePath: string, moduleRootOverride?: string): string {
414
- const hash = createHash('sha256')
415
- hash.update(packagePath)
416
- const projectInputs = new Set([
417
- path.join(packagePath, 'Package.swift'),
418
- ...sourceFiles(packagePath),
419
- ...nativeSourceFiles(packagePath),
420
- ...buildSwiftBundleFiles(packagePath).watched.map((file) =>
421
- path.join(packagePath, ...file.split(path.posix.sep)),
422
- ),
423
- ...buildSwiftResourceTable(projectFor(packagePath), '').watched.map((file) =>
424
- path.join(packagePath, ...file.split(path.posix.sep)),
425
- ),
426
- ])
427
- for (const file of [...projectInputs].sort()) {
428
- hash.update(path.relative(packagePath, file))
429
- hash.update(String(fs.statSync(file).mtimeMs))
430
- }
431
- const moduleRoot =
432
- moduleRootOverride ??
433
- linkedModuleRoots(packagePath).find((root) =>
434
- fs.existsSync(path.join(root, 'toolchain.json')),
435
- )
436
- if (moduleRoot) {
437
- hash.update(moduleRoot)
438
- for (const relative of swiftModuleInputFiles(moduleRoot)) {
439
- hash.update(relative)
440
- hash.update(String(fs.statSync(path.join(moduleRoot, relative)).mtimeMs))
441
- }
442
- }
443
- return hash.digest('hex').slice(0, 16)
444
- }
445
-
446
- // the registry slot for an entry: the owning package's own name plus the
447
- // source within it. two packages in one app can hold the same filename, and
448
- // a bare relative path would share one slot, one instance, and its state.
449
- // the name travels instead of the path so identical source in two checkouts
450
- // emits the identical module; two same-named packages sharing one app still
451
- // collide, and take the rootId override for that.
452
- export function entryRootId(packagePath: string, file: string): string {
453
- const relative = path.relative(packagePath, file).split(path.sep).join(path.posix.sep)
454
- return `${path.basename(packagePath)}${path.posix.sep}${relative}`
455
- }
456
-
457
40
  // the app fetches from a worker whose origin is the shell, so urls have to
458
41
  // name this dev server rather than resolve relative to the shell.
459
42
  function serverOrigin(): string {
@@ -468,276 +51,6 @@ function artifactUrl(project: SwiftProject, artifact: SwiftArtifact): string {
468
51
  return `${serverOrigin()}${ARTIFACT_PATH}${artifact.hash}.wasm`
469
52
  }
470
53
 
471
- function icuUrl(origin: string, artifact: SwiftArtifact): string {
472
- return 'url' in artifact.icu
473
- ? artifact.icu.url
474
- : `${origin}${ICU_PATH}${artifact.icu.sha256}`
475
- }
476
-
477
- function packageSources(packagePath: string): Record<string, string> {
478
- const files: Record<string, string> = {}
479
- for (const file of sourceFiles(packagePath)) {
480
- const relative = path.relative(packagePath, file).split(path.sep).join(path.posix.sep)
481
- files[relative] = fs.readFileSync(file, 'utf8')
482
- }
483
- const manifest = path.join(packagePath, 'Package.swift')
484
- if (fs.existsSync(manifest)) files['Package.swift'] = fs.readFileSync(manifest, 'utf8')
485
- return files
486
- }
487
-
488
- export function compileInput(packagePath: string, entry: string): SwiftCompileInput {
489
- const input: SwiftCompileInput = {
490
- files: packageSources(packagePath),
491
- resources: buildSwiftBundleFiles(packagePath).files,
492
- images: buildSwiftResourceTable(projectFor(packagePath), '').images,
493
- entry,
494
- oneModule: true,
495
- }
496
- return nativeSourceDescriptor(packagePath)
497
- ? withNativeSourceGlue(
498
- input,
499
- fs.readFileSync(path.join(packagePath, NATIVE_GLUE), 'utf8'),
500
- )
501
- : input
502
- }
503
-
504
- async function compile(
505
- packagePath: string,
506
- entry: string,
507
- stamp: string,
508
- moduleRootOverride?: string,
509
- ): Promise<SwiftArtifact> {
510
- const moduleRoot =
511
- moduleRootOverride ??
512
- linkedModuleRoots(packagePath).find((root) =>
513
- fs.existsSync(path.join(root, 'toolchain.json')),
514
- )
515
- if (!moduleRoot) return { ...(await compileRemote(packagePath, entry)), stamp }
516
- const toolchain: unknown = JSON.parse(
517
- fs.readFileSync(path.join(moduleRoot, 'toolchain.json'), 'utf8'),
518
- )
519
- const release =
520
- toolchain && typeof toolchain === 'object'
521
- ? Reflect.get(toolchain, 'release')
522
- : undefined
523
- if (typeof release !== 'string') {
524
- throw new Error(`swift toolchain manifest at ${moduleRoot} has no release`)
525
- }
526
- const artifact = await compileSwiftProjectUnit(compileInput(packagePath, entry), {
527
- // the compile rewrites Sources/App in its root before swiftpm builds, and
528
- // its lock orders one process only: two vites on one root (a lane's dev
529
- // server and a capture, or two checkouts) build each other's sources. so
530
- // each app and module checkout gets its own root, which keeps the same
531
- // app's incremental build across restarts.
532
- buildRoot:
533
- process.env.CONTRAST_SWIFT_BUILD_ROOT ??
534
- path.join(
535
- os.tmpdir(),
536
- 'contrast-swift-build',
537
- createHash('sha256')
538
- .update(`${moduleRoot}\0${path.resolve(packagePath)}`)
539
- .digest('hex')
540
- .slice(0, 16),
541
- ),
542
- moduleRoot,
543
- release,
544
- toolchainBin: path.join(
545
- os.homedir(),
546
- `Library/Developer/Toolchains/${release}.xctoolchain/usr/bin`,
547
- ),
548
- })
549
- return { ...artifact, stamp }
550
- }
551
-
552
- // an npm-installed peachsim ships neither the SwiftUI module nor a toolchain,
553
- // so the package compiles where a Contrast project's does: Contrast's compile
554
- // route, authorized by `peach login` or PEACH_API_KEY. the service renders the
555
- // native dispatch glue itself under the package's id and answers the wasm at a
556
- // public content address, which is fetched once and served from this dev
557
- // server like a local build. bundle images, colors and json stay local.
558
- async function compileRemote(
559
- packagePath: string,
560
- entry: string,
561
- ): Promise<Omit<SwiftArtifact, 'stamp'>> {
562
- const auth = resolveCliAuth()
563
- if (!auth || auth.kind === 'github') {
564
- throw new Error(
565
- `compiling ${path.basename(packagePath)} for Peach needs \`peach login\` or PEACH_API_KEY: this peachsim has no local SwiftUI module, so Contrast compiles it`,
566
- )
567
- }
568
- const origin = resolveContrastBackendOrigin(
569
- auth.kind === 'session' ? auth.origin : null,
570
- )
571
- const submit = httpSwiftCompileSubmit({
572
- endpoint: origin,
573
- authorization: authHeaderValue(auth),
574
- })
575
- if (!submit) throw new Error('swift compile: no credential to submit with')
576
- const descriptor = nativeSourceDescriptor(packagePath)
577
- const address = await compileSwiftProject(
578
- submit,
579
- {
580
- files: packageSources(packagePath),
581
- entry,
582
- packageId: descriptor?.packageId ?? swiftPackageId(packagePath),
583
- },
584
- origin,
585
- )
586
- // the app's JS calls with the hash its own `one typegen` wrote; glue the
587
- // service rendered from another One would reject every call as stale.
588
- if (descriptor && address.native?.hash !== descriptor.hash) {
589
- throw new Error(
590
- `${path.basename(packagePath)}: Contrast compiled native contract ${address.native?.hash ?? 'none'} but one typegen wrote ${descriptor.hash}; the app's One and the compile host's differ, or the compile host is older than this peachsim`,
591
- )
592
- }
593
- const response = await fetch(address.url)
594
- if (!response.ok) {
595
- throw new Error(`swift compile: ${address.url} answered ${response.status}`)
596
- }
597
- return {
598
- hash: address.hash,
599
- bytes: Buffer.from(await response.arrayBuffer()),
600
- icu: { url: address.icuUrl },
601
- }
602
- }
603
-
604
- function artifactFor(
605
- project: SwiftProject,
606
- entry: string,
607
- moduleRoot?: string,
608
- ): Promise<SwiftArtifact> {
609
- const stamp = sourceStamp(project.packagePath, moduleRoot)
610
- if (project.artifact?.stamp === stamp) return Promise.resolve(project.artifact)
611
- if (project.building) {
612
- // a second edit during a build: the build in flight was started from
613
- // older sources, so its artifact is not the answer for these. wait it out
614
- // and ask again, which starts the build these sources need; joining it
615
- // would emit the older artifact and nothing would trigger another load.
616
- const running = project.building
617
- if (running.stamp === stamp) return running.promise
618
- const again = () => artifactFor(project, entry, moduleRoot)
619
- return running.promise.then(again, again)
620
- }
621
- const promise = compile(project.packagePath, entry, stamp, moduleRoot).then(
622
- (artifact) => {
623
- project.artifact = artifact
624
- project.building = null
625
- return artifact
626
- },
627
- (error: unknown) => {
628
- project.building = null
629
- throw error
630
- },
631
- )
632
- project.building = { stamp, promise }
633
- return promise
634
- }
635
-
636
- export function swiftPackageWatchFiles(
637
- packagePath: string,
638
- moduleRoot: string | undefined,
639
- ): string[] {
640
- return [
641
- path.join(packagePath, 'Package.swift'),
642
- ...sourceFiles(packagePath),
643
- ...nativeSourceFiles(packagePath),
644
- ...resourceCandidateFiles(packagePath),
645
- ...(moduleRoot
646
- ? swiftModuleInputFiles(moduleRoot).map((file) => path.join(moduleRoot, file))
647
- : []),
648
- ]
649
- }
650
-
651
- export async function swiftPackageMetadata(
652
- packagePath: string,
653
- packageName: string,
654
- moduleRoot: string | undefined,
655
- origin: string,
656
- ): Promise<SwiftEntryModuleArgs> {
657
- const mains = sourceFiles(packagePath).filter((file) =>
658
- /@main\s+(?:public\s+)?struct\s+\w+\s*:/.test(fs.readFileSync(file, 'utf8')),
659
- )
660
- const native = nativeSourceDescriptor(packagePath)
661
- if (mains.length > 1 || (mains.length === 0 && !native)) {
662
- throw new Error(
663
- `swift package ${packageName} needs exactly one @main source or generated native modules; found ${mains.length}`,
664
- )
665
- }
666
- const project = projectFor(packagePath)
667
- const entry =
668
- mains.length === 1
669
- ? path.relative(packagePath, mains[0]).split(path.sep).join(path.posix.sep)
670
- : Object.entries(native?.files ?? {}).find(
671
- ([_file, names]) => names.length > 0,
672
- )?.[0]
673
- if (!entry) throw new Error(`swift package ${packageName} has no native module source`)
674
- const artifact = await artifactFor(project, entry, moduleRoot)
675
- const resources = buildSwiftResourceTable(project, origin)
676
- const bundleFiles = buildSwiftBundleFiles(packagePath)
677
- return {
678
- url: `${origin}${ARTIFACT_PATH}${artifact.hash}.wasm`,
679
- hash: artifact.hash,
680
- icuUrl: icuUrl(origin, artifact),
681
- rootId: `${packageName}/${entry}`,
682
- resources: resources.table,
683
- bundleFiles: bundleFiles.files,
684
- colors: resources.colors,
685
- symbols: resources.symbols,
686
- native: native
687
- ? {
688
- packageId: native.packageId,
689
- contractHash: native.hash,
690
- modules: native.modules,
691
- defaultView: mains.length === 1,
692
- }
693
- : undefined,
694
- }
695
- }
696
-
697
- export function serveSwiftPackageAsset(pathname: string, res: ServerResponse): boolean {
698
- if (pathname.startsWith(RESOURCE_PATH)) {
699
- for (const project of projects.values()) {
700
- const served = serveSwiftResource(project, pathname.slice(RESOURCE_PATH.length))
701
- if (!served) continue
702
- res.setHeader('access-control-allow-origin', '*')
703
- res.setHeader('content-type', served.contentType)
704
- res.end(served.bytes)
705
- return true
706
- }
707
- res.statusCode = 404
708
- res.end('swift resource is stale')
709
- return true
710
- }
711
- if (pathname.startsWith(ICU_PATH)) {
712
- for (const project of projects.values()) {
713
- const icu = project.artifact?.icu
714
- if (!icu || 'url' in icu || pathname !== `${ICU_PATH}${icu.sha256}`) continue
715
- // the address is the content, so the browser keeps it across reloads,
716
- // restarts and every app built with this toolchain
717
- res.setHeader('access-control-allow-origin', '*')
718
- res.setHeader('content-type', 'application/gzip')
719
- res.setHeader('cache-control', 'public, max-age=31536000, immutable')
720
- res.end(icu.gzip)
721
- return true
722
- }
723
- res.statusCode = 404
724
- res.end('swift icu data is stale')
725
- return true
726
- }
727
- if (!pathname.startsWith(ARTIFACT_PATH)) return false
728
- for (const project of projects.values()) {
729
- const artifact = project.artifact
730
- if (!artifact || pathname !== `${ARTIFACT_PATH}${artifact.hash}.wasm`) continue
731
- res.setHeader('access-control-allow-origin', '*')
732
- res.setHeader('content-type', 'application/wasm')
733
- res.end(artifact.bytes)
734
- return true
735
- }
736
- res.statusCode = 404
737
- res.end('swift artifact is stale')
738
- return true
739
- }
740
-
741
54
  // the swiftpm package that owns an imported file: the nearest directory at
742
55
  // or above the file that holds a Package.swift, stopping at the vite root
743
56
  // once the dev server has named it. a swift-only project keeps its sources at
@@ -839,45 +152,20 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
839
152
  this.warn(error instanceof Error ? error.message : String(error))
840
153
  artifact = project.artifact
841
154
  }
842
- // the bundle image table rides the same module: a photo edit moves
843
- // its urls without rebuilding the artifact, and the watched files
844
- // re-run this load so the mount sees the new table on hot reload.
845
- let resources: Record<string, SwiftImageSource> = {}
846
- let colors: Record<string, AppCatalogColor> = {}
847
- let symbols: Record<string, SfSymbolVector> = {}
848
- try {
849
- const built = buildSwiftResourceTable(project, serverOrigin())
850
- resources = built.table
851
- colors = built.colors
852
- symbols = built.symbols
853
- for (const watched of built.watched) {
854
- this.addWatchFile(path.join(packagePath, ...watched.split(path.posix.sep)))
855
- }
856
- } catch (error) {
857
- this.warn(
858
- `swift resources for ${packagePath}: ${error instanceof Error ? error.message : String(error)}`,
859
- )
860
- }
861
- // the /res pack rides the same module as bytes: a json edit moves the
862
- // pack, the watched files re-run this load, and the mount's
863
- // bundleFiles compare treats it as a new app and remounts through it.
864
- let bundleFiles: Record<string, string> = {}
865
- try {
866
- const packed = buildSwiftBundleFiles(packagePath)
867
- bundleFiles = packed.files
868
- for (const watched of packed.watched) {
869
- this.addWatchFile(path.join(packagePath, ...watched.split(path.posix.sep)))
870
- }
871
- if (packed.skipped.length > 0) {
872
- this.warn(
873
- `swift bundle files skipped for ${packagePath}: ${packed.skipped.join('; ')}`,
874
- )
875
- }
876
- } catch (error) {
877
- this.warn(
878
- `swift bundle files for ${packagePath}: ${error instanceof Error ? error.message : String(error)}`,
879
- )
880
- }
155
+ const resources = Object.fromEntries(
156
+ Object.entries(artifact.package.resources.table).map(([name, image]) => [
157
+ name,
158
+ { ...image, uri: `${serverOrigin()}${image.uri}` },
159
+ ]),
160
+ )
161
+ for (const watched of [
162
+ ...artifact.package.resources.watched,
163
+ ...artifact.package.bundleFiles.watched,
164
+ ])
165
+ this.addWatchFile(path.join(packagePath, ...watched.split(path.posix.sep)))
166
+ const colors = artifact.package.resources.colors
167
+ const symbols = artifact.package.resources.symbols
168
+ const bundleFiles = artifact.package.bundleFiles.files
881
169
  // every import mounts through the one entry: the artifact answers
882
170
  // whether it is a view or an app root at runtime, so the transform
883
171
  // never re-derives the conformance from source. the entry is rooted by