clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/141-demo-review-fixes
lofi / package / tooling / project_test.ts
project_test.ts 7.9K
1import { join } from "node:path";2import {3 duplicateBuildAssets,4 engineWasmAssets,5 engineWasmPreloadTag,6 heaviestShellAsset,7 injectHeadTags,8 precacheUrls,9 scanSecrets,10 shellWeightBytes,11 sourceFingerprint,12 walkFiles,13} from "./project.ts";1415function assert(condition: unknown, message: string): asserts condition {16 if (!condition) throw new Error(message);17}1819function makeTestRoot(): Promise<string> {20 return Deno.makeTempDir({ dir: ".", prefix: ".lofi-project-test-" });21}2223Deno.test("source fingerprint is stable across file creation order and changes with content", async () => {24 const left = await makeTestRoot();25 const right = await makeTestRoot();26 try {27 await Deno.mkdir(join(left, "src"));28 await Deno.writeTextFile(join(left, "src", "b.ts"), "export const b = 2;\n");29 await Deno.writeTextFile(join(left, "src", "a.ts"), "export const a = 1;\n");30 await Deno.mkdir(join(right, "src"));31 await Deno.writeTextFile(join(right, "src", "a.ts"), "export const a = 1;\n");32 await Deno.writeTextFile(join(right, "src", "b.ts"), "export const b = 2;\n");33 assert(34 await sourceFingerprint(left) === await sourceFingerprint(right),35 "fingerprint depends on file creation order",36 );37 await Deno.writeTextFile(join(right, "src", "b.ts"), "export const b = 3;\n");38 assert(39 await sourceFingerprint(left) !== await sourceFingerprint(right),40 "fingerprint ignored source content",41 );42 } finally {43 await Deno.remove(left, { recursive: true });44 await Deno.remove(right, { recursive: true });45 }46});4748Deno.test("source fingerprint includes authored public JavaScript", async () => {49 const root = await makeTestRoot();50 try {51 await Deno.mkdir(join(root, "public"));52 await Deno.writeTextFile(join(root, "public", "app.js"), "const revision = 'one';\n");53 const first = await sourceFingerprint(root);54 await Deno.writeTextFile(join(root, "public", "app.js"), "const revision = 'two';\n");55 assert(first !== await sourceFingerprint(root), "fingerprint ignored authored public source");56 } finally {57 await Deno.remove(root, { recursive: true });58 }59});6061Deno.test("precache URLs are portable app-shell URLs", () => {62 const urls = precacheUrls([63 "sw.js",64 "index.html",65 "lofi-build.json",66 "lofi-precache.json",67 // The schema compatibility manifest is shell state: an offline shell must68 // still know its own schema range, so it stays in the precache.69 "lofi-schema.json",70 "assets\\client.js",71 "manifest.webmanifest",72 "screenshot-wide.png",73 ], ["screenshot-wide.png"]);74 assert(75 JSON.stringify(urls) === JSON.stringify([76 "./",77 "./assets/client.js",78 "./lofi-schema.json",79 "./manifest.webmanifest",80 ]),81 `unexpected precache URLs: ${JSON.stringify(urls)}`,82 );83});8485Deno.test({86 name: "project walking names unsupported symbolic links and the remediation",87 ignore: Deno.build.os === "windows",88 async fn() {89 const root = await makeTestRoot();90 try {91 await Deno.writeTextFile(join(root, "target.ts"), "export const target = true;\n");92 const link = await new Deno.Command("ln", {93 args: ["-s", "target.ts", join(root, "linked.ts")],94 stdout: "null",95 stderr: "piped",96 }).output();97 assert(98 link.success,99 `failed to create test symlink: ${new TextDecoder().decode(link.stderr)}`,100 );101 let message = "";102 try {103 await walkFiles(root);104 } catch (error) {105 message = error instanceof Error ? error.message : String(error);106 }107 assert(message.includes("linked.ts"), `error did not name the symbolic link: ${message}`);108 assert(109 message.includes("replace it with a regular file or directory"),110 `error did not include remediation: ${message}`,111 );112 } finally {113 await Deno.remove(root, { recursive: true });114 }115 },116});117118Deno.test("secret scan ignores the local env source but catches source and build leaks", async () => {119 const root = await makeTestRoot();120 const secret = "generated-command-secret-fixture";121 try {122 await Deno.mkdir(join(root, "src"));123 await Deno.mkdir(join(root, "dist"));124 await Deno.writeTextFile(join(root, ".env"), `BACKEND_SECRET=${secret}\n`);125 await Deno.writeTextFile(join(root, "src", "safe.ts"), "export const safe = true;\n");126 await Deno.writeTextFile(127 join(root, "dist", "client.js"),128 `const leaked = ${JSON.stringify(secret)};`,129 );130 const result = await scanSecrets({ BACKEND_SECRET: secret }, root);131 assert(result.leaks.length === 1, `expected one build leak, received ${result.leaks.length}`);132 assert(result.leaks[0].path === "dist/client.js", "secret scan reported the wrong file");133 } finally {134 await Deno.remove(root, { recursive: true });135 }136});137138Deno.test("duplicate build assets are grouped by content and scoped to _astro", async () => {139 const root = await makeTestRoot();140 try {141 await Deno.mkdir(join(root, "_astro"));142 await Deno.writeTextFile(join(root, "_astro", "engine_bg.HASH.wasm"), "same payload");143 await Deno.writeTextFile(join(root, "_astro", "engine_bg-HASH.wasm"), "same payload");144 await Deno.writeTextFile(join(root, "_astro", "one.js"), "payload one");145 await Deno.writeTextFile(join(root, "_astro", "two.js"), "payload two");146 await Deno.writeTextFile(join(root, "copy.txt"), "same payload");147 const groups = await duplicateBuildAssets([148 "_astro/engine_bg.HASH.wasm",149 "_astro/engine_bg-HASH.wasm",150 "_astro/one.js",151 "_astro/two.js",152 "copy.txt",153 ], root);154 assert(groups.length === 1, `expected one duplicate group, received ${groups.length}`);155 assert(156 groups[0].join(", ") === "_astro/engine_bg-HASH.wasm, _astro/engine_bg.HASH.wasm",157 `same-size files were grouped by name, not content: ${groups[0].join(", ")}`,158 );159 } finally {160 await Deno.remove(root, { recursive: true });161 }162});163164Deno.test("shell weight sums the bytes of the precached set", async () => {165 const root = await makeTestRoot();166 try {167 await Deno.writeTextFile(join(root, "index.html"), "12345");168 await Deno.mkdir(join(root, "_astro"));169 await Deno.writeTextFile(join(root, "_astro", "client.js"), "1234567");170 const total = await shellWeightBytes(["index.html", "_astro/client.js"], root);171 assert(total === 12, `expected 12 bytes, received ${total}`);172 const heaviest = await heaviestShellAsset(["index.html", "_astro/client.js"], root);173 assert(174 heaviest?.path === "_astro/client.js" && heaviest.bytes === 7,175 `expected _astro/client.js at 7 bytes, received ${JSON.stringify(heaviest)}`,176 );177 } finally {178 await Deno.remove(root, { recursive: true });179 }180});181182Deno.test("engine preload tags target the engine binary and land inside head", () => {183 const distFiles = [184 "_astro/jazz_wasm_bg.MOpZeXEV.wasm",185 "_astro/runtime.CCX0HbMH.js",186 "index.html",187 "media/jazz_wasm_bg.notes.wasm.txt",188 ];189 const assets = engineWasmAssets(distFiles);190 assert(191 assets.length === 1 && assets[0] === "_astro/jazz_wasm_bg.MOpZeXEV.wasm",192 `expected the one engine binary, received ${JSON.stringify(assets)}`,193 );194 const tag = engineWasmPreloadTag(assets[0], "/app", 9_300_000);195 assert(196 tag ===197 '<link rel="preload" href="/app/_astro/jazz_wasm_bg.MOpZeXEV.wasm" as="fetch" crossorigin data-lofi-engine="9300000">',198 `unexpected preload tag: ${tag}`,199 );200 const html = "<html><head><title>t</title></head><body></body></html>";201 const injected = injectHeadTags(html, [tag]);202 assert(203 injected === `<html><head><title>t</title>${tag}</head><body></body></html>`,204 `tag was not injected before </head>: ${injected}`,205 );206 assert(207 injectHeadTags("<body>no head</body>", [tag]) === "<body>no head</body>",208 "a document without a head must pass through unchanged",209 );210 assert(injectHeadTags(html, []) === html, "no tags must leave the document unchanged");211});