UNPKG

@uswds/uswds

Version:

Open source UI components and visual style guide for U.S. government websites

214 lines (194 loc) • 7.61 kB
/** * Unit tests for the USWDS Twig Vite plugin. * * Focused coverage of the plugin's core hooks: * - resolveId: virtual runtime id, namespace aliases, pass-through * - transform: .twig → ESM render function, dependency imports for {% include %} * - load: the esbuild-bundled twig runtime (and that it carries no throwing * `require("fs")` shim) * * Run with: npm run test:tasks */ import assert from "node:assert"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { writeFile, mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import twigPlugin from "./vite-plugin-twig.mjs"; const currentDir = path.dirname(fileURLToPath(import.meta.url)); const packagesDir = path.resolve(currentDir, "../packages"); const templatesDir = path.resolve(packagesDir, "templates"); const root = path.resolve(currentDir, ".."); const TWIG_RUNTIME_ID = "virtual:uswds-twig-runtime"; const RESOLVED_TWIG_RUNTIME_ID = `\0${TWIG_RUNTIME_ID}`; function makePlugin() { const plugin = twigPlugin({ namespaces: { "@components": packagesDir, "@templates": templatesDir }, }); // Seed `root` the way Vite would via configResolved. plugin.configResolved({ root }); return plugin; } /** * Minimal Rollup/Vite plugin context. `resolve` mimics the plugin's own * resolveId aliasing so transform's dependency walk can resolve @components/… */ function makeContext(plugin) { return { warnings: [], errors: [], warn(msg) { this.warnings.push(String(msg)); }, error(msg) { this.errors.push(String(msg)); throw new Error(String(msg)); }, async resolve(source) { const resolved = plugin.resolveId.call(this, source, undefined); if (resolved) return { id: resolved }; // Fall back to a plausible absolute id for relative twig deps. return { id: path.resolve(packagesDir, source.replace(/^\.\//, "")) }; }, }; } describe("vite-plugin-twig", () => { describe("resolveId", () => { it("resolves the virtual twig runtime id", () => { const plugin = makePlugin(); const ctx = makeContext(plugin); assert.strictEqual( plugin.resolveId.call(ctx, TWIG_RUNTIME_ID, undefined), RESOLVED_TWIG_RUNTIME_ID, ); }); it("resolves @components namespace aliases for .twig imports", () => { const plugin = makePlugin(); const ctx = makeContext(plugin); const resolved = plugin.resolveId.call( ctx, "@components/usa-button/src/usa-button.twig", undefined, ); assert.strictEqual( resolved, path.resolve(packagesDir, "usa-button/src/usa-button.twig"), ); }); it("resolves @templates namespace aliases", () => { const plugin = makePlugin(); const ctx = makeContext(plugin); const resolved = plugin.resolveId.call( ctx, "@templates/some.twig", undefined, ); assert.strictEqual(resolved, path.resolve(templatesDir, "some.twig")); }); it("passes through relative .twig imports (returns null)", () => { const plugin = makePlugin(); const ctx = makeContext(plugin); assert.strictEqual( plugin.resolveId.call(ctx, "./sibling.twig", undefined), null, ); }); it("ignores non-.twig, non-runtime sources", () => { const plugin = makePlugin(); const ctx = makeContext(plugin); assert.strictEqual( plugin.resolveId.call(ctx, "./styles.css", undefined), null, ); }); }); describe("transform", () => { it("returns null for non-.twig ids", async () => { const plugin = makePlugin(); const ctx = makeContext(plugin); const result = await plugin.transform.call(ctx, "irrelevant", "/x/y.js"); assert.strictEqual(result, null); }); it("compiles a plain .twig into an ESM render function", async () => { const plugin = makePlugin(); const ctx = makeContext(plugin); const id = path.join(packagesDir, "usa-x/src/plain.twig"); const result = await plugin.transform.call( ctx, "<p>Hello {{ name }}</p>", id, ); assert.ok(result && typeof result.code === "string"); // Imports the virtual twig runtime. assert.match( result.code, /import Twig from "virtual:uswds-twig-runtime";/, ); // Exports a render function. assert.match(result.code, /export default function\(context\)/); assert.match(result.code, /tpl\.render\(context\)/); }); it("emits a dependency import for {% include %}", async () => { const plugin = makePlugin(); const ctx = makeContext(plugin); const id = path.join(packagesDir, "usa-x/src/parent.twig"); const source = '{% include "@components/usa-button/src/usa-button.twig" %}'; const result = await plugin.transform.call(ctx, source, id); assert.ok(result && typeof result.code === "string"); assert.match( result.code, /import "@components\/usa-button\/src\/usa-button\.twig";/, ); }); }); describe("load", () => { it("returns bundled twig runtime code for the virtual id", async () => { const plugin = makePlugin(); const ctx = makeContext(plugin); const code = await plugin.load.call(ctx, RESOLVED_TWIG_RUNTIME_ID); assert.ok(typeof code === "string" && code.length > 0); }); it("bundles a runtime free of the throwing require(fs) shim", async () => { const plugin = makePlugin(); const ctx = makeContext(plugin); const code = await plugin.load.call(ctx, RESOLVED_TWIG_RUNTIME_ID); assert.doesNotMatch(code, /Dynamic require of/); assert.doesNotMatch(code, /__require\("(fs|path|module)"\)/); }); it("returns null for unrelated ids", async () => { const plugin = makePlugin(); const ctx = makeContext(plugin); assert.strictEqual(await plugin.load.call(ctx, "/some/other.js"), null); }); it("disables the browser runtime template cache (HMR guard)", async () => { // Regression: without cache(false) on the bundled runtime, re-executing // a .twig module during HMR re-registers its template id and throws // "There is already a template with the ID …". The patch must land on // the exact instance exported by the runtime. const plugin = makePlugin(); const ctx = makeContext(plugin); const code = await plugin.load.call(ctx, RESOLVED_TWIG_RUNTIME_ID); assert.match(code, /\.cache\(false\)/); }); it("re-registering a template id does not throw (HMR simulation)", async () => { // Import the actual bundled runtime and prove the fix end-to-end: // registering the same id twice (as HMR does) must be a no-op, not throw. const plugin = makePlugin(); const ctx = makeContext(plugin); const code = await plugin.load.call(ctx, RESOLVED_TWIG_RUNTIME_ID); const dir = await mkdtemp(path.join(tmpdir(), "uswds-twig-rt-")); const file = path.join(dir, "runtime.mjs"); try { await writeFile(file, code); const { default: Twig } = await import(pathToFileURL(file).href); Twig.twig({ id: "hmr/dup.twig", data: "<p>first</p>" }); assert.doesNotThrow(() => { Twig.twig({ id: "hmr/dup.twig", data: "<p>second</p>" }); }, "re-registering an existing template id must not throw with cache disabled"); } finally { await rm(dir, { recursive: true, force: true }); } }); }); });