rnxsim 0.1.521 → 0.1.523

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 (56) hide show
  1. package/cli/commands/control.ts +31 -15
  2. package/dist-lib/agent-daemon-client.cjs +1 -1
  3. package/dist-lib/agent-events.cjs +1 -1
  4. package/dist-lib/agent-identity.cjs +1 -1
  5. package/dist-lib/agent-sessions.cjs +1 -1
  6. package/dist-lib/attached-projects.cjs +1 -1
  7. package/dist-lib/auth/shared-session.cjs +1 -1
  8. package/dist-lib/backend-origin.cjs +1 -1
  9. package/dist-lib/beta.cjs +1 -1
  10. package/dist-lib/beta.mjs +1 -1
  11. package/dist-lib/bridge-constants.cjs +1 -1
  12. package/dist-lib/bridge-contract-input.cjs +1 -1
  13. package/dist-lib/bridge-contract-input.mjs +1 -1
  14. package/dist-lib/bridge-contract.cjs +1 -1
  15. package/dist-lib/bridge-contract.mjs +1 -1
  16. package/dist-lib/capture-contract.cjs +1 -1
  17. package/dist-lib/capture-contract.mjs +1 -1
  18. package/dist-lib/cli-constants.cjs +1 -1
  19. package/dist-lib/cloud-contract.cjs +1 -1
  20. package/dist-lib/cloud-contract.mjs +1 -1
  21. package/dist-lib/cloud.cjs +1 -1
  22. package/dist-lib/cloud.mjs +1 -1
  23. package/dist-lib/config.cjs +1 -1
  24. package/dist-lib/detox/index.cjs +1 -1
  25. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  26. package/dist-lib/home-paths.cjs +1 -1
  27. package/dist-lib/host/bridge-host.cjs +7 -3
  28. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  29. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  30. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  31. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  32. package/dist-lib/host/websocket-proxy.cjs +1 -1
  33. package/dist-lib/index.cjs +1 -1
  34. package/dist-lib/jump-to-source-babel.cjs +1 -1
  35. package/dist-lib/jump-to-source-native.cjs +1 -1
  36. package/dist-lib/menu.cjs +1 -1
  37. package/dist-lib/menu.mjs +1 -1
  38. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  39. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  40. package/dist-lib/metro-production-bundle.cjs +1 -1
  41. package/dist-lib/metro-production-bundle.mjs +1 -1
  42. package/dist-lib/metro.cjs +1 -1
  43. package/dist-lib/profiles.cjs +1 -1
  44. package/dist-lib/public-brand.cjs +1 -1
  45. package/dist-lib/react-native-host-modules.cjs +1 -1
  46. package/dist-lib/react-native-host-modules.mjs +1 -1
  47. package/dist-lib/render-mode.cjs +1 -1
  48. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  49. package/dist-lib/sdk.cjs +1 -1
  50. package/dist-lib/sdk.mjs +1 -1
  51. package/dist-lib/skills.cjs +32 -14
  52. package/dist-lib/swift.cjs +155 -72
  53. package/dist-lib/vite.cjs +38 -42
  54. package/package.json +1 -1
  55. package/src/swift.ts +2 -3
  56. package/src/vite-plugin-swift.ts +81 -59
@@ -7,7 +7,7 @@
7
7
  // artifact. both share the build below, so the artifact is compiled once.
8
8
  //
9
9
  // the hash is the hash of the built wasm, so a new hash means a genuinely
10
- // different app: createSwiftModule moves the @State store across it instead of
10
+ // different app: createSwiftEntry moves the @State store across it instead of
11
11
  // remounting.
12
12
 
13
13
  import { execFile } from 'node:child_process'
@@ -47,7 +47,8 @@ interface SwiftArtifact {
47
47
  interface SwiftProject {
48
48
  packagePath: string
49
49
  artifact: SwiftArtifact | null
50
- building: Promise<SwiftArtifact> | null
50
+ // the build in flight and the sources it was started from
51
+ building: { stamp: string; promise: Promise<SwiftArtifact> } | null
51
52
  }
52
53
 
53
54
  // a project is its swiftpm package: the build, the artifact, and the dev
@@ -107,8 +108,12 @@ export function linkedModuleRoots(packagePath: string): string[] {
107
108
  // the stamp covers the project plus the modules it links: a running dev
108
109
  // server rebuilds only when this moves, so leaving the linked sources out
109
110
  // would keep serving the artifact built before a SwiftUI module edit for as
110
- // long as the project's own sources stayed the same. mirrors the compile
111
- // service's artifact hash, which names the same closure.
111
+ // long as the project's own sources stayed the same. it names the same
112
+ // closure the compile service's artifact hash names, but it is not the same
113
+ // function: the stamp hashes mtimes, which is the cheap rebuild signal for a
114
+ // watcher, while the artifact address hashes content, which is what stays
115
+ // stable when a checkout restores byte-identical files. a restore rebuilds
116
+ // here and reuses the stored artifact there, by design.
112
117
  export function sourceStamp(packagePath: string): string {
113
118
  const hash = createHash('sha256')
114
119
  for (const root of [packagePath, ...linkedModuleRoots(packagePath)]) {
@@ -121,11 +126,15 @@ export function sourceStamp(packagePath: string): string {
121
126
  return hash.digest('hex').slice(0, 16)
122
127
  }
123
128
 
124
- // the registry slot for a view: the owning package plus the source within
125
- // it. two packages in one app can hold the same filename, and a bare
126
- // relative path would share one slot, one instance, and its state.
127
- export function viewRootId(packagePath: string, file: string): string {
128
- return path.join(packagePath, path.relative(packagePath, file))
129
+ // the registry slot for an entry: the owning package's own name plus the
130
+ // source within it. two packages in one app can hold the same filename, and
131
+ // a bare relative path would share one slot, one instance, and its state.
132
+ // the name travels instead of the path so identical source in two checkouts
133
+ // emits the identical module; two same-named packages sharing one app still
134
+ // collide, and take the rootId override for that.
135
+ export function entryRootId(packagePath: string, file: string): string {
136
+ const relative = path.relative(packagePath, file).split(path.sep).join(path.posix.sep)
137
+ return `${path.basename(packagePath)}${path.posix.sep}${relative}`
129
138
  }
130
139
 
131
140
  // the app fetches the artifact from a worker whose origin is the shell, so the
@@ -156,6 +165,12 @@ function builtArtifact(packagePath: string): string {
156
165
  return path.join(binDir, artifacts[0])
157
166
  }
158
167
 
168
+ function streamText(value: unknown): string {
169
+ if (typeof value === 'string') return value.trim()
170
+ if (Buffer.isBuffer(value)) return value.toString('utf8').trim()
171
+ return ''
172
+ }
173
+
159
174
  async function compile(packagePath: string, stamp: string): Promise<SwiftArtifact> {
160
175
  const swift = path.join(TOOLCHAIN_BIN, 'swift')
161
176
  if (!fs.existsSync(swift)) {
@@ -180,9 +195,24 @@ async function compile(packagePath: string, stamp: string): Promise<SwiftArtifac
180
195
  { maxBuffer: 64 * 1024 * 1024 },
181
196
  )
182
197
  } catch (error) {
183
- throw new Error(
184
- `swift build failed for ${packagePath}\n${error instanceof Error ? error.message : String(error)}`,
185
- )
198
+ // `swift build` prints the compiler's diagnostics on stdout and swiftpm's
199
+ // own on stderr; the failure's message holds only the command line, so
200
+ // both streams are what names the line that broke. stdout leads because
201
+ // compiler diagnostics are what the editor needs to show.
202
+ const streams =
203
+ error && typeof error === 'object'
204
+ ? [
205
+ streamText(Reflect.get(error, 'stdout')),
206
+ streamText(Reflect.get(error, 'stderr')),
207
+ ].filter(Boolean)
208
+ : []
209
+ const detail =
210
+ streams.length > 0
211
+ ? streams.join('\n')
212
+ : error instanceof Error
213
+ ? error.message
214
+ : String(error)
215
+ throw new Error(`swift build failed for ${packagePath}\n${detail}`)
186
216
  }
187
217
  const bytes = fs.readFileSync(builtArtifact(packagePath))
188
218
  return {
@@ -195,7 +225,17 @@ async function compile(packagePath: string, stamp: string): Promise<SwiftArtifac
195
225
  function artifactFor(project: SwiftProject): Promise<SwiftArtifact> {
196
226
  const stamp = sourceStamp(project.packagePath)
197
227
  if (project.artifact?.stamp === stamp) return Promise.resolve(project.artifact)
198
- project.building ??= compile(project.packagePath, stamp).then(
228
+ if (project.building) {
229
+ // a second edit during a build: the build in flight was started from
230
+ // older sources, so its artifact is not the answer for these. wait it out
231
+ // and ask again, which starts the build these sources need; joining it
232
+ // would emit the older artifact and nothing would trigger another load.
233
+ const running = project.building
234
+ if (running.stamp === stamp) return running.promise
235
+ const again = () => artifactFor(project)
236
+ return running.promise.then(again, again)
237
+ }
238
+ const promise = compile(project.packagePath, stamp).then(
199
239
  (artifact) => {
200
240
  project.artifact = artifact
201
241
  project.building = null
@@ -206,7 +246,8 @@ function artifactFor(project: SwiftProject): Promise<SwiftArtifact> {
206
246
  throw error
207
247
  },
208
248
  )
209
- return project.building
249
+ project.building = { stamp, promise }
250
+ return promise
210
251
  }
211
252
 
212
253
  // the swiftpm package that owns an imported file: the nearest directory at
@@ -228,20 +269,6 @@ function packageFor(file: string): string {
228
269
  )
229
270
  }
230
271
 
231
- // whether the package mounts as a view: its @main type conforms to
232
- // RNXPackage instead of to App. the symbol graph is the exact read once the
233
- // transform emits one; until then the conformance on the @main declaration is
234
- // unambiguous, comments aside.
235
- function isViewPackage(packagePath: string): boolean {
236
- const main =
237
- /@main\s+(?:\w+\s+)*(?:struct|class|enum|actor)\s+\w+[^{]*:\s*[^{]*\bRNXPackage\b/
238
- for (const file of sourceFiles(packagePath)) {
239
- const text = fs.readFileSync(file, 'utf8').replace(/^\s*\/\/.*$/gm, '')
240
- if (main.test(text)) return true
241
- }
242
- return false
243
- }
244
-
245
272
  export interface SwiftPluginOptions {
246
273
  // swiftpm package that owns the project's .swift sources. defaults to the
247
274
  // nearest Package.swift above each imported file, which is the vite root
@@ -274,6 +301,10 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
274
301
  async load(id) {
275
302
  if (!id.startsWith(VIRTUAL_PREFIX)) return null
276
303
  const file = id.slice(VIRTUAL_PREFIX.length)
304
+ // the module is the whole package's artifact, so an import of a file
305
+ // that was deleted would keep answering with it. a deleted .tsx fails
306
+ // its importer; so does a deleted .swift.
307
+ if (!fs.existsSync(file)) throw new Error(`swift source ${file} no longer exists`)
277
308
  const packagePath = options.packagePath ?? packageFor(file)
278
309
  const project = projectFor(packagePath)
279
310
  // every source in the package is a dependency of this module, so a
@@ -301,21 +332,13 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
301
332
  this.warn(error instanceof Error ? error.message : String(error))
302
333
  artifact = project.artifact
303
334
  }
304
- // a view package mounts into its importer: the default export is the
305
- // view component, rooted by its package and source path so a hot reload
306
- // finds the same instance. an app package keeps the root component it
307
- // always had.
308
- if (isViewPackage(packagePath)) {
309
- const rootId = viewRootId(packagePath, file)
310
- return [
311
- `import { createSwiftView } from 'rnxsim/swift'`,
312
- `export default createSwiftView({ url: ${JSON.stringify(artifactUrl(project, artifact))}, hash: ${JSON.stringify(artifact.hash)}, rootId: ${JSON.stringify(rootId)} })`,
313
- '',
314
- ].join('\n')
315
- }
335
+ // every import mounts through the one entry: the artifact answers
336
+ // whether it is a view or an app root at runtime, so the transform
337
+ // never re-derives the conformance from source. the entry is rooted by
338
+ // its package and source path so a hot reload finds the same instance.
316
339
  return [
317
- `import { createSwiftModule } from 'rnxsim/swift'`,
318
- `export default createSwiftModule({ url: ${JSON.stringify(artifactUrl(project, artifact))}, hash: ${JSON.stringify(artifact.hash)} })`,
340
+ `import { createSwiftEntry } from 'rnxsim/swift'`,
341
+ `export default createSwiftEntry({ url: ${JSON.stringify(artifactUrl(project, artifact))}, hash: ${JSON.stringify(artifact.hash)}, rootId: ${JSON.stringify(entryRootId(packagePath, file))} })`,
319
342
  '',
320
343
  ].join('\n')
321
344
  },
@@ -332,24 +355,23 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
332
355
  // the url names a content hash, not a package: the request is answered
333
356
  // by whichever known project built it, so a React Native app whose
334
357
  // swift package sits below its root serves the same path its imports
335
- // compiled.
336
- void (async () => {
337
- for (const project of projects.values()) {
338
- const artifact = await artifactFor(project)
339
- if (url === `${ARTIFACT_PATH}${artifact.hash}.wasm`) {
340
- // the tenant worker's origin is the shell, never this dev server
341
- res.setHeader('access-control-allow-origin', '*')
342
- res.setHeader('content-type', 'application/wasm')
343
- res.end(artifact.bytes)
344
- return
345
- }
358
+ // compiled. served from what is already built, never rebuilt here: the
359
+ // page only knows a hash that `load` emitted, and `load` under a compile
360
+ // error emits the last good hash, whose bytes are still the project's
361
+ // artifact. rebuilding would fail that fetch with the compile error and
362
+ // fail every other project's artifact with it.
363
+ for (const project of projects.values()) {
364
+ const artifact = project.artifact
365
+ if (artifact && url === `${ARTIFACT_PATH}${artifact.hash}.wasm`) {
366
+ // the tenant worker's origin is the shell, never this dev server
367
+ res.setHeader('access-control-allow-origin', '*')
368
+ res.setHeader('content-type', 'application/wasm')
369
+ res.end(artifact.bytes)
370
+ return
346
371
  }
347
- res.statusCode = 404
348
- res.end('swift artifact is stale')
349
- })().catch((error: unknown) => {
350
- res.statusCode = 500
351
- res.end(error instanceof Error ? error.message : String(error))
352
- })
372
+ }
373
+ res.statusCode = 404
374
+ res.end('swift artifact is stale')
353
375
  })
354
376
  },
355
377
  }