clone

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

fix/175-template-quality-pass
lofi / package / astro / alias_coverage_test.ts
alias_coverage_test.ts 6.5K
1import { prepareLofiAstroConfig } from "./mod.ts";23// The vendored package sources under .lofi/ are served by Vite, not Deno:4// when a generated project resolves them from the published package, every5// mapped import has been rewritten to a fully qualified npm:/jsr: specifier,6// and only the alias table in the generated Astro config maps those back to7// resolvable packages. A vendored import without an alias therefore works in8// every workspace-adjacent test and fails only in a clean-room generated9// app. These tests pin the invariant statically: every specifier the10// vendored tree can produce must be covered by the alias table, and every11// aliased npm package must be materialized at config time.1213function assert(condition: unknown, message: string): asserts condition {14 if (!condition) throw new Error(message);15}1617type AliasTable = { regexes: RegExp[]; literals: string[] };1819async function walkFiles(directory: string, collected: string[]): Promise<void> {20 for await (const entry of Deno.readDir(directory)) {21 const path = `${directory}/${entry.name}`;22 if (entry.isDirectory) await walkFiles(path, collected);23 else if (path.endsWith(".ts") || path.endsWith(".tsx")) collected.push(path);24 }25}2627function importSpecifiers(source: string): string[] {28 const found = new Set<string>();29 const statements = [30 /^\s*import\s+[^"'\n]*?from\s*"([^"\n]+)"/gm,31 /^\s*export\s+[^"'\n]*?from\s*"([^"\n]+)"/gm,32 /^\s*import\s*"([^"\n]+)"/gm,33 /import\(\s*"([^"\n]+)"\s*\)/g,34 ];35 for (const pattern of statements) {36 for (const match of source.matchAll(pattern)) found.add(match[1]);37 }38 return [...found];39}4041function aliasTable(configSource: string): AliasTable {42 const regexes = [...configSource.matchAll(/find:\s*\/(.+?)\/,/g)]43 .map((match) => new RegExp(match[1]));44 const literals = [...configSource.matchAll(/find:\s*"([^"\n]+)"/g)]45 .map((match) => match[1]);46 return { regexes, literals };47}4849function covered(table: AliasTable, specifier: string): boolean {50 return table.literals.includes(specifier) ||51 table.regexes.some((regex) => regex.test(specifier));52}5354/** Resolves a bare specifier the way the root import map (and JSR) would. */55function mapSpecifier(imports: Record<string, string>, specifier: string): string | undefined {56 const exact = imports[specifier];57 if (exact !== undefined) return exact;58 const scopeEnd = specifier.startsWith("@") ? specifier.indexOf("/") + 1 : 0;59 const slash = specifier.indexOf("/", scopeEnd);60 if (slash < 0) return undefined;61 const base = imports[specifier.slice(0, slash)];62 if (base === undefined) return undefined;63 return base + specifier.slice(slash);64}6566async function vendoredInventory(): Promise<67 { table: AliasTable; configModule: string; bySpecifier: Map<string, string[]> }68> {69 const root = await Deno.makeTempDir({ dir: ".", prefix: ".lofi-alias-test-" });70 try {71 const configPath = await prepareLofiAstroConfig({ root });72 const table = aliasTable(await Deno.readTextFile(configPath));73 const files: string[] = [];74 await walkFiles(`${root}/.lofi/package`, files);75 const bySpecifier = new Map<string, string[]>();76 for (const file of files.sort()) {77 const relative = file.slice(root.length + 1);78 for (const specifier of importSpecifiers(await Deno.readTextFile(file))) {79 if (specifier.startsWith(".")) continue;80 const users = bySpecifier.get(specifier) ?? [];81 users.push(relative);82 bySpecifier.set(specifier, users);83 }84 }85 const configModule = await Deno.readTextFile(86 new URL("./mod.ts", import.meta.url),87 );88 return { table, configModule, bySpecifier };89 } finally {90 await Deno.remove(root, { recursive: true });91 }92}9394const rootImports = (JSON.parse(95 await Deno.readTextFile(new URL("../../deno.json", import.meta.url)),96) as { imports: Record<string, string> }).imports;9798Deno.test("every vendored import is covered by the generated alias table", async () => {99 const { table, bySpecifier } = await vendoredInventory();100 assert(bySpecifier.size > 0, "vendored tree produced no imports; the scan is broken");101 const misses: string[] = [];102 for (const [specifier, users] of bySpecifier) {103 const sample = users.slice(0, 3).join(", ");104 if (specifier.startsWith("@nzip/lofi")) {105 // Self-imports stay bare in development and become jsr:@nzip/lofi@<v>106 // in the published package; the table must cover both forms.107 const subpath = specifier.slice("@nzip/lofi".length);108 if (!table.literals.includes(specifier)) {109 misses.push(`${specifier} has no bare alias (used by ${sample})`);110 }111 if (!covered(table, `jsr:@nzip/lofi@9.9.9${subpath}`)) {112 misses.push(`${specifier} has no jsr:-form alias (used by ${sample})`);113 }114 continue;115 }116 const mapped = mapSpecifier(rootImports, specifier);117 if (mapped === undefined || mapped.startsWith("node:")) continue;118 if (mapped.startsWith("./") || mapped.startsWith("../")) continue;119 if (!covered(table, mapped)) {120 misses.push(121 `${specifier} rewrites to ${mapped} in the published package but no alias matches it ` +122 `(used by ${sample})`,123 );124 }125 }126 assert(127 misses.length === 0,128 `vendored imports without alias coverage; add matching { find, replacement } entries ` +129 `(and a config-time import) in package/astro/mod.ts:\n ${misses.join("\n ")}`,130 );131});132133Deno.test("aliased npm packages used by vendored sources materialize at config time", async () => {134 const { configModule, bySpecifier } = await vendoredInventory();135 // preact reaches node_modules through the generated app's own islands and136 // jazz-tools through the config's jazzPlugin import; everything else must137 // be imported by the config module itself so npm materializes it.138 const materializedElsewhere = ["preact", "jazz-tools"];139 const misses: string[] = [];140 for (const specifier of bySpecifier.keys()) {141 if (specifier.startsWith("@nzip/lofi")) continue;142 const mapped = mapSpecifier(rootImports, specifier);143 if (mapped === undefined || !mapped.startsWith("npm:")) continue;144 const packageName = specifier.startsWith("@")145 ? specifier.split("/").slice(0, 2).join("/")146 : specifier.split("/")[0];147 if (materializedElsewhere.includes(packageName)) continue;148 if (!configModule.includes(`import "${specifier}";`)) {149 misses.push(specifier);150 }151 }152 assert(153 misses.length === 0,154 `vendored npm imports without a config-time materialization import in ` +155 `package/astro/mod.ts:\n ${misses.join("\n ")}`,156 );157});