peachsim 0.1.527
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/LICENSE +22 -0
- package/README.md +640 -0
- package/cli/app-config.ts +82 -0
- package/cli/app-fonts.ts +388 -0
- package/cli/app-project.ts +59 -0
- package/cli/app-splash.ts +171 -0
- package/cli/app-state-reset.ts +26 -0
- package/cli/auth.ts +184 -0
- package/cli/bin.ts +59 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2806 -0
- package/cli/browser-backend.ts +44 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/cli-update.ts +261 -0
- package/cli/cloud-client.ts +1530 -0
- package/cli/cloud-dispatch.ts +206 -0
- package/cli/cloud-session.ts +227 -0
- package/cli/command-contract.ts +221 -0
- package/cli/command-registry.ts +304 -0
- package/cli/commands/agent-wrapper.ts +1048 -0
- package/cli/commands/agent.ts +439 -0
- package/cli/commands/app-fonts.ts +99 -0
- package/cli/commands/assert.ts +542 -0
- package/cli/commands/auth.ts +71 -0
- package/cli/commands/box/bundle-plane.ts +71 -0
- package/cli/commands/box/checkout-plane.ts +256 -0
- package/cli/commands/box/plane-share.ts +208 -0
- package/cli/commands/box/rnx-command.ts +60 -0
- package/cli/commands/box/search-command.ts +157 -0
- package/cli/commands/box.ts +925 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +176 -0
- package/cli/commands/config.ts +34 -0
- package/cli/commands/control.ts +2154 -0
- package/cli/commands/cpu-profile.ts +941 -0
- package/cli/commands/daemon-mac-app.ts +165 -0
- package/cli/commands/daemon.ts +800 -0
- package/cli/commands/debug.ts +751 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +201 -0
- package/cli/commands/detox.ts +481 -0
- package/cli/commands/device.ts +134 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/do-chain.ts +408 -0
- package/cli/commands/electron.ts +94 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1240 -0
- package/cli/commands/inspect/actions.ts +516 -0
- package/cli/commands/inspect/core.ts +2086 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +119 -0
- package/cli/commands/inspect/env.ts +25 -0
- package/cli/commands/inspect/find.ts +169 -0
- package/cli/commands/inspect/get-layout.ts +53 -0
- package/cli/commands/inspect/keyboard.ts +54 -0
- package/cli/commands/inspect/list.ts +175 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +77 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +32 -0
- package/cli/commands/inspect/settling.ts +197 -0
- package/cli/commands/inspect/shared.ts +259 -0
- package/cli/commands/inspect/sleep.ts +15 -0
- package/cli/commands/inspect/tree.ts +23 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +211 -0
- package/cli/commands/inspect/wait-idle.ts +25 -0
- package/cli/commands/inspect/wait-ready.ts +87 -0
- package/cli/commands/inspect/wait-selector.ts +59 -0
- package/cli/commands/inspect/wait.ts +32 -0
- package/cli/commands/inspect.ts +4738 -0
- package/cli/commands/install-cli.ts +275 -0
- package/cli/commands/install-desktop.ts +385 -0
- package/cli/commands/login.ts +357 -0
- package/cli/commands/logout.ts +24 -0
- package/cli/commands/maestro-generate.ts +375 -0
- package/cli/commands/maestro.ts +536 -0
- package/cli/commands/mode.ts +58 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +67 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/platform.ts +410 -0
- package/cli/commands/preview.ts +103 -0
- package/cli/commands/profile.ts +106 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1435 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +89 -0
- package/cli/commands/runtime.ts +348 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +144 -0
- package/cli/commands/screenshot-mode.ts +38 -0
- package/cli/commands/screenshot.ts +492 -0
- package/cli/commands/screenshots-capture.ts +608 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +139 -0
- package/cli/commands/setup.ts +416 -0
- package/cli/commands/shell-boolean-mode.ts +82 -0
- package/cli/commands/skills.ts +468 -0
- package/cli/commands/slides.ts +357 -0
- package/cli/commands/state.ts +94 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +55 -0
- package/cli/commands/test.ts +535 -0
- package/cli/commands/three-mode.ts +771 -0
- package/cli/commands/timeline.ts +121 -0
- package/cli/commands/upgrade.ts +167 -0
- package/cli/commands/upload.ts +1203 -0
- package/cli/commands/version.ts +63 -0
- package/cli/commands/welcome.ts +17 -0
- package/cli/commands/what-happened.ts +328 -0
- package/cli/current-sim.ts +217 -0
- package/cli/desktop-companion.ts +315 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +184 -0
- package/cli/drivers/playwright-sim-host.ts +537 -0
- package/cli/drivers/playwright.ts +209 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-export.ts +55 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +126 -0
- package/cli/flow-session.ts +140 -0
- package/cli/help-core.ts +143 -0
- package/cli/help.ts +44 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +214 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/internal-child.ts +176 -0
- package/cli/maestro-js.ts +323 -0
- package/cli/main.ts +704 -0
- package/cli/open-url.ts +8 -0
- package/cli/outbound-endpoints.ts +830 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +189 -0
- package/cli/privacy.ts +53 -0
- package/cli/prompt.ts +56 -0
- package/cli/recording-access.ts +108 -0
- package/cli/registry.ts +2 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/rnx-in-process.ts +98 -0
- package/cli/run-registry.ts +226 -0
- package/cli/run-rnx-core.ts +148 -0
- package/cli/run-rnx.ts +137 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/secure-file.ts +31 -0
- package/cli/self-invocation.ts +58 -0
- package/cli/send-to-box.ts +109 -0
- package/cli/setup-repository.ts +191 -0
- package/cli/shell-init.ts +98 -0
- package/cli/standalone.ts +4 -0
- package/cli/telemetry.ts +84 -0
- package/cli/ui.ts +68 -0
- package/cli/ws-bridge.ts +1020 -0
- package/detox/attempt-events.cjs +23 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +48 -0
- package/detox/expectations.ts +534 -0
- package/detox/gestures.ts +501 -0
- package/detox/index.ts +2694 -0
- package/detox/jest-preset.cjs +29 -0
- package/detox/jest-setup-after-env.cjs +22 -0
- package/detox/matchers.ts +30 -0
- package/detox/motion-change.cjs +109 -0
- package/detox/motion-change.d.cts +35 -0
- package/detox/navigation-lifecycle.ts +290 -0
- package/detox/navigation.ts +43 -0
- package/detox/proof-frame.cjs +114 -0
- package/detox/record-block-execution.cjs +78 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-lib/agent-daemon-client.cjs +502 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +759 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +208 -0
- package/dist-lib/backend-origin.cjs +88 -0
- package/dist-lib/beta.cjs +41 -0
- package/dist-lib/beta.mjs +13 -0
- package/dist-lib/bridge-constants.cjs +161 -0
- package/dist-lib/bridge-contract-input.cjs +451 -0
- package/dist-lib/bridge-contract-input.mjs +414 -0
- package/dist-lib/bridge-contract.cjs +130 -0
- package/dist-lib/bridge-contract.mjs +94 -0
- package/dist-lib/capture-contract.cjs +220 -0
- package/dist-lib/capture-contract.mjs +190 -0
- package/dist-lib/cli-constants.cjs +32 -0
- package/dist-lib/cloud-contract.cjs +194 -0
- package/dist-lib/cloud-contract.mjs +145 -0
- package/dist-lib/cloud.cjs +670 -0
- package/dist-lib/cloud.mjs +639 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +3789 -0
- package/dist-lib/detox/jest-preset.cjs +29 -0
- package/dist-lib/dev-bundle-resolution.cjs +290 -0
- package/dist-lib/home-paths.cjs +638 -0
- package/dist-lib/host/bridge-host.cjs +10903 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
- package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
- package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
- package/dist-lib/host/replacement-module-handler.cjs +469 -0
- package/dist-lib/host/websocket-proxy.cjs +298 -0
- package/dist-lib/index.cjs +4268 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/jump-to-source-native.cjs +112 -0
- package/dist-lib/menu.cjs +646 -0
- package/dist-lib/menu.mjs +604 -0
- package/dist-lib/metro-fingerprint-registry.cjs +186 -0
- package/dist-lib/metro-fingerprint-registry.mjs +156 -0
- package/dist-lib/metro-production-bundle.cjs +329 -0
- package/dist-lib/metro-production-bundle.mjs +295 -0
- package/dist-lib/metro.cjs +904 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/react-native-host-modules.cjs +51 -0
- package/dist-lib/react-native-host-modules.mjs +25 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
- package/dist-lib/sdk.cjs +2433 -0
- package/dist-lib/sdk.mjs +2336 -0
- package/dist-lib/skills.cjs +39132 -0
- package/dist-lib/swift-submit.cjs +92 -0
- package/dist-lib/swift-submit.mjs +65 -0
- package/dist-lib/swift.cjs +1646 -0
- package/dist-lib/vite.cjs +1591 -0
- package/package.json +290 -0
- package/scripts/dev-server-scanner.ts +1291 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/skills/contrast/SKILL.md +165 -0
- package/skills/rnx-debug/SKILL.md +433 -0
- package/skills/rnx-perf/SKILL.md +399 -0
- package/skills/rnx-setup/SKILL.md +172 -0
- package/skills/rnx-test/SKILL.md +321 -0
- package/skills/rnx-visual/SKILL.md +126 -0
- package/src/agent-daemon-client.ts +410 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-identity.ts +23 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +580 -0
- package/src/app-url.ts +39 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +235 -0
- package/src/backend-origin.ts +70 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +175 -0
- package/src/bridge-contract-input.ts +502 -0
- package/src/bridge-contract.ts +558 -0
- package/src/browser-cache.ts +16 -0
- package/src/capture-contract.ts +587 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +40 -0
- package/src/cloud-contract.ts +274 -0
- package/src/cloud.ts +762 -0
- package/src/codex-client.ts +234 -0
- package/src/config.ts +132 -0
- package/src/connect.ts +101 -0
- package/src/dev-bundle-resolution.ts +346 -0
- package/src/dev-server-open.ts +198 -0
- package/src/disk-cleanup.ts +540 -0
- package/src/flow-timeout-scale.ts +15 -0
- package/src/home-paths.ts +806 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2788 -0
- package/src/host/fetch-proxy-handler.ts +449 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/plane-host.ts +203 -0
- package/src/host/replacement-module-handler.ts +158 -0
- package/src/host/websocket-proxy.ts +315 -0
- package/src/index.ts +71 -0
- package/src/jump-to-source-babel.ts +135 -0
- package/src/jump-to-source-native.ts +102 -0
- package/src/menu.ts +777 -0
- package/src/metro-fingerprint-registry.ts +302 -0
- package/src/metro-plugin.ts +341 -0
- package/src/metro-production-bundle.ts +471 -0
- package/src/native-dev-bundle-url.ts +118 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/playwright-host-log.ts +16 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +123 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/react-native-host-modules.ts +26 -0
- package/src/render-mode.ts +27 -0
- package/src/rolldown-plugin-swift.ts +240 -0
- package/src/runtime-assets.ts +233 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +514 -0
- package/src/screenshots/orchestrate.ts +256 -0
- package/src/screenshots/org-deck.ts +91 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/sdk.ts +15 -0
- package/src/settings-bundle.ts +418 -0
- package/src/shell-bridge-identity.ts +15 -0
- package/src/sim-client.ts +697 -0
- package/src/skills/builtin/a11y-review.ts +132 -0
- package/src/skills/builtin/compat-check.ts +71 -0
- package/src/skills/builtin/maestro-test.ts +121 -0
- package/src/skills/builtin/perf-profile.ts +90 -0
- package/src/skills/builtin/screenshot-all.ts +52 -0
- package/src/skills/builtin/visual-diff.ts +100 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/swift-submit.ts +185 -0
- package/src/swift.ts +7 -0
- package/src/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +264 -0
- package/src/vite-plugin-swift.ts +620 -0
- package/src/vite-plugin.ts +1946 -0
- package/src/worklets-babel.ts +250 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
// `.swift` handling for a vite dev server: compile the project's swift sources
|
|
2
|
+
// with the swift.org toolchain, serve the artifact at a content-hashed url,
|
|
3
|
+
// and answer every `.swift` import with the module that mounts it.
|
|
4
|
+
//
|
|
5
|
+
// two hosts use this. the bundler that builds the app's native bundle runs
|
|
6
|
+
// resolveId/load, and the dev server runs the middleware that serves the
|
|
7
|
+
// artifact. both share the build below, so the artifact is compiled once.
|
|
8
|
+
//
|
|
9
|
+
// the hash is the hash of the built wasm, so a new hash means a genuinely
|
|
10
|
+
// different app: createSwiftEntry moves the @State store across it instead of
|
|
11
|
+
// remounting.
|
|
12
|
+
|
|
13
|
+
import { execFile } from 'node:child_process'
|
|
14
|
+
import { createHash } from 'node:crypto'
|
|
15
|
+
import fs from 'node:fs'
|
|
16
|
+
import os from 'node:os'
|
|
17
|
+
import path from 'node:path'
|
|
18
|
+
import { promisify } from 'node:util'
|
|
19
|
+
import {
|
|
20
|
+
enumerateBundleImages,
|
|
21
|
+
enumerateBundleJson,
|
|
22
|
+
} from '../../sootsim-swift/src/bundleAssets'
|
|
23
|
+
import toolchain from '../../sootsim-swift/toolchain.json'
|
|
24
|
+
import type { Plugin, ViteDevServer } from 'vite'
|
|
25
|
+
|
|
26
|
+
const run = promisify(execFile)
|
|
27
|
+
|
|
28
|
+
// the swift.org release scripts/build.sh builds with, from the one place it is
|
|
29
|
+
// named: the toolchain is <release>.xctoolchain and its matching wasm sdk is
|
|
30
|
+
// <release>_wasm. xcode's swift has no wasm backend, so this toolchain is the
|
|
31
|
+
// only one that can build the target.
|
|
32
|
+
const TOOLCHAIN_BIN = path.join(
|
|
33
|
+
os.homedir(),
|
|
34
|
+
`Library/Developer/Toolchains/${toolchain.release}.xctoolchain/usr/bin`,
|
|
35
|
+
)
|
|
36
|
+
const WASM_SDK = `${toolchain.release}_wasm`
|
|
37
|
+
const WASM_TRIPLE = 'wasm32-unknown-wasip1'
|
|
38
|
+
|
|
39
|
+
const ARTIFACT_PATH = '/__rnx/swift/'
|
|
40
|
+
const RESOURCE_PATH = '/__rnx/swift-res/'
|
|
41
|
+
const VIRTUAL_PREFIX = '\0rnx-swift:'
|
|
42
|
+
|
|
43
|
+
interface SwiftArtifact {
|
|
44
|
+
hash: string
|
|
45
|
+
bytes: Buffer
|
|
46
|
+
// the sources this artifact was built from. a watcher event and a `load`
|
|
47
|
+
// reach the project in either order, so freshness is read from the sources
|
|
48
|
+
// rather than from whichever of them ran first.
|
|
49
|
+
stamp: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface SwiftProject {
|
|
53
|
+
packagePath: string
|
|
54
|
+
artifact: SwiftArtifact | null
|
|
55
|
+
// the build in flight and the sources it was started from
|
|
56
|
+
building: { stamp: string; promise: Promise<SwiftArtifact> } | null
|
|
57
|
+
// content hash to absolute path for this load's bundle images: the served
|
|
58
|
+
// urls name a hash, never a path, so there is nothing to traverse.
|
|
59
|
+
resourceFiles: Map<string, string>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// a project is its swiftpm package: the build, the artifact, and the dev
|
|
63
|
+
// server that serves it are shared by every plugin instance in this process.
|
|
64
|
+
const projects = new Map<string, SwiftProject>()
|
|
65
|
+
let devServer: ViteDevServer | undefined
|
|
66
|
+
|
|
67
|
+
function projectFor(packagePath: string): SwiftProject {
|
|
68
|
+
const existing = projects.get(packagePath)
|
|
69
|
+
if (existing) return existing
|
|
70
|
+
const created: SwiftProject = {
|
|
71
|
+
packagePath,
|
|
72
|
+
artifact: null,
|
|
73
|
+
building: null,
|
|
74
|
+
resourceFiles: new Map(),
|
|
75
|
+
}
|
|
76
|
+
projects.set(packagePath, created)
|
|
77
|
+
return created
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// the compile unit is the whole package: `swift build` compiles every source
|
|
81
|
+
// in it, so one file's edit can change the artifact for all of them. also
|
|
82
|
+
// used for a linked module below, whose own nested packages are separate
|
|
83
|
+
// units by the same rule.
|
|
84
|
+
function sourceFiles(root: string): string[] {
|
|
85
|
+
const found: string[] = []
|
|
86
|
+
const walk = (dir: string): void => {
|
|
87
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
88
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue
|
|
89
|
+
const full = path.join(dir, entry.name)
|
|
90
|
+
// a nested Package.swift is a separate package with its own compile:
|
|
91
|
+
// its sources belong to that unit's stamp, not this one's
|
|
92
|
+
if (entry.isDirectory()) {
|
|
93
|
+
if (full !== root && fs.existsSync(path.join(full, 'Package.swift'))) continue
|
|
94
|
+
walk(full)
|
|
95
|
+
} else if (entry.name.endsWith('.swift')) found.push(full)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
walk(root)
|
|
99
|
+
return found
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// every file that can carry bundle content: catalogs and flat Resources
|
|
103
|
+
// rasters anywhere under the package. `.build` is skipped where sourceFiles
|
|
104
|
+
// does not need to: a release tree holds tens of thousands of files and no
|
|
105
|
+
// bundle content, and walking it per import would tax every hot reload.
|
|
106
|
+
function resourceCandidateFiles(root: string): string[] {
|
|
107
|
+
const found: string[] = []
|
|
108
|
+
const walk = (dir: string): void => {
|
|
109
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
110
|
+
if (
|
|
111
|
+
entry.name.startsWith('.') ||
|
|
112
|
+
entry.name === 'node_modules' ||
|
|
113
|
+
entry.name === '.build'
|
|
114
|
+
)
|
|
115
|
+
continue
|
|
116
|
+
const full = path.join(dir, entry.name)
|
|
117
|
+
if (entry.isDirectory()) {
|
|
118
|
+
if (full !== root && fs.existsSync(path.join(full, 'Package.swift'))) continue
|
|
119
|
+
walk(full)
|
|
120
|
+
} else {
|
|
121
|
+
found.push(full)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
walk(root)
|
|
126
|
+
return found
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const RESOURCE_CONTENT_TYPES: Record<string, string> = {
|
|
130
|
+
'.avif': 'image/avif',
|
|
131
|
+
'.bmp': 'image/bmp',
|
|
132
|
+
'.gif': 'image/gif',
|
|
133
|
+
'.ico': 'image/x-icon',
|
|
134
|
+
'.jpeg': 'image/jpeg',
|
|
135
|
+
'.jpg': 'image/jpeg',
|
|
136
|
+
'.png': 'image/png',
|
|
137
|
+
'.webp': 'image/webp',
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface SwiftResourceTable {
|
|
141
|
+
/** bundle asset name to absolute serving url. */
|
|
142
|
+
table: Record<string, string>
|
|
143
|
+
/** project-relative image files this load's table covers (for watching). */
|
|
144
|
+
watched: string[]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// the producer for the local vehicle: enumerate the package's bundle images
|
|
148
|
+
// through the shared rule both hosts use, hash each raster's bytes into its
|
|
149
|
+
// url (an edited photo moves its url, so no stale paint survives a reload),
|
|
150
|
+
// and register hash to path for the serving middleware below.
|
|
151
|
+
export function buildSwiftResourceTable(
|
|
152
|
+
project: SwiftProject,
|
|
153
|
+
baseUrl: string,
|
|
154
|
+
): SwiftResourceTable {
|
|
155
|
+
const absolute = resourceCandidateFiles(project.packagePath)
|
|
156
|
+
const relative = absolute.map((file) =>
|
|
157
|
+
path.relative(project.packagePath, file).split(path.sep).join(path.posix.sep),
|
|
158
|
+
)
|
|
159
|
+
const byRelative = new Map(absolute.map((file, index) => [relative[index], file]))
|
|
160
|
+
const found = enumerateBundleImages(relative, (candidate) => {
|
|
161
|
+
const file = byRelative.get(candidate)
|
|
162
|
+
if (!file || path.basename(file) !== 'Contents.json') return null
|
|
163
|
+
try {
|
|
164
|
+
return fs.readFileSync(file, 'utf8')
|
|
165
|
+
} catch {
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
const table: Record<string, string> = {}
|
|
170
|
+
const watched: string[] = []
|
|
171
|
+
const registered = new Map<string, string>()
|
|
172
|
+
for (const { name, file } of found) {
|
|
173
|
+
const disk = byRelative.get(file)
|
|
174
|
+
if (!disk) continue
|
|
175
|
+
let bytes: Buffer
|
|
176
|
+
try {
|
|
177
|
+
bytes = fs.readFileSync(disk)
|
|
178
|
+
} catch {
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 16)
|
|
182
|
+
const ext = path.posix.extname(file).toLowerCase()
|
|
183
|
+
table[name] = `${baseUrl}${RESOURCE_PATH}${hash}${ext}`
|
|
184
|
+
watched.push(file)
|
|
185
|
+
// a Contents.json edit (scale swap, file rename) re-resolves the same
|
|
186
|
+
// way a raster edit does, so the catalog rides the watcher too.
|
|
187
|
+
const contents = file.replace(/\/[^/]+$/, '/Contents.json')
|
|
188
|
+
if (byRelative.has(contents)) watched.push(contents)
|
|
189
|
+
registered.set(`${hash}${ext}`, disk)
|
|
190
|
+
}
|
|
191
|
+
project.resourceFiles = registered
|
|
192
|
+
return { table, watched }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// the /res pack: every Resources/ json in the package, basename-keyed and
|
|
196
|
+
// base64, riding the entry module so the mount serves it without a fetch.
|
|
197
|
+
// the namespace is flat like the bundle the wasm side reads: shallower
|
|
198
|
+
// paths pack first (the Resources root is the bundle root) with a lexical
|
|
199
|
+
// tiebreak, and the first spelling of a basename wins a collision. caps are
|
|
200
|
+
// INFERRED (no app in either rung is near them; landmarkData-scale json is
|
|
201
|
+
// tens of KB): a file past them is skipped and named in `skipped` so the
|
|
202
|
+
// load warns instead of silently serving a partial bundle. binary bytes
|
|
203
|
+
// travel the same map when a port reaches past json; the worker half packs
|
|
204
|
+
// text only until then (see the gaps ledger).
|
|
205
|
+
const BUNDLE_MAX_FILES = 128
|
|
206
|
+
const BUNDLE_MAX_FILE_BYTES = 2 * 1024 * 1024
|
|
207
|
+
const BUNDLE_MAX_TOTAL_BYTES = 8 * 1024 * 1024
|
|
208
|
+
|
|
209
|
+
export interface SwiftBundlePack {
|
|
210
|
+
/** basename to base64 bytes for the mount's /res. */
|
|
211
|
+
files: Record<string, string>
|
|
212
|
+
/** project-relative json files this load's pack covers (for watching). */
|
|
213
|
+
watched: string[]
|
|
214
|
+
/** project-relative files skipped by a cap, for the load warning. */
|
|
215
|
+
skipped: string[]
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function buildSwiftBundleFiles(packagePath: string): SwiftBundlePack {
|
|
219
|
+
// resourceCandidateFiles walks every non-ignored file with the nested
|
|
220
|
+
// package exclusion; the shared enumeration decides namespace, order and
|
|
221
|
+
// collisions, identically for every host that packs.
|
|
222
|
+
const relative = resourceCandidateFiles(packagePath).map((file) =>
|
|
223
|
+
path.relative(packagePath, file).split(path.sep).join(path.posix.sep),
|
|
224
|
+
)
|
|
225
|
+
const files: Record<string, string> = {}
|
|
226
|
+
const watched: string[] = []
|
|
227
|
+
const skipped: string[] = []
|
|
228
|
+
let total = 0
|
|
229
|
+
for (const { base, file } of enumerateBundleJson(relative)) {
|
|
230
|
+
watched.push(file)
|
|
231
|
+
let bytes: Buffer
|
|
232
|
+
try {
|
|
233
|
+
bytes = fs.readFileSync(path.join(packagePath, ...file.split('/')))
|
|
234
|
+
} catch {
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
if (bytes.length > BUNDLE_MAX_FILE_BYTES) {
|
|
238
|
+
skipped.push(`${file} (${bytes.length} bytes past the per-file cap)`)
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
if (
|
|
242
|
+
Object.keys(files).length >= BUNDLE_MAX_FILES ||
|
|
243
|
+
total + bytes.length > BUNDLE_MAX_TOTAL_BYTES
|
|
244
|
+
) {
|
|
245
|
+
skipped.push(
|
|
246
|
+
`${file} (pack caps: ${BUNDLE_MAX_FILES} files / ${BUNDLE_MAX_TOTAL_BYTES} bytes)`,
|
|
247
|
+
)
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
files[base] = bytes.toString('base64')
|
|
251
|
+
total += bytes.length
|
|
252
|
+
}
|
|
253
|
+
return { files, watched, skipped }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// the middleware's lookup, exported for the producer's own proof: a hit is
|
|
257
|
+
// known bytes for a known hash, anything else is null, never a file.
|
|
258
|
+
export function serveSwiftResource(
|
|
259
|
+
project: SwiftProject,
|
|
260
|
+
key: string,
|
|
261
|
+
): { bytes: Buffer; contentType: string } | null {
|
|
262
|
+
const disk = project.resourceFiles.get(key)
|
|
263
|
+
if (!disk) return null
|
|
264
|
+
try {
|
|
265
|
+
return {
|
|
266
|
+
bytes: fs.readFileSync(disk),
|
|
267
|
+
contentType:
|
|
268
|
+
RESOURCE_CONTENT_TYPES[path.extname(disk).toLowerCase()] ??
|
|
269
|
+
'application/octet-stream',
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
return null
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// the SwiftUI module the project links: its manifest names it as a path
|
|
277
|
+
// dependency, and `swift build` compiles it with the project, so an edit
|
|
278
|
+
// there changes the artifact without moving any of the project's own mtimes.
|
|
279
|
+
export function linkedModuleRoots(packagePath: string): string[] {
|
|
280
|
+
let manifest: string
|
|
281
|
+
try {
|
|
282
|
+
manifest = fs.readFileSync(path.join(packagePath, 'Package.swift'), 'utf8')
|
|
283
|
+
} catch {
|
|
284
|
+
return []
|
|
285
|
+
}
|
|
286
|
+
const roots = new Set<string>()
|
|
287
|
+
const dependency = /\.package\(\s*path:\s*"([^"]+)"\s*\)/g
|
|
288
|
+
for (const match of manifest.replace(/^\s*\/\/.*$/gm, '').matchAll(dependency)) {
|
|
289
|
+
const root = path.resolve(packagePath, match[1])
|
|
290
|
+
if (fs.existsSync(path.join(root, 'Package.swift'))) roots.add(root)
|
|
291
|
+
}
|
|
292
|
+
return [...roots].sort()
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// the stamp covers the project plus the modules it links: a running dev
|
|
296
|
+
// server rebuilds only when this moves, so leaving the linked sources out
|
|
297
|
+
// would keep serving the artifact built before a SwiftUI module edit for as
|
|
298
|
+
// long as the project's own sources stayed the same. it names the same
|
|
299
|
+
// closure the compile service's artifact hash names, but it is not the same
|
|
300
|
+
// function: the stamp hashes mtimes, which is the cheap rebuild signal for a
|
|
301
|
+
// watcher, while the artifact address hashes content, which is what stays
|
|
302
|
+
// stable when a checkout restores byte-identical files. a restore rebuilds
|
|
303
|
+
// here and reuses the stored artifact there, by design.
|
|
304
|
+
export function sourceStamp(packagePath: string): string {
|
|
305
|
+
const hash = createHash('sha256')
|
|
306
|
+
for (const root of [packagePath, ...linkedModuleRoots(packagePath)]) {
|
|
307
|
+
hash.update(root)
|
|
308
|
+
for (const file of sourceFiles(root).sort()) {
|
|
309
|
+
hash.update(path.relative(root, file))
|
|
310
|
+
hash.update(String(fs.statSync(file).mtimeMs))
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return hash.digest('hex').slice(0, 16)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// the registry slot for an entry: the owning package's own name plus the
|
|
317
|
+
// source within it. two packages in one app can hold the same filename, and
|
|
318
|
+
// a bare relative path would share one slot, one instance, and its state.
|
|
319
|
+
// the name travels instead of the path so identical source in two checkouts
|
|
320
|
+
// emits the identical module; two same-named packages sharing one app still
|
|
321
|
+
// collide, and take the rootId override for that.
|
|
322
|
+
export function entryRootId(packagePath: string, file: string): string {
|
|
323
|
+
const relative = path.relative(packagePath, file).split(path.sep).join(path.posix.sep)
|
|
324
|
+
return `${path.basename(packagePath)}${path.posix.sep}${relative}`
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// the app fetches from a worker whose origin is the shell, so urls have to
|
|
328
|
+
// name this dev server rather than resolve relative to the shell.
|
|
329
|
+
function serverOrigin(): string {
|
|
330
|
+
const address = devServer?.httpServer?.address()
|
|
331
|
+
const port =
|
|
332
|
+
address && typeof address === 'object' ? address.port : devServer?.config.server.port
|
|
333
|
+
if (!port) throw new Error('the swift urls need the dev server port')
|
|
334
|
+
return `http://localhost:${port}`
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function artifactUrl(project: SwiftProject, artifact: SwiftArtifact): string {
|
|
338
|
+
return `${serverOrigin()}${ARTIFACT_PATH}${artifact.hash}.wasm`
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// one executable is the app. a package with several is ambiguous: the host
|
|
342
|
+
// cannot know which one the page is, so it says so instead of guessing.
|
|
343
|
+
function builtArtifact(packagePath: string): string {
|
|
344
|
+
const binDir = path.join(packagePath, '.build', WASM_TRIPLE, 'release')
|
|
345
|
+
const artifacts = fs.existsSync(binDir)
|
|
346
|
+
? fs
|
|
347
|
+
.readdirSync(binDir)
|
|
348
|
+
.filter((name) => name.endsWith('.wasm'))
|
|
349
|
+
.sort()
|
|
350
|
+
: []
|
|
351
|
+
if (artifacts.length !== 1) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
`swift project ${packagePath} built ${artifacts.length} wasm artifacts; exactly one executable is the app`,
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
return path.join(binDir, artifacts[0])
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function streamText(value: unknown): string {
|
|
360
|
+
if (typeof value === 'string') return value.trim()
|
|
361
|
+
if (Buffer.isBuffer(value)) return value.toString('utf8').trim()
|
|
362
|
+
return ''
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function compile(packagePath: string, stamp: string): Promise<SwiftArtifact> {
|
|
366
|
+
const swift = path.join(TOOLCHAIN_BIN, 'swift')
|
|
367
|
+
if (!fs.existsSync(swift)) {
|
|
368
|
+
throw new Error(
|
|
369
|
+
`no swift.org toolchain at ${TOOLCHAIN_BIN}. install it and the ${WASM_SDK} sdk; see packages/sootsim-swift/README.md`,
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
await run(
|
|
374
|
+
swift,
|
|
375
|
+
[
|
|
376
|
+
'build',
|
|
377
|
+
'--package-path',
|
|
378
|
+
packagePath,
|
|
379
|
+
'--swift-sdk',
|
|
380
|
+
WASM_SDK,
|
|
381
|
+
'-c',
|
|
382
|
+
'release',
|
|
383
|
+
'-Xswiftc',
|
|
384
|
+
'-Osize',
|
|
385
|
+
],
|
|
386
|
+
{ maxBuffer: 64 * 1024 * 1024 },
|
|
387
|
+
)
|
|
388
|
+
} catch (error) {
|
|
389
|
+
// `swift build` prints the compiler's diagnostics on stdout and swiftpm's
|
|
390
|
+
// own on stderr; the failure's message holds only the command line, so
|
|
391
|
+
// both streams are what names the line that broke. stdout leads because
|
|
392
|
+
// compiler diagnostics are what the editor needs to show.
|
|
393
|
+
const streams =
|
|
394
|
+
error && typeof error === 'object'
|
|
395
|
+
? [
|
|
396
|
+
streamText(Reflect.get(error, 'stdout')),
|
|
397
|
+
streamText(Reflect.get(error, 'stderr')),
|
|
398
|
+
].filter(Boolean)
|
|
399
|
+
: []
|
|
400
|
+
const detail =
|
|
401
|
+
streams.length > 0
|
|
402
|
+
? streams.join('\n')
|
|
403
|
+
: error instanceof Error
|
|
404
|
+
? error.message
|
|
405
|
+
: String(error)
|
|
406
|
+
throw new Error(`swift build failed for ${packagePath}\n${detail}`)
|
|
407
|
+
}
|
|
408
|
+
const bytes = fs.readFileSync(builtArtifact(packagePath))
|
|
409
|
+
return {
|
|
410
|
+
hash: createHash('sha256').update(bytes).digest('hex').slice(0, 16),
|
|
411
|
+
bytes,
|
|
412
|
+
stamp,
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function artifactFor(project: SwiftProject): Promise<SwiftArtifact> {
|
|
417
|
+
const stamp = sourceStamp(project.packagePath)
|
|
418
|
+
if (project.artifact?.stamp === stamp) return Promise.resolve(project.artifact)
|
|
419
|
+
if (project.building) {
|
|
420
|
+
// a second edit during a build: the build in flight was started from
|
|
421
|
+
// older sources, so its artifact is not the answer for these. wait it out
|
|
422
|
+
// and ask again, which starts the build these sources need; joining it
|
|
423
|
+
// would emit the older artifact and nothing would trigger another load.
|
|
424
|
+
const running = project.building
|
|
425
|
+
if (running.stamp === stamp) return running.promise
|
|
426
|
+
const again = () => artifactFor(project)
|
|
427
|
+
return running.promise.then(again, again)
|
|
428
|
+
}
|
|
429
|
+
const promise = compile(project.packagePath, stamp).then(
|
|
430
|
+
(artifact) => {
|
|
431
|
+
project.artifact = artifact
|
|
432
|
+
project.building = null
|
|
433
|
+
return artifact
|
|
434
|
+
},
|
|
435
|
+
(error: unknown) => {
|
|
436
|
+
project.building = null
|
|
437
|
+
throw error
|
|
438
|
+
},
|
|
439
|
+
)
|
|
440
|
+
project.building = { stamp, promise }
|
|
441
|
+
return promise
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// the swiftpm package that owns an imported file: the nearest directory at
|
|
445
|
+
// or above the file that holds a Package.swift, stopping at the vite root
|
|
446
|
+
// once the dev server has named it. a swift-only project keeps its sources at
|
|
447
|
+
// the root so that is its package; a React Native app keeps its in a
|
|
448
|
+
// subdirectory (a `native/` package beside the js), and each such package
|
|
449
|
+
// compiles on its own.
|
|
450
|
+
function packageFor(file: string): string {
|
|
451
|
+
let dir = path.dirname(file)
|
|
452
|
+
const stop = devServer?.config.root
|
|
453
|
+
for (;;) {
|
|
454
|
+
if (fs.existsSync(path.join(dir, 'Package.swift'))) return dir
|
|
455
|
+
if ((stop !== undefined && dir === stop) || path.dirname(dir) === dir) break
|
|
456
|
+
dir = path.dirname(dir)
|
|
457
|
+
}
|
|
458
|
+
throw new Error(
|
|
459
|
+
`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`,
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export interface SwiftPluginOptions {
|
|
464
|
+
// swiftpm package that owns the project's .swift sources. defaults to the
|
|
465
|
+
// nearest Package.swift above each imported file, which is the vite root
|
|
466
|
+
// for the layout a swift-only project uses.
|
|
467
|
+
packagePath?: string
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
|
|
471
|
+
return {
|
|
472
|
+
name: 'rnx-swift',
|
|
473
|
+
enforce: 'pre',
|
|
474
|
+
|
|
475
|
+
async resolveId(source, importer) {
|
|
476
|
+
if (source.startsWith(VIRTUAL_PREFIX)) return source
|
|
477
|
+
// import-analysis re-resolves the served URL form of the id
|
|
478
|
+
// (/@id/__x00__..., with the null byte encoded) while transforming the
|
|
479
|
+
// importer. it still names this module, so answer the decoded id; any
|
|
480
|
+
// other /@id/ url is already resolved and must not fall through to the
|
|
481
|
+
// filesystem resolve below, which would throw for it.
|
|
482
|
+
if (source.startsWith('/@id/')) {
|
|
483
|
+
const decoded = source.slice('/@id/'.length).replace(/^__x00__/, '\0')
|
|
484
|
+
return decoded.startsWith(VIRTUAL_PREFIX) ? decoded : null
|
|
485
|
+
}
|
|
486
|
+
if (!source.endsWith('.swift')) return null
|
|
487
|
+
const resolved = await this.resolve(source, importer, { skipSelf: true })
|
|
488
|
+
if (!resolved) throw new Error(`cannot resolve swift source ${source}`)
|
|
489
|
+
return VIRTUAL_PREFIX + resolved.id
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
async load(id) {
|
|
493
|
+
if (!id.startsWith(VIRTUAL_PREFIX)) return null
|
|
494
|
+
const file = id.slice(VIRTUAL_PREFIX.length)
|
|
495
|
+
// the module is the whole package's artifact, so an import of a file
|
|
496
|
+
// that was deleted would keep answering with it. a deleted .tsx fails
|
|
497
|
+
// its importer; so does a deleted .swift.
|
|
498
|
+
if (!fs.existsSync(file)) throw new Error(`swift source ${file} no longer exists`)
|
|
499
|
+
const packagePath = options.packagePath ?? packageFor(file)
|
|
500
|
+
const project = projectFor(packagePath)
|
|
501
|
+
// every source in the package is a dependency of this module, so a
|
|
502
|
+
// bundler watching this module learns about a `.swift` edit and re-runs
|
|
503
|
+
// load. that is the whole hot reload: the new hash in the emitted module
|
|
504
|
+
// is what tells the mount it is looking at a different app.
|
|
505
|
+
for (const source of sourceFiles(packagePath)) this.addWatchFile(source)
|
|
506
|
+
// an edit to the linked SwiftUI module rebuilds the artifact without
|
|
507
|
+
// touching the project's own sources, so those files gate the reload
|
|
508
|
+
// too: the new hash in the emitted module is what tells the mount it
|
|
509
|
+
// is looking at a different app.
|
|
510
|
+
for (const root of linkedModuleRoots(packagePath)) {
|
|
511
|
+
for (const source of sourceFiles(root)) this.addWatchFile(source)
|
|
512
|
+
}
|
|
513
|
+
let artifact: SwiftArtifact
|
|
514
|
+
try {
|
|
515
|
+
artifact = await artifactFor(project)
|
|
516
|
+
} catch (error) {
|
|
517
|
+
// a compile error must not fail this module. a module whose load threw
|
|
518
|
+
// leaves the graph unwatched, so the edit that fixes the error would
|
|
519
|
+
// never rebuild it and the app would sit on a stale frame until the
|
|
520
|
+
// dev server restarted. the previous artifact keeps rendering, and the
|
|
521
|
+
// error is reported here instead.
|
|
522
|
+
if (!project.artifact) throw error
|
|
523
|
+
this.warn(error instanceof Error ? error.message : String(error))
|
|
524
|
+
artifact = project.artifact
|
|
525
|
+
}
|
|
526
|
+
// the bundle image table rides the same module: a photo edit moves
|
|
527
|
+
// its urls without rebuilding the artifact, and the watched files
|
|
528
|
+
// re-run this load so the mount sees the new table on hot reload.
|
|
529
|
+
let resources: Record<string, string> = {}
|
|
530
|
+
try {
|
|
531
|
+
const built = buildSwiftResourceTable(project, serverOrigin())
|
|
532
|
+
resources = built.table
|
|
533
|
+
for (const watched of built.watched) this.addWatchFile(watched)
|
|
534
|
+
} catch (error) {
|
|
535
|
+
this.warn(
|
|
536
|
+
`swift resources for ${packagePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
// the /res pack rides the same module as bytes: a json edit moves the
|
|
540
|
+
// pack, the watched files re-run this load, and the mount's
|
|
541
|
+
// bundleFiles compare treats it as a new app and remounts through it.
|
|
542
|
+
let bundleFiles: Record<string, string> = {}
|
|
543
|
+
try {
|
|
544
|
+
const packed = buildSwiftBundleFiles(packagePath)
|
|
545
|
+
bundleFiles = packed.files
|
|
546
|
+
for (const watched of packed.watched) {
|
|
547
|
+
this.addWatchFile(path.join(packagePath, ...watched.split(path.posix.sep)))
|
|
548
|
+
}
|
|
549
|
+
if (packed.skipped.length > 0) {
|
|
550
|
+
this.warn(
|
|
551
|
+
`swift bundle files skipped for ${packagePath}: ${packed.skipped.join('; ')}`,
|
|
552
|
+
)
|
|
553
|
+
}
|
|
554
|
+
} catch (error) {
|
|
555
|
+
this.warn(
|
|
556
|
+
`swift bundle files for ${packagePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
557
|
+
)
|
|
558
|
+
}
|
|
559
|
+
// every import mounts through the one entry: the artifact answers
|
|
560
|
+
// whether it is a view or an app root at runtime, so the transform
|
|
561
|
+
// never re-derives the conformance from source. the entry is rooted by
|
|
562
|
+
// its package and source path so a hot reload finds the same instance.
|
|
563
|
+
return [
|
|
564
|
+
`import { createSwiftEntry } from 'rnxsim/swift'`,
|
|
565
|
+
`export default createSwiftEntry({ url: ${JSON.stringify(artifactUrl(project, artifact))}, hash: ${JSON.stringify(artifact.hash)}, rootId: ${JSON.stringify(entryRootId(packagePath, file))}, resources: ${JSON.stringify(resources)}, bundleFiles: ${JSON.stringify(bundleFiles)} })`,
|
|
566
|
+
'',
|
|
567
|
+
].join('\n')
|
|
568
|
+
},
|
|
569
|
+
|
|
570
|
+
configureServer(server) {
|
|
571
|
+
devServer = server
|
|
572
|
+
|
|
573
|
+
server.middlewares.use((req, res, next) => {
|
|
574
|
+
const url = (req.url ?? '').split('?')[0]
|
|
575
|
+
if (url.startsWith(RESOURCE_PATH)) {
|
|
576
|
+
// hash-addressed: the table this load emitted names these hashes,
|
|
577
|
+
// so a hit serves known bytes and anything else is a 404, never a
|
|
578
|
+
// neighboring file. the tenant worker is cross-origin like the
|
|
579
|
+
// artifact fetch, hence the same cors header.
|
|
580
|
+
for (const project of projects.values()) {
|
|
581
|
+
const served = serveSwiftResource(project, url.slice(RESOURCE_PATH.length))
|
|
582
|
+
if (served) {
|
|
583
|
+
res.setHeader('access-control-allow-origin', '*')
|
|
584
|
+
res.setHeader('content-type', served.contentType)
|
|
585
|
+
res.end(served.bytes)
|
|
586
|
+
return
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
res.statusCode = 404
|
|
590
|
+
res.end('swift resource is stale')
|
|
591
|
+
return
|
|
592
|
+
}
|
|
593
|
+
if (!url.startsWith(ARTIFACT_PATH)) {
|
|
594
|
+
next()
|
|
595
|
+
return
|
|
596
|
+
}
|
|
597
|
+
// the url names a content hash, not a package: the request is answered
|
|
598
|
+
// by whichever known project built it, so a React Native app whose
|
|
599
|
+
// swift package sits below its root serves the same path its imports
|
|
600
|
+
// compiled. served from what is already built, never rebuilt here: the
|
|
601
|
+
// page only knows a hash that `load` emitted, and `load` under a compile
|
|
602
|
+
// error emits the last good hash, whose bytes are still the project's
|
|
603
|
+
// artifact. rebuilding would fail that fetch with the compile error and
|
|
604
|
+
// fail every other project's artifact with it.
|
|
605
|
+
for (const project of projects.values()) {
|
|
606
|
+
const artifact = project.artifact
|
|
607
|
+
if (artifact && url === `${ARTIFACT_PATH}${artifact.hash}.wasm`) {
|
|
608
|
+
// the tenant worker's origin is the shell, never this dev server
|
|
609
|
+
res.setHeader('access-control-allow-origin', '*')
|
|
610
|
+
res.setHeader('content-type', 'application/wasm')
|
|
611
|
+
res.end(artifact.bytes)
|
|
612
|
+
return
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
res.statusCode = 404
|
|
616
|
+
res.end('swift artifact is stale')
|
|
617
|
+
})
|
|
618
|
+
},
|
|
619
|
+
}
|
|
620
|
+
}
|