adds frontend

This commit is contained in:
2026-05-16 16:36:51 -07:00
parent 40c63dc4e2
commit c00913ec35
17 changed files with 1473 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { ArchiveCatalog, archiveFileNameForUrl, archiveIdForUrl, normalizeArchiveUrl } from "../src/archive-catalog.mjs";
test("normalizes only http and https archive URLs", () => {
assert.equal(normalizeArchiveUrl(" https://example.com/path "), "https://example.com/path");
assert.throws(() => normalizeArchiveUrl("file:///tmp/page.html"), /Only http and https/);
assert.throws(() => normalizeArchiveUrl("not a url"), /valid URL/);
});
test("builds stable archive ids from the full URL", () => {
const first = archiveIdForUrl("https://example.com/article?x=1");
const second = archiveIdForUrl("https://example.com/article?x=1");
const third = archiveIdForUrl("https://example.com/article?x=2");
assert.equal(first, second);
assert.notEqual(first, third);
assert.match(first, /^example-com-article-[a-f0-9]{16}$/);
});
test("finds stable archive files without rerendering", async () => {
const archivePath = await fs.mkdtemp(path.join(os.tmpdir(), "archive-catalog-"));
const sourceUrl = "https://example.com/";
const fileName = archiveFileNameForUrl(sourceUrl);
await fs.writeFile(path.join(archivePath, fileName), "<!doctype html>", "utf8");
const catalog = new ArchiveCatalog({ archivePath });
const record = await catalog.findByUrl(sourceUrl);
assert.equal(record.fileName, fileName);
assert.equal(record.archiveUrl, `/archives/${encodeURIComponent(fileName)}`);
});
test("indexes older timestamped archives from the archive comment", async () => {
const archivePath = await fs.mkdtemp(path.join(os.tmpdir(), "archive-catalog-"));
await fs.writeFile(
path.join(archivePath, "example-com-2026-05-16T00-00-00-000Z.html"),
'<!doctype html>\n<!-- Archived locally. Source: https://example.com/story. Created: 2026-05-16T00:00:00.000Z. -->\n<html></html>',
"utf8"
);
const catalog = new ArchiveCatalog({ archivePath });
const record = await catalog.findByUrl("https://example.com/story");
assert.equal(record.fileName, "example-com-2026-05-16T00-00-00-000Z.html");
assert.equal(record.sourceUrl, "https://example.com/story");
});

26
test/archiver.test.mjs Normal file
View File

@@ -0,0 +1,26 @@
import assert from "node:assert/strict";
import test from "node:test";
import { renderPage } from "../src/archiver.mjs";
test("renderPage serializes CSSOM-inserted style rules", async () => {
const html = `<!doctype html>
<html>
<head>
<style id="runtime-style"></style>
<script>
document
.getElementById("runtime-style")
.sheet
.insertRule(".runtime-rule { color: rgb(1, 2, 3); }", 0);
</script>
</head>
<body><div class="runtime-rule">Styled by CSSOM</div></body>
</html>`;
const rendered = await renderPage(`data:text/html,${encodeURIComponent(html)}`, {
userscriptDelay: 0
});
assert.match(rendered, /<style id="runtime-style">[\s\S]*\.runtime-rule/);
assert.match(rendered, /color:\s*rgb\(1,\s*2,\s*3\)/);
});

View File

@@ -0,0 +1,83 @@
import assert from "node:assert/strict";
import test from "node:test";
import { AssetInliner, splitSrcset } from "../src/asset-inliner.mjs";
import { findExternalAssetRefs } from "../src/archiver.mjs";
test("inlines real srcset attributes without reading escaped src text from srcdoc", async () => {
const fetched = [];
const inliner = new AssetInliner();
inliner.fetchAsset = async (rawUrl) => {
fetched.push(rawUrl);
return {
bytes: Buffer.from("asset"),
contentType: "image/png"
};
};
const html = `
<img srcset="/small.png 1x, /large.png 2x">
<iframe srcdoc="&lt;script src=&quot;https://js.stripe.com/v3/foo.js&quot;&gt;&lt;/script&gt;&lt;img src=&quot;/nested.png&quot;&gt;"></iframe>
`;
const output = await inliner.inlineHtml(html, "https://example.com/article");
assert.deepEqual(fetched.sort(), [
"https://example.com/large.png",
"https://example.com/nested.png",
"https://example.com/small.png",
]);
assert.doesNotMatch(output, /js\.stripe\.com/);
assert.equal(inliner.warnings.length, 0);
});
test("external asset reporting ignores escaped nested attributes inside srcdoc", () => {
const refs = findExternalAssetRefs(`
<iframe srcdoc="&lt;img src=&quot;https://tracker.example/pixel.gif&quot;&gt;"></iframe>
<img src="https://cdn.example/picture.jpg">
`);
assert.deepEqual(refs, ["https://cdn.example/picture.jpg"]);
});
test("srcset parsing keeps image CDN transform commas inside URLs", async () => {
assert.deepEqual(splitSrcset([
"https://media.example/photos/id/master/w_120,c_limit/photo.jpg 120w",
"https://media.example/photos/id/master/w_240,c_limit/photo.jpg 240w"
].join(", ")), [
"https://media.example/photos/id/master/w_120,c_limit/photo.jpg 120w",
"https://media.example/photos/id/master/w_240,c_limit/photo.jpg 240w"
]);
const fetched = [];
const inliner = new AssetInliner();
inliner.fetchAsset = async (rawUrl) => {
fetched.push(rawUrl);
return {
bytes: Buffer.from("asset"),
contentType: "image/jpeg"
};
};
await inliner.inlineSrcset(
"https://media.example/photos/id/master/w_120,c_limit/photo.jpg 120w, https://media.example/photos/id/master/w_240,c_limit/photo.jpg 240w",
"https://example.com/article"
);
assert.deepEqual(fetched, [
"https://media.example/photos/id/master/w_120,c_limit/photo.jpg",
"https://media.example/photos/id/master/w_240,c_limit/photo.jpg"
]);
});
test("external asset reporting parses srcset-like attributes without splitting URL commas", () => {
const refs = findExternalAssetRefs(`
<img srcset="data:image/gif;base64,R0lGODlhAQABAAAAACw= 1x, https://cdn.example/image.jpg 2x">
<link rel="preload" as="image" imagesrcset="https://media.example/photos/id/master/w_120,c_limit/photo.jpg 120w, https://media.example/photos/id/master/w_240,c_limit/photo.jpg 240w">
`);
assert.deepEqual(refs, [
"https://cdn.example/image.jpg",
"https://media.example/photos/id/master/w_120,c_limit/photo.jpg",
"https://media.example/photos/id/master/w_240,c_limit/photo.jpg"
]);
});