peachsim 0.1.1625 → 0.1.1640
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 +1 -1
- package/README.md +15 -2
- package/cli/app-config.ts +18 -6
- package/cli/app-fonts.ts +37 -17
- package/cli/app-stubs.ts +284 -20
- package/cli/bridge-flow-runner.ts +66 -45
- package/cli/cloud-client.ts +60 -40
- package/cli/command-registry.ts +1 -0
- package/cli/commands/control.ts +283 -49
- package/cli/commands/doctor.ts +30 -189
- package/cli/commands/flow.ts +79 -11
- package/cli/commands/inspect/core.ts +10 -5
- package/cli/commands/inspect/settling.ts +14 -2
- package/cli/commands/inspect/shared.ts +9 -4
- package/cli/commands/inspect/wait-ready.ts +12 -0
- package/cli/commands/inspect.ts +91 -50
- package/cli/commands/install-desktop.ts +1 -0
- package/cli/commands/maestro-compare.ts +97 -38
- package/cli/commands/maestro.ts +9 -1
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/record.ts +118 -48
- package/cli/commands/screenshot.ts +23 -1
- package/cli/commands/serve.ts +7 -2
- package/cli/commands/setup.ts +19 -42
- package/cli/commands/swift.ts +14 -3
- package/cli/commands/upload.ts +93 -48
- package/cli/desktop-companion.ts +102 -17
- package/cli/drivers/desktop.ts +14 -1
- package/cli/drivers/playwright-sim-host.ts +103 -45
- package/cli/drivers/playwright.ts +9 -8
- package/cli/drivers/types.ts +5 -0
- package/cli/maestro-accessibility.ts +1 -1
- package/cli/maestro-compare.ts +84 -12
- package/cli/main.ts +3 -2
- package/cli/outbound-endpoints.ts +20 -6
- package/cli/pause-aware-timeout.ts +106 -0
- package/cli/scene-video.ts +89 -0
- package/cli/ws-bridge.ts +37 -19
- package/detox/attempt-events.cjs +66 -5
- package/detox/attempt-events.d.cts +7 -0
- package/detox/expectations.ts +7 -3
- package/detox/index.ts +269 -170
- package/detox/motion-change.cjs +206 -21
- package/detox/motion-change.d.cts +39 -2
- package/detox/navigation-lifecycle.ts +35 -3
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +8 -7
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +13 -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 +14 -8
- package/dist-lib/bridge-contract.mjs +13 -7
- package/dist-lib/bundle-channel.cjs +11887 -7900
- 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 +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +6 -3
- package/dist-lib/config.mjs +6 -3
- package/dist-lib/detox/index.cjs +1379 -725
- package/dist-lib/dev-bundle-resolution.cjs +14 -2
- package/dist-lib/expo-swift-bundle.cjs +9341 -5462
- package/dist-lib/expo-swift-metadata.cjs +11 -7
- package/dist-lib/expo-swift-metadata.mjs +11 -7
- package/dist-lib/home-paths.cjs +32 -3
- package/dist-lib/host/bridge-host.cjs +50204 -7258
- 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 +3541 -448
- 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 +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +2931 -104
- package/dist-lib/native-app-identity-project.cjs +289 -49
- package/dist-lib/native-app-identity-project.mjs +289 -49
- package/dist-lib/native-app-identity.cjs +77 -26
- package/dist-lib/native-app-identity.mjs +76 -26
- package/dist-lib/native-firebase-metadata.cjs +1 -1
- package/dist-lib/native-firebase-metadata.mjs +1 -1
- 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 +1 -1
- package/dist-lib/react-native-host-modules.cjs +6 -2
- package/dist-lib/react-native-host-modules.mjs +6 -2
- package/dist-lib/scripts/dev-server-scanner.cjs +22 -8
- package/dist-lib/sdk.cjs +27 -11
- package/dist-lib/sdk.mjs +27 -10
- package/dist-lib/skills.cjs +72201 -26208
- package/dist-lib/swift-compile.cjs +1007 -140
- package/dist-lib/swift-compile.mjs +1049 -174
- package/dist-lib/swift-package.cjs +12 -3
- package/dist-lib/swift-package.mjs +12 -3
- package/dist-lib/swift-submit.cjs +13 -5
- package/dist-lib/swift-submit.mjs +11 -5
- package/dist-lib/swift-toolchain.cjs +423 -33
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +8628 -5268
- package/dist-lib/vite.cjs +2859 -12
- package/dist-lib/worklet-executable.cjs +14 -2
- package/dist-lib/worklet-executable.mjs +14 -2
- package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
- package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +2 -1
- package/skills/peach-perf/SKILL.md +11 -5
- package/skills/peach-setup/SKILL.md +16 -19
- package/src/backend-origin.ts +13 -6
- package/src/bridge-constants.ts +11 -0
- package/src/bridge-contract.ts +22 -34
- package/src/config.ts +15 -0
- package/src/desktop-host-delivery.ts +5 -1
- package/src/dev-bundle-resolution.ts +2 -1
- package/src/expo-manifest-body.ts +13 -0
- package/src/expo-swift-host.ts +196 -49
- package/src/expo-swift-ready.ts +78 -0
- package/src/expoKotlinContract.ts +4 -7
- package/src/expoSwiftContract.ts +17 -6
- package/src/home-paths.ts +48 -2
- package/src/host/bridge-host.ts +5 -2
- package/src/host/staged-host-files.ts +9 -0
- package/src/native-app-identity-project.ts +27 -6
- package/src/native-app-identity.ts +91 -4
- package/src/native-firebase-config.ts +209 -13
- package/src/nativeAppIconContract.ts +1 -4
- package/src/output-lock-stdio.ts +36 -0
- package/src/process-group.ts +25 -2
- package/src/react-native-bridge-modules.ts +431 -0
- package/src/react-native-host-modules.ts +8 -1
- package/src/settings-bundle.ts +1 -4
- package/src/swift-binary-modules.ts +2 -2
- package/src/swift-compile.ts +918 -109
- package/src/swift-compiler-digest.ts +14 -0
- package/src/swift-distribution.lock.json +24 -12
- package/src/swift-distribution.ts +103 -4
- package/src/swift-host.ts +180 -55
- package/src/swift-icu.ts +27 -13
- package/src/swift-objc-compile.ts +355 -0
- package/src/swift-objc-sdk.ts +821 -27
- package/src/swift-objc-standin.ts +148 -14
- package/src/swift-package-manifest.ts +60 -7
- package/src/swift-package.ts +15 -1
- package/src/swift-packages.ts +154 -81
- package/src/swift-standin-emit.ts +96 -15
- package/src/swift-standins.ts +530 -116
- package/src/swift-submit.ts +13 -5
- package/src/swift-toolchain.ts +500 -42
- package/src/vite-plugin.ts +12 -1
- package/src/worker-system-chunks.ts +11 -12
- package/src/worklet-executable.ts +19 -1
- package/src/xcode-project.ts +270 -30
package/src/expo-swift-host.ts
CHANGED
|
@@ -9,6 +9,7 @@ import path from 'node:path'
|
|
|
9
9
|
import { promisify } from 'node:util'
|
|
10
10
|
import { RNX_FORKED_SCRIPT_ENV } from '../cli/internal-child'
|
|
11
11
|
import { isLoopbackHost } from './backend-origin'
|
|
12
|
+
import { collectBridgeModuleSources } from './react-native-bridge-modules'
|
|
12
13
|
import { swiftBinaryModules } from './swift-binary-modules'
|
|
13
14
|
import {
|
|
14
15
|
swiftAvailableModules,
|
|
@@ -24,7 +25,7 @@ import {
|
|
|
24
25
|
icuUrl,
|
|
25
26
|
projectFor,
|
|
26
27
|
serveSwiftPackageAsset,
|
|
27
|
-
|
|
28
|
+
sourceContentStamp,
|
|
28
29
|
standinLibraries,
|
|
29
30
|
SwiftMissingModulesError,
|
|
30
31
|
} from './swift-host'
|
|
@@ -48,6 +49,37 @@ const run = promisify(execFile)
|
|
|
48
49
|
const scriptEnv = { ...process.env, [RNX_FORKED_SCRIPT_ENV]: '1' }
|
|
49
50
|
export const EXPO_SWIFT_ROUTE = '/__peach/expo-swift'
|
|
50
51
|
export const EXPO_SWIFT_ENTRY = 'Sources/PeachExpoApp/PeachExpoEntry.swift'
|
|
52
|
+
// a connected caller (the conformance waiter) waits this long. the old 10
|
|
53
|
+
// minute cap aborted a compile that caller was still holding.
|
|
54
|
+
const EXPO_SWIFT_REQUEST_TIMEOUT_MS = 30 * 60_000
|
|
55
|
+
// bun drops an unanswered fetch at six minutes and the waiter retries. aborting
|
|
56
|
+
// in that gap restarts the compile. a reconnect inside this window rejoins it.
|
|
57
|
+
const EXPO_SWIFT_CLIENT_GAP_MS = 5_000
|
|
58
|
+
|
|
59
|
+
// the autolinker's command line changed across Expo releases: before 1.8.0 it
|
|
60
|
+
// takes the project from its working directory, before 1.10.3 the Apple
|
|
61
|
+
// platform is named `ios`.
|
|
62
|
+
function autolinkerAppleFlags(autolinkerManifest: string, appRoot: string) {
|
|
63
|
+
const manifest: unknown = JSON.parse(fs.readFileSync(autolinkerManifest, 'utf8'))
|
|
64
|
+
const version =
|
|
65
|
+
manifest && typeof manifest === 'object' && Reflect.get(manifest, 'version')
|
|
66
|
+
const parts =
|
|
67
|
+
typeof version === 'string'
|
|
68
|
+
? version.split('.').map((part) => Number.parseInt(part, 10))
|
|
69
|
+
: []
|
|
70
|
+
const [major, minor, patch] = parts
|
|
71
|
+
if (!Number.isInteger(major) || !Number.isInteger(minor) || !Number.isInteger(patch))
|
|
72
|
+
throw new Error(
|
|
73
|
+
`unreadable expo-modules-autolinking version in ${autolinkerManifest}`,
|
|
74
|
+
)
|
|
75
|
+
const atLeast = (m: number, n: number, p: number) =>
|
|
76
|
+
major !== m ? major > m : minor !== n ? minor > n : patch >= p
|
|
77
|
+
if (atLeast(1, 10, 3))
|
|
78
|
+
return { args: ['--platform', 'apple', '--project-root', appRoot], cwd: undefined }
|
|
79
|
+
if (atLeast(1, 8, 0))
|
|
80
|
+
return { args: ['--platform', 'ios', '--project-root', appRoot], cwd: undefined }
|
|
81
|
+
return { args: ['--platform', 'ios'], cwd: appRoot }
|
|
82
|
+
}
|
|
51
83
|
|
|
52
84
|
export async function collectExpoSwiftSources(
|
|
53
85
|
appRoot: string,
|
|
@@ -68,23 +100,25 @@ export async function collectExpoSwiftSources(
|
|
|
68
100
|
return null
|
|
69
101
|
throw error
|
|
70
102
|
}
|
|
103
|
+
// autolinkers before 1.0 predate app-owned Swift modules and `--project-root`.
|
|
104
|
+
const autolinkerVersion: unknown = JSON.parse(
|
|
105
|
+
fs.readFileSync(autolinker, 'utf8'),
|
|
106
|
+
).version
|
|
107
|
+
if (typeof autolinkerVersion === 'string' && Number.parseInt(autolinkerVersion, 10) < 1)
|
|
108
|
+
return null
|
|
109
|
+
const cli = path.join(path.dirname(autolinker), 'bin/expo-modules-autolinking.js')
|
|
110
|
+
const flags = autolinkerAppleFlags(autolinker, appRoot)
|
|
71
111
|
// both upstream commands discover their inputs independently. settle both
|
|
72
112
|
// before propagating errors so a failed request leaves no child running.
|
|
73
113
|
const [searched, resolvedModules] = await Promise.allSettled(
|
|
74
114
|
['search', 'resolve'].map((command) =>
|
|
75
|
-
run(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
'--project-root',
|
|
83
|
-
appRoot,
|
|
84
|
-
'--json',
|
|
85
|
-
],
|
|
86
|
-
{ timeout: 30_000, maxBuffer: 8 * 1024 * 1024, env: scriptEnv, signal },
|
|
87
|
-
),
|
|
115
|
+
run(process.execPath, [cli, command, ...flags.args, '--json'], {
|
|
116
|
+
timeout: 120_000,
|
|
117
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
118
|
+
env: scriptEnv,
|
|
119
|
+
cwd: flags.cwd,
|
|
120
|
+
signal,
|
|
121
|
+
}),
|
|
88
122
|
),
|
|
89
123
|
)
|
|
90
124
|
if (searched.status === 'rejected') throw searched.reason
|
|
@@ -102,7 +136,10 @@ export async function collectExpoSwiftSources(
|
|
|
102
136
|
let bytes = 0
|
|
103
137
|
let fileCount = 0
|
|
104
138
|
// native Expo resolves packages by name before generating its Apple provider.
|
|
105
|
-
|
|
139
|
+
// the search result is keyed by that name; older autolinkers add no name field.
|
|
140
|
+
for (const [packageName, entry] of Object.entries(search).sort(([a], [b]) =>
|
|
141
|
+
a.localeCompare(b),
|
|
142
|
+
)) {
|
|
106
143
|
if (!entry || typeof entry !== 'object')
|
|
107
144
|
throw new Error('invalid Expo autolinking entry')
|
|
108
145
|
const sourceRoot = Reflect.get(entry, 'path')
|
|
@@ -115,7 +152,7 @@ export async function collectExpoSwiftSources(
|
|
|
115
152
|
(candidate) =>
|
|
116
153
|
candidate &&
|
|
117
154
|
typeof candidate === 'object' &&
|
|
118
|
-
Reflect.get(candidate, 'packageName') ===
|
|
155
|
+
Reflect.get(candidate, 'packageName') === packageName,
|
|
119
156
|
)
|
|
120
157
|
const namespaces =
|
|
121
158
|
resolution && typeof resolution === 'object'
|
|
@@ -226,10 +263,11 @@ export async function collectExpoSwiftSources(
|
|
|
226
263
|
}
|
|
227
264
|
walk(ios)
|
|
228
265
|
for (const module of classes) {
|
|
229
|
-
const unit = {
|
|
266
|
+
const unit: ExpoSwiftSourceUnit = {
|
|
230
267
|
namespace,
|
|
231
268
|
moduleId: `${namespace}.${module.name}`,
|
|
232
269
|
classes: [module.registration],
|
|
270
|
+
bridgeModules: [],
|
|
233
271
|
files: swiftSourceClosure(files, [module.name]),
|
|
234
272
|
frameworks: [...frameworks],
|
|
235
273
|
}
|
|
@@ -282,7 +320,12 @@ export async function collectExpoSwiftSources(
|
|
|
282
320
|
return units
|
|
283
321
|
}
|
|
284
322
|
|
|
285
|
-
|
|
323
|
+
// an app target's Swift compiles in its own language mode; Expo packages in the default
|
|
324
|
+
function expoManifest(
|
|
325
|
+
files: Record<string, string>,
|
|
326
|
+
generation = '',
|
|
327
|
+
modes: Map<string, number> = new Map(),
|
|
328
|
+
): string {
|
|
286
329
|
const targets = new Map<string, string>()
|
|
287
330
|
for (const file of Object.keys(files)) {
|
|
288
331
|
const match = /^(Sources|__standins__)\/([^/]+)\//.exec(file)
|
|
@@ -292,11 +335,29 @@ function expoManifest(files: Record<string, string>, generation = ''): string {
|
|
|
292
335
|
throw new Error(`duplicate Expo Swift compile target ${match[2]}`)
|
|
293
336
|
targets.set(match[2]!, dir)
|
|
294
337
|
}
|
|
295
|
-
return `// swift-tools-version: 6.0\nimport PackageDescription\nlet package = Package(name: "PeachExpo", targets: [${[...targets].map(([name, dir]) => `.target(name: ${JSON.stringify(name)}, path: ${JSON.stringify(dir)}${dir.startsWith(`${SWIFT_STANDINS_DIR}/`) ? ', swiftSettings: [.swiftLanguageMode(.v5)]' : ''})`).join(', ')}, .executableTarget(name: "PeachExpoApp", path: "Sources/PeachExpoApp")])\n// stand-in inputs: ${generation}\n`
|
|
338
|
+
return `// swift-tools-version: 6.0\nimport PackageDescription\nlet package = Package(name: "PeachExpo", targets: [${[...targets].map(([name, dir]) => `.target(name: ${JSON.stringify(name)}, path: ${JSON.stringify(dir)}${dir.startsWith(`${SWIFT_STANDINS_DIR}/`) ? ', swiftSettings: [.swiftLanguageMode(.v5)]' : modes.has(name) ? `, swiftSettings: [.swiftLanguageMode(.v${modes.get(name)})]` : ''})`).join(', ')}, .executableTarget(name: "PeachExpoApp", path: "Sources/PeachExpoApp")])\n// stand-in inputs: ${generation}\n`
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function languageModes(units: ExpoSwiftSourceUnit[]): Map<string, number> {
|
|
342
|
+
return new Map(
|
|
343
|
+
units.flatMap((unit) =>
|
|
344
|
+
unit.swiftLanguageMode === undefined
|
|
345
|
+
? []
|
|
346
|
+
: [[unit.namespace, unit.swiftLanguageMode]],
|
|
347
|
+
),
|
|
348
|
+
)
|
|
296
349
|
}
|
|
297
350
|
|
|
298
351
|
function expoEntry(units: ExpoSwiftSourceUnit[]): string {
|
|
299
|
-
|
|
352
|
+
// an Objective-C bridge module registers with no Swift of its own
|
|
353
|
+
const namespaces = [...new Set(units.map((unit) => unit.namespace))].filter(
|
|
354
|
+
(namespace) =>
|
|
355
|
+
units.some((unit) =>
|
|
356
|
+
Object.keys(unit.files).some((file) => file.startsWith(`Sources/${namespace}/`)),
|
|
357
|
+
),
|
|
358
|
+
)
|
|
359
|
+
const bridgeModules = units.flatMap((unit) => unit.bridgeModules)
|
|
360
|
+
return `import SwiftUI\nimport ExpoModulesCore\n${bridgeModules.length ? 'import React\n' : ''}${namespaces.map((namespace) => `import ${namespace}`).join('\n')}\n\n@main struct PeachExpoApp: PeachPackage {\n init() { MainActor.assumeIsolated { PeachExpoRuntime.register([${units.flatMap((unit) => unit.classes).join(', ')}])${bridgeModules.length ? `; RCTModuleHost.register([${bridgeModules.join(', ')}])` : ''} } }\n func view(props: JSON) -> PeachExpoNativeView { PeachExpoNativeView(props: props) }\n}\n\n@_expose(wasm, "peach_expo_call")\n@_cdecl("peach_expo_call")\n@MainActor public func peachExpoCall(_ payload: UnsafePointer<UInt8>?, _ length: Int32) -> UnsafeMutablePointer<UInt8>? {\n PeachExpoRuntime.dispatch(payload, length)\n}\n`
|
|
300
361
|
}
|
|
301
362
|
|
|
302
363
|
const compiling = new Map<
|
|
@@ -307,12 +368,41 @@ const compiling = new Map<
|
|
|
307
368
|
clients: number
|
|
308
369
|
}
|
|
309
370
|
>()
|
|
371
|
+
// identical sources already produced a wasm that is still registered on its
|
|
372
|
+
// project. a second caller otherwise relinks namespaces whose failure records
|
|
373
|
+
// did not stick, and that link blows the flow's ready bound.
|
|
374
|
+
const preparedExpoSwift = new Map<string, ExpoSwiftCompilationResult>()
|
|
375
|
+
|
|
376
|
+
function expoSwiftPreparedKey(base: string, units: ExpoSwiftSourceUnit[]): string {
|
|
377
|
+
const hash = createHash('sha256')
|
|
378
|
+
hash.update(base)
|
|
379
|
+
for (const unit of units) {
|
|
380
|
+
hash.update('\0')
|
|
381
|
+
hash.update(unit.moduleId)
|
|
382
|
+
for (const name of Object.keys(unit.files).sort()) {
|
|
383
|
+
hash.update('\0')
|
|
384
|
+
hash.update(name)
|
|
385
|
+
hash.update('\0')
|
|
386
|
+
hash.update(unit.files[name] ?? '')
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return hash.digest('hex')
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// this process's copy of the app's expo sources. the build key covers the
|
|
393
|
+
// same directory, so another server compiling the app keeps its own tree.
|
|
394
|
+
function expoSwiftPackage(appKey: string, name: string): string {
|
|
395
|
+
return path.join(os.tmpdir(), 'peach-expo-swift', appKey, `p${process.pid}`, name)
|
|
396
|
+
}
|
|
397
|
+
|
|
310
398
|
class ExpoSwiftLinkRefusal extends Error {}
|
|
311
399
|
// a failed compiler result is valid only for the exact app and framework inputs.
|
|
312
|
-
// it is kept
|
|
313
|
-
// the namespace instead of relinking every namespace and
|
|
400
|
+
// it is kept at the app level, above each process's build copy, so a restarted
|
|
401
|
+
// dev server skips the namespace instead of relinking every namespace and
|
|
402
|
+
// isolating each again.
|
|
314
403
|
function failureRecordPath(packagePath: string) {
|
|
315
|
-
|
|
404
|
+
const app = path.dirname(path.dirname(packagePath))
|
|
405
|
+
return path.join(app, `${path.basename(packagePath)}.failed.json`)
|
|
316
406
|
}
|
|
317
407
|
|
|
318
408
|
function readFailedCompilation(packagePath: string) {
|
|
@@ -350,7 +440,9 @@ export async function compileExpoSwiftModules(
|
|
|
350
440
|
) => Promise<void>,
|
|
351
441
|
options: ExpoSwiftPreparationOptions = {},
|
|
352
442
|
): Promise<ExpoSwiftCompilationResult | null> {
|
|
353
|
-
|
|
443
|
+
// a cold isolation compiles the sdk once per module. one module ran past
|
|
444
|
+
// six minutes, and the readiness wait allows thirty, so the compile does too.
|
|
445
|
+
const timeoutMs = options.timeoutMs ?? 30 * 60_000
|
|
354
446
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647)
|
|
355
447
|
throw new Error('Expo Swift preparation timeout must be between 1 and 2147483647ms')
|
|
356
448
|
const controller = new AbortController()
|
|
@@ -367,9 +459,20 @@ export async function compileExpoSwiftModules(
|
|
|
367
459
|
try {
|
|
368
460
|
signal.throwIfAborted()
|
|
369
461
|
report(`discovering app-owned modules (budget ${timeoutMs}ms): ${root}`)
|
|
370
|
-
const
|
|
462
|
+
const expoUnits = await collectExpoSwiftSources(root, signal)
|
|
371
463
|
signal.throwIfAborted()
|
|
372
|
-
|
|
464
|
+
const bridge = collectBridgeModuleSources(fs.realpathSync(root), signal)
|
|
465
|
+
for (const unit of bridge?.units ?? []) {
|
|
466
|
+
if (expoUnits?.some((expo) => expo.namespace === unit.namespace))
|
|
467
|
+
throw new Error(
|
|
468
|
+
`app target ${unit.namespace} shares its Swift module name with a local Expo module`,
|
|
469
|
+
)
|
|
470
|
+
unit.files[EXPO_SWIFT_ENTRY] = expoEntry([unit])
|
|
471
|
+
unit.files['Package.swift'] = expoManifest(unit.files, '', languageModes([unit]))
|
|
472
|
+
}
|
|
473
|
+
const units = [...(expoUnits ?? []), ...(bridge?.units ?? [])]
|
|
474
|
+
const bridgeFailures = bridge?.failures ?? []
|
|
475
|
+
if (units.length === 0 && bridgeFailures.length === 0) return null
|
|
373
476
|
report(
|
|
374
477
|
`preparing ${units.length} modules: ${units.map((unit) => unit.moduleId).join(', ')}`,
|
|
375
478
|
)
|
|
@@ -380,9 +483,16 @@ export async function compileExpoSwiftModules(
|
|
|
380
483
|
if (!module && publishArtifact)
|
|
381
484
|
throw new Error('No local Swift toolchain or distribution is available')
|
|
382
485
|
const result: ExpoSwiftCompilationResult = {
|
|
383
|
-
namespaces:
|
|
486
|
+
namespaces: [
|
|
487
|
+
...units.map((unit) => unit.moduleId),
|
|
488
|
+
...bridgeFailures.map((failure) => failure.namespace),
|
|
489
|
+
],
|
|
490
|
+
bridgeModules: [
|
|
491
|
+
...(bridge?.units ?? []).map((unit) => unit.moduleId),
|
|
492
|
+
...bridgeFailures.map((failure) => failure.namespace),
|
|
493
|
+
],
|
|
384
494
|
modules: [],
|
|
385
|
-
failures: [],
|
|
495
|
+
failures: [...bridgeFailures],
|
|
386
496
|
}
|
|
387
497
|
if (!module) {
|
|
388
498
|
for (const unit of units)
|
|
@@ -475,10 +585,14 @@ export async function compileExpoSwiftModules(
|
|
|
475
585
|
unit,
|
|
476
586
|
[...built.gaps].map(([name, reason]) => `${name}: no stand-in: ${reason}`),
|
|
477
587
|
)
|
|
478
|
-
unit.files['Package.swift'] = expoManifest(
|
|
588
|
+
unit.files['Package.swift'] = expoManifest(
|
|
589
|
+
unit.files,
|
|
590
|
+
generation,
|
|
591
|
+
languageModes([unit]),
|
|
592
|
+
)
|
|
479
593
|
}
|
|
480
594
|
}
|
|
481
|
-
const packagePath =
|
|
595
|
+
const packagePath = expoSwiftPackage(key, unit.moduleId)
|
|
482
596
|
// preserve the compiler's input stamp across unchanged reloads.
|
|
483
597
|
// only this adapter's build copy is synchronized; app sources are read only.
|
|
484
598
|
syncSwiftTree(
|
|
@@ -491,7 +605,7 @@ export async function compileExpoSwiftModules(
|
|
|
491
605
|
),
|
|
492
606
|
)
|
|
493
607
|
projectFor(packagePath).libraries = libraries.get(unit)
|
|
494
|
-
const stamp =
|
|
608
|
+
const stamp = sourceContentStamp(packagePath, module.root)
|
|
495
609
|
const failed = readFailedCompilation(packagePath)
|
|
496
610
|
if (failed?.stamp === stamp) {
|
|
497
611
|
failures.set(unit.moduleId, failed.diagnostic)
|
|
@@ -501,6 +615,14 @@ export async function compileExpoSwiftModules(
|
|
|
501
615
|
stamps.set(unit, stamp)
|
|
502
616
|
candidates.push(unit)
|
|
503
617
|
}
|
|
618
|
+
const preparedKey = expoSwiftPreparedKey(base, units)
|
|
619
|
+
if (!publishArtifact) {
|
|
620
|
+
const prepared = preparedExpoSwift.get(preparedKey)
|
|
621
|
+
if (prepared) {
|
|
622
|
+
report('reusing prepared expo swift artifact')
|
|
623
|
+
return prepared
|
|
624
|
+
}
|
|
625
|
+
}
|
|
504
626
|
const link = async (linked: ExpoSwiftSourceUnit[]) => {
|
|
505
627
|
const files: Record<string, string> = {}
|
|
506
628
|
for (const unit of linked)
|
|
@@ -517,8 +639,9 @@ export async function compileExpoSwiftModules(
|
|
|
517
639
|
files['Package.swift'] = expoManifest(
|
|
518
640
|
files,
|
|
519
641
|
linked.map((unit) => generations.get(unit) ?? '').join(','),
|
|
642
|
+
languageModes(linked),
|
|
520
643
|
)
|
|
521
|
-
const packagePath =
|
|
644
|
+
const packagePath = expoSwiftPackage(key, 'linked-app')
|
|
522
645
|
syncSwiftTree(
|
|
523
646
|
packagePath,
|
|
524
647
|
new Map(
|
|
@@ -538,7 +661,7 @@ export async function compileExpoSwiftModules(
|
|
|
538
661
|
projectFor(packagePath).libraries =
|
|
539
662
|
linkedLibraries.size > 0 ? Object.fromEntries(linkedLibraries) : undefined
|
|
540
663
|
signal.throwIfAborted()
|
|
541
|
-
const stamp =
|
|
664
|
+
const stamp = sourceContentStamp(packagePath, module.root)
|
|
542
665
|
const refused = readFailedCompilation(packagePath)
|
|
543
666
|
if (refused?.stamp === stamp) {
|
|
544
667
|
report('reusing recent link refusal')
|
|
@@ -563,7 +686,7 @@ export async function compileExpoSwiftModules(
|
|
|
563
686
|
throw error
|
|
564
687
|
// admission and interrupted builds say nothing about individual modules.
|
|
565
688
|
// share their result briefly, including across dev server restarts.
|
|
566
|
-
if (
|
|
689
|
+
if (sourceContentStamp(packagePath, module.root) === stamp)
|
|
567
690
|
failureRecords.set(packagePath, {
|
|
568
691
|
stamp,
|
|
569
692
|
diagnostic,
|
|
@@ -587,12 +710,7 @@ export async function compileExpoSwiftModules(
|
|
|
587
710
|
report('linked build failed; isolating module diagnostics')
|
|
588
711
|
linked = []
|
|
589
712
|
for (const unit of candidates) {
|
|
590
|
-
const packagePath =
|
|
591
|
-
os.tmpdir(),
|
|
592
|
-
'peach-expo-swift',
|
|
593
|
-
key,
|
|
594
|
-
unit.moduleId,
|
|
595
|
-
)
|
|
713
|
+
const packagePath = expoSwiftPackage(key, unit.moduleId)
|
|
596
714
|
try {
|
|
597
715
|
signal.throwIfAborted()
|
|
598
716
|
report(`checking ${unit.moduleId}`)
|
|
@@ -630,13 +748,14 @@ export async function compileExpoSwiftModules(
|
|
|
630
748
|
)
|
|
631
749
|
throw error
|
|
632
750
|
// a source error or a module peach does not provide holds until the
|
|
633
|
-
// stamp changes
|
|
634
|
-
//
|
|
751
|
+
// stamp changes. the compiler also reports the same error at the
|
|
752
|
+
// build copy, so the app lines are enough to hold it. retrying
|
|
753
|
+
// either relinks every namespace and rewrites the linked package,
|
|
754
|
+
// so no reload would ever reuse its artifact.
|
|
635
755
|
const missing = error instanceof SwiftMissingModulesError
|
|
636
756
|
if (
|
|
637
|
-
(
|
|
638
|
-
|
|
639
|
-
sourceStamp(packagePath, module.root) === stamps.get(unit)
|
|
757
|
+
(missing || sourceErrors.length > 0) &&
|
|
758
|
+
sourceContentStamp(packagePath, module.root) === stamps.get(unit)
|
|
640
759
|
) {
|
|
641
760
|
failureRecords.set(packagePath, {
|
|
642
761
|
stamp: stamps.get(unit)!,
|
|
@@ -651,7 +770,29 @@ export async function compileExpoSwiftModules(
|
|
|
651
770
|
}
|
|
652
771
|
// every namespace compiles alone, so the link itself is what failed.
|
|
653
772
|
if (linked.length === candidates.length) throw linkError
|
|
654
|
-
if (linked.length)
|
|
773
|
+
if (linked.length) {
|
|
774
|
+
try {
|
|
775
|
+
artifact = await link(linked)
|
|
776
|
+
} catch (error) {
|
|
777
|
+
signal.throwIfAborted()
|
|
778
|
+
if (!(error instanceof SwiftMissingModulesError)) throw error
|
|
779
|
+
// the merged sources import a module none of the pieces needed
|
|
780
|
+
// alone. drop the pieces that import it and link the rest; a
|
|
781
|
+
// miss that none of them import is still a failed link.
|
|
782
|
+
const missing = new Set(
|
|
783
|
+
[...error.message.matchAll(/^ (\S+)/gm)].map((match) => match[1]),
|
|
784
|
+
)
|
|
785
|
+
const dropped = linked.filter((unit) =>
|
|
786
|
+
Object.values(unit.files).some((source) =>
|
|
787
|
+
swiftImportNames(source).some((name) => missing.has(name)),
|
|
788
|
+
),
|
|
789
|
+
)
|
|
790
|
+
if (dropped.length === 0) throw error
|
|
791
|
+
for (const unit of dropped) failures.set(unit.moduleId, error.message)
|
|
792
|
+
linked = linked.filter((unit) => !dropped.includes(unit))
|
|
793
|
+
if (linked.length) artifact = await link(linked)
|
|
794
|
+
}
|
|
795
|
+
}
|
|
655
796
|
}
|
|
656
797
|
}
|
|
657
798
|
}
|
|
@@ -677,6 +818,7 @@ export async function compileExpoSwiftModules(
|
|
|
677
818
|
report(
|
|
678
819
|
`ready: ${result.modules.length} artifact, ${result.failures.length} unavailable modules`,
|
|
679
820
|
)
|
|
821
|
+
if (!publishArtifact) preparedExpoSwift.set(preparedKey, result)
|
|
680
822
|
return result
|
|
681
823
|
} catch (error) {
|
|
682
824
|
const failure: unknown = signal.aborted ? signal.reason : error
|
|
@@ -746,6 +888,7 @@ export async function handleExpoSwiftRequest(
|
|
|
746
888
|
const controller = new AbortController()
|
|
747
889
|
const promise = compileExpoSwiftModules(key, `http://${host}`, undefined, {
|
|
748
890
|
signal: controller.signal,
|
|
891
|
+
timeoutMs: EXPO_SWIFT_REQUEST_TIMEOUT_MS,
|
|
749
892
|
}).finally(() => {
|
|
750
893
|
if (compiling.get(flightKey)?.controller === controller) compiling.delete(flightKey)
|
|
751
894
|
})
|
|
@@ -758,9 +901,13 @@ export async function handleExpoSwiftRequest(
|
|
|
758
901
|
const release = () => {
|
|
759
902
|
if (released) return
|
|
760
903
|
released = true
|
|
761
|
-
if (--current.clients
|
|
762
|
-
|
|
763
|
-
|
|
904
|
+
if (--current.clients > 0) return
|
|
905
|
+
const flight = current
|
|
906
|
+
setTimeout(() => {
|
|
907
|
+
if (flight.clients > 0 || flight.controller.signal.aborted) return
|
|
908
|
+
if (compiling.get(flightKey)?.controller !== flight.controller) return
|
|
909
|
+
flight.controller.abort(new Error('Expo Swift preparation client disconnected'))
|
|
910
|
+
}, EXPO_SWIFT_CLIENT_GAP_MS)
|
|
764
911
|
}
|
|
765
912
|
res.once('close', release)
|
|
766
913
|
try {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// an app with local Expo Swift modules only renders once the bridge compiles
|
|
2
|
+
// them, and a cold /__peach/expo-swift artifact compiles for minutes (a linked
|
|
3
|
+
// build plus per-namespace isolation builds in sequence) while a flow waits
|
|
4
|
+
// 120s for its first tree. so `peach open` GETs the same endpoint the engine
|
|
5
|
+
// will fetch and returns only once it answers; the bridge shares one compile
|
|
6
|
+
// per root, so the engine's own fetch then finds it done. a warm artifact
|
|
7
|
+
// answers in ~1s and an app without local modules answers null fast.
|
|
8
|
+
export const EXPO_SWIFT_READY_TIMEOUT_MS = 30 * 60_000
|
|
9
|
+
const EXPO_SWIFT_READY_PROGRESS_MS = 30_000
|
|
10
|
+
|
|
11
|
+
export type ExpoSwiftReadyOptions = {
|
|
12
|
+
timeoutMs?: number
|
|
13
|
+
progressMs?: number
|
|
14
|
+
signal?: AbortSignal
|
|
15
|
+
onProgress?: (elapsedMs: number) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function awaitExpoSwiftReady(
|
|
19
|
+
endpointUrl: string,
|
|
20
|
+
options: ExpoSwiftReadyOptions = {},
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
const timeoutMs = options.timeoutMs ?? EXPO_SWIFT_READY_TIMEOUT_MS
|
|
23
|
+
const progressMs = options.progressMs ?? EXPO_SWIFT_READY_PROGRESS_MS
|
|
24
|
+
const start = Date.now()
|
|
25
|
+
const report =
|
|
26
|
+
options.onProgress ??
|
|
27
|
+
((elapsedMs: number) => {
|
|
28
|
+
console.log(
|
|
29
|
+
` still waiting on ${endpointUrl} after ${Math.round(elapsedMs / 1000)}s — expo swift compile in progress`,
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
const progress = setInterval(() => report(Date.now() - start), progressMs)
|
|
33
|
+
try {
|
|
34
|
+
options.signal?.throwIfAborted()
|
|
35
|
+
const timeout = AbortSignal.timeout(timeoutMs)
|
|
36
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout
|
|
37
|
+
let res: Response | undefined
|
|
38
|
+
while (!res) {
|
|
39
|
+
try {
|
|
40
|
+
// bun ends an unanswered fetch at six minutes. that close is the last
|
|
41
|
+
// client, so the bridge aborts the shared compile and the retry starts
|
|
42
|
+
// over. hold the socket for the caller's bound instead.
|
|
43
|
+
const init: RequestInit = { signal }
|
|
44
|
+
Object.assign(init, { timeout: false })
|
|
45
|
+
res = await fetch(endpointUrl, init)
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (options.signal?.aborted) throw error
|
|
48
|
+
if (timeout.aborted) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`timed out after ${timeoutMs}ms waiting for ${endpointUrl} — the expo swift compile did not answer within the bound`,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
// a dropped socket still rejoins the same compile when the bridge has
|
|
54
|
+
// another client. retry only that case.
|
|
55
|
+
if (error instanceof Error && error.name === 'TimeoutError') continue
|
|
56
|
+
throw new Error(`could not reach ${endpointUrl}`, { cause: error })
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
const body = (await res.text()).trim().slice(0, 500)
|
|
61
|
+
throw new Error(`${endpointUrl} answered ${res.status}${body ? `: ${body}` : ''}`)
|
|
62
|
+
}
|
|
63
|
+
await res.text()
|
|
64
|
+
console.log(
|
|
65
|
+
` expo swift modules ready in ${((Date.now() - start) / 1000).toFixed(1)}s`,
|
|
66
|
+
)
|
|
67
|
+
} finally {
|
|
68
|
+
clearInterval(progress)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// the expoSwift endpoint a built shell url carries, or null for an app with no
|
|
73
|
+
// project root
|
|
74
|
+
export async function awaitStampedExpoSwiftReady(shellUrl: string): Promise<void> {
|
|
75
|
+
const endpoint = new URL(shellUrl).searchParams.get('expoSwift')
|
|
76
|
+
if (!endpoint) return
|
|
77
|
+
await awaitExpoSwiftReady(endpoint)
|
|
78
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { insertBundleGlobalFooter, readBundleGlobalFooter } from './bundleGlobalFooter'
|
|
2
|
+
import { isRecord } from './is-record'
|
|
2
3
|
|
|
3
4
|
// what the dev server answers for an app's local Expo Kotlin modules on an
|
|
4
5
|
// Android device: one linked Kotlin/Wasm process, or each module's diagnostic.
|
|
@@ -10,14 +11,10 @@ export interface ExpoKotlinCompilationResult {
|
|
|
10
11
|
failures: { namespace: string; diagnostic: string }[]
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
function record(value: unknown): value is Record<string, unknown> {
|
|
14
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
14
|
export function readExpoKotlinMetadata(value: unknown): ExpoKotlinCompilationResult {
|
|
18
15
|
const invalid = () => new Error('invalid Expo Kotlin compilation metadata')
|
|
19
16
|
if (
|
|
20
|
-
!
|
|
17
|
+
!isRecord(value) ||
|
|
21
18
|
!Array.isArray(value.namespaces) ||
|
|
22
19
|
!Array.isArray(value.failures)
|
|
23
20
|
)
|
|
@@ -30,7 +27,7 @@ export function readExpoKotlinMetadata(value: unknown): ExpoKotlinCompilationRes
|
|
|
30
27
|
const failures: ExpoKotlinCompilationResult['failures'] = []
|
|
31
28
|
for (const failure of value.failures) {
|
|
32
29
|
if (
|
|
33
|
-
!
|
|
30
|
+
!isRecord(failure) ||
|
|
34
31
|
typeof failure.namespace !== 'string' ||
|
|
35
32
|
typeof failure.diagnostic !== 'string'
|
|
36
33
|
)
|
|
@@ -40,7 +37,7 @@ export function readExpoKotlinMetadata(value: unknown): ExpoKotlinCompilationRes
|
|
|
40
37
|
const module = value.module
|
|
41
38
|
if (module === null) return { namespaces, module: null, failures }
|
|
42
39
|
if (
|
|
43
|
-
!
|
|
40
|
+
!isRecord(module) ||
|
|
44
41
|
typeof module.url !== 'string' ||
|
|
45
42
|
typeof module.hash !== 'string' ||
|
|
46
43
|
typeof module.compose !== 'boolean'
|
package/src/expoSwiftContract.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { insertBundleGlobalFooter, readBundleGlobalFooter } from './bundleGlobalFooter'
|
|
2
|
+
import { isRecord } from './is-record'
|
|
2
3
|
|
|
3
4
|
export interface ExpoSwiftPreparationOptions {
|
|
4
5
|
signal?: AbortSignal
|
|
@@ -9,8 +10,12 @@ export interface ExpoSwiftSourceUnit {
|
|
|
9
10
|
namespace: string
|
|
10
11
|
moduleId: string
|
|
11
12
|
classes: string[]
|
|
13
|
+
// legacy bridge module registrations (react-native-bridge-modules.ts)
|
|
14
|
+
bridgeModules: string[]
|
|
12
15
|
files: Record<string, string>
|
|
13
16
|
frameworks: string[]
|
|
17
|
+
// the app target's Swift language mode; Expo packages compile in the default
|
|
18
|
+
swiftLanguageMode?: number
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
export interface ExpoSwiftCompiledModule {
|
|
@@ -23,13 +28,12 @@ export interface ExpoSwiftCompiledModule {
|
|
|
23
28
|
|
|
24
29
|
export interface ExpoSwiftCompilationResult {
|
|
25
30
|
namespaces: string[]
|
|
31
|
+
// the namespaces that are legacy bridge modules rather than Expo modules
|
|
32
|
+
bridgeModules?: string[]
|
|
26
33
|
modules: ExpoSwiftCompiledModule[]
|
|
27
34
|
failures: { namespace: string; diagnostic: string }[]
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
function record(value: unknown): value is Record<string, unknown> {
|
|
31
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
32
|
-
}
|
|
33
37
|
function strings(value: unknown): value is string[] {
|
|
34
38
|
return Array.isArray(value) && value.every((item) => typeof item === 'string')
|
|
35
39
|
}
|
|
@@ -38,7 +42,7 @@ export function isExpoSwiftCompilationResult(
|
|
|
38
42
|
value: unknown,
|
|
39
43
|
): value is ExpoSwiftCompilationResult {
|
|
40
44
|
if (
|
|
41
|
-
!
|
|
45
|
+
!isRecord(value) ||
|
|
42
46
|
!strings(value.namespaces) ||
|
|
43
47
|
!Array.isArray(value.modules) ||
|
|
44
48
|
value.modules.length > 1 ||
|
|
@@ -47,9 +51,16 @@ export function isExpoSwiftCompilationResult(
|
|
|
47
51
|
return false
|
|
48
52
|
const remaining = new Set(value.namespaces)
|
|
49
53
|
if (remaining.size !== value.namespaces.length) return false
|
|
54
|
+
// bundles published before legacy bridge modules carry no list
|
|
55
|
+
if (
|
|
56
|
+
value.bridgeModules !== undefined &&
|
|
57
|
+
(!strings(value.bridgeModules) ||
|
|
58
|
+
!value.bridgeModules.every((name) => remaining.has(name)))
|
|
59
|
+
)
|
|
60
|
+
return false
|
|
50
61
|
for (const artifact of value.modules) {
|
|
51
62
|
if (
|
|
52
|
-
!
|
|
63
|
+
!isRecord(artifact) ||
|
|
53
64
|
!strings(artifact.namespaces) ||
|
|
54
65
|
artifact.namespaces.length === 0 ||
|
|
55
66
|
!artifact.namespaces.every((namespace) => remaining.delete(namespace)) ||
|
|
@@ -62,7 +73,7 @@ export function isExpoSwiftCompilationResult(
|
|
|
62
73
|
}
|
|
63
74
|
for (const failure of value.failures) {
|
|
64
75
|
if (
|
|
65
|
-
!
|
|
76
|
+
!isRecord(failure) ||
|
|
66
77
|
typeof failure.namespace !== 'string' ||
|
|
67
78
|
!remaining.delete(failure.namespace) ||
|
|
68
79
|
typeof failure.diagnostic !== 'string'
|