rnxsim 0.1.522 → 0.1.524
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/cli/commands/control.ts +31 -15
- 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 +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- 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 +1 -1
- package/dist-lib/cloud.mjs +1 -1
- 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 +1 -1
- 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 +1 -1
- 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 +26 -12
- package/dist-lib/swift.cjs +151 -72
- package/dist-lib/vite.cjs +189 -24
- package/package.json +1 -1
- package/src/swift.ts +2 -3
- package/src/vite-plugin-swift.ts +185 -41
package/src/swift.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// the runtime the `.swift` vite transform emits its module against. the
|
|
2
2
|
// implementation lives beside the toolchain it drives, in sootsim-swift.
|
|
3
3
|
export {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
createSwiftEntry,
|
|
5
|
+
type SwiftEntryOptions,
|
|
6
6
|
type SwiftModuleOptions,
|
|
7
|
-
type SwiftViewOptions,
|
|
8
7
|
} from '../../sootsim-swift/src/mount.tsx'
|
package/src/vite-plugin-swift.ts
CHANGED
|
@@ -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:
|
|
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'
|
|
@@ -16,6 +16,7 @@ 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 { enumerateBundleImages } from '../../sootsim-swift/src/bundleAssets'
|
|
19
20
|
import toolchain from '../../sootsim-swift/toolchain.json'
|
|
20
21
|
import type { Plugin, ViteDevServer } from 'vite'
|
|
21
22
|
|
|
@@ -33,6 +34,7 @@ const WASM_SDK = `${toolchain.release}_wasm`
|
|
|
33
34
|
const WASM_TRIPLE = 'wasm32-unknown-wasip1'
|
|
34
35
|
|
|
35
36
|
const ARTIFACT_PATH = '/__rnx/swift/'
|
|
37
|
+
const RESOURCE_PATH = '/__rnx/swift-res/'
|
|
36
38
|
const VIRTUAL_PREFIX = '\0rnx-swift:'
|
|
37
39
|
|
|
38
40
|
interface SwiftArtifact {
|
|
@@ -49,6 +51,9 @@ interface SwiftProject {
|
|
|
49
51
|
artifact: SwiftArtifact | null
|
|
50
52
|
// the build in flight and the sources it was started from
|
|
51
53
|
building: { stamp: string; promise: Promise<SwiftArtifact> } | null
|
|
54
|
+
// content hash to absolute path for this load's bundle images: the served
|
|
55
|
+
// urls name a hash, never a path, so there is nothing to traverse.
|
|
56
|
+
resourceFiles: Map<string, string>
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
// a project is its swiftpm package: the build, the artifact, and the dev
|
|
@@ -59,7 +64,12 @@ let devServer: ViteDevServer | undefined
|
|
|
59
64
|
function projectFor(packagePath: string): SwiftProject {
|
|
60
65
|
const existing = projects.get(packagePath)
|
|
61
66
|
if (existing) return existing
|
|
62
|
-
const created: SwiftProject = {
|
|
67
|
+
const created: SwiftProject = {
|
|
68
|
+
packagePath,
|
|
69
|
+
artifact: null,
|
|
70
|
+
building: null,
|
|
71
|
+
resourceFiles: new Map(),
|
|
72
|
+
}
|
|
63
73
|
projects.set(packagePath, created)
|
|
64
74
|
return created
|
|
65
75
|
}
|
|
@@ -86,6 +96,119 @@ function sourceFiles(root: string): string[] {
|
|
|
86
96
|
return found
|
|
87
97
|
}
|
|
88
98
|
|
|
99
|
+
// every file that can carry bundle content: catalogs and flat Resources
|
|
100
|
+
// rasters anywhere under the package. `.build` is skipped where sourceFiles
|
|
101
|
+
// does not need to: a release tree holds tens of thousands of files and no
|
|
102
|
+
// bundle content, and walking it per import would tax every hot reload.
|
|
103
|
+
function resourceCandidateFiles(root: string): string[] {
|
|
104
|
+
const found: string[] = []
|
|
105
|
+
const walk = (dir: string): void => {
|
|
106
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
107
|
+
if (
|
|
108
|
+
entry.name.startsWith('.') ||
|
|
109
|
+
entry.name === 'node_modules' ||
|
|
110
|
+
entry.name === '.build'
|
|
111
|
+
)
|
|
112
|
+
continue
|
|
113
|
+
const full = path.join(dir, entry.name)
|
|
114
|
+
if (entry.isDirectory()) {
|
|
115
|
+
if (full !== root && fs.existsSync(path.join(full, 'Package.swift'))) continue
|
|
116
|
+
walk(full)
|
|
117
|
+
} else {
|
|
118
|
+
found.push(full)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
walk(root)
|
|
123
|
+
return found
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const RESOURCE_CONTENT_TYPES: Record<string, string> = {
|
|
127
|
+
'.avif': 'image/avif',
|
|
128
|
+
'.bmp': 'image/bmp',
|
|
129
|
+
'.gif': 'image/gif',
|
|
130
|
+
'.ico': 'image/x-icon',
|
|
131
|
+
'.jpeg': 'image/jpeg',
|
|
132
|
+
'.jpg': 'image/jpeg',
|
|
133
|
+
'.png': 'image/png',
|
|
134
|
+
'.webp': 'image/webp',
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface SwiftResourceTable {
|
|
138
|
+
/** bundle asset name to absolute serving url. */
|
|
139
|
+
table: Record<string, string>
|
|
140
|
+
/** project-relative image files this load's table covers (for watching). */
|
|
141
|
+
watched: string[]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// the producer for the local vehicle: enumerate the package's bundle images
|
|
145
|
+
// through the shared rule both hosts use, hash each raster's bytes into its
|
|
146
|
+
// url (an edited photo moves its url, so no stale paint survives a reload),
|
|
147
|
+
// and register hash to path for the serving middleware below.
|
|
148
|
+
export function buildSwiftResourceTable(
|
|
149
|
+
project: SwiftProject,
|
|
150
|
+
baseUrl: string,
|
|
151
|
+
): SwiftResourceTable {
|
|
152
|
+
const absolute = resourceCandidateFiles(project.packagePath)
|
|
153
|
+
const relative = absolute.map((file) =>
|
|
154
|
+
path.relative(project.packagePath, file).split(path.sep).join(path.posix.sep),
|
|
155
|
+
)
|
|
156
|
+
const byRelative = new Map(absolute.map((file, index) => [relative[index], file]))
|
|
157
|
+
const found = enumerateBundleImages(relative, (candidate) => {
|
|
158
|
+
const file = byRelative.get(candidate)
|
|
159
|
+
if (!file || path.basename(file) !== 'Contents.json') return null
|
|
160
|
+
try {
|
|
161
|
+
return fs.readFileSync(file, 'utf8')
|
|
162
|
+
} catch {
|
|
163
|
+
return null
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
const table: Record<string, string> = {}
|
|
167
|
+
const watched: string[] = []
|
|
168
|
+
const registered = new Map<string, string>()
|
|
169
|
+
for (const { name, file } of found) {
|
|
170
|
+
const disk = byRelative.get(file)
|
|
171
|
+
if (!disk) continue
|
|
172
|
+
let bytes: Buffer
|
|
173
|
+
try {
|
|
174
|
+
bytes = fs.readFileSync(disk)
|
|
175
|
+
} catch {
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 16)
|
|
179
|
+
const ext = path.posix.extname(file).toLowerCase()
|
|
180
|
+
table[name] = `${baseUrl}${RESOURCE_PATH}${hash}${ext}`
|
|
181
|
+
watched.push(file)
|
|
182
|
+
// a Contents.json edit (scale swap, file rename) re-resolves the same
|
|
183
|
+
// way a raster edit does, so the catalog rides the watcher too.
|
|
184
|
+
const contents = file.replace(/\/[^/]+$/, '/Contents.json')
|
|
185
|
+
if (byRelative.has(contents)) watched.push(contents)
|
|
186
|
+
registered.set(`${hash}${ext}`, disk)
|
|
187
|
+
}
|
|
188
|
+
project.resourceFiles = registered
|
|
189
|
+
return { table, watched }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// the middleware's lookup, exported for the producer's own proof: a hit is
|
|
193
|
+
// known bytes for a known hash, anything else is null, never a file.
|
|
194
|
+
export function serveSwiftResource(
|
|
195
|
+
project: SwiftProject,
|
|
196
|
+
key: string,
|
|
197
|
+
): { bytes: Buffer; contentType: string } | null {
|
|
198
|
+
const disk = project.resourceFiles.get(key)
|
|
199
|
+
if (!disk) return null
|
|
200
|
+
try {
|
|
201
|
+
return {
|
|
202
|
+
bytes: fs.readFileSync(disk),
|
|
203
|
+
contentType:
|
|
204
|
+
RESOURCE_CONTENT_TYPES[path.extname(disk).toLowerCase()] ??
|
|
205
|
+
'application/octet-stream',
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
89
212
|
// the SwiftUI module the project links: its manifest names it as a path
|
|
90
213
|
// dependency, and `swift build` compiles it with the project, so an edit
|
|
91
214
|
// there changes the artifact without moving any of the project's own mtimes.
|
|
@@ -108,8 +231,12 @@ export function linkedModuleRoots(packagePath: string): string[] {
|
|
|
108
231
|
// the stamp covers the project plus the modules it links: a running dev
|
|
109
232
|
// server rebuilds only when this moves, so leaving the linked sources out
|
|
110
233
|
// would keep serving the artifact built before a SwiftUI module edit for as
|
|
111
|
-
// long as the project's own sources stayed the same.
|
|
112
|
-
// service's artifact hash,
|
|
234
|
+
// long as the project's own sources stayed the same. it names the same
|
|
235
|
+
// closure the compile service's artifact hash names, but it is not the same
|
|
236
|
+
// function: the stamp hashes mtimes, which is the cheap rebuild signal for a
|
|
237
|
+
// watcher, while the artifact address hashes content, which is what stays
|
|
238
|
+
// stable when a checkout restores byte-identical files. a restore rebuilds
|
|
239
|
+
// here and reuses the stored artifact there, by design.
|
|
113
240
|
export function sourceStamp(packagePath: string): string {
|
|
114
241
|
const hash = createHash('sha256')
|
|
115
242
|
for (const root of [packagePath, ...linkedModuleRoots(packagePath)]) {
|
|
@@ -122,21 +249,29 @@ export function sourceStamp(packagePath: string): string {
|
|
|
122
249
|
return hash.digest('hex').slice(0, 16)
|
|
123
250
|
}
|
|
124
251
|
|
|
125
|
-
// the registry slot for
|
|
126
|
-
// it. two packages in one app can hold the same filename, and
|
|
127
|
-
// relative path would share one slot, one instance, and its state.
|
|
128
|
-
|
|
129
|
-
|
|
252
|
+
// the registry slot for an entry: the owning package's own name plus the
|
|
253
|
+
// source within it. two packages in one app can hold the same filename, and
|
|
254
|
+
// a bare relative path would share one slot, one instance, and its state.
|
|
255
|
+
// the name travels instead of the path so identical source in two checkouts
|
|
256
|
+
// emits the identical module; two same-named packages sharing one app still
|
|
257
|
+
// collide, and take the rootId override for that.
|
|
258
|
+
export function entryRootId(packagePath: string, file: string): string {
|
|
259
|
+
const relative = path.relative(packagePath, file).split(path.sep).join(path.posix.sep)
|
|
260
|
+
return `${path.basename(packagePath)}${path.posix.sep}${relative}`
|
|
130
261
|
}
|
|
131
262
|
|
|
132
|
-
// the app fetches
|
|
133
|
-
//
|
|
134
|
-
function
|
|
263
|
+
// the app fetches from a worker whose origin is the shell, so urls have to
|
|
264
|
+
// name this dev server rather than resolve relative to the shell.
|
|
265
|
+
function serverOrigin(): string {
|
|
135
266
|
const address = devServer?.httpServer?.address()
|
|
136
267
|
const port =
|
|
137
268
|
address && typeof address === 'object' ? address.port : devServer?.config.server.port
|
|
138
|
-
if (!port) throw new Error('the swift
|
|
139
|
-
return `http://localhost:${port}
|
|
269
|
+
if (!port) throw new Error('the swift urls need the dev server port')
|
|
270
|
+
return `http://localhost:${port}`
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function artifactUrl(project: SwiftProject, artifact: SwiftArtifact): string {
|
|
274
|
+
return `${serverOrigin()}${ARTIFACT_PATH}${artifact.hash}.wasm`
|
|
140
275
|
}
|
|
141
276
|
|
|
142
277
|
// one executable is the app. a package with several is ambiguous: the host
|
|
@@ -261,20 +396,6 @@ function packageFor(file: string): string {
|
|
|
261
396
|
)
|
|
262
397
|
}
|
|
263
398
|
|
|
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
|
|
276
|
-
}
|
|
277
|
-
|
|
278
399
|
export interface SwiftPluginOptions {
|
|
279
400
|
// swiftpm package that owns the project's .swift sources. defaults to the
|
|
280
401
|
// nearest Package.swift above each imported file, which is the vite root
|
|
@@ -338,21 +459,26 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
|
|
|
338
459
|
this.warn(error instanceof Error ? error.message : String(error))
|
|
339
460
|
artifact = project.artifact
|
|
340
461
|
}
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
462
|
+
// the bundle image table rides the same module: a photo edit moves
|
|
463
|
+
// its urls without rebuilding the artifact, and the watched files
|
|
464
|
+
// re-run this load so the mount sees the new table on hot reload.
|
|
465
|
+
let resources: Record<string, string> = {}
|
|
466
|
+
try {
|
|
467
|
+
const built = buildSwiftResourceTable(project, serverOrigin())
|
|
468
|
+
resources = built.table
|
|
469
|
+
for (const watched of built.watched) this.addWatchFile(watched)
|
|
470
|
+
} catch (error) {
|
|
471
|
+
this.warn(
|
|
472
|
+
`swift resources for ${packagePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
473
|
+
)
|
|
352
474
|
}
|
|
475
|
+
// every import mounts through the one entry: the artifact answers
|
|
476
|
+
// whether it is a view or an app root at runtime, so the transform
|
|
477
|
+
// never re-derives the conformance from source. the entry is rooted by
|
|
478
|
+
// its package and source path so a hot reload finds the same instance.
|
|
353
479
|
return [
|
|
354
|
-
`import {
|
|
355
|
-
`export default
|
|
480
|
+
`import { createSwiftEntry } from 'rnxsim/swift'`,
|
|
481
|
+
`export default createSwiftEntry({ url: ${JSON.stringify(artifactUrl(project, artifact))}, hash: ${JSON.stringify(artifact.hash)}, rootId: ${JSON.stringify(entryRootId(packagePath, file))}, resources: ${JSON.stringify(resources)} })`,
|
|
356
482
|
'',
|
|
357
483
|
].join('\n')
|
|
358
484
|
},
|
|
@@ -362,6 +488,24 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
|
|
|
362
488
|
|
|
363
489
|
server.middlewares.use((req, res, next) => {
|
|
364
490
|
const url = (req.url ?? '').split('?')[0]
|
|
491
|
+
if (url.startsWith(RESOURCE_PATH)) {
|
|
492
|
+
// hash-addressed: the table this load emitted names these hashes,
|
|
493
|
+
// so a hit serves known bytes and anything else is a 404, never a
|
|
494
|
+
// neighboring file. the tenant worker is cross-origin like the
|
|
495
|
+
// artifact fetch, hence the same cors header.
|
|
496
|
+
for (const project of projects.values()) {
|
|
497
|
+
const served = serveSwiftResource(project, url.slice(RESOURCE_PATH.length))
|
|
498
|
+
if (served) {
|
|
499
|
+
res.setHeader('access-control-allow-origin', '*')
|
|
500
|
+
res.setHeader('content-type', served.contentType)
|
|
501
|
+
res.end(served.bytes)
|
|
502
|
+
return
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
res.statusCode = 404
|
|
506
|
+
res.end('swift resource is stale')
|
|
507
|
+
return
|
|
508
|
+
}
|
|
365
509
|
if (!url.startsWith(ARTIFACT_PATH)) {
|
|
366
510
|
next()
|
|
367
511
|
return
|