Skip to content

Commit 4ae4546

Browse files
authored
Merge pull request #357 from knockout/fix/tests-import-retry
fix(tests): modulepreload chunks in <head>, richer import error
2 parents 4e20972 + 31e3528 commit 4ae4546

4 files changed

Lines changed: 51 additions & 10 deletions

File tree

tko.io/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
"postinstall": "patch-package",
88
"prebuild": "bun ./scripts/generate-verified-behaviors.mjs && mkdir -p public/lib && cd ../builds/knockout && bun run build && cp dist/browser.min.js ../../tko.io/public/lib/ko.js && cd ../reference && bun run build && cp dist/browser.min.js ../../tko.io/public/lib/tko.js && cd ../../tko.io && bun ./scripts/bundle-tests.mjs",
99
"predev": "bun run prebuild",
10-
"dev": "ASTRO_TELEMETRY_DISABLED=1 astro dev",
11-
"build": "ASTRO_TELEMETRY_DISABLED=1 astro build --force",
12-
"preview": "ASTRO_TELEMETRY_DISABLED=1 astro preview",
13-
"check": "ASTRO_TELEMETRY_DISABLED=1 astro check"
10+
"dev": "ASTRO_TELEMETRY_DISABLED=1 bun --bun astro dev",
11+
"build": "ASTRO_TELEMETRY_DISABLED=1 bun --bun astro build --force",
12+
"preview": "ASTRO_TELEMETRY_DISABLED=1 bun --bun astro preview",
13+
"check": "ASTRO_TELEMETRY_DISABLED=1 bun --bun astro check"
1414
},
1515
"keywords": ["tko", "knockout", "documentation"],
1616
"author": "",

tko.io/public/tests-frame.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,9 @@
8080
runner.on('end', () => parent.postMessage({ type: 'end', slug, stats: runner.stats }, '*'))
8181
parent.postMessage({ type: 'start', slug, total: runner.total }, '*')
8282
} catch (err) {
83-
document.getElementById('err').textContent = 'import failed: ' + (err?.message || err)
84-
parent.postMessage({ type: 'import-error', slug, err: err?.message || String(err) }, '*')
83+
const msg = err?.message || String(err)
84+
document.getElementById('err').textContent = 'import failed: ' + msg
85+
parent.postMessage({ type: 'import-error', slug, err: msg }, '*')
8586
}
8687
}
8788
</script>

tko.io/scripts/bundle-tests.mjs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,14 @@ async function buildSourceBundles({ buildVersion, tsconfig, alias }) {
256256
const specs = await collectSpecs(scope)
257257
if (specs.length === 0) throw new Error('source-bundle scope matched no specs')
258258

259+
// Clean previous chunk-*.js emissions so removed specs / shifted
260+
// dependency graphs don't leak stale hashed chunks into the new
261+
// manifest. Wrappers + per-spec bundles regenerate from fresh
262+
// entry points, so leaving them alone is fine.
259263
await fs.mkdir(sourceOutputDir, { recursive: true })
264+
for await (const name of new Bun.Glob('chunk-*.js').scan({ cwd: sourceOutputDir })) {
265+
await fs.unlink(path.join(sourceOutputDir, name))
266+
}
260267
const { entryPoints, manifest } = await writeSpecWrappers(specs)
261268

262269
await esbuild.build({
@@ -275,9 +282,21 @@ async function buildSourceBundles({ buildVersion, tsconfig, alias }) {
275282
logLevel: 'warning'
276283
})
277284

285+
// Enumerate the shared chunk files esbuild just emitted. The
286+
// /tests page preloads these via <link rel="modulepreload"> in
287+
// <head> so the browser HTTP cache is warm before iframes race
288+
// to dynamic-import them — otherwise WebKit occasionally reports
289+
// the opaque "Importing a module script failed." error. See
290+
// tko.io/src/pages/tests.astro.
291+
const chunks = []
292+
for await (const name of new Bun.Glob('chunk-*.js').scan({ cwd: sourceOutputDir })) {
293+
chunks.push(name)
294+
}
295+
chunks.sort()
296+
278297
await fs.writeFile(
279298
path.join(sourceOutputDir, 'manifest.json'),
280-
JSON.stringify({ specs: manifest }, null, 2),
299+
JSON.stringify({ specs: manifest, chunks }, null, 2),
281300
'utf8'
282301
)
283302

tko.io/src/pages/tests.astro

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
---
2+
import { fileURLToPath } from 'node:url'
3+
4+
// Read the source-mode chunk list at SSR time so the HTML ships
5+
// with <link rel="modulepreload"> for every shared chunk already
6+
// in <head>. The browser starts warming the HTTP cache during
7+
// initial parse — well before any JS runs — which side-steps the
8+
// concurrent-fetch race that makes WebKit occasionally fail spec
9+
// iframes with the opaque "Importing a module script failed."
10+
// error. `prebuild` always runs bundle-tests.mjs before Astro
11+
// builds the site, so manifest.json is guaranteed to exist.
12+
const manifestPath = fileURLToPath(new URL('../../public/tests/source/manifest.json', import.meta.url))
13+
const sourceChunks: string[] = (await Bun.file(manifestPath).json()).chunks ?? []
14+
215
// TKO Browser Test Runner — written in TKO itself.
316
//
417
// Two modes:
@@ -32,6 +45,8 @@
3245
<title>TKO · Browser Tests</title>
3346
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Lobster&display=swap" />
3447
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/mocha@10/mocha.css" />
48+
{sourceChunks.map(name => <link rel="modulepreload" href={`/tests/source/${name}`} />)}
49+
<link rel="preload" as="script" href="/tests/source/setup.js" />
3550
<style>
3651
:root {
3752
--bg: #0b0d11;
@@ -362,7 +377,7 @@
362377
self.pkg = ko.observable(safePkg)
363378
self.ver = ko.observable(safeVer)
364379
self.grep = ko.observable(qs.get('grep') || '')
365-
// Pool for the parallel (non-focus) phase. Focus-needing
380+
// Pool for the parallel-hidden run. Focus-needing
366381
// specs — flagged at bundle time via `manifest.specs[].needsFocus`
367382
// — always run serially in the visible #workarea so each
368383
// has sole system focus; Chromium only grants
@@ -520,6 +535,7 @@
520535
const res = await fetch('/tests/source/manifest.json')
521536
const manifest = await res.json()
522537
let specs = manifest.specs
538+
523539
const grep = page.grep().trim()
524540
if (grep) {
525541
// Invalid regex patterns (e.g. bare `(`) throw from the
@@ -632,7 +648,11 @@
632648
})
633649
}
634650

635-
// Phase 1: parallel hidden specs.
651+
// Parallel-hidden run: non-focus specs, `pool`-at-a-time
652+
// inside the offscreen `hiddenHost`. The <link rel=modulepreload>
653+
// tags in <head> warmed the HTTP cache, so iframes can
654+
// race on dynamic import() without tripping WebKit's
655+
// dependency-tree fetcher.
636656
async function hiddenWorker() {
637657
while (hiddenQueue.length) {
638658
const spec = hiddenQueue.shift()
@@ -642,7 +662,8 @@
642662
}
643663
await Promise.all(Array.from({ length: page.pool }, hiddenWorker))
644664

645-
// Phase 2: serial focus specs in the visible workarea.
665+
// Serial-focus run: one focus-needing spec at a time in
666+
// the visible #workarea so each has sole system focus.
646667
for (const spec of focusQueue) {
647668
await runOne(spec, { host: workarea, label: workareaLabel, grantFocus: true })
648669
}

0 commit comments

Comments
 (0)