rnxsim 0.1.520 → 0.1.522
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.
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +7 -2
- package/dist-lib/bridge-contract-input.mjs +7 -2
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud.cjs +18 -2
- package/dist-lib/cloud.mjs +18 -2
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +7 -3
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +3 -2
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +7 -3
- package/dist-lib/swift.cjs +119 -14
- package/dist-lib/vite.cjs +95 -34
- package/package.json +1 -1
- package/src/bridge-contract-input.ts +10 -0
- package/src/bridge-contract.ts +5 -0
- package/src/cloud.ts +34 -2
- package/src/swift.ts +2 -0
- package/src/vite-plugin-swift.ts +164 -38
package/src/vite-plugin-swift.ts
CHANGED
|
@@ -47,7 +47,8 @@ interface SwiftArtifact {
|
|
|
47
47
|
interface SwiftProject {
|
|
48
48
|
packagePath: string
|
|
49
49
|
artifact: SwiftArtifact | null
|
|
50
|
-
|
|
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
|
|
@@ -63,35 +64,71 @@ function projectFor(packagePath: string): SwiftProject {
|
|
|
63
64
|
return created
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
function rootFor(file: string): string {
|
|
67
|
-
return devServer?.config.root ?? path.dirname(file)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
67
|
// the compile unit is the whole package: `swift build` compiles every source
|
|
71
|
-
// in it, so one file's edit can change the artifact for all of them.
|
|
72
|
-
|
|
68
|
+
// in it, so one file's edit can change the artifact for all of them. also
|
|
69
|
+
// used for a linked module below, whose own nested packages are separate
|
|
70
|
+
// units by the same rule.
|
|
71
|
+
function sourceFiles(root: string): string[] {
|
|
73
72
|
const found: string[] = []
|
|
74
73
|
const walk = (dir: string): void => {
|
|
75
74
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
76
75
|
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue
|
|
77
76
|
const full = path.join(dir, entry.name)
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
// a nested Package.swift is a separate package with its own compile:
|
|
78
|
+
// its sources belong to that unit's stamp, not this one's
|
|
79
|
+
if (entry.isDirectory()) {
|
|
80
|
+
if (full !== root && fs.existsSync(path.join(full, 'Package.swift'))) continue
|
|
81
|
+
walk(full)
|
|
82
|
+
} else if (entry.name.endsWith('.swift')) found.push(full)
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
|
-
walk(
|
|
85
|
+
walk(root)
|
|
83
86
|
return found
|
|
84
87
|
}
|
|
85
88
|
|
|
86
|
-
|
|
89
|
+
// the SwiftUI module the project links: its manifest names it as a path
|
|
90
|
+
// dependency, and `swift build` compiles it with the project, so an edit
|
|
91
|
+
// there changes the artifact without moving any of the project's own mtimes.
|
|
92
|
+
export function linkedModuleRoots(packagePath: string): string[] {
|
|
93
|
+
let manifest: string
|
|
94
|
+
try {
|
|
95
|
+
manifest = fs.readFileSync(path.join(packagePath, 'Package.swift'), 'utf8')
|
|
96
|
+
} catch {
|
|
97
|
+
return []
|
|
98
|
+
}
|
|
99
|
+
const roots = new Set<string>()
|
|
100
|
+
const dependency = /\.package\(\s*path:\s*"([^"]+)"\s*\)/g
|
|
101
|
+
for (const match of manifest.replace(/^\s*\/\/.*$/gm, '').matchAll(dependency)) {
|
|
102
|
+
const root = path.resolve(packagePath, match[1])
|
|
103
|
+
if (fs.existsSync(path.join(root, 'Package.swift'))) roots.add(root)
|
|
104
|
+
}
|
|
105
|
+
return [...roots].sort()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// the stamp covers the project plus the modules it links: a running dev
|
|
109
|
+
// server rebuilds only when this moves, so leaving the linked sources out
|
|
110
|
+
// would keep serving the artifact built before a SwiftUI module edit for as
|
|
111
|
+
// long as the project's own sources stayed the same. mirrors the compile
|
|
112
|
+
// service's artifact hash, which names the same closure.
|
|
113
|
+
export function sourceStamp(packagePath: string): string {
|
|
87
114
|
const hash = createHash('sha256')
|
|
88
|
-
for (const
|
|
89
|
-
hash.update(
|
|
90
|
-
|
|
115
|
+
for (const root of [packagePath, ...linkedModuleRoots(packagePath)]) {
|
|
116
|
+
hash.update(root)
|
|
117
|
+
for (const file of sourceFiles(root).sort()) {
|
|
118
|
+
hash.update(path.relative(root, file))
|
|
119
|
+
hash.update(String(fs.statSync(file).mtimeMs))
|
|
120
|
+
}
|
|
91
121
|
}
|
|
92
122
|
return hash.digest('hex').slice(0, 16)
|
|
93
123
|
}
|
|
94
124
|
|
|
125
|
+
// the registry slot for a view: the owning package plus the source within
|
|
126
|
+
// it. two packages in one app can hold the same filename, and a bare
|
|
127
|
+
// relative path would share one slot, one instance, and its state.
|
|
128
|
+
export function viewRootId(packagePath: string, file: string): string {
|
|
129
|
+
return path.join(packagePath, path.relative(packagePath, file))
|
|
130
|
+
}
|
|
131
|
+
|
|
95
132
|
// the app fetches the artifact from a worker whose origin is the shell, so the
|
|
96
133
|
// url has to name this dev server rather than resolve relative to the shell.
|
|
97
134
|
function artifactUrl(project: SwiftProject, artifact: SwiftArtifact): string {
|
|
@@ -120,6 +157,12 @@ function builtArtifact(packagePath: string): string {
|
|
|
120
157
|
return path.join(binDir, artifacts[0])
|
|
121
158
|
}
|
|
122
159
|
|
|
160
|
+
function streamText(value: unknown): string {
|
|
161
|
+
if (typeof value === 'string') return value.trim()
|
|
162
|
+
if (Buffer.isBuffer(value)) return value.toString('utf8').trim()
|
|
163
|
+
return ''
|
|
164
|
+
}
|
|
165
|
+
|
|
123
166
|
async function compile(packagePath: string, stamp: string): Promise<SwiftArtifact> {
|
|
124
167
|
const swift = path.join(TOOLCHAIN_BIN, 'swift')
|
|
125
168
|
if (!fs.existsSync(swift)) {
|
|
@@ -144,9 +187,24 @@ async function compile(packagePath: string, stamp: string): Promise<SwiftArtifac
|
|
|
144
187
|
{ maxBuffer: 64 * 1024 * 1024 },
|
|
145
188
|
)
|
|
146
189
|
} catch (error) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
190
|
+
// `swift build` prints the compiler's diagnostics on stdout and swiftpm's
|
|
191
|
+
// own on stderr; the failure's message holds only the command line, so
|
|
192
|
+
// both streams are what names the line that broke. stdout leads because
|
|
193
|
+
// compiler diagnostics are what the editor needs to show.
|
|
194
|
+
const streams =
|
|
195
|
+
error && typeof error === 'object'
|
|
196
|
+
? [
|
|
197
|
+
streamText(Reflect.get(error, 'stdout')),
|
|
198
|
+
streamText(Reflect.get(error, 'stderr')),
|
|
199
|
+
].filter(Boolean)
|
|
200
|
+
: []
|
|
201
|
+
const detail =
|
|
202
|
+
streams.length > 0
|
|
203
|
+
? streams.join('\n')
|
|
204
|
+
: error instanceof Error
|
|
205
|
+
? error.message
|
|
206
|
+
: String(error)
|
|
207
|
+
throw new Error(`swift build failed for ${packagePath}\n${detail}`)
|
|
150
208
|
}
|
|
151
209
|
const bytes = fs.readFileSync(builtArtifact(packagePath))
|
|
152
210
|
return {
|
|
@@ -159,7 +217,17 @@ async function compile(packagePath: string, stamp: string): Promise<SwiftArtifac
|
|
|
159
217
|
function artifactFor(project: SwiftProject): Promise<SwiftArtifact> {
|
|
160
218
|
const stamp = sourceStamp(project.packagePath)
|
|
161
219
|
if (project.artifact?.stamp === stamp) return Promise.resolve(project.artifact)
|
|
162
|
-
|
|
220
|
+
if (project.building) {
|
|
221
|
+
// a second edit during a build: the build in flight was started from
|
|
222
|
+
// older sources, so its artifact is not the answer for these. wait it out
|
|
223
|
+
// and ask again, which starts the build these sources need; joining it
|
|
224
|
+
// would emit the older artifact and nothing would trigger another load.
|
|
225
|
+
const running = project.building
|
|
226
|
+
if (running.stamp === stamp) return running.promise
|
|
227
|
+
const again = () => artifactFor(project)
|
|
228
|
+
return running.promise.then(again, again)
|
|
229
|
+
}
|
|
230
|
+
const promise = compile(project.packagePath, stamp).then(
|
|
163
231
|
(artifact) => {
|
|
164
232
|
project.artifact = artifact
|
|
165
233
|
project.building = null
|
|
@@ -170,18 +238,51 @@ function artifactFor(project: SwiftProject): Promise<SwiftArtifact> {
|
|
|
170
238
|
throw error
|
|
171
239
|
},
|
|
172
240
|
)
|
|
173
|
-
|
|
241
|
+
project.building = { stamp, promise }
|
|
242
|
+
return promise
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// the swiftpm package that owns an imported file: the nearest directory at
|
|
246
|
+
// or above the file that holds a Package.swift, stopping at the vite root
|
|
247
|
+
// once the dev server has named it. a swift-only project keeps its sources at
|
|
248
|
+
// the root so that is its package; a React Native app keeps its in a
|
|
249
|
+
// subdirectory (a `native/` package beside the js), and each such package
|
|
250
|
+
// compiles on its own.
|
|
251
|
+
function packageFor(file: string): string {
|
|
252
|
+
let dir = path.dirname(file)
|
|
253
|
+
const stop = devServer?.config.root
|
|
254
|
+
for (;;) {
|
|
255
|
+
if (fs.existsSync(path.join(dir, 'Package.swift'))) return dir
|
|
256
|
+
if ((stop !== undefined && dir === stop) || path.dirname(dir) === dir) break
|
|
257
|
+
dir = path.dirname(dir)
|
|
258
|
+
}
|
|
259
|
+
throw new Error(
|
|
260
|
+
`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`,
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// whether the package mounts as a view: its @main type conforms to
|
|
265
|
+
// RNXPackage instead of to App. the symbol graph is the exact read once the
|
|
266
|
+
// transform emits one; until then the conformance on the @main declaration is
|
|
267
|
+
// unambiguous, comments aside.
|
|
268
|
+
function isViewPackage(packagePath: string): boolean {
|
|
269
|
+
const main =
|
|
270
|
+
/@main\s+(?:\w+\s+)*(?:struct|class|enum|actor)\s+\w+[^{]*:\s*[^{]*\bRNXPackage\b/
|
|
271
|
+
for (const file of sourceFiles(packagePath)) {
|
|
272
|
+
const text = fs.readFileSync(file, 'utf8').replace(/^\s*\/\/.*$/gm, '')
|
|
273
|
+
if (main.test(text)) return true
|
|
274
|
+
}
|
|
275
|
+
return false
|
|
174
276
|
}
|
|
175
277
|
|
|
176
278
|
export interface SwiftPluginOptions {
|
|
177
279
|
// swiftpm package that owns the project's .swift sources. defaults to the
|
|
178
|
-
//
|
|
280
|
+
// nearest Package.swift above each imported file, which is the vite root
|
|
281
|
+
// for the layout a swift-only project uses.
|
|
179
282
|
packagePath?: string
|
|
180
283
|
}
|
|
181
284
|
|
|
182
285
|
export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
|
|
183
|
-
const packagePathFor = (file: string): string => options.packagePath ?? rootFor(file)
|
|
184
|
-
|
|
185
286
|
return {
|
|
186
287
|
name: 'rnx-swift',
|
|
187
288
|
enforce: 'pre',
|
|
@@ -205,13 +306,25 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
|
|
|
205
306
|
|
|
206
307
|
async load(id) {
|
|
207
308
|
if (!id.startsWith(VIRTUAL_PREFIX)) return null
|
|
208
|
-
const
|
|
309
|
+
const file = id.slice(VIRTUAL_PREFIX.length)
|
|
310
|
+
// the module is the whole package's artifact, so an import of a file
|
|
311
|
+
// that was deleted would keep answering with it. a deleted .tsx fails
|
|
312
|
+
// its importer; so does a deleted .swift.
|
|
313
|
+
if (!fs.existsSync(file)) throw new Error(`swift source ${file} no longer exists`)
|
|
314
|
+
const packagePath = options.packagePath ?? packageFor(file)
|
|
209
315
|
const project = projectFor(packagePath)
|
|
210
316
|
// every source in the package is a dependency of this module, so a
|
|
211
317
|
// bundler watching this module learns about a `.swift` edit and re-runs
|
|
212
318
|
// load. that is the whole hot reload: the new hash in the emitted module
|
|
213
319
|
// is what tells the mount it is looking at a different app.
|
|
214
320
|
for (const source of sourceFiles(packagePath)) this.addWatchFile(source)
|
|
321
|
+
// an edit to the linked SwiftUI module rebuilds the artifact without
|
|
322
|
+
// touching the project's own sources, so those files gate the reload
|
|
323
|
+
// too: the new hash in the emitted module is what tells the mount it
|
|
324
|
+
// is looking at a different app.
|
|
325
|
+
for (const root of linkedModuleRoots(packagePath)) {
|
|
326
|
+
for (const source of sourceFiles(root)) this.addWatchFile(source)
|
|
327
|
+
}
|
|
215
328
|
let artifact: SwiftArtifact
|
|
216
329
|
try {
|
|
217
330
|
artifact = await artifactFor(project)
|
|
@@ -225,6 +338,18 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
|
|
|
225
338
|
this.warn(error instanceof Error ? error.message : String(error))
|
|
226
339
|
artifact = project.artifact
|
|
227
340
|
}
|
|
341
|
+
// a view package mounts into its importer: the default export is the
|
|
342
|
+
// view component, rooted by its package and source path so a hot reload
|
|
343
|
+
// finds the same instance. an app package keeps the root component it
|
|
344
|
+
// always had.
|
|
345
|
+
if (isViewPackage(packagePath)) {
|
|
346
|
+
const rootId = viewRootId(packagePath, file)
|
|
347
|
+
return [
|
|
348
|
+
`import { createSwiftView } from 'rnxsim/swift'`,
|
|
349
|
+
`export default createSwiftView({ url: ${JSON.stringify(artifactUrl(project, artifact))}, hash: ${JSON.stringify(artifact.hash)}, rootId: ${JSON.stringify(rootId)} })`,
|
|
350
|
+
'',
|
|
351
|
+
].join('\n')
|
|
352
|
+
}
|
|
228
353
|
return [
|
|
229
354
|
`import { createSwiftModule } from 'rnxsim/swift'`,
|
|
230
355
|
`export default createSwiftModule({ url: ${JSON.stringify(artifactUrl(project, artifact))}, hash: ${JSON.stringify(artifact.hash)} })`,
|
|
@@ -234,7 +359,6 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
|
|
|
234
359
|
|
|
235
360
|
configureServer(server) {
|
|
236
361
|
devServer = server
|
|
237
|
-
const packagePath = packagePathFor('')
|
|
238
362
|
|
|
239
363
|
server.middlewares.use((req, res, next) => {
|
|
240
364
|
const url = (req.url ?? '').split('?')[0]
|
|
@@ -242,24 +366,26 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
|
|
|
242
366
|
next()
|
|
243
367
|
return
|
|
244
368
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
369
|
+
// the url names a content hash, not a package: the request is answered
|
|
370
|
+
// by whichever known project built it, so a React Native app whose
|
|
371
|
+
// swift package sits below its root serves the same path its imports
|
|
372
|
+
// compiled. served from what is already built, never rebuilt here: the
|
|
373
|
+
// page only knows a hash that `load` emitted, and `load` under a compile
|
|
374
|
+
// error emits the last good hash, whose bytes are still the project's
|
|
375
|
+
// artifact. rebuilding would fail that fetch with the compile error and
|
|
376
|
+
// fail every other project's artifact with it.
|
|
377
|
+
for (const project of projects.values()) {
|
|
378
|
+
const artifact = project.artifact
|
|
379
|
+
if (artifact && url === `${ARTIFACT_PATH}${artifact.hash}.wasm`) {
|
|
253
380
|
// the tenant worker's origin is the shell, never this dev server
|
|
254
381
|
res.setHeader('access-control-allow-origin', '*')
|
|
255
382
|
res.setHeader('content-type', 'application/wasm')
|
|
256
383
|
res.end(artifact.bytes)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
)
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
res.statusCode = 404
|
|
388
|
+
res.end('swift artifact is stale')
|
|
263
389
|
})
|
|
264
390
|
},
|
|
265
391
|
}
|