peachsim 0.1.527 → 0.1.571
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/README.md +28 -28
- package/cli/app-fonts.ts +1 -1
- package/cli/app-splash.ts +1 -1
- package/cli/bin.ts +1 -1
- package/cli/bridge-diagnostics.ts +1 -1
- package/cli/bridge-flow-runner.ts +72 -12
- package/cli/cloud-client.ts +273 -6
- package/cli/cloud-dispatch.ts +23 -2
- package/cli/cloud-session.ts +60 -4
- package/cli/cloud-test-result-files.ts +97 -0
- package/cli/cloud-test-tunnel.ts +354 -0
- package/cli/commands/box/bundle-plane.ts +2 -2
- package/cli/commands/box/checkout-plane.ts +1 -1
- package/cli/commands/box/rnx-command.ts +2 -2
- package/cli/commands/box/search-command.ts +2 -2
- package/cli/commands/box.ts +8 -8
- package/cli/commands/compat.ts +1 -1
- package/cli/commands/control.ts +22 -3
- package/cli/commands/detox.ts +1 -1
- package/cli/commands/device.ts +1 -1
- package/cli/commands/diagnose.ts +1 -1
- package/cli/commands/film.ts +1 -1
- package/cli/commands/flow.ts +60 -3
- package/cli/commands/inspect/actions.ts +1 -1
- package/cli/commands/inspect/core.ts +6 -7
- package/cli/commands/inspect/find.ts +2 -1
- package/cli/commands/inspect/resolve-target.ts +5 -3
- package/cli/commands/inspect/shared.ts +1 -1
- package/cli/commands/inspect/wait-event.ts +1 -1
- package/cli/commands/inspect/wait-selector.ts +15 -4
- package/cli/commands/inspect.ts +19 -14
- package/cli/commands/maestro.ts +582 -69
- package/cli/commands/permissions.ts +1 -1
- package/cli/commands/platform.ts +205 -8
- package/cli/commands/preview.ts +2 -2
- package/cli/commands/record.ts +2 -2
- package/cli/commands/report-issue.ts +2 -2
- package/cli/commands/screenshot-layers.ts +1 -1
- package/cli/commands/screenshot.ts +1 -1
- package/cli/commands/setup.ts +2 -2
- package/cli/commands/test.ts +600 -117
- package/cli/commands/three-mode.ts +1 -1
- package/cli/commands/upload.ts +5 -5
- package/cli/commands/what-happened.ts +1 -1
- package/cli/desktop-companion.ts +3 -3
- package/cli/drivers/playwright-sim-host.ts +2 -2
- package/cli/flow-file.ts +7 -0
- package/cli/flow-live-status.ts +65 -8
- package/cli/help-core.ts +3 -3
- package/cli/hidden-runtime-alias.ts +1 -1
- package/cli/maestro-report.ts +180 -0
- package/cli/main.ts +1 -1
- package/cli/outbound-endpoints.ts +187 -136
- package/cli/parse-args.ts +1 -1
- package/cli/registry.ts +1 -1
- package/cli/resolve-assets.ts +1 -1
- package/cli/run-registry.ts +10 -1
- package/cli/send-to-box.ts +4 -4
- package/cli/ws-bridge.ts +3 -5
- package/detox/expectations.ts +1 -1
- package/detox/gestures.ts +2 -2
- package/detox/index.ts +71 -29
- package/detox/jest-preset.cjs +5 -5
- package/detox/jest-setup-after-env.cjs +1 -1
- package/dist-lib/agent-daemon-client.cjs +2 -2
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +2 -2
- package/dist-lib/attached-projects.cjs +2 -2
- 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 +94 -3
- package/dist-lib/capture-contract.mjs +93 -3
- 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-test-args.cjs +116 -0
- package/dist-lib/cloud-test-args.mjs +86 -0
- package/dist-lib/cloud.cjs +2 -2
- package/dist-lib/cloud.mjs +2 -2
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +106 -25
- package/dist-lib/detox/jest-preset.cjs +5 -5
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +150 -65
- package/dist-lib/host/fetch-proxy-handler.cjs +28 -2
- package/dist-lib/host/fetch-proxy-overrides.cjs +33 -3
- package/dist-lib/host/fetch-proxy-overrides.mjs +30 -2
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +224 -41
- 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 +2 -2
- package/dist-lib/menu.mjs +2 -2
- 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 +3 -3
- package/dist-lib/metro-production-bundle.mjs +3 -3
- package/dist-lib/metro.cjs +8 -8
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +2 -2
- 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 +31 -15
- package/dist-lib/sdk.cjs +3 -2
- package/dist-lib/sdk.mjs +3 -2
- package/dist-lib/skills.cjs +478 -266
- package/dist-lib/swift-compile.cjs +372 -0
- package/dist-lib/swift-compile.mjs +339 -0
- package/dist-lib/swift-package.cjs +78 -0
- package/dist-lib/swift-package.mjs +52 -0
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +583 -16
- package/dist-lib/vite.cjs +483 -150
- package/package.json +30 -9
- package/scripts/dev-server-scanner.ts +41 -14
- package/scripts/optional-demo-registry.ts +3 -3
- package/skills/contrast/SKILL.md +9 -9
- package/skills/rnx-debug/SKILL.md +14 -4
- package/skills/rnx-perf/SKILL.md +5 -5
- package/skills/rnx-setup/SKILL.md +11 -11
- package/skills/rnx-test/SKILL.md +14 -14
- package/skills/rnx-visual/SKILL.md +6 -6
- package/src/agent-sessions.ts +3 -3
- package/src/attached-projects.ts +1 -1
- package/src/bridge-contract-input.ts +1 -1
- package/src/bridge-contract.ts +23 -5
- package/src/capture-contract.ts +145 -3
- package/src/cloud-test-args.ts +100 -0
- package/src/config.ts +2 -2
- package/src/connect.ts +2 -2
- package/src/home-paths.ts +3 -3
- package/src/host/bridge-host.ts +2 -2
- package/src/host/fetch-proxy-handler.ts +4 -3
- package/src/host/fetch-proxy-overrides.ts +41 -0
- package/src/host/plane-host.ts +2 -2
- package/src/index.ts +3 -0
- package/src/metro-plugin.ts +4 -4
- package/src/metro-production-bundle.ts +2 -2
- package/src/native-seam-manifest.ts +6 -6
- package/src/public-brand.ts +1 -1
- package/src/rolldown-plugin-swift.ts +16 -12
- package/src/runtime-assets.ts +8 -6
- package/src/screenshots/frame-compose.ts +24 -56
- package/src/screenshots/org-deck.ts +1 -1
- package/src/screenshots/registry.ts +4 -4
- package/src/screenshots/schema.ts +1 -1
- package/src/sdk.ts +2 -2
- package/src/settings-bundle.ts +1 -1
- package/src/sim-client.ts +2 -4
- package/src/skills/builtin/compat-check.ts +1 -1
- package/src/swift-compile.ts +359 -0
- package/src/swift-package.ts +69 -0
- package/src/swift-submit.ts +3 -2
- package/src/vite-plugin-one.ts +1 -1
- package/src/vite-plugin-swift.ts +114 -113
- package/src/vite-plugin.ts +30 -17
- package/src/worklets-babel.ts +2 -2
|
@@ -5,12 +5,13 @@ import {
|
|
|
5
5
|
LEGACY_HOME_BUTTON_SIZE,
|
|
6
6
|
LEGACY_SIDE_BEZEL,
|
|
7
7
|
LEGACY_TOP_BEZEL,
|
|
8
|
-
HINGE_BARREL_BACKGROUND,
|
|
9
|
-
HINGE_BARREL_SHADOW,
|
|
10
|
-
HOUSING_RAIL_COLOR,
|
|
11
|
-
HOUSING_RAIL_OUTLINE,
|
|
12
8
|
SIDE_BUTTON_RESERVE,
|
|
13
|
-
} from '
|
|
9
|
+
} from 'peach-engine/ios/chrome-metrics'
|
|
10
|
+
import {
|
|
11
|
+
housingLayerHtml,
|
|
12
|
+
resolveHousingArt,
|
|
13
|
+
type HousingLayer,
|
|
14
|
+
} from 'peach-engine/ios/duo-housing'
|
|
14
15
|
import {
|
|
15
16
|
cornerRadii,
|
|
16
17
|
formatCornerRadii,
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
resolveDeviceFrameGeometry,
|
|
19
20
|
type CornerRadii,
|
|
20
21
|
type DeviceModel,
|
|
21
|
-
} from '
|
|
22
|
+
} from 'peach-engine/settings'
|
|
22
23
|
import { launchReapedChrome } from '../../../../scripts/lib/reap-browser'
|
|
23
24
|
|
|
24
25
|
const METALLIC_RING_SHADOW =
|
|
@@ -56,8 +57,8 @@ export interface FramedScreenshotLayout {
|
|
|
56
57
|
logicalScreenTop: number
|
|
57
58
|
logicalScreenRadius: CornerRadii
|
|
58
59
|
logicalFrameRadius: CornerRadii
|
|
59
|
-
//
|
|
60
|
-
|
|
60
|
+
// folding body art, painted before the screen; null draws one black body
|
|
61
|
+
housingLayers: HousingLayer[] | null
|
|
61
62
|
frameBackground: string
|
|
62
63
|
frameOutline: string
|
|
63
64
|
metallicRingShadow: string | null
|
|
@@ -87,7 +88,9 @@ export function getFramedScreenshotLayout(model: DeviceModel): FramedScreenshotL
|
|
|
87
88
|
const logicalScreenWidth = spec.width
|
|
88
89
|
const logicalScreenHeight = spec.height
|
|
89
90
|
const modernGeometry = resolveDeviceFrameGeometry(spec)
|
|
90
|
-
const
|
|
91
|
+
const housingLayers = isLegacyHardware
|
|
92
|
+
? null
|
|
93
|
+
: resolveHousingArt(spec.posture ?? 'closed', modernGeometry)
|
|
91
94
|
const logicalFrameWidth = isLegacyHardware
|
|
92
95
|
? spec.width + LEGACY_SIDE_BEZEL * 2
|
|
93
96
|
: modernGeometry.frameWidth
|
|
@@ -148,18 +151,20 @@ export function getFramedScreenshotLayout(model: DeviceModel): FramedScreenshotL
|
|
|
148
151
|
logicalScreenTop,
|
|
149
152
|
logicalScreenRadius: isLegacyHardware ? cornerRadii(0) : modernGeometry.screenRadius,
|
|
150
153
|
logicalFrameRadius,
|
|
151
|
-
|
|
154
|
+
housingLayers,
|
|
155
|
+
// the housing paints its own body layer, shadow included: its frame box is
|
|
156
|
+
// wider than the body, because the hinge barrel protrudes past the spine.
|
|
152
157
|
frameBackground: isLegacyHardware
|
|
153
158
|
? 'linear-gradient(180deg, #1b1c20 0%, #0f1012 42%, #050608 100%)'
|
|
154
|
-
:
|
|
155
|
-
?
|
|
159
|
+
: housingLayers
|
|
160
|
+
? 'transparent'
|
|
156
161
|
: '#000000',
|
|
157
162
|
frameOutline: isLegacyHardware
|
|
158
163
|
? '0 0 0 1px #1f2125'
|
|
159
|
-
:
|
|
160
|
-
?
|
|
164
|
+
: housingLayers
|
|
165
|
+
? 'none'
|
|
161
166
|
: '0 0 0 1px #333',
|
|
162
|
-
metallicRingShadow: isLegacyHardware ||
|
|
167
|
+
metallicRingShadow: isLegacyHardware || housingLayers ? null : METALLIC_RING_SHADOW,
|
|
163
168
|
buttons,
|
|
164
169
|
legacyHomeButton: isLegacyHardware
|
|
165
170
|
? {
|
|
@@ -230,46 +235,9 @@ function renderFrameHtml(
|
|
|
230
235
|
`
|
|
231
236
|
: ''
|
|
232
237
|
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
const housingHtml = layout.
|
|
236
|
-
? `
|
|
237
|
-
<div
|
|
238
|
-
aria-hidden="true"
|
|
239
|
-
style="
|
|
240
|
-
position:absolute;
|
|
241
|
-
top:${layout.housing.rail.top}px;
|
|
242
|
-
right:${layout.housing.rail.right}px;
|
|
243
|
-
bottom:${layout.housing.rail.bottom}px;
|
|
244
|
-
left:${layout.housing.rail.left}px;
|
|
245
|
-
border-radius:${formatCornerRadii(layout.housing.glassRadius)};
|
|
246
|
-
background:#000000;
|
|
247
|
-
pointer-events:none;
|
|
248
|
-
"
|
|
249
|
-
></div>
|
|
250
|
-
${
|
|
251
|
-
layout.housing.hingeBarrel
|
|
252
|
-
? `
|
|
253
|
-
<div
|
|
254
|
-
aria-hidden="true"
|
|
255
|
-
style="
|
|
256
|
-
position:absolute;
|
|
257
|
-
top:${layout.housing.hingeBarrel.endInset}px;
|
|
258
|
-
bottom:${layout.housing.hingeBarrel.endInset}px;
|
|
259
|
-
left:0;
|
|
260
|
-
width:${layout.housing.hingeBarrel.width}px;
|
|
261
|
-
border-radius:${layout.housing.hingeBarrel.width / 2}px 0 0 ${layout.housing.hingeBarrel.width / 2}px;
|
|
262
|
-
background:${HINGE_BARREL_BACKGROUND};
|
|
263
|
-
box-shadow:${HINGE_BARREL_SHADOW};
|
|
264
|
-
pointer-events:none;
|
|
265
|
-
z-index:11;
|
|
266
|
-
"
|
|
267
|
-
></div>
|
|
268
|
-
`
|
|
269
|
-
: ''
|
|
270
|
-
}
|
|
271
|
-
`
|
|
272
|
-
: ''
|
|
238
|
+
// the folding body's rail, glass ring, corner art, and hinge barrel. no
|
|
239
|
+
// z-index, so the screen div after it paints on top.
|
|
240
|
+
const housingHtml = (layout.housingLayers ?? []).map(housingLayerHtml).join('')
|
|
273
241
|
|
|
274
242
|
const legacyHomeButtonHtml = layout.legacyHomeButton
|
|
275
243
|
? `
|
|
@@ -390,7 +358,7 @@ function renderFrameHtml(
|
|
|
390
358
|
left:${layout.logicalFrameLeft}px;
|
|
391
359
|
width:${layout.logicalFrameWidth}px;
|
|
392
360
|
height:${layout.logicalFrameHeight}px;
|
|
393
|
-
border-radius:${formatCornerRadii(layout.logicalFrameRadius)};
|
|
361
|
+
border-radius:${layout.housingLayers ? '0' : formatCornerRadii(layout.logicalFrameRadius)};
|
|
394
362
|
box-sizing:border-box;
|
|
395
363
|
overflow:visible;
|
|
396
364
|
background:${layout.frameBackground};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// colors. 3d scenes don't translate to the cli's 2d composer, so only copy and
|
|
7
7
|
// colors carry over (the 3d hero look stays the in-browser screenshot mode).
|
|
8
8
|
|
|
9
|
-
import { resolveBgColorValue } from '
|
|
9
|
+
import { resolveBgColorValue } from 'peach-engine/screenshots/tokens'
|
|
10
10
|
import type { BackgroundSpec } from './registry'
|
|
11
11
|
import type { NormalizedScreenshotsPlan } from './schema'
|
|
12
12
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// this module re-exports those tokens plus the composer-specific
|
|
7
7
|
// `FrameComposeSpec` shape.
|
|
8
8
|
|
|
9
|
-
import type { DeviceModel } from '
|
|
9
|
+
import type { DeviceModel } from 'peach-engine/settings'
|
|
10
10
|
|
|
11
11
|
export {
|
|
12
12
|
BACKGROUND_PRESETS,
|
|
@@ -19,7 +19,7 @@ export {
|
|
|
19
19
|
cloneBackgroundSpec,
|
|
20
20
|
resolveBackgroundPreset,
|
|
21
21
|
resolveCanvasPreset,
|
|
22
|
-
} from '
|
|
22
|
+
} from 'peach-engine/screenshots/tokens'
|
|
23
23
|
|
|
24
24
|
export type {
|
|
25
25
|
BackgroundPresetName,
|
|
@@ -30,9 +30,9 @@ export type {
|
|
|
30
30
|
PosePresetName,
|
|
31
31
|
ScreenshotCanvasName,
|
|
32
32
|
TextPresetName,
|
|
33
|
-
} from '
|
|
33
|
+
} from 'peach-engine/screenshots/tokens'
|
|
34
34
|
|
|
35
|
-
import type { FrameShadowSpec, PosePresetName } from '
|
|
35
|
+
import type { FrameShadowSpec, PosePresetName } from 'peach-engine/screenshots/tokens'
|
|
36
36
|
|
|
37
37
|
export interface FrameComposeSpec {
|
|
38
38
|
show: boolean
|
package/src/sdk.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
//
|
|
1
|
+
// peachsim/sdk — the scriptable sim-driving surface.
|
|
2
2
|
//
|
|
3
3
|
// exports the SimClient class plus the transport-agnostic inspect verbs it is
|
|
4
4
|
// built from. this entry MUST stay browser-safe (Contrast's in-browser agent
|
|
5
5
|
// imports it against an in-process preview bridge), so the node-only ws
|
|
6
|
-
// `connect()` lives on the ROOT `
|
|
6
|
+
// `connect()` lives on the ROOT `peachsim` entry, not here.
|
|
7
7
|
|
|
8
8
|
export * from './sim-client'
|
|
9
9
|
export * from '../cli/commands/inspect/core'
|
package/src/settings-bundle.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { parsePlistSource } from './plist.ts'
|
|
4
|
-
import type { AppPreferenceValue, AppSettingsSpecifier } from '@
|
|
4
|
+
import type { AppPreferenceValue, AppSettingsSpecifier } from '@peach/globals'
|
|
5
5
|
|
|
6
6
|
const MAX_PLIST_BYTES = 1_048_576
|
|
7
7
|
const MAX_CHILD_DEPTH = 8
|
package/src/sim-client.ts
CHANGED
|
@@ -60,6 +60,7 @@ import type {
|
|
|
60
60
|
SimSemanticResolveSelector,
|
|
61
61
|
SimStateOptions,
|
|
62
62
|
SimStateResult,
|
|
63
|
+
WsBridgeCommand,
|
|
63
64
|
} from './bridge-contract'
|
|
64
65
|
|
|
65
66
|
// ---------------------------------------------------------------------------
|
|
@@ -250,10 +251,7 @@ export class SimClient implements WsBridge, InspectBridge {
|
|
|
250
251
|
|
|
251
252
|
// --- transport (WsBridge/InspectBridge) ---
|
|
252
253
|
|
|
253
|
-
send(
|
|
254
|
-
cmd: { type: string; [k: string]: any },
|
|
255
|
-
opts?: { timeoutMs?: number },
|
|
256
|
-
): Promise<any> {
|
|
254
|
+
send(cmd: WsBridgeCommand, opts?: { timeoutMs?: number }): Promise<any> {
|
|
257
255
|
if (this.closed) {
|
|
258
256
|
return Promise.reject(new SimConnectError('sim client is closed'))
|
|
259
257
|
}
|
|
@@ -22,7 +22,7 @@ export const skill: RNXSkill = {
|
|
|
22
22
|
const fs = await import('node:fs')
|
|
23
23
|
const path = await import('node:path')
|
|
24
24
|
const { COMPAT_RESULT_CAVEAT, formatPackageStatus, formatScanCounts, scanDeps } =
|
|
25
|
-
await import('../../../../compat/src/web.ts')
|
|
25
|
+
await import('../../../../peach-compat/src/web.ts')
|
|
26
26
|
|
|
27
27
|
const packagePath = path.join(context.projectDir, 'package.json')
|
|
28
28
|
if (!fs.existsSync(packagePath)) {
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
statSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs'
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import { promisify } from 'node:util'
|
|
15
|
+
import { swiftManifestTargets, type SwiftPackageTarget } from './swift-package'
|
|
16
|
+
import type { SwiftImageUpload } from './swift-submit'
|
|
17
|
+
|
|
18
|
+
const run = promisify(execFile)
|
|
19
|
+
const WASM_TRIPLE = 'wasm32-unknown-wasip1'
|
|
20
|
+
|
|
21
|
+
export interface SwiftCompileInput {
|
|
22
|
+
files: Record<string, string>
|
|
23
|
+
resources?: Record<string, string>
|
|
24
|
+
images?: Record<string, SwiftImageUpload>
|
|
25
|
+
entry: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SwiftCompileOptions {
|
|
29
|
+
buildRoot: string
|
|
30
|
+
moduleRoot: string
|
|
31
|
+
release: string
|
|
32
|
+
toolchainBin: string
|
|
33
|
+
timeoutMs?: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SwiftCompileArtifact {
|
|
37
|
+
hash: string
|
|
38
|
+
bytes: Buffer
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SwiftProjectLayout {
|
|
42
|
+
root: string
|
|
43
|
+
sources: string
|
|
44
|
+
multi: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function swiftModuleInputFiles(root: string): string[] {
|
|
48
|
+
const files: string[] = ['Package.swift', 'scripts/contrast-app-Package.swift']
|
|
49
|
+
const walk = (dir: string) => {
|
|
50
|
+
for (const entry of readdirSync(path.join(root, dir), { withFileTypes: true })) {
|
|
51
|
+
const relative = `${dir}/${entry.name}`
|
|
52
|
+
if (entry.isDirectory()) walk(relative)
|
|
53
|
+
else if (entry.name.endsWith('.swift')) files.push(relative)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
walk('Sources')
|
|
57
|
+
return files.sort()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const linkedModuleDigests = new Map<string, { stamp: string; digest: Buffer }>()
|
|
61
|
+
|
|
62
|
+
export function clearLinkedModuleDigests(root?: string): void {
|
|
63
|
+
if (root === undefined) linkedModuleDigests.clear()
|
|
64
|
+
else linkedModuleDigests.delete(root)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function linkedModuleDigest(root: string): Buffer {
|
|
68
|
+
const files = swiftModuleInputFiles(root)
|
|
69
|
+
const stampHash = createHash('sha256')
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
const stat = statSync(path.join(root, file), { bigint: true })
|
|
72
|
+
stampHash.update(file)
|
|
73
|
+
stampHash.update(
|
|
74
|
+
`${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}\n`,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
const stamp = stampHash.digest('hex')
|
|
78
|
+
const cached = linkedModuleDigests.get(root)
|
|
79
|
+
if (cached?.stamp === stamp) return cached.digest
|
|
80
|
+
const hash = createHash('sha256')
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
hash.update(createHash('sha256').update(file).digest())
|
|
83
|
+
hash.update(
|
|
84
|
+
createHash('sha256')
|
|
85
|
+
.update(readFileSync(path.join(root, file)))
|
|
86
|
+
.digest(),
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
const digest = hash.digest()
|
|
90
|
+
linkedModuleDigests.set(root, { stamp, digest })
|
|
91
|
+
return digest
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function compileTargets(input: SwiftCompileInput): SwiftPackageTarget[] {
|
|
95
|
+
const manifest = input.files['Package.swift']
|
|
96
|
+
return manifest === undefined ? [] : swiftManifestTargets(manifest)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function swiftSiblingDependencies(
|
|
100
|
+
files: Record<string, string>,
|
|
101
|
+
targets: SwiftPackageTarget[],
|
|
102
|
+
): Map<string, string[]> {
|
|
103
|
+
const names = new Set(targets.map((target) => target.name))
|
|
104
|
+
const siblings = new Map<string, string[]>()
|
|
105
|
+
for (const target of targets) {
|
|
106
|
+
const prefix = `${target.path}/`
|
|
107
|
+
const deps = new Set<string>()
|
|
108
|
+
for (const [file, contents] of Object.entries(files)) {
|
|
109
|
+
if (file === 'Package.swift' || !file.startsWith(prefix)) continue
|
|
110
|
+
for (const match of contents.matchAll(/^\s*import\s+([A-Za-z_][A-Za-z0-9_]*)/gm)) {
|
|
111
|
+
if (names.has(match[1]) && match[1] !== target.name) deps.add(match[1])
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
siblings.set(target.name, [...deps].sort())
|
|
115
|
+
}
|
|
116
|
+
return siblings
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** bump with any change to the emitted multi-target manifest text. */
|
|
120
|
+
export const MULTI_MANIFEST_VERSION = 1
|
|
121
|
+
|
|
122
|
+
export function swiftArtifactHash(
|
|
123
|
+
input: SwiftCompileInput,
|
|
124
|
+
release: string,
|
|
125
|
+
moduleRoot: string,
|
|
126
|
+
): string {
|
|
127
|
+
const hash = createHash('sha256')
|
|
128
|
+
hash.update(`swift-compile/v1\n${release}\n`)
|
|
129
|
+
hash.update(linkedModuleDigest(moduleRoot))
|
|
130
|
+
for (const file of Object.keys(input.files).sort()) {
|
|
131
|
+
hash.update(createHash('sha256').update(file).digest())
|
|
132
|
+
hash.update(createHash('sha256').update(input.files[file]).digest())
|
|
133
|
+
}
|
|
134
|
+
for (const base of Object.keys(input.resources ?? {}).sort()) {
|
|
135
|
+
hash.update(createHash('sha256').update(base).digest())
|
|
136
|
+
hash.update(
|
|
137
|
+
createHash('sha256')
|
|
138
|
+
.update(input.resources?.[base] ?? '')
|
|
139
|
+
.digest(),
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
for (const name of Object.keys(input.images ?? {}).sort()) {
|
|
143
|
+
const image = input.images?.[name]
|
|
144
|
+
if (!image) continue
|
|
145
|
+
hash.update(createHash('sha256').update(name).digest())
|
|
146
|
+
hash.update(createHash('sha256').update(image.file).digest())
|
|
147
|
+
hash.update(createHash('sha256').update(image.data).digest())
|
|
148
|
+
}
|
|
149
|
+
const targets = compileTargets(input)
|
|
150
|
+
if (targets.length > 0) {
|
|
151
|
+
hash.update(`swift-multi/v${MULTI_MANIFEST_VERSION}\n`)
|
|
152
|
+
hash.update(
|
|
153
|
+
JSON.stringify({
|
|
154
|
+
targets,
|
|
155
|
+
siblings: [...swiftSiblingDependencies(input.files, targets)].sort((a, b) =>
|
|
156
|
+
String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0,
|
|
157
|
+
),
|
|
158
|
+
}),
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
return hash.digest('hex')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function emitSwiftPackageManifest(args: {
|
|
165
|
+
targets: SwiftPackageTarget[]
|
|
166
|
+
siblings: Map<string, string[]>
|
|
167
|
+
moduleRoot: string
|
|
168
|
+
}): string {
|
|
169
|
+
const { targets, siblings, moduleRoot } = args
|
|
170
|
+
const executable = targets.find((target) => target.executable)
|
|
171
|
+
if (!executable) throw new Error('swift-compile: no executable target to emit')
|
|
172
|
+
const swiftSettings =
|
|
173
|
+
'swiftSettings: [.swiftLanguageMode(.v5), .unsafeFlags(["-Osize"])]'
|
|
174
|
+
const stanza = (target: SwiftPackageTarget): string => {
|
|
175
|
+
const deps = [
|
|
176
|
+
'.product(name: "SwiftUI", package: "sootsim-swift")',
|
|
177
|
+
...(siblings.get(target.name) ?? []).map((sibling) => `"${sibling}"`),
|
|
178
|
+
].join(', ')
|
|
179
|
+
const kind = target.executable ? '.executableTarget' : '.target'
|
|
180
|
+
const link = target.executable
|
|
181
|
+
? `,
|
|
182
|
+
linkerSettings: [
|
|
183
|
+
.unsafeFlags([
|
|
184
|
+
"-Xclang-linker", "-mexec-model=reactor",
|
|
185
|
+
"-Xlinker", "--export-if-defined=__main_argc_argv",
|
|
186
|
+
]),
|
|
187
|
+
.unsafeFlags(["-Xlinker", "--strip-all"], .when(configuration: .release)),
|
|
188
|
+
]`
|
|
189
|
+
: ''
|
|
190
|
+
return [
|
|
191
|
+
` ${kind}(`,
|
|
192
|
+
` name: "${target.name}",`,
|
|
193
|
+
` dependencies: [${deps}],`,
|
|
194
|
+
` path: "${target.path}",`,
|
|
195
|
+
` ${swiftSettings}${link}`,
|
|
196
|
+
` ),`,
|
|
197
|
+
].join('\n')
|
|
198
|
+
}
|
|
199
|
+
return [
|
|
200
|
+
'// swift-tools-version: 6.0',
|
|
201
|
+
'import PackageDescription',
|
|
202
|
+
'',
|
|
203
|
+
'// generated by swift-compile from the project package partition.',
|
|
204
|
+
'let package = Package(',
|
|
205
|
+
' name: "app",',
|
|
206
|
+
` products: [.executable(name: "${executable.name}", targets: ["${executable.name}"])],`,
|
|
207
|
+
` dependencies: [.package(path: "${moduleRoot}")],`,
|
|
208
|
+
' targets: [',
|
|
209
|
+
...targets.map(stanza),
|
|
210
|
+
' ]',
|
|
211
|
+
')',
|
|
212
|
+
'',
|
|
213
|
+
].join('\n')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function writeSwiftProject(
|
|
217
|
+
layout: SwiftProjectLayout,
|
|
218
|
+
input: SwiftCompileInput,
|
|
219
|
+
moduleRoot: string,
|
|
220
|
+
): void {
|
|
221
|
+
const targets = compileTargets(input)
|
|
222
|
+
if (targets.length === 0) {
|
|
223
|
+
rmSync(layout.sources, { recursive: true, force: true })
|
|
224
|
+
for (const [file, contents] of Object.entries(input.files)) {
|
|
225
|
+
const target = path.join(layout.sources, file)
|
|
226
|
+
mkdirSync(path.dirname(target), { recursive: true })
|
|
227
|
+
writeFileSync(target, contents, 'utf8')
|
|
228
|
+
}
|
|
229
|
+
const manifest = readFileSync(
|
|
230
|
+
path.join(moduleRoot, 'scripts/contrast-app-Package.swift'),
|
|
231
|
+
'utf8',
|
|
232
|
+
).replaceAll('__SOOTSIM_SWIFT_PACKAGE__', moduleRoot)
|
|
233
|
+
writeFileSync(path.join(layout.root, 'Package.swift'), manifest, 'utf8')
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
mkdirSync(layout.root, { recursive: true })
|
|
237
|
+
for (const child of readdirSync(layout.root)) {
|
|
238
|
+
if (child === '.build') continue
|
|
239
|
+
rmSync(path.join(layout.root, child), { recursive: true, force: true })
|
|
240
|
+
}
|
|
241
|
+
for (const [file, contents] of Object.entries(input.files)) {
|
|
242
|
+
if (file === 'Package.swift') continue
|
|
243
|
+
const target = path.join(layout.root, file)
|
|
244
|
+
mkdirSync(path.dirname(target), { recursive: true })
|
|
245
|
+
writeFileSync(target, contents, 'utf8')
|
|
246
|
+
}
|
|
247
|
+
writeFileSync(
|
|
248
|
+
path.join(layout.root, 'Package.swift'),
|
|
249
|
+
emitSwiftPackageManifest({
|
|
250
|
+
targets,
|
|
251
|
+
siblings: swiftSiblingDependencies(input.files, targets),
|
|
252
|
+
moduleRoot,
|
|
253
|
+
}),
|
|
254
|
+
'utf8',
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function streamText(value: unknown): string {
|
|
259
|
+
if (typeof value === 'string') return value.trim()
|
|
260
|
+
if (Buffer.isBuffer(value)) return value.toString('utf8').trim()
|
|
261
|
+
return ''
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function outputOf(error: unknown): string {
|
|
265
|
+
if (error && typeof error === 'object') {
|
|
266
|
+
const streams = [
|
|
267
|
+
streamText(Reflect.get(error, 'stdout')),
|
|
268
|
+
streamText(Reflect.get(error, 'stderr')),
|
|
269
|
+
].filter(Boolean)
|
|
270
|
+
if (streams.length > 0) return streams.join('\n')
|
|
271
|
+
}
|
|
272
|
+
return error instanceof Error ? error.message : String(error)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function builtArtifact(layout: SwiftProjectLayout): string {
|
|
276
|
+
const binDir = path.join(layout.root, '.build', WASM_TRIPLE, 'release')
|
|
277
|
+
const artifacts = existsSync(binDir)
|
|
278
|
+
? readdirSync(binDir)
|
|
279
|
+
.filter((name) => name.endsWith('.wasm'))
|
|
280
|
+
.sort()
|
|
281
|
+
: []
|
|
282
|
+
if (artifacts.length !== 1) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`swift-compile: the build produced ${artifacts.length} wasm artifacts; exactly one executable is the app`,
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
return path.join(binDir, artifacts[0])
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function projectDiagnostic(output: string, layout: SwiftProjectLayout): string {
|
|
291
|
+
const base = layout.multi ? layout.root : layout.sources
|
|
292
|
+
const prefix = `${base}${path.sep}`
|
|
293
|
+
return output
|
|
294
|
+
.split('\n')
|
|
295
|
+
.map((line) => (line.startsWith(prefix) ? line.slice(prefix.length) : line))
|
|
296
|
+
.join('\n')
|
|
297
|
+
.trim()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const ROOT_LOCKS = Symbol.for('peachsim.swiftCompile.rootLocks')
|
|
301
|
+
const host = globalThis as typeof globalThis & {
|
|
302
|
+
[ROOT_LOCKS]?: Map<string, Promise<unknown>>
|
|
303
|
+
}
|
|
304
|
+
const rootLocks = (host[ROOT_LOCKS] ??= new Map())
|
|
305
|
+
|
|
306
|
+
function serialize<T>(root: string, work: () => Promise<T>): Promise<T> {
|
|
307
|
+
const current = rootLocks.get(root) ?? Promise.resolve()
|
|
308
|
+
const next = current.then(work, work)
|
|
309
|
+
rootLocks.set(
|
|
310
|
+
root,
|
|
311
|
+
next.catch(() => {}),
|
|
312
|
+
)
|
|
313
|
+
return next
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** the one local compiler used by the route host and Vite host. */
|
|
317
|
+
export function compileSwiftProjectUnit(
|
|
318
|
+
input: SwiftCompileInput,
|
|
319
|
+
options: SwiftCompileOptions,
|
|
320
|
+
): Promise<SwiftCompileArtifact> {
|
|
321
|
+
mkdirSync(options.buildRoot, { recursive: true })
|
|
322
|
+
const root = realpathSync(options.buildRoot)
|
|
323
|
+
const moduleRoot = path.resolve(options.moduleRoot)
|
|
324
|
+
const layout: SwiftProjectLayout = {
|
|
325
|
+
root,
|
|
326
|
+
sources: path.join(root, 'Sources', 'App'),
|
|
327
|
+
multi: compileTargets(input).length > 0,
|
|
328
|
+
}
|
|
329
|
+
const hash = swiftArtifactHash(input, options.release, moduleRoot)
|
|
330
|
+
return serialize(root, async () => {
|
|
331
|
+
writeSwiftProject(layout, input, moduleRoot)
|
|
332
|
+
const swift = path.join(options.toolchainBin, 'swift')
|
|
333
|
+
if (!existsSync(swift)) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`swift-compile: no swift.org toolchain at ${options.toolchainBin}; install ${options.release} and its matching wasm sdk`,
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
await run(
|
|
340
|
+
swift,
|
|
341
|
+
[
|
|
342
|
+
'build',
|
|
343
|
+
'--package-path',
|
|
344
|
+
root,
|
|
345
|
+
'--swift-sdk',
|
|
346
|
+
`${options.release}_wasm`,
|
|
347
|
+
'-c',
|
|
348
|
+
'release',
|
|
349
|
+
'-Xswiftc',
|
|
350
|
+
'-Osize',
|
|
351
|
+
],
|
|
352
|
+
{ maxBuffer: 64 * 1024 * 1024, timeout: options.timeoutMs },
|
|
353
|
+
)
|
|
354
|
+
} catch (error) {
|
|
355
|
+
throw new Error(projectDiagnostic(outputOf(error), layout), { cause: error })
|
|
356
|
+
}
|
|
357
|
+
return { hash, bytes: readFileSync(builtArtifact(layout)) }
|
|
358
|
+
})
|
|
359
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** one SwiftPM target a sent root manifest declares. */
|
|
2
|
+
export interface SwiftPackageTarget {
|
|
3
|
+
/** the target name, as `import <name>` spells it. */
|
|
4
|
+
name: string
|
|
5
|
+
/** project-relative directory holding the target's sources. */
|
|
6
|
+
path: string
|
|
7
|
+
/** true for `.executableTarget`: the one that links the wasm artifact. */
|
|
8
|
+
executable: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** whether a project-relative key is a SwiftPM manifest, not a source. */
|
|
12
|
+
export function isSwiftManifestFile(file: string): boolean {
|
|
13
|
+
const segments = file.split('/')
|
|
14
|
+
return segments[segments.length - 1] === 'Package.swift'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* the library and executable targets a sent root manifest declares, in order.
|
|
19
|
+
*
|
|
20
|
+
* A manifest is Swift code, not data, so this is a scanner, not a parser:
|
|
21
|
+
* each `.target(` / `.executableTarget(` call is matched to its balanced
|
|
22
|
+
* close paren (string-aware, so a paren inside a literal cannot end it
|
|
23
|
+
* early), and `name:` / `path:` are read from the call's own depth only, so
|
|
24
|
+
* a nested `.product(name: "SwiftUI", ...)` inside dependencies never wins.
|
|
25
|
+
*/
|
|
26
|
+
export function swiftManifestTargets(manifest: string): SwiftPackageTarget[] {
|
|
27
|
+
const code = manifest.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '')
|
|
28
|
+
const targets: SwiftPackageTarget[] = []
|
|
29
|
+
const pattern = /\.(executableTarget|target)\s*\(/g
|
|
30
|
+
let match: RegExpExecArray | null
|
|
31
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
32
|
+
const executable = match[1] === 'executableTarget'
|
|
33
|
+
let depth = 1
|
|
34
|
+
let i = pattern.lastIndex
|
|
35
|
+
let inString = false
|
|
36
|
+
let escaped = false
|
|
37
|
+
let flat = ''
|
|
38
|
+
while (i < code.length && depth > 0) {
|
|
39
|
+
const ch = code[i]
|
|
40
|
+
if (inString) {
|
|
41
|
+
if (escaped) escaped = false
|
|
42
|
+
else if (ch === '\\') escaped = true
|
|
43
|
+
else if (ch === '"') inString = false
|
|
44
|
+
if (depth === 1) flat += ch
|
|
45
|
+
} else if (ch === '"') {
|
|
46
|
+
inString = true
|
|
47
|
+
if (depth === 1) flat += ch
|
|
48
|
+
} else if (ch === '(') {
|
|
49
|
+
depth++
|
|
50
|
+
if (depth === 1) flat += ch
|
|
51
|
+
} else if (ch === ')') {
|
|
52
|
+
depth--
|
|
53
|
+
if (depth >= 1) flat += ch
|
|
54
|
+
} else if (depth === 1) {
|
|
55
|
+
flat += ch
|
|
56
|
+
}
|
|
57
|
+
i++
|
|
58
|
+
}
|
|
59
|
+
const name = /\bname\s*:\s*"([^"]+)"/.exec(flat)?.[1]
|
|
60
|
+
if (!name) continue
|
|
61
|
+
const rawPath = /\bpath\s*:\s*"([^"]*)"/.exec(flat)?.[1]
|
|
62
|
+
const targetPath =
|
|
63
|
+
rawPath == null || rawPath === ''
|
|
64
|
+
? `Sources/${name}`
|
|
65
|
+
: rawPath.replace(/^\.\//, '').replace(/\/+$/, '')
|
|
66
|
+
targets.push({ name, path: targetPath, executable })
|
|
67
|
+
}
|
|
68
|
+
return targets
|
|
69
|
+
}
|
package/src/swift-submit.ts
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* its own origin's `/api/swift-compile`), the node bundle builder used by
|
|
8
8
|
* the test-drive CLI and the deploy publisher (posts an absolute endpoint),
|
|
9
9
|
* and the One/rolldown `.swift` transform (same, with a `ccdn_` credential).
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Remote callers never compile in-process: the container is the compiler for
|
|
11
|
+
* this path. The local Vite host uses the same project-unit compiler directly
|
|
12
|
+
* and does not pass through this HTTP adapter.
|
|
12
13
|
*
|
|
13
14
|
* It lives here (rather than in the app's `src/bundler/`) because the One
|
|
14
15
|
* transform ships in this package and cannot import app code; the app's own
|
package/src/vite-plugin-one.ts
CHANGED