peachsim 0.1.1619 → 0.1.1625
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 +35 -5
- package/cli/app-fonts.ts +2 -1
- package/cli/bridge-flow-runner.ts +222 -175
- package/cli/cloud-client.ts +81 -48
- package/cli/commands/box/search-command.ts +3 -2
- package/cli/commands/box.ts +10 -29
- package/cli/commands/control.ts +108 -43
- package/cli/commands/inspect/settling.ts +57 -20
- package/cli/commands/maestro-compare.ts +11 -32
- package/cli/commands/platform.ts +74 -17
- package/cli/commands/record.ts +20 -15
- package/cli/commands/upload.ts +5 -1
- package/cli/dev-build-freshness.ts +37 -2
- package/cli/drivers/playwright-sim-host.ts +18 -6
- package/cli/maestro-accessibility.ts +53 -0
- package/cli/maestro-compare.ts +24 -0
- package/cli/outbound-endpoints.ts +27 -8
- package/cli/parent-pid.ts +0 -9
- package/cli/remote-simulators.ts +112 -0
- package/cli/remoteSimulatorContract.ts +21 -0
- package/detox/expectations.ts +5 -1
- package/detox/index.ts +133 -98
- package/detox/motion-change.cjs +37 -0
- package/detox/motion-change.d.cts +16 -0
- package/detox/navigation-lifecycle.ts +22 -4
- package/dist-lib/agent-daemon-client.cjs +3 -2
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +3 -2
- 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/bundle-channel.cjs +1528 -937
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +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 +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +4 -3
- package/dist-lib/cloud.mjs +4 -3
- package/dist-lib/config.cjs +5 -4
- package/dist-lib/config.mjs +5 -4
- package/dist-lib/detox/index.cjs +1021 -364
- package/dist-lib/dev-bundle-resolution.cjs +51 -23
- package/dist-lib/expo-swift-bundle.cjs +1351 -804
- package/dist-lib/expo-swift-metadata.cjs +5 -4
- package/dist-lib/expo-swift-metadata.mjs +5 -4
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +2475 -1343
- 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 +1269 -300
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.cjs +1 -1
- package/dist-lib/menu.cjs +3 -2
- package/dist-lib/menu.mjs +3 -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 +5 -4
- package/dist-lib/metro-production-bundle.mjs +5 -4
- package/dist-lib/metro.cjs +806 -398
- package/dist-lib/native-app-identity-project.cjs +729 -21
- package/dist-lib/native-app-identity-project.mjs +728 -21
- package/dist-lib/native-app-identity.cjs +232 -9
- package/dist-lib/native-app-identity.mjs +230 -8
- package/dist-lib/native-firebase-metadata.cjs +5 -4
- package/dist-lib/native-firebase-metadata.mjs +5 -4
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.mjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +3 -2
- package/dist-lib/react-native-host-modules.cjs +4 -1
- package/dist-lib/react-native-host-modules.mjs +4 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +94 -28
- package/dist-lib/sdk.cjs +89 -24
- package/dist-lib/sdk.mjs +89 -24
- package/dist-lib/skills.cjs +4471 -2394
- package/dist-lib/swift-compile.cjs +336 -145
- package/dist-lib/swift-compile.mjs +350 -150
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift-toolchain.cjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +891 -557
- package/dist-lib/vite.cjs +68 -14
- package/dist-lib/worklet-executable.cjs +25 -2
- package/dist-lib/worklet-executable.mjs +25 -2
- package/package.json +2 -2
- package/scripts/dev-server-scanner.ts +33 -13
- package/src/admitted-mac-build.ts +34 -8
- package/src/android-app-identity.ts +146 -0
- package/src/android-autolinking.ts +75 -0
- package/src/android-string-value.ts +36 -0
- package/src/android-strings.ts +5 -36
- package/src/androidAutolinkingContract.ts +40 -0
- package/src/bundleGlobalFooter.ts +26 -7
- package/src/cloud.ts +2 -2
- package/src/codex-client.ts +3 -2
- package/src/dev-bundle-resolution.ts +58 -25
- package/src/expo-kotlin-host.ts +6 -36
- package/src/expo-swift-bundle.ts +17 -12
- package/src/expo-swift-host.ts +477 -173
- package/src/expoSwiftContract.ts +6 -0
- package/src/host/bridge-host.ts +7 -55
- package/src/host/staged-host-files.ts +29 -2
- package/src/installed-package-versions.ts +54 -14
- package/src/launchMetadataContract.ts +24 -0
- package/src/metro-plugin.ts +37 -42
- package/src/native-app-identity-project.ts +118 -3
- package/src/native-app-identity.ts +103 -11
- package/src/nitro-compile.ts +2 -5
- package/src/nitro-source.ts +147 -63
- package/src/process-group.ts +90 -60
- package/src/process-output.ts +0 -9
- package/src/public-brand.ts +1 -0
- package/src/react-native-host-modules.ts +3 -0
- package/src/sim-client.ts +6 -4
- package/src/swift-compile.ts +185 -31
- package/src/swift-distribution.lock.json +37 -11
- package/src/swift-distribution.ts +61 -9
- package/src/swift-host.ts +58 -27
- package/src/swift-objc-standin.ts +13 -0
- package/src/swift-standin-emit.ts +1 -1
- package/src/swift-standins.ts +17 -5
- package/src/vite-plugin.ts +8 -19
- package/src/worklet-executable.ts +30 -3
- package/src/worklets-babel.ts +35 -14
- package/src/xcode-project.ts +10 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
accessibilityBounds,
|
|
3
|
+
sameNativeAccessibilityShape,
|
|
4
|
+
} from '../../maestro-accessibility'
|
|
1
5
|
import {
|
|
2
6
|
inspectReadySnapshot,
|
|
3
7
|
inspectWaitReady,
|
|
@@ -16,9 +20,11 @@ export type WaitForSootsimIdleOptions = {
|
|
|
16
20
|
// in fewer polls instead of paying the count at its slower rate.
|
|
17
21
|
stablePolls?: number
|
|
18
22
|
strict?: boolean
|
|
19
|
-
// compare consecutive full-screen pixels
|
|
23
|
+
// compare consecutive full-screen pixels; explicit animation waits use this.
|
|
20
24
|
// frame publications and caret blinks do not require a continuous window.
|
|
21
25
|
paintStill?: boolean
|
|
26
|
+
// native maestro ios: pixel stability first, then native hierarchy equality.
|
|
27
|
+
maestro?: boolean
|
|
22
28
|
// settle on motion alone, for the waits around a gesture: image loads and
|
|
23
29
|
// network traffic move nothing on screen, and the hierarchy walk costs
|
|
24
30
|
// seconds per poll on a large tree under load.
|
|
@@ -75,6 +81,7 @@ export async function waitForSootsimIdle({
|
|
|
75
81
|
stablePolls = 3,
|
|
76
82
|
strict = false,
|
|
77
83
|
paintStill = false,
|
|
84
|
+
maestro = false,
|
|
78
85
|
motionOnly = false,
|
|
79
86
|
transitionStartWindowMs = 80,
|
|
80
87
|
transitionTimeoutMs = 4000,
|
|
@@ -108,12 +115,15 @@ export async function waitForSootsimIdle({
|
|
|
108
115
|
const pollMs = ${Math.max(1, Math.round(pollMs))}
|
|
109
116
|
const strict = ${strict ? 'true' : 'false'}
|
|
110
117
|
const paintStill = ${paintStill ? 'true' : 'false'}
|
|
118
|
+
const maestro = ${maestro ? 'true' : 'false'}
|
|
119
|
+
const accessibilityBounds = ${accessibilityBounds.toString()}
|
|
120
|
+
const sameNativeAccessibilityShape = ${sameNativeAccessibilityShape.toString()}
|
|
111
121
|
const motionOnly = ${motionOnly ? 'true' : 'false'}
|
|
112
122
|
// full mounted-geometry observation needs at least two hierarchy reads
|
|
113
123
|
// plus the stillness window, which a sub-second budget cannot afford:
|
|
114
124
|
// tight budgets (auto-wait) settle on worker stillness plus root
|
|
115
125
|
// membership, generous ones confirm the geometry too.
|
|
116
|
-
const fullGeometry = ${!motionOnly && maxMs >= 1000 ? 'true' : 'false'}
|
|
126
|
+
const fullGeometry = ${!maestro && !motionOnly && maxMs >= 1000 ? 'true' : 'false'}
|
|
117
127
|
const requiredStillMs = paintStill ? 0 : ${Math.max(1, Math.round(stablePolls)) * Math.max(1, Math.round(pollMs))}
|
|
118
128
|
const minimumStablePolls = paintStill ? 1 : ${Math.min(2, Math.max(1, Math.round(stablePolls)))}
|
|
119
129
|
const composite = paintStill ? window.SootSim?.bridges?.liveComposite : null
|
|
@@ -235,6 +245,9 @@ export async function waitForSootsimIdle({
|
|
|
235
245
|
// strict also waits out shell motion: a system alert, sheet or menu
|
|
236
246
|
// animates in the shell worker while the app's own stats read still,
|
|
237
247
|
// so its presentation is complete only once the shell's list empties.
|
|
248
|
+
const accessibilityRead = maestro
|
|
249
|
+
? answer(() => window.__sootsimTest.captureAccessibilityTree()).catch(() => null)
|
|
250
|
+
: null
|
|
238
251
|
const motionRead = strict
|
|
239
252
|
? answer(() => window.__sootsimTest?.listAnimations?.()).catch(() => null)
|
|
240
253
|
: null
|
|
@@ -244,9 +257,12 @@ export async function waitForSootsimIdle({
|
|
|
244
257
|
// the compositor rejects a stats request it is too busy to answer.
|
|
245
258
|
const compositorRead = answer(() => window.__sootsimCompositor.getStats()).catch(() => BUSY)
|
|
246
259
|
const countsRead = motionOnly ? null : answer(() => window.__sootsimTest?.getRequestCounts?.()).catch(() => null)
|
|
247
|
-
// mounted-root membership: the
|
|
248
|
-
// mounts, unmounts or recycles, in
|
|
249
|
-
|
|
260
|
+
// mounted-root membership: the count of nodes that can reach the
|
|
261
|
+
// window moves while the screen mounts, unmounts or recycles, in
|
|
262
|
+
// either render mode. a screen a pushed one covers or an inactive
|
|
263
|
+
// tab stays mounted out of the window, so what it mounts is not
|
|
264
|
+
// screen motion.
|
|
265
|
+
const nodeCountRead = answer(() => window.__sootsimTest?.getWindowNodeCount?.()).catch(() => null)
|
|
250
266
|
// the mounted hierarchy in render-worker mode comes from the test
|
|
251
267
|
// bridge: window.__sootsimRoot is an empty placeholder there, so a
|
|
252
268
|
// walk over it hashes every screen to the same empty string while
|
|
@@ -276,7 +292,7 @@ export async function waitForSootsimIdle({
|
|
|
276
292
|
const motion = await motionRead
|
|
277
293
|
if (motion === BUSY) workerBusy = true
|
|
278
294
|
else if (Array.isArray(motion)) {
|
|
279
|
-
const blocking = motion.filter((anim) => anim.realm === 'shell' && anim.priority !== 'low')
|
|
295
|
+
const blocking = motion.filter((anim) => anim.realm === 'shell' && anim.priority !== 'low' && anim.visible !== false)
|
|
280
296
|
shellAnimating = blocking.length > 0
|
|
281
297
|
// name what holds the settle so a timeout points at its owner.
|
|
282
298
|
shellBlockers = blocking
|
|
@@ -308,6 +324,8 @@ export async function waitForSootsimIdle({
|
|
|
308
324
|
stats.hasActiveAnims === true ||
|
|
309
325
|
stats.hasActiveBlockingNativeAnimations === true ||
|
|
310
326
|
(!paintStill && stats.hasPendingBlockingAnimationFrames === true)
|
|
327
|
+
// the runtime reports only layout work that can reach the window:
|
|
328
|
+
// a covered screen or inactive tab committing does not hold it.
|
|
311
329
|
layoutDirty =
|
|
312
330
|
stats.layoutDirty === true && stats.renderRequested === true
|
|
313
331
|
// a freshly-pushed screen showing a skeleton is layout-stable
|
|
@@ -340,6 +358,11 @@ export async function waitForSootsimIdle({
|
|
|
340
358
|
const total = await nodeCountRead
|
|
341
359
|
if (total === BUSY) workerBusy = true
|
|
342
360
|
nodeCount = typeof total === 'number' ? total : null
|
|
361
|
+
const accessibility = accessibilityRead ? await accessibilityRead : null
|
|
362
|
+
const nativeHierarchy = Array.isArray(accessibility) && accessibility.length > 0
|
|
363
|
+
? accessibility
|
|
364
|
+
: null
|
|
365
|
+
if (accessibility === BUSY) workerBusy = true
|
|
343
366
|
let hierarchy = hierarchyRead ? await hierarchyRead : null
|
|
344
367
|
if (hierarchy === BUSY) {
|
|
345
368
|
workerBusy = true
|
|
@@ -420,7 +443,7 @@ export async function waitForSootsimIdle({
|
|
|
420
443
|
pixelFrameToken = token
|
|
421
444
|
}
|
|
422
445
|
}
|
|
423
|
-
return { layout: nodes.join(';'), screenPixels, animating, shellAnimating, shellBlockers, layoutDirty, pendingFetches, workerBusy, webViewStarting, webViewBlocker, renderStatsAvailable, requestTotal, requestInFlight, appCommits, nodeCount }
|
|
446
|
+
return { layout: nodes.join(';'), nativeHierarchy, screenPixels, animating, shellAnimating, shellBlockers, layoutDirty, pendingFetches, workerBusy, webViewStarting, webViewBlocker, renderStatsAvailable, requestTotal, requestInFlight, appCommits, nodeCount }
|
|
424
447
|
}
|
|
425
448
|
|
|
426
449
|
// how long generic background network is allowed to keep us waiting AFTER
|
|
@@ -434,6 +457,7 @@ export async function waitForSootsimIdle({
|
|
|
434
457
|
// so the generic request counters are a courtesy, not a gate: honor them
|
|
435
458
|
// only within this grace once the screen is visually still.
|
|
436
459
|
const networkQuietGraceMs = 1200
|
|
460
|
+
let lastNativeHierarchy = null
|
|
437
461
|
let lastLayout = ''
|
|
438
462
|
let lastScreenPixels = null
|
|
439
463
|
let screenChangedPolls = 0
|
|
@@ -461,6 +485,9 @@ export async function waitForSootsimIdle({
|
|
|
461
485
|
}
|
|
462
486
|
return true
|
|
463
487
|
}
|
|
488
|
+
const hierarchyUnchanged = (current, previous) =>
|
|
489
|
+
current !== null && previous !== null && current.length === previous.length &&
|
|
490
|
+
current.every((node, index) => sameNativeAccessibilityShape(node, previous[index], accessibilityBounds))
|
|
464
491
|
const IDLE_HELD = { idleHeld: true }
|
|
465
492
|
while (true) {
|
|
466
493
|
if (clockNow() >= deadline) {
|
|
@@ -506,7 +533,7 @@ export async function waitForSootsimIdle({
|
|
|
506
533
|
// the loop head drains a transition still in flight before it ends.
|
|
507
534
|
if (snapshot.workerBusy && clockNow() >= deadline && report) continue
|
|
508
535
|
polls++
|
|
509
|
-
report = { snapshot, layout: lastLayout, screenPixels: lastScreenPixels, requestTotal: lastRequestTotal, appCommits: lastAppCommits, nodeCount: lastNodeCount }
|
|
536
|
+
report = { snapshot, nativeHierarchy: lastNativeHierarchy, layout: lastLayout, screenPixels: lastScreenPixels, requestTotal: lastRequestTotal, appCommits: lastAppCommits, nodeCount: lastNodeCount }
|
|
510
537
|
if (snapshot.layoutDirty) dirtyPolls++
|
|
511
538
|
if (snapshot.animating || snapshot.shellAnimating) animatingPolls++
|
|
512
539
|
if (snapshot.webViewStarting) webViewStartingPolls++
|
|
@@ -527,7 +554,15 @@ export async function waitForSootsimIdle({
|
|
|
527
554
|
// strictOk and adds shell motion plus the blocking-stats floor. a
|
|
528
555
|
// null count or commit reading means the runtime does not report
|
|
529
556
|
// it, which reads as stable the way a missing stats block does.
|
|
530
|
-
|
|
557
|
+
// native screen checks observe output, not animation ownership. after
|
|
558
|
+
// its 3 s pixel phase, iOS accepts two equal native TreeNodes even
|
|
559
|
+
// while paint-only playback continues. overlap hierarchy reads so
|
|
560
|
+
// neither the gesture nor screenshot deadline grows.
|
|
561
|
+
const maestroStill = snapshot.screenPixels !== null && !snapshot.workerBusy && !snapshot.webViewStarting && (
|
|
562
|
+
!screenChanged || (clockNow() - start >= 3000 &&
|
|
563
|
+
hierarchyUnchanged(snapshot.nativeHierarchy, lastNativeHierarchy))
|
|
564
|
+
)
|
|
565
|
+
const visuallyStill = maestro ? maestroStill :
|
|
531
566
|
strictOk &&
|
|
532
567
|
!snapshot.animating &&
|
|
533
568
|
!snapshot.layoutDirty &&
|
|
@@ -555,7 +590,7 @@ export async function waitForSootsimIdle({
|
|
|
555
590
|
// grace (continuous background traffic must not block forever).
|
|
556
591
|
if (
|
|
557
592
|
stillProven() &&
|
|
558
|
-
(motionOnly || networkQuiet || clockNow() - visuallyStillSince >= networkQuietGraceMs)
|
|
593
|
+
(maestro || motionOnly || networkQuiet || clockNow() - visuallyStillSince >= networkQuietGraceMs)
|
|
559
594
|
) {
|
|
560
595
|
if (transitionWait) {
|
|
561
596
|
await transitionWait
|
|
@@ -567,6 +602,7 @@ export async function waitForSootsimIdle({
|
|
|
567
602
|
stillPolls = []
|
|
568
603
|
visuallyStillSince = 0
|
|
569
604
|
}
|
|
605
|
+
lastNativeHierarchy = snapshot.nativeHierarchy
|
|
570
606
|
lastLayout = snapshot.layout
|
|
571
607
|
lastScreenPixels = snapshot.screenPixels
|
|
572
608
|
lastRequestTotal = snapshot.requestTotal
|
|
@@ -589,20 +625,21 @@ export async function waitForSootsimIdle({
|
|
|
589
625
|
}
|
|
590
626
|
const last = report.snapshot
|
|
591
627
|
const blockers = []
|
|
592
|
-
if (strict && !last.renderStatsAvailable) blockers.push('render stats unavailable')
|
|
593
|
-
if (last.animating) blockers.push('animations still running')
|
|
594
|
-
if (last.shellAnimating) blockers.push('shell animations still running (' + last.shellBlockers + ')')
|
|
595
|
-
if (last.layoutDirty) blockers.push('layout still dirty')
|
|
596
|
-
if (last.appCommits !== null && report.appCommits !== null && last.appCommits !== report.appCommits) blockers.push('layout commits still landing')
|
|
597
|
-
if (last.nodeCount !== null && report.nodeCount !== null && last.nodeCount !== report.nodeCount) blockers.push('node count still changing')
|
|
598
|
-
if (!motionOnly && last.pendingFetches > 0) blockers.push(last.pendingFetches + ' image fetches pending')
|
|
628
|
+
if (!maestro && strict && !last.renderStatsAvailable) blockers.push('render stats unavailable')
|
|
629
|
+
if (!maestro && last.animating) blockers.push('animations still running')
|
|
630
|
+
if (!maestro && last.shellAnimating) blockers.push('shell animations still running (' + last.shellBlockers + ')')
|
|
631
|
+
if (!maestro && last.layoutDirty) blockers.push('layout still dirty')
|
|
632
|
+
if (!maestro && last.appCommits !== null && report.appCommits !== null && last.appCommits !== report.appCommits) blockers.push('layout commits still landing')
|
|
633
|
+
if (!maestro && last.nodeCount !== null && report.nodeCount !== null && last.nodeCount !== report.nodeCount) blockers.push('node count still changing')
|
|
634
|
+
if (!maestro && !motionOnly && last.pendingFetches > 0) blockers.push(last.pendingFetches + ' image fetches pending')
|
|
599
635
|
if (last.workerBusy) blockers.push('a render worker too busy to report')
|
|
600
636
|
if (last.webViewStarting) blockers.push(last.webViewBlocker || 'webView still starting')
|
|
601
|
-
if (last.layout !== report.layout) blockers.push('layout still changing')
|
|
637
|
+
if (!maestro && last.layout !== report.layout) blockers.push('layout still changing')
|
|
638
|
+
if (maestro && !hierarchyUnchanged(last.nativeHierarchy, report.nativeHierarchy)) blockers.push(last.nativeHierarchy === null ? 'native accessibility hierarchy unavailable' : 'native accessibility hierarchy still changing')
|
|
602
639
|
if (paintStill && !last.screenPixels) blockers.push('screen pixels unavailable')
|
|
603
640
|
else if (paintStill && !screenUnchanged(last.screenPixels, report.screenPixels)) blockers.push('screen pixels still changing')
|
|
604
|
-
if (!motionOnly && last.requestInFlight > 0) blockers.push(last.requestInFlight + ' requests in flight')
|
|
605
|
-
else if (!motionOnly && last.requestTotal !== report.requestTotal) blockers.push('new requests issued')
|
|
641
|
+
if (!maestro && !motionOnly && last.requestInFlight > 0) blockers.push(last.requestInFlight + ' requests in flight')
|
|
642
|
+
else if (!maestro && !motionOnly && last.requestTotal !== report.requestTotal) blockers.push('new requests issued')
|
|
606
643
|
if (polls < 2) blockers.push('sim too busy to poll twice')
|
|
607
644
|
// the last read is still: motion ended too near the deadline for the
|
|
608
645
|
// stillness window to hold before it.
|
|
@@ -12,6 +12,7 @@ import { rnxHomeDir } from '../../src/home-paths'
|
|
|
12
12
|
import { parseFlowFile } from '../flow-file'
|
|
13
13
|
import {
|
|
14
14
|
buildCompareHtml,
|
|
15
|
+
buildCompareSide,
|
|
15
16
|
compareScreens,
|
|
16
17
|
comparePassed,
|
|
17
18
|
instrumentFlowSource,
|
|
@@ -22,7 +23,6 @@ import {
|
|
|
22
23
|
type CompareCapture,
|
|
23
24
|
type CompareFlow,
|
|
24
25
|
type CompareReport,
|
|
25
|
-
type CompareSide,
|
|
26
26
|
} from '../maestro-compare'
|
|
27
27
|
import { MaestroJsContext } from '../maestro-js'
|
|
28
28
|
import { collectFlowFiles, loadMaestroWorkspaceConfig } from '../maestro-workspace'
|
|
@@ -201,29 +201,6 @@ function formatFailedRequest(entry: unknown): string {
|
|
|
201
201
|
return `request failed: ${e.method ?? 'GET'} ${e.url ?? '?'} ${e.status ?? e.error ?? ''}`.trim()
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
function sideFromSteps(
|
|
205
|
-
steps: CompareSide['steps'],
|
|
206
|
-
ran: boolean,
|
|
207
|
-
extraErrors: string[],
|
|
208
|
-
// a run that failed outside its steps (the sim or app never came up)
|
|
209
|
-
runFailure?: string,
|
|
210
|
-
): CompareSide {
|
|
211
|
-
const failed = steps.find((s) => s.status === 'failed')
|
|
212
|
-
return {
|
|
213
|
-
status: !ran ? 'not run' : failed || runFailure ? 'failed' : 'passed',
|
|
214
|
-
durationMs: steps.reduce((sum, s) => sum + s.durationMs, 0),
|
|
215
|
-
steps,
|
|
216
|
-
errors: [
|
|
217
|
-
...(failed
|
|
218
|
-
? [`step "${failed.label}" failed: ${failed.error ?? 'no message'}`]
|
|
219
|
-
: runFailure
|
|
220
|
-
? [runFailure]
|
|
221
|
-
: []),
|
|
222
|
-
...extraErrors,
|
|
223
|
-
],
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
204
|
// peach names each flow's output dir after the file stem, suffixing repeats
|
|
228
205
|
// in run order (commands/maestro.ts uniqueFlowDir).
|
|
229
206
|
function peachFlowDirs(root: string, files: string[]): string[] {
|
|
@@ -460,14 +437,14 @@ export async function runMaestroCompare(
|
|
|
460
437
|
|
|
461
438
|
// 2. peach, same instrumented flows (its --env already applied above)
|
|
462
439
|
const peachOut = path.join(out, 'peach')
|
|
463
|
-
const
|
|
440
|
+
const peachResults = new Map<string, { exit: number; playbackFailure: string }>()
|
|
464
441
|
await runMaestro(
|
|
465
442
|
['test', runTarget, '--test-output-dir', peachOut, ...shared, ...peachArgs],
|
|
466
443
|
{
|
|
467
444
|
...opts,
|
|
468
445
|
captureConsole: true,
|
|
469
446
|
onFlowResult: ({ file, exit, playbackFailure }) => {
|
|
470
|
-
|
|
447
|
+
peachResults.set(path.resolve(file), { exit, playbackFailure })
|
|
471
448
|
},
|
|
472
449
|
},
|
|
473
450
|
)
|
|
@@ -491,6 +468,8 @@ export async function runMaestroCompare(
|
|
|
491
468
|
const nativeDir = nativeRun ? path.join(nativeRun, flow.name) : null
|
|
492
469
|
const peachDir = peachDirs[i]!
|
|
493
470
|
const nativeRan = Boolean(nativeDir && fs.existsSync(nativeDir))
|
|
471
|
+
const peachResult = peachResults.get(path.resolve(flow.copy))
|
|
472
|
+
const nativeFailure = nativeFlowFailureFromLog(nativeLog, flow.name)
|
|
494
473
|
const peachRan = fs.existsSync(path.join(peachDir, 'commands.json'))
|
|
495
474
|
const nativeSteps = nativeRan
|
|
496
475
|
? nativeStepsFromCommandsJson(readJson(path.join(nativeDir!, 'commands.json')))
|
|
@@ -545,21 +524,21 @@ export async function runMaestroCompare(
|
|
|
545
524
|
flows.push({
|
|
546
525
|
name: flow.name,
|
|
547
526
|
file: displayPath(cwd, flow.file),
|
|
548
|
-
native:
|
|
527
|
+
native: buildCompareSide(
|
|
549
528
|
nativeSteps,
|
|
550
|
-
nativeRan,
|
|
529
|
+
nativeFailure ? 'failed' : nativeRan ? 'passed' : 'not run',
|
|
551
530
|
nativeRan
|
|
552
531
|
? []
|
|
553
532
|
: [
|
|
554
533
|
`maestro produced no output for this flow (exit ${nativeExit}); see ios.log`,
|
|
555
534
|
],
|
|
556
|
-
|
|
535
|
+
nativeFailure,
|
|
557
536
|
),
|
|
558
|
-
peach:
|
|
537
|
+
peach: buildCompareSide(
|
|
559
538
|
peachSteps,
|
|
560
|
-
|
|
539
|
+
peachResult ? (peachResult.exit === 0 ? 'passed' : 'failed') : 'not run',
|
|
561
540
|
peachConsole,
|
|
562
|
-
|
|
541
|
+
peachResult?.exit ? peachResult.playbackFailure : undefined,
|
|
563
542
|
),
|
|
564
543
|
screens,
|
|
565
544
|
})
|
package/cli/commands/platform.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { settingsStore } from 'peach-engine/settings/store'
|
|
|
17
17
|
import { rnxPublicBrand } from '../../src/public-brand'
|
|
18
18
|
import { UnsupportedRemoteHostError } from '../cloud-client'
|
|
19
19
|
import { rethrowIfExit } from '../run-rnx'
|
|
20
|
-
import type { CloudSession } from '../cloud-session'
|
|
20
|
+
import type { CloudBoxSession, CloudSession } from '../cloud-session'
|
|
21
21
|
|
|
22
22
|
const PLATFORM_LABEL: Record<RuntimePlatform, string> = {
|
|
23
23
|
ios: 'iOS',
|
|
@@ -275,6 +275,7 @@ async function runRemoteCreate(
|
|
|
275
275
|
const [
|
|
276
276
|
{ authHeaderOrExit, cloudAccountIdOrExit },
|
|
277
277
|
{
|
|
278
|
+
assertBundleBuiltFor,
|
|
278
279
|
claimCloudSimulator,
|
|
279
280
|
closeCloudBox,
|
|
280
281
|
closeCloudSession,
|
|
@@ -300,6 +301,7 @@ async function runRemoteCreate(
|
|
|
300
301
|
undefined,
|
|
301
302
|
input.assetsDirectory,
|
|
302
303
|
)
|
|
304
|
+
assertBundleBuiltFor(artifact.bytes, platform, `peach ${platform} --remote`)
|
|
303
305
|
const descriptor = requireCloudSessionShellUpdate()
|
|
304
306
|
const { auth, header } = authHeaderOrExit(
|
|
305
307
|
`${platform} ${args.includes('--nano') ? '--nano' : '--remote'}`,
|
|
@@ -401,8 +403,8 @@ async function runRemoteCreate(
|
|
|
401
403
|
* a nano upload answers to does not apply.
|
|
402
404
|
*
|
|
403
405
|
* a box created from the already-built app boots it in its own page, and this
|
|
404
|
-
* opens the
|
|
405
|
-
*
|
|
406
|
+
* opens the watch url the box service mints with its display grant, on the
|
|
407
|
+
* deploy that serves that page, as the nano service mints one on its own.
|
|
406
408
|
*/
|
|
407
409
|
async function runRemoteBox(
|
|
408
410
|
platform: RuntimePlatform,
|
|
@@ -412,7 +414,15 @@ async function runRemoteBox(
|
|
|
412
414
|
const command = `${platform} --remote`
|
|
413
415
|
const input = remoteInputFromArgs(args, platform)
|
|
414
416
|
const [
|
|
415
|
-
{
|
|
417
|
+
{
|
|
418
|
+
assertBundleBuiltFor,
|
|
419
|
+
BoxBootFailedError,
|
|
420
|
+
closeCloudBox,
|
|
421
|
+
closeCloudSession,
|
|
422
|
+
produceCloudBoxArtifact,
|
|
423
|
+
awaitBoxBoot,
|
|
424
|
+
requestBoxWatchUrl,
|
|
425
|
+
},
|
|
416
426
|
box,
|
|
417
427
|
{ cloudBoxConnectUrl, ensureCloudBoxBrowser, readCloudBoxSession },
|
|
418
428
|
{ authHeaderOrExit, cloudAccountIdOrExit },
|
|
@@ -431,13 +441,35 @@ async function runRemoteBox(
|
|
|
431
441
|
import('../cloud-session'),
|
|
432
442
|
])
|
|
433
443
|
const { openInBrowser } = box
|
|
444
|
+
// an app that cannot boot never gets a simulator, so the box ends with the
|
|
445
|
+
// error instead of billing until someone finds it.
|
|
446
|
+
const awaitBootOrCloseBox = async (session: CloudBoxSession) => {
|
|
447
|
+
try {
|
|
448
|
+
await awaitBoxBoot({
|
|
449
|
+
apiOrigin: session.apiOrigin,
|
|
450
|
+
boxId: session.boxId,
|
|
451
|
+
boxToken: session.boxToken,
|
|
452
|
+
})
|
|
453
|
+
} catch (error) {
|
|
454
|
+
if (error instanceof BoxBootFailedError) {
|
|
455
|
+
await closeCloudBox(session)
|
|
456
|
+
try {
|
|
457
|
+
publishCloudSessionToShell(null)
|
|
458
|
+
} catch {
|
|
459
|
+
// no shell update channel is required outside an interactive peach shell
|
|
460
|
+
}
|
|
461
|
+
delete process.env[PEACH_CLOUD_SESSION_ENV]
|
|
462
|
+
}
|
|
463
|
+
throw error
|
|
464
|
+
}
|
|
465
|
+
}
|
|
434
466
|
const artifact = await produceCloudBoxArtifact(input.bundlePath, input.assetsDirectory)
|
|
467
|
+
assertBundleBuiltFor(artifact.bytes, platform, `peach ${command}`)
|
|
435
468
|
// the session records which bundle a box already holds, so reattaching can
|
|
436
469
|
// tell the same app from a rebuilt one. the box producer returns no digest
|
|
437
470
|
// of its own, so take it here over the bytes that actually crossed.
|
|
438
471
|
const artifactSha256 = createHash('sha256').update(artifact.bytes).digest('hex')
|
|
439
|
-
const endpoint = box.cloudBoxEndpoint(args
|
|
440
|
-
if (!endpoint) return 1
|
|
472
|
+
const endpoint = box.cloudBoxEndpoint(args)
|
|
441
473
|
|
|
442
474
|
const existingSession = readCloudSession()
|
|
443
475
|
// a launch link only means something to an app that is launching, so a box
|
|
@@ -464,6 +496,9 @@ async function runRemoteBox(
|
|
|
464
496
|
connectUrl: cloudBoxConnectUrl(existingSession.apiOrigin, existingSession.boxId),
|
|
465
497
|
accessToken: existingSession.boxToken,
|
|
466
498
|
})
|
|
499
|
+
// a lapsed tab reboots the app, so a reconnect reads that boot's outcome
|
|
500
|
+
// before waiting on a simulator a throwing app never attaches.
|
|
501
|
+
await awaitBootOrCloseBox(existingSession)
|
|
467
502
|
// grants are short-lived, so a reconnect mints a fresh one rather than
|
|
468
503
|
// reopening whatever url the create printed.
|
|
469
504
|
const watchUrl = await requestBoxWatchUrl({
|
|
@@ -551,7 +586,7 @@ async function runRemoteBox(
|
|
|
551
586
|
// `peach remote close` cannot end it; name the command that does.
|
|
552
587
|
if (!attachedToShell) {
|
|
553
588
|
console.log(
|
|
554
|
-
` this shell has no Peach integration; the Box bills until: ${rnxPublicBrand.commandName} box
|
|
589
|
+
` this shell has no Peach integration; the Box bills until: ${rnxPublicBrand.commandName} box delete ${created.id}`,
|
|
555
590
|
)
|
|
556
591
|
}
|
|
557
592
|
// the session is published before the grant is minted, so a grant that never
|
|
@@ -561,6 +596,7 @@ async function runRemoteBox(
|
|
|
561
596
|
boxId: created.id,
|
|
562
597
|
boxToken: created.accessToken ?? '',
|
|
563
598
|
})
|
|
599
|
+
await awaitBootOrCloseBox(session)
|
|
564
600
|
console.log(` open ${watchUrl}`)
|
|
565
601
|
return openInBrowser(watchUrl)
|
|
566
602
|
}
|
|
@@ -576,11 +612,22 @@ export async function runRemoteList(args: string[]): Promise<number> {
|
|
|
576
612
|
}
|
|
577
613
|
throw new Error(`usage: peach remote list [--json]`)
|
|
578
614
|
}
|
|
579
|
-
const [
|
|
580
|
-
|
|
615
|
+
const [
|
|
616
|
+
{ authHeaderOrExit, cloudAccountIdOrExit },
|
|
617
|
+
{ listAccountRemoteSimulators },
|
|
618
|
+
{ cloudBoxApiOrigin },
|
|
619
|
+
] = await Promise.all([
|
|
620
|
+
import('../auth'),
|
|
621
|
+
import('../remote-simulators'),
|
|
622
|
+
import('./box'),
|
|
623
|
+
])
|
|
581
624
|
const { auth, header } = authHeaderOrExit('remote list')
|
|
582
625
|
const accountId = cloudAccountIdOrExit(auth)
|
|
583
|
-
const sims = await
|
|
626
|
+
const sims = await listAccountRemoteSimulators({
|
|
627
|
+
apiOrigin: cloudBoxApiOrigin(args, auth),
|
|
628
|
+
authorization: header,
|
|
629
|
+
accountId,
|
|
630
|
+
})
|
|
584
631
|
if (json) {
|
|
585
632
|
console.log(JSON.stringify({ sims }))
|
|
586
633
|
return 0
|
|
@@ -590,6 +637,10 @@ export async function runRemoteList(args: string[]): Promise<number> {
|
|
|
590
637
|
return 0
|
|
591
638
|
}
|
|
592
639
|
for (const sim of sims) {
|
|
640
|
+
if (sim.service === 'box') {
|
|
641
|
+
console.log(` ${sim.simId} ${sim.status} ${sim.name ?? ''}`)
|
|
642
|
+
continue
|
|
643
|
+
}
|
|
593
644
|
const platformLabel = sim.labels['peach.platform'] ?? 'unknown'
|
|
594
645
|
console.log(
|
|
595
646
|
` ${sim.simId} ${sim.status} ${platformLabel} sha256:${sim.artifact.sha256.slice(0, 12)}`,
|
|
@@ -621,11 +672,13 @@ export async function runRemoteStop(args: string[]): Promise<number> {
|
|
|
621
672
|
const simId = positional[0]
|
|
622
673
|
const [
|
|
623
674
|
{ authHeaderOrExit, cloudAccountIdOrExit },
|
|
624
|
-
{
|
|
675
|
+
{ stopAccountRemoteSimulator },
|
|
676
|
+
{ cloudBoxApiOrigin },
|
|
625
677
|
{ publishCloudSessionToShell, readCloudSession, requireCloudSessionShellUpdate },
|
|
626
678
|
] = await Promise.all([
|
|
627
679
|
import('../auth'),
|
|
628
|
-
import('../
|
|
680
|
+
import('../remote-simulators'),
|
|
681
|
+
import('./box'),
|
|
629
682
|
import('../cloud-session'),
|
|
630
683
|
])
|
|
631
684
|
let current: CloudSession | null = null
|
|
@@ -637,16 +690,20 @@ export async function runRemoteStop(args: string[]): Promise<number> {
|
|
|
637
690
|
// stopping the shell's own simulator clears the shell with it, so the
|
|
638
691
|
// update channel is required before anything is deleted.
|
|
639
692
|
const descriptor =
|
|
640
|
-
current?.service === 'sim' && current.simId === simId
|
|
693
|
+
(current?.service === 'sim' && current.simId === simId) ||
|
|
694
|
+
(current?.service === 'box' && current.boxId === simId)
|
|
641
695
|
? requireCloudSessionShellUpdate()
|
|
642
696
|
: null
|
|
643
697
|
const { auth, header } = authHeaderOrExit('remote stop')
|
|
644
698
|
const accountId = cloudAccountIdOrExit(auth)
|
|
645
|
-
const deleted = await
|
|
699
|
+
const deleted = await stopAccountRemoteSimulator(
|
|
700
|
+
{
|
|
701
|
+
apiOrigin: cloudBoxApiOrigin(args, auth),
|
|
702
|
+
authorization: header,
|
|
703
|
+
accountId,
|
|
704
|
+
},
|
|
646
705
|
simId,
|
|
647
|
-
|
|
648
|
-
accountId,
|
|
649
|
-
})
|
|
706
|
+
)
|
|
650
707
|
if (!deleted) {
|
|
651
708
|
console.error(
|
|
652
709
|
` ${rnxPublicBrand.commandName} remote stop: ${simId} does not exist`,
|
package/cli/commands/record.ts
CHANGED
|
@@ -416,40 +416,45 @@ export async function runRecord(args: string[], opts: RecordOptions) {
|
|
|
416
416
|
}
|
|
417
417
|
await new Promise((r) => setTimeout(r, durationMs))
|
|
418
418
|
const deadline = Date.now() + Math.max(5_000, durationMs)
|
|
419
|
-
|
|
419
|
+
// frames come back a few at a time: one message holding every base64 png
|
|
420
|
+
// exceeds the bridge socket's message cap and closes it.
|
|
421
|
+
const FRAMES_PER_PAGE = 2
|
|
422
|
+
let saved = 0
|
|
420
423
|
for (;;) {
|
|
421
424
|
const result: {
|
|
422
425
|
ok: boolean
|
|
423
426
|
done: boolean
|
|
427
|
+
totalFrames?: number
|
|
424
428
|
frames?: Array<{ data: string; size: number }>
|
|
425
429
|
error?: string
|
|
426
430
|
} | null = await bridge.send({
|
|
427
431
|
type: 'evaluate',
|
|
428
|
-
code: `window.__sootsimRecorder.getFrameCaptureResult(${start.requestId})`,
|
|
432
|
+
code: `window.__sootsimRecorder.getFrameCaptureResult(${start.requestId}, { offset: ${saved}, limit: ${FRAMES_PER_PAGE} })`,
|
|
429
433
|
})
|
|
430
434
|
if (!result) {
|
|
431
435
|
console.error(' frame capture result missing')
|
|
432
436
|
peachExit(1)
|
|
433
437
|
}
|
|
434
|
-
if (result.done) {
|
|
435
|
-
if (
|
|
436
|
-
console.error(
|
|
438
|
+
if (!result.done) {
|
|
439
|
+
if (Date.now() >= deadline) {
|
|
440
|
+
console.error(' frame capture timed out')
|
|
437
441
|
peachExit(1)
|
|
438
442
|
}
|
|
439
|
-
|
|
440
|
-
|
|
443
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
444
|
+
continue
|
|
441
445
|
}
|
|
442
|
-
if (
|
|
443
|
-
console.error(
|
|
446
|
+
if (!result.ok) {
|
|
447
|
+
console.error(` frame capture failed: ${result.error ?? 'unknown error'}`)
|
|
444
448
|
peachExit(1)
|
|
445
449
|
}
|
|
446
|
-
|
|
450
|
+
for (const f of result.frames ?? []) {
|
|
451
|
+
saved++
|
|
452
|
+
const p = `${outDir}/frame-${String(saved).padStart(3, '0')}.png`
|
|
453
|
+
writeFileSync(p, Buffer.from(f.data, 'base64'))
|
|
454
|
+
}
|
|
455
|
+
if (saved >= (result.totalFrames ?? 0)) break
|
|
447
456
|
}
|
|
448
|
-
|
|
449
|
-
const p = `${outDir}/frame-${String(i + 1).padStart(3, '0')}.png`
|
|
450
|
-
writeFileSync(p, Buffer.from(f.data, 'base64'))
|
|
451
|
-
})
|
|
452
|
-
console.log(` saved ${frames.length} frames`)
|
|
457
|
+
console.log(` saved ${saved} frames`)
|
|
453
458
|
return
|
|
454
459
|
}
|
|
455
460
|
|
package/cli/commands/upload.ts
CHANGED
|
@@ -14,6 +14,7 @@ import path from 'path'
|
|
|
14
14
|
import { gzipSync, gunzipSync } from 'zlib'
|
|
15
15
|
import { readProfileTrace } from '@peach/globals/profile-trace'
|
|
16
16
|
import { toProfileFile, type ProfileFile } from '@peach/globals/profiler'
|
|
17
|
+
import { resolveAppFontsWire } from 'peach-engine/engine/config-plugin-fonts'
|
|
17
18
|
import { shouldCompress } from 'peach-engine/preview/compress'
|
|
18
19
|
import { brotliBundle } from 'peach-engine/preview/compress-node'
|
|
19
20
|
import {
|
|
@@ -939,7 +940,10 @@ export async function runUpload(
|
|
|
939
940
|
// fetcher (same code path as the build's `peach app-fonts stage`) so both
|
|
940
941
|
// pipelines store identical bytes keyed by sha256(url).
|
|
941
942
|
const appFontFiles: PreparedFile[] = []
|
|
942
|
-
const appFontsWire =
|
|
943
|
+
const appFontsWire = await resolveAppFontsWire(
|
|
944
|
+
appFontsWireFromPageHref(snapshot.pageHref),
|
|
945
|
+
fetch,
|
|
946
|
+
)
|
|
943
947
|
if (appFontsWire) {
|
|
944
948
|
const fonts = await fetchAppFontFiles(appFontsWire, {
|
|
945
949
|
onStaged: ({ url, byteLength }) =>
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
// watcher would bundle chokidar and private engine/bundler modules into the cli.
|
|
15
15
|
// deps-web belongs to a different served tree.
|
|
16
16
|
|
|
17
|
-
import { readdirSync, statSync, type Dirent } from 'node:fs'
|
|
18
|
-
import { join, relative, sep } from 'node:path'
|
|
17
|
+
import { readdirSync, statSync, utimesSync, type Dirent } from 'node:fs'
|
|
18
|
+
import { join, relative, resolve, sep } from 'node:path'
|
|
19
19
|
|
|
20
20
|
export const DEV_BUILD_REBUILD_COMMAND = 'bun run build:peach'
|
|
21
21
|
|
|
@@ -159,6 +159,41 @@ function newestForEntry(monorepoRoot: string, repoPath: string): NewestFile | nu
|
|
|
159
159
|
return null
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
/** stamps the sentinels under `installedRoot` as built at `sourceCapturedAt`,
|
|
163
|
+
* when the build captured its source. an install that finds identical bytes
|
|
164
|
+
* (remote cache hit, byte-identical promotion) leaves their old mtimes, which
|
|
165
|
+
* the gate would read as a stale build; stamping the install time instead
|
|
166
|
+
* would hide a source edit made while the build ran. */
|
|
167
|
+
export function markDevBuildCurrent(
|
|
168
|
+
monorepoRoot: string,
|
|
169
|
+
installedRoot: string,
|
|
170
|
+
sourceCapturedAt: Date,
|
|
171
|
+
): void {
|
|
172
|
+
const installed = resolve(installedRoot)
|
|
173
|
+
for (const route of ROUTES) {
|
|
174
|
+
for (const artifact of route.artifacts) {
|
|
175
|
+
const absolute = join(monorepoRoot, artifact)
|
|
176
|
+
if (absolute !== installed && !absolute.startsWith(installed + sep)) continue
|
|
177
|
+
const stack = [absolute]
|
|
178
|
+
while (stack.length > 0) {
|
|
179
|
+
const path = stack.pop()
|
|
180
|
+
if (path === undefined) continue
|
|
181
|
+
let stat: ReturnType<typeof statSync>
|
|
182
|
+
try {
|
|
183
|
+
stat = statSync(path)
|
|
184
|
+
} catch {
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
if (stat.isDirectory()) {
|
|
188
|
+
for (const name of readdirSync(path)) stack.push(join(path, name))
|
|
189
|
+
} else {
|
|
190
|
+
utimesSync(path, sourceCapturedAt, sourceCapturedAt)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
162
197
|
function toRepoPath(monorepoRoot: string, absolutePath: string): string {
|
|
163
198
|
return relative(monorepoRoot, absolutePath).split(sep).join('/')
|
|
164
199
|
}
|