Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/public/catalog/blocks/code-slice-hero.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/public/catalog/blocks/cuboid-carousel.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/public/catalog/blocks/glass-shard-title.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/public/catalog/blocks/orbit-card.json

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion docs/public/catalog/vendor/BufferGeometryUtils.json

This file was deleted.

1 change: 0 additions & 1 deletion docs/public/catalog/vendor/RoomEnvironment.json

This file was deleted.

1 change: 1 addition & 0 deletions docs/public/catalog/vendor/three-modules.json

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion docs/public/catalog/vendor/three.core.min.json

This file was deleted.

1 change: 0 additions & 1 deletion docs/public/catalog/vendor/three.module.min.json

This file was deleted.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"@hyperframes/player": "workspace:*",
"@types/node": "^25.0.10",
"concurrently": "^8.2.0",
"esbuild": "^0.25.12",
"fallow": "^2.75.0",
"happy-dom": "^20.9.0",
"knip": "^6.0.3",
Expand Down
94 changes: 71 additions & 23 deletions scripts/catalog-script-inlining.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { runInNewContext } from "node:vm";
import { inlineCatalogScripts, withHostedRefs } from "./catalog-script-inlining.ts";

const CDN = "https://static.example.com/registry-assets";
Expand Down Expand Up @@ -75,6 +76,22 @@ describe("glass-shard-title payload", () => {
});
});

it("preserves script delimiters as data without exposing them to the HTML parser", () => {
withGlassMain((dir) => {
const value = "</script></ScRiPt><!--<script>\u2028\u2029";
writeFileSync(
join(dir, "assets/glass-main.js"),
`globalThis.payload = ${JSON.stringify(value)};`,
);
const out = inlineCatalogScripts("glass-shard-title", scriptTag, dir, {});
const body = out.slice("<script>".length, -"</script>".length);
assert.doesNotMatch(body, /[<\u2028\u2029]/);
const context: { payload?: string } = {};
runInNewContext(body, context);
assert.equal(context.payload, value);
});
});

it("refuses a payload whose hdr link still points at the local file", () => {
withGlassMain((dir) => {
for (const link of [
Expand All @@ -90,16 +107,53 @@ describe("glass-shard-title payload", () => {
});
});

describe("module blocks' catalog import map", () => {
const vendorUrls = Object.fromEntries(
[
"gsap-3.14.2.min",
"three.core.min",
"three.module.min",
"RoomEnvironment",
"BufferGeometryUtils",
].map((k) => [k, `https://cdn.example/${k}.js`]),
const vendorUrls = Object.fromEntries(
["gsap-3.14.2.min", "three-modules"].map((k) => [k, `https://cdn.example/${k}.js`]),
);
/** How a loader turns fetched code into a script URL; library code may build blob URLs for its own data. */
const BLOB_SCRIPT = /\{type:"text\/javascript"\}/;
const INLINED_BLOCKS = [
"code-slice-hero",
"cuboid-carousel",
"frost-sequence-camera-orbit",
"orbit-card",
];

function inlinedBlock(name: string): string {
const dir = join("registry/blocks", name);
return inlineCatalogScripts(
name,
readFileSync(join(dir, `${name}.html`), "utf-8"),
dir,
vendorUrls,
);
}

// The docs host's CSP allows inline, eval'd and https: scripts but no blob: script, so a
// preview whose libraries load from a blob: URL renders its empty base (code-slice-hero, 2026-10-05).
describe("catalog payloads under the docs host's script policy", () => {
for (const name of INLINED_BLOCKS) {
it(`${name}: loads no script from a blob: URL`, () => {
assert.doesNotMatch(inlinedBlock(name), BLOB_SCRIPT);
});
}

it("no committed payload loads a script from a blob: URL", () => {
const root = "docs/public/catalog";
const payloads = readdirSync(root, { recursive: true, encoding: "utf-8" }).filter((f) =>
/^(blocks|components)\/[^/]+\.json$/.test(f),
);
assert.ok(payloads.length > 100);
const html = (f: string) =>
(JSON.parse(readFileSync(join(root, f), "utf-8")) as { html: string }).html;
assert.deepEqual(
payloads.filter((f) => BLOB_SCRIPT.test(html(f))),
[],
);
});
});

describe("module blocks' shared catalog bundle", () => {
it("frost: runs the script that drives frost after frost.js, not an earlier inline script", () => {
const name = "frost-sequence-camera-orbit";
const dir = join("registry/blocks", name);
Expand All @@ -116,19 +170,13 @@ describe("module blocks' catalog import map", () => {
});

for (const name of ["cuboid-carousel", "orbit-card"]) {
it(`${name}: maps every specifier its entry module imports, with no import map of its own left`, () => {
const dir = join("registry/blocks", name);
const html = readFileSync(join(dir, `${name}.html`), "utf-8");
const entry = html.slice(html.indexOf('<script type="module">'));
const imported = [...entry.matchAll(/^\s*import [^;]*? from "([^"]+)"/gm)].flatMap(
(m) => m[1] ?? [],
);
assert.ok(imported.length > 0);
const out = inlineCatalogScripts(name, html, dir, vendorUrls);
const mapped = /imports:\{(.*?)\}\}\);/.exec(out)?.[1] ?? "";
const keys = [...mapped.matchAll(/(?:^|,)"?([^",:]+)"?:/g)].map((m) => m[1]);
for (const spec of imported) assert.ok(keys.includes(spec), spec);
assert.doesNotMatch(out, /<script type="importmap">/);
it(`${name}: bundles its own modules and takes only three.js from the shared bundle`, () => {
const out = inlinedBlock(name);
const required = [...out.matchAll(/__require\(\\"([^"\\]+)\\"\)/g)].map((m) => m[1] ?? "");
assert.ok(required.length > 0);
for (const spec of required)
assert.match(spec, /^(three(\/addons\/.+)?|\.\/three\.module\.min\.js)$/);
assert.doesNotMatch(out, /<script type="(importmap|module)">/);
});
}
});
Loading
Loading