rnxsim 0.1.519 → 0.1.521

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 (71) hide show
  1. package/cli/cloud-client.ts +419 -287
  2. package/cli/cloud-dispatch.ts +2 -21
  3. package/cli/cloud-session.ts +31 -85
  4. package/cli/commands/box.ts +15 -11
  5. package/cli/commands/inspect.ts +34 -1
  6. package/cli/commands/platform.ts +184 -232
  7. package/cli/main.ts +10 -2
  8. package/cli/outbound-endpoints.ts +21 -6
  9. package/dist-lib/agent-daemon-client.cjs +1 -1
  10. package/dist-lib/agent-events.cjs +1 -1
  11. package/dist-lib/agent-identity.cjs +1 -1
  12. package/dist-lib/agent-sessions.cjs +1 -1
  13. package/dist-lib/attached-projects.cjs +1 -1
  14. package/dist-lib/auth/shared-session.cjs +1 -1
  15. package/dist-lib/backend-origin.cjs +1 -1
  16. package/dist-lib/beta.cjs +1 -1
  17. package/dist-lib/beta.mjs +1 -1
  18. package/dist-lib/bridge-constants.cjs +1 -1
  19. package/dist-lib/bridge-contract-input.cjs +7 -2
  20. package/dist-lib/bridge-contract-input.mjs +7 -2
  21. package/dist-lib/bridge-contract.cjs +1 -1
  22. package/dist-lib/bridge-contract.mjs +1 -1
  23. package/dist-lib/capture-contract.cjs +1 -1
  24. package/dist-lib/capture-contract.mjs +1 -1
  25. package/dist-lib/cli-constants.cjs +1 -1
  26. package/dist-lib/cloud-contract.cjs +1 -1
  27. package/dist-lib/cloud-contract.mjs +1 -1
  28. package/dist-lib/cloud.cjs +18 -2
  29. package/dist-lib/cloud.mjs +18 -2
  30. package/dist-lib/config.cjs +1 -1
  31. package/dist-lib/detox/index.cjs +1 -1
  32. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  33. package/dist-lib/home-paths.cjs +1 -1
  34. package/dist-lib/host/bridge-host.cjs +112 -39
  35. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  36. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  37. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  38. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  39. package/dist-lib/host/websocket-proxy.cjs +1 -1
  40. package/dist-lib/index.cjs +44 -117
  41. package/dist-lib/jump-to-source-babel.cjs +1 -1
  42. package/dist-lib/jump-to-source-native.cjs +1 -1
  43. package/dist-lib/menu.cjs +30 -1
  44. package/dist-lib/menu.mjs +27 -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/profiles.cjs +1 -1
  51. package/dist-lib/public-brand.cjs +1 -1
  52. package/dist-lib/react-native-host-modules.cjs +1 -1
  53. package/dist-lib/react-native-host-modules.mjs +1 -1
  54. package/dist-lib/render-mode.cjs +1 -1
  55. package/dist-lib/scripts/dev-server-scanner.cjs +41 -4
  56. package/dist-lib/sdk.cjs +1 -1
  57. package/dist-lib/sdk.mjs +1 -1
  58. package/dist-lib/skills.cjs +138 -164
  59. package/dist-lib/swift.cjs +766 -24
  60. package/dist-lib/vite.cjs +103 -30
  61. package/package.json +1 -1
  62. package/scripts/dev-server-scanner.ts +72 -5
  63. package/src/bridge-contract-input.ts +10 -0
  64. package/src/bridge-contract.ts +5 -0
  65. package/src/cloud-contract.ts +7 -1
  66. package/src/cloud.ts +45 -6
  67. package/src/connect.ts +1 -1
  68. package/src/menu.ts +44 -0
  69. package/src/swift.ts +2 -0
  70. package/src/vite-plugin-one.ts +12 -0
  71. package/src/vite-plugin-swift.ts +141 -39
@@ -16,18 +16,20 @@ import fs from 'node:fs'
16
16
  import os from 'node:os'
17
17
  import path from 'node:path'
18
18
  import { promisify } from 'node:util'
19
+ import toolchain from '../../sootsim-swift/toolchain.json'
19
20
  import type { Plugin, ViteDevServer } from 'vite'
20
21
 
21
22
  const run = promisify(execFile)
22
23
 
23
- // the toolchain layout scripts/build.sh pins. xcode's swift has no wasm
24
- // backend, so the swift.org toolchain and its matching wasm sdk are the only
25
- // ones that can build this target.
24
+ // the swift.org release scripts/build.sh builds with, from the one place it is
25
+ // named: the toolchain is <release>.xctoolchain and its matching wasm sdk is
26
+ // <release>_wasm. xcode's swift has no wasm backend, so this toolchain is the
27
+ // only one that can build the target.
26
28
  const TOOLCHAIN_BIN = path.join(
27
29
  os.homedir(),
28
- 'Library/Developer/Toolchains/swift-6.2-RELEASE.xctoolchain/usr/bin',
30
+ `Library/Developer/Toolchains/${toolchain.release}.xctoolchain/usr/bin`,
29
31
  )
30
- const WASM_SDK = 'swift-6.2-RELEASE_wasm'
32
+ const WASM_SDK = `${toolchain.release}_wasm`
31
33
  const WASM_TRIPLE = 'wasm32-unknown-wasip1'
32
34
 
33
35
  const ARTIFACT_PATH = '/__rnx/swift/'
@@ -61,35 +63,71 @@ function projectFor(packagePath: string): SwiftProject {
61
63
  return created
62
64
  }
63
65
 
64
- function rootFor(file: string): string {
65
- return devServer?.config.root ?? path.dirname(file)
66
- }
67
-
68
66
  // the compile unit is the whole package: `swift build` compiles every source
69
- // in it, so one file's edit can change the artifact for all of them.
70
- function sourceFiles(packagePath: string): string[] {
67
+ // in it, so one file's edit can change the artifact for all of them. also
68
+ // used for a linked module below, whose own nested packages are separate
69
+ // units by the same rule.
70
+ function sourceFiles(root: string): string[] {
71
71
  const found: string[] = []
72
72
  const walk = (dir: string): void => {
73
73
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
74
74
  if (entry.name.startsWith('.') || entry.name === 'node_modules') continue
75
75
  const full = path.join(dir, entry.name)
76
- if (entry.isDirectory()) walk(full)
77
- else if (entry.name.endsWith('.swift')) found.push(full)
76
+ // a nested Package.swift is a separate package with its own compile:
77
+ // its sources belong to that unit's stamp, not this one's
78
+ if (entry.isDirectory()) {
79
+ if (full !== root && fs.existsSync(path.join(full, 'Package.swift'))) continue
80
+ walk(full)
81
+ } else if (entry.name.endsWith('.swift')) found.push(full)
78
82
  }
79
83
  }
80
- walk(packagePath)
84
+ walk(root)
81
85
  return found
82
86
  }
83
87
 
84
- function sourceStamp(packagePath: string): string {
88
+ // the SwiftUI module the project links: its manifest names it as a path
89
+ // dependency, and `swift build` compiles it with the project, so an edit
90
+ // there changes the artifact without moving any of the project's own mtimes.
91
+ export function linkedModuleRoots(packagePath: string): string[] {
92
+ let manifest: string
93
+ try {
94
+ manifest = fs.readFileSync(path.join(packagePath, 'Package.swift'), 'utf8')
95
+ } catch {
96
+ return []
97
+ }
98
+ const roots = new Set<string>()
99
+ const dependency = /\.package\(\s*path:\s*"([^"]+)"\s*\)/g
100
+ for (const match of manifest.replace(/^\s*\/\/.*$/gm, '').matchAll(dependency)) {
101
+ const root = path.resolve(packagePath, match[1])
102
+ if (fs.existsSync(path.join(root, 'Package.swift'))) roots.add(root)
103
+ }
104
+ return [...roots].sort()
105
+ }
106
+
107
+ // the stamp covers the project plus the modules it links: a running dev
108
+ // server rebuilds only when this moves, so leaving the linked sources out
109
+ // 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.
112
+ export function sourceStamp(packagePath: string): string {
85
113
  const hash = createHash('sha256')
86
- for (const file of sourceFiles(packagePath).sort()) {
87
- hash.update(path.relative(packagePath, file))
88
- hash.update(String(fs.statSync(file).mtimeMs))
114
+ for (const root of [packagePath, ...linkedModuleRoots(packagePath)]) {
115
+ hash.update(root)
116
+ for (const file of sourceFiles(root).sort()) {
117
+ hash.update(path.relative(root, file))
118
+ hash.update(String(fs.statSync(file).mtimeMs))
119
+ }
89
120
  }
90
121
  return hash.digest('hex').slice(0, 16)
91
122
  }
92
123
 
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
+ }
130
+
93
131
  // the app fetches the artifact from a worker whose origin is the shell, so the
94
132
  // url has to name this dev server rather than resolve relative to the shell.
95
133
  function artifactUrl(project: SwiftProject, artifact: SwiftArtifact): string {
@@ -171,20 +209,62 @@ function artifactFor(project: SwiftProject): Promise<SwiftArtifact> {
171
209
  return project.building
172
210
  }
173
211
 
212
+ // the swiftpm package that owns an imported file: the nearest directory at
213
+ // or above the file that holds a Package.swift, stopping at the vite root
214
+ // once the dev server has named it. a swift-only project keeps its sources at
215
+ // the root so that is its package; a React Native app keeps its in a
216
+ // subdirectory (a `native/` package beside the js), and each such package
217
+ // compiles on its own.
218
+ function packageFor(file: string): string {
219
+ let dir = path.dirname(file)
220
+ const stop = devServer?.config.root
221
+ for (;;) {
222
+ if (fs.existsSync(path.join(dir, 'Package.swift'))) return dir
223
+ if ((stop !== undefined && dir === stop) || path.dirname(dir) === dir) break
224
+ dir = path.dirname(dir)
225
+ }
226
+ throw new Error(
227
+ `no Package.swift above ${file}${stop === undefined ? '' : ` (searched up to ${stop})`}; a .swift import needs a swiftpm package like the one in packages/sootsim-swift/example`,
228
+ )
229
+ }
230
+
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
+
174
245
  export interface SwiftPluginOptions {
175
246
  // swiftpm package that owns the project's .swift sources. defaults to the
176
- // vite root, which is the layout a swift-only project uses.
247
+ // nearest Package.swift above each imported file, which is the vite root
248
+ // for the layout a swift-only project uses.
177
249
  packagePath?: string
178
250
  }
179
251
 
180
252
  export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
181
- const packagePathFor = (file: string): string => options.packagePath ?? rootFor(file)
182
-
183
253
  return {
184
254
  name: 'rnx-swift',
185
255
  enforce: 'pre',
186
256
 
187
257
  async resolveId(source, importer) {
258
+ if (source.startsWith(VIRTUAL_PREFIX)) return source
259
+ // import-analysis re-resolves the served URL form of the id
260
+ // (/@id/__x00__..., with the null byte encoded) while transforming the
261
+ // importer. it still names this module, so answer the decoded id; any
262
+ // other /@id/ url is already resolved and must not fall through to the
263
+ // filesystem resolve below, which would throw for it.
264
+ if (source.startsWith('/@id/')) {
265
+ const decoded = source.slice('/@id/'.length).replace(/^__x00__/, '\0')
266
+ return decoded.startsWith(VIRTUAL_PREFIX) ? decoded : null
267
+ }
188
268
  if (!source.endsWith('.swift')) return null
189
269
  const resolved = await this.resolve(source, importer, { skipSelf: true })
190
270
  if (!resolved) throw new Error(`cannot resolve swift source ${source}`)
@@ -193,13 +273,21 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
193
273
 
194
274
  async load(id) {
195
275
  if (!id.startsWith(VIRTUAL_PREFIX)) return null
196
- const packagePath = packagePathFor(id.slice(VIRTUAL_PREFIX.length))
276
+ const file = id.slice(VIRTUAL_PREFIX.length)
277
+ const packagePath = options.packagePath ?? packageFor(file)
197
278
  const project = projectFor(packagePath)
198
279
  // every source in the package is a dependency of this module, so a
199
280
  // bundler watching this module learns about a `.swift` edit and re-runs
200
281
  // load. that is the whole hot reload: the new hash in the emitted module
201
282
  // is what tells the mount it is looking at a different app.
202
283
  for (const source of sourceFiles(packagePath)) this.addWatchFile(source)
284
+ // an edit to the linked SwiftUI module rebuilds the artifact without
285
+ // touching the project's own sources, so those files gate the reload
286
+ // too: the new hash in the emitted module is what tells the mount it
287
+ // is looking at a different app.
288
+ for (const root of linkedModuleRoots(packagePath)) {
289
+ for (const source of sourceFiles(root)) this.addWatchFile(source)
290
+ }
203
291
  let artifact: SwiftArtifact
204
292
  try {
205
293
  artifact = await artifactFor(project)
@@ -213,6 +301,18 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
213
301
  this.warn(error instanceof Error ? error.message : String(error))
214
302
  artifact = project.artifact
215
303
  }
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
+ }
216
316
  return [
217
317
  `import { createSwiftModule } from 'rnxsim/swift'`,
218
318
  `export default createSwiftModule({ url: ${JSON.stringify(artifactUrl(project, artifact))}, hash: ${JSON.stringify(artifact.hash)} })`,
@@ -222,7 +322,6 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
222
322
 
223
323
  configureServer(server) {
224
324
  devServer = server
225
- const packagePath = packagePathFor('')
226
325
 
227
326
  server.middlewares.use((req, res, next) => {
228
327
  const url = (req.url ?? '').split('?')[0]
@@ -230,24 +329,27 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
230
329
  next()
231
330
  return
232
331
  }
233
- const project = projectFor(packagePath)
234
- void artifactFor(project).then(
235
- (artifact) => {
236
- if (url !== `${ARTIFACT_PATH}${artifact.hash}.wasm`) {
237
- res.statusCode = 404
238
- res.end('swift artifact is stale')
332
+ // the url names a content hash, not a package: the request is answered
333
+ // by whichever known project built it, so a React Native app whose
334
+ // 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)
239
344
  return
240
345
  }
241
- // the tenant worker's origin is the shell, never this dev server
242
- res.setHeader('access-control-allow-origin', '*')
243
- res.setHeader('content-type', 'application/wasm')
244
- res.end(artifact.bytes)
245
- },
246
- (error: unknown) => {
247
- res.statusCode = 500
248
- res.end(error instanceof Error ? error.message : String(error))
249
- },
250
- )
346
+ }
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
+ })
251
353
  })
252
354
  },
253
355
  }