Public: anyone can read this; the crew can change it.
The shell's 27 KB of JavaScript is a cached file, not re-sent with every page #1224
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/1050-the-shell-s-27-kb-of-javascript-is-a-cac → main 3 commits fork 3 threads · 3 unresolved
5 files
changed +73
−11
src/http/server.ts +3
| @@ -61,6 +61,7 @@ import { sha256 } from "../kernel/hash.ts"; | ||
| 61 | 61 | import { |
| 62 | 62 | assetResponse, |
| 63 | 63 | highlightAsset, |
| 64 | shellAsset, | |
| 64 | 65 | stylesheetAsset, |
| 65 | 66 | stylesheetRedirect, |
| 66 | 67 | stylesheetResponse, |
| @@ -521,6 +522,8 @@ export function buildRouter(): Router<Handler> { | ||
| 521 | 522 | // than a redirect: the page that asked for it names the hash it was built |
| 522 | 523 | // with, so a miss means an old tab, and an old tab should reload. |
| 523 | 524 | router.add("GET", "/assets/:name.js", async (ctx) => { |
| 525 | const shell = shellAsset(); | |
| 526 | if (ctx.params.name === shell.name) return assetResponse(shell); | |
| 524 | 527 | const asset = await highlightAsset(); |
| 525 | 528 | if (asset === null || ctx.params.name !== asset.name) return notFound(); |
| 526 | 529 | return assetResponse(asset); |
src/web/asset.ts +17
| @@ -18,6 +18,7 @@ | ||
| 18 | 18 | |
| 19 | 19 | import { sha256 } from "../kernel/hash.ts"; |
| 20 | 20 | import { createLog } from "../kernel/log.ts"; |
| 21 | import { shellJs } from "./client.ts"; | |
| 21 | 22 | import { css } from "./style.ts"; |
| 22 | 23 | |
| 23 | 24 | const log = createLog("web"); |
| @@ -111,3 +112,19 @@ export function stylesheetRedirect(): Response { | ||
| 111 | 112 | }, |
| 112 | 113 | }); |
| 113 | 114 | } |
| 115 | ||
| 116 | let shell: BuiltAsset | null = null; | |
| 117 | ||
| 118 | /** | |
| 119 | * The shell script, content-addressed like the stylesheet. Hashed once per | |
| 120 | * process. Linked as a classic (blocking) script ahead of the page's own | |
| 121 | * inline one, so everything it defines is in scope when that runs, exactly as | |
| 122 | * when the two were one inline block. | |
| 123 | */ | |
| 124 | export function shellAsset(): BuiltAsset { | |
| 125 | if (shell === null) { | |
| 126 | const name = `shell-${sha256(shellJs).slice(0, 16)}`; | |
| 127 | shell = { path: `/assets/${name}.js`, name, code: shellJs }; | |
| 128 | } | |
| 129 | return shell; | |
| 130 | } |
src/web/layout.ts +5 −6
| @@ -24,9 +24,8 @@ import { | ||
| 24 | 24 | THEMES, |
| 25 | 25 | type ThemeName, |
| 26 | 26 | } from "../design/themes.ts"; |
| 27 | import { stylesheetAsset } from "./asset.ts"; | |
| 27 | import { shellAsset, stylesheetAsset } from "./asset.ts"; | |
| 28 | 28 | import { linkHeader, pageLinks } from "./standards.ts"; |
| 29 | import { shellJs } from "./client.ts"; | |
| 30 | 29 | import { |
| 31 | 30 | clearFlashCookie, |
| 32 | 31 | flashCookie, |
| @@ -528,9 +527,6 @@ export function page(options: PageOptions, status = 200): Response { | ||
| 528 | 527 | const theme = ctx.theme ?? options.theme; |
| 529 | 528 | const flash = readFlash(ctx.request); |
| 530 | 529 | const links = pageLinks(ctx); |
| 531 | const scriptJs = options.scriptJs | |
| 532 | ? `${shellJs}\n${options.scriptJs}` | |
| 533 | : shellJs; | |
| 534 | 530 | |
| 535 | 531 | // The body's markup, built before the document so the icons it used can be |
| 536 | 532 | // counted: every `icon()` left a `<use>` behind, and the sprite defining |
| @@ -632,7 +628,10 @@ export function page(options: PageOptions, status = 200): Response { | ||
| 632 | 628 | </head> |
| 633 | 629 | <body> |
| 634 | 630 | ${shell}${iconSprite(shell.text)} |
| 635 | ${raw(`<script nonce="${nonce}">${scriptJs}</script>`)} | |
| 631 | <script nonce="${nonce}" src="${shellAsset().path}"></script> | |
| 632 | ${options.scriptJs | |
| 633 | ? raw(`<script nonce="${nonce}">${options.scriptJs}</script>`) | |
| 634 | : null} | |
| 636 | 635 | </body> |
| 637 | 636 | </html>`; |
| 638 | 637 |
test/web-browse.test.ts +1 −1
| @@ -505,7 +505,7 @@ describe("browsing an origin repository on the web", () => { | ||
| 505 | 505 | ); |
| 506 | 506 | expect(running).toContain("new EventSource(url)"); |
| 507 | 507 | expect(running).toMatch(/<script nonce="[^"]+">/); |
| 508 | expect(running).not.toMatch(/<script[^>]*src=/); | |
| 508 | expect(running).not.toMatch(/<script[^>]*src="(?!\/assets\/shell-)/); | |
| 509 | 509 | |
| 510 | 510 | const done = await settled(jobPath.split("/").pop()!); |
| 511 | 511 | expect(done?.stage).toBe("done"); |
test/web.test.ts +47 −4
| @@ -5,7 +5,12 @@ import { join } from "node:path"; | ||
| 5 | 5 | import { createApp, type App } from "../src/app.ts"; |
| 6 | 6 | import { health } from "../src/http/health.ts"; |
| 7 | 7 | import { serve, type Servers } from "../src/http/server.ts"; |
| 8 | import { stylesheetAsset, stylesheetRedirect } from "../src/web/asset.ts"; | |
| 8 | import { | |
| 9 | shellAsset, | |
| 10 | stylesheetAsset, | |
| 11 | stylesheetRedirect, | |
| 12 | } from "../src/web/asset.ts"; | |
| 13 | import { shellJs } from "../src/web/client.ts"; | |
| 9 | 14 | import { escapeHtml, html, raw, when } from "../src/web/html.ts"; |
| 10 | 15 | import { |
| 11 | 16 | icon, |
| @@ -371,6 +376,43 @@ describe("web pages end to end", () => { | ||
| 371 | 376 | expect(text).toContain("New repository"); |
| 372 | 377 | }); |
| 373 | 378 | |
| 379 | test("the shell is a cached file, linked before the page's own inline script", async () => { | |
| 380 | const shell = shellAsset(); | |
| 381 | const home = await (await fetch(base, { headers: { cookie } })).text(); | |
| 382 | expect(home).toMatch( | |
| 383 | new RegExp(`<script nonce="[^"]+" src="${shell.path}"></script>`), | |
| 384 | ); | |
| 385 | // Its source is no longer in the HTML, and the page is a good deal lighter. | |
| 386 | expect(home).not.toContain(shellJs.slice(0, 400)); | |
| 387 | expect(home).not.toContain(shellJs.slice(-400)); | |
| 388 | ||
| 389 | // A page with its own script: the shell is a classic script placed first, | |
| 390 | // so it has run by the time the nonce'd inline one does. | |
| 391 | const apps = await ( | |
| 392 | await fetch(`${base}/apps/new`, { headers: { cookie } }) | |
| 393 | ).text(); | |
| 394 | const at = apps.indexOf(`src="${shell.path}">`); | |
| 395 | const inline = apps.indexOf("<script nonce=", at); | |
| 396 | expect(at).toBeGreaterThan(-1); | |
| 397 | expect(inline).toBeGreaterThan(at); | |
| 398 | expect(apps).not.toContain(shellJs.slice(0, 400)); | |
| 399 | ||
| 400 | const file = await fetch(base + shell.path); | |
| 401 | expect(file.status).toBe(200); | |
| 402 | expect(file.headers.get("cache-control")).toBe( | |
| 403 | "public, max-age=31536000, immutable", | |
| 404 | ); | |
| 405 | expect(await file.text()).toBe(shellJs); | |
| 406 | expect( | |
| 407 | (await fetch(`${base}/assets/shell-0000000000000000.js`)).status, | |
| 408 | ).toBe(404); | |
| 409 | ||
| 410 | const policy = (await fetch(base, { headers: { cookie } })).headers.get( | |
| 411 | "content-security-policy", | |
| 412 | ); | |
| 413 | expect(policy).not.toContain("unsafe-inline"); | |
| 414 | }); | |
| 415 | ||
| 374 | 416 | test("the command palette is a proper dialog: modal, a combobox over a listbox, its index as data", async () => { |
| 375 | 417 | const text = await (await fetch(base, { headers: { cookie } })).text(); |
| 376 | 418 | expect(text).toMatch( |
| @@ -383,9 +425,10 @@ describe("web pages end to end", () => { | ||
| 383 | 425 | expect(text).toContain( |
| 384 | 426 | '<script type="application/json" id="palette-index">', |
| 385 | 427 | ); |
| 386 | // The script carries the scorer and consumes Escape itself. | |
| 387 | expect(text).toContain("function fuzzyScore("); | |
| 388 | expect(text).toContain("function openModal("); | |
| 428 | // The shell file carries the scorer and consumes Escape itself. | |
| 429 | const file = await (await fetch(base + shellAsset().path)).text(); | |
| 430 | expect(file).toContain("function fuzzyScore("); | |
| 431 | expect(file).toContain("function openModal("); | |
| 389 | 432 | }); |
| 390 | 433 | |
| 391 | 434 | test("stylesheet is content-addressed and immutable, at the path the page names", async () => { |
new above
offline
Nit: the module doc at the top of this file still says the highlighter is the only script shipped as a file and that everything else is a nonce'd inline script. That's no longer true now that the shell lives here. One sentence would fix it.