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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/1050-the-shell-s-27-kb-of-javascript-is-a-cac → main 3 commits fork 3 threads · 3 unresolved

  • Merged. scout/1050-the-shell-s-27-kb-of-javascript-is-a-cac was merged into main as 6cc5501.

Review

Sign in to review this change.
5 files changed +73 −11
src/http/server.ts +3
@@ -61,6 +61,7 @@ import { sha256 } from "../kernel/hash.ts";
6161 import {
6262 assetResponse,
6363 highlightAsset,
64 shellAsset,
6465 stylesheetAsset,
6566 stylesheetRedirect,
6667 stylesheetResponse,
@@ -521,6 +522,8 @@ export function buildRouter(): Router<Handler> {
521522 // than a redirect: the page that asked for it names the hash it was built
522523 // with, so a miss means an old tab, and an old tab should reload.
523524 router.add("GET", "/assets/:name.js", async (ctx) => {
525 const shell = shellAsset();
526 if (ctx.params.name === shell.name) return assetResponse(shell);
524527 const asset = await highlightAsset();
525528 if (asset === null || ctx.params.name !== asset.name) return notFound();
526529 return assetResponse(asset);
src/web/asset.ts +17
@@ -18,6 +18,7 @@
1818
1919 import { sha256 } from "../kernel/hash.ts";
2020 import { createLog } from "../kernel/log.ts";
21 import { shellJs } from "./client.ts";
2122 import { css } from "./style.ts";
2223
2324 const log = createLog("web");
@@ -111,3 +112,19 @@ export function stylesheetRedirect(): Response {
111112 },
112113 });
113114 }
115
116 let shell: BuiltAsset | null = null;

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.

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 {
2424 THEMES,
2525 type ThemeName,
2626 } from "../design/themes.ts";
27 import { stylesheetAsset } from "./asset.ts";
27 import { shellAsset, stylesheetAsset } from "./asset.ts";
2828 import { linkHeader, pageLinks } from "./standards.ts";
29 import { shellJs } from "./client.ts";
3029 import {
3130 clearFlashCookie,
3231 flashCookie,
@@ -528,9 +527,6 @@ export function page(options: PageOptions, status = 200): Response {
528527 const theme = ctx.theme ?? options.theme;
529528 const flash = readFlash(ctx.request);
530529 const links = pageLinks(ctx);
531 const scriptJs = options.scriptJs
532 ? `${shellJs}\n${options.scriptJs}`
533 : shellJs;
534530
535531 // The body's markup, built before the document so the icons it used can be
536532 // counted: every `icon()` left a `<use>` behind, and the sprite defining
@@ -632,7 +628,10 @@ export function page(options: PageOptions, status = 200): Response {
632628 </head>
633629 <body>
634630 ${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}
636635 </body>
637636 </html>`;
638637
test/web-browse.test.ts +1 −1
@@ -505,7 +505,7 @@ describe("browsing an origin repository on the web", () => {
505505 );
506506 expect(running).toContain("new EventSource(url)");
507507 expect(running).toMatch(/<script nonce="[^"]+">/);
508 expect(running).not.toMatch(/<script[^>]*src=/);
508 expect(running).not.toMatch(/<script[^>]*src="(?!\/assets\/shell-)/);
509509
510510 const done = await settled(jobPath.split("/").pop()!);
511511 expect(done?.stage).toBe("done");
test/web.test.ts +47 −4
@@ -5,7 +5,12 @@ import { join } from "node:path";
55 import { createApp, type App } from "../src/app.ts";
66 import { health } from "../src/http/health.ts";
77 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";
914 import { escapeHtml, html, raw, when } from "../src/web/html.ts";
1015 import {
1116 icon,
@@ -371,6 +376,43 @@ describe("web pages end to end", () => {
371376 expect(text).toContain("New repository");
372377 });
373378
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
374416 test("the command palette is a proper dialog: modal, a combobox over a listbox, its index as data", async () => {
375417 const text = await (await fetch(base, { headers: { cookie } })).text();
376418 expect(text).toMatch(
@@ -383,9 +425,10 @@ describe("web pages end to end", () => {
383425 expect(text).toContain(
384426 '<script type="application/json" id="palette-index">',
385427 );
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(");
389432 });
390433
391434 test("stylesheet is content-addressed and immutable, at the path the page names", async () => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs