UNPKG

@uswds/uswds

Version:

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

165 lines (145 loc) • 6.08 kB
/** * Unit tests for the USWDS CJS→ESM Vite plugin. * * These lock in the mechanical transforms (require/module.exports/exports.*), * the skip rules (non-packages files, stories, already-ESM), and — importantly — * guard against a ReDoS regression in the destructure-require handling. * * Run with: npm run test:tasks */ import assert from "node:assert"; import path from "node:path"; import { fileURLToPath } from "node:url"; import uswdsCjsPlugin from "./vite-plugin-uswds-cjs.mjs"; const currentDir = path.dirname(fileURLToPath(import.meta.url)); const packagesDir = path.resolve(currentDir, "../packages"); // A representative source file id inside packages/ that passes the plugin's // path filter. const SAMPLE_ID = path.join(packagesDir, "usa-x", "src", "index.js"); /** * Invoke the plugin's `transform` hook with a minimal Rollup/Vite plugin * context. Returns the transformed code (or null when the plugin skips). * @param {string} code * @param {string} [id] * @returns {{ code: string, warnings: string[] } | null} */ function transform(code, id = SAMPLE_ID) { const warnings = []; const ctx = { warn: (msg) => warnings.push(String(msg)) }; const plugin = uswdsCjsPlugin({ packagesDir }); const result = plugin.transform.call(ctx, code, id); if (result === null) return null; return { code: result.code, warnings }; } describe("vite-plugin-uswds-cjs", () => { describe("require() imports", () => { it("converts a default require to a default import", () => { const out = transform( `const foo = require("./foo");\nmodule.exports = foo;`, ); assert.match(out.code, /import foo from "\.\/foo";/); }); it("converts a single-line destructure require", () => { const out = transform( `const { CLICK } = require("../events");\nmodule.exports = 1;`, ); assert.match(out.code, /import _mod0 from "\.\.\/events";/); assert.match(out.code, /const \{ CLICK \} = _mod0;/); }); it("preserves rename syntax in a destructure require", () => { const out = transform( `const { prefix: PREFIX } = require("../config");\nmodule.exports = 1;`, ); assert.match(out.code, /const \{ prefix: PREFIX \} = _mod0;/); }); it("converts a multi-line destructure require and collapses to one line", () => { const out = transform( `const {\n a,\n b,\n c\n} = require("./mod");\nmodule.exports = 1;`, ); assert.match(out.code, /import _mod0 from "\.\/mod";/); assert.match(out.code, /const \{ a, b, c \} = _mod0;/); }); it("handles multiple destructure requires with unique module vars", () => { const out = transform( `const { x } = require("./x");\nconst { y } = require("./y");\nmodule.exports = 1;`, ); assert.match(out.code, /import _mod0 from "\.\/x";/); assert.match(out.code, /import _mod1 from "\.\/y";/); }); it("does not treat an object-literal assignment as a destructure require", () => { const out = transform(`const obj = { a: 1 };\nmodule.exports = obj;`); // The `{ a: 1 }` object literal must be left intact. assert.match(out.code, /const obj = \{ a: 1 \};/); assert.doesNotMatch(out.code, /import _mod/); }); }); describe("exports", () => { it("converts module.exports = x to export default", () => { const out = transform(`module.exports = foo;`); assert.match(out.code, /export default foo;/); }); it("converts exports.name = value to a named export", () => { const out = transform(`exports.myThing = myThing;`); assert.match(out.code, /export \{ myThing \};/); }); it("aliases exports.name = otherValue", () => { const out = transform(`exports.publicName = internalName;`); assert.match(out.code, /export \{ internalName as publicName \};/); }); }); describe("skip rules", () => { it("skips files outside the packages directory", () => { const out = transform( `module.exports = 1;`, path.resolve(currentDir, "somewhere-else/index.js"), ); assert.strictEqual(out, null); }); it("skips .stories. files", () => { const out = transform( `module.exports = 1;`, path.join(packagesDir, "usa-x", "src", "usa-x.stories.js"), ); assert.strictEqual(out, null); }); it("skips files with no CJS patterns", () => { const out = transform(`export const foo = 1;`); assert.strictEqual(out, null); }); it("skips non-.js files", () => { const out = transform( `module.exports = 1;`, path.join(packagesDir, "usa-x", "src", "index.twig"), ); assert.strictEqual(out, null); }); }); describe("ReDoS regression", () => { // The destructure-require handling previously used a nested-quantifier // regex (`(?:\n[^}\n]*)*`) that static analysis flagged as a ReDoS risk. // It has been replaced with an index scan. This test feeds the pathological // input from the original CodeQL alert ("const {{\n" + many "\n") and a long // unterminated brace, asserting the transform completes well under a // generous time budget — catastrophic backtracking would time out. it("completes quickly on the CodeQL pathological input", () => { const evil = `const {{\n${"\n".repeat(50000)}`; const start = process.hrtime.bigint(); transform(evil); const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6; assert.ok( elapsedMs < 1000, `transform took ${elapsedMs.toFixed(1)}ms — possible ReDoS regression`, ); }); it("completes quickly on a long unterminated destructure", () => { const evil = `const {\n${"a,\n".repeat(50000)}`; const start = process.hrtime.bigint(); transform(evil); const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6; assert.ok( elapsedMs < 1000, `transform took ${elapsedMs.toFixed(1)}ms — possible ReDoS regression`, ); }); }); });