From 128ca35c2af4c96ebafe48ed40de8ab624dce9d7 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 28 Sep 2026 14:44:34 +0000 Subject: [PATCH] test: find the imports of the builds with the compiler's scanner The test that compares the imports of dist/ with "dependencies" matched a regular expression against the whole file. tsc keeps comments in its output, so a usage line in a doc comment (import { ActionMapping } from "alex2node") counted as an import and failed the commit gate with a message about dependencies. ts.preProcessFile lists the imports instead. Stripping comments with two more expressions was tried first and reads strings wrong: in `"src/*.ts"; require("real-one"); "*/"` it drops the require, and it never saw import("lazy-one"); the scanner returns both. The declarations are scanned too: a .d.ts that imports a package the consumer does not get breaks the consumer's type check. Checked in a scratch copy: a doc comment and a string with an import in them pass; require("left-pad") in dist/cjs, an aedes import in a declaration and an unused declared dependency each fail. npm test: 30 pass in 8.9-10.0 s. Co-Authored-By: Claude Fable 5.1 --- test/packaging.test.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/test/packaging.test.js b/test/packaging.test.js index cf109e7..b463828 100644 --- a/test/packaging.test.js +++ b/test/packaging.test.js @@ -8,6 +8,7 @@ const os = require("node:os"); const path = require("node:path"); const { builtinModules } = require("node:module"); const { spawnSync } = require("node:child_process"); +const ts = require("typescript"); const pkg = require("alex2node/package.json"); const ROOT = path.join(__dirname, ".."); @@ -53,14 +54,17 @@ test("each build has its own declarations, and \"exports\" names them before the } }); -test("the builds import Node built-ins and the declared dependencies, nothing else", () => { +test("the builds and their declarations import Node built-ins and the declared dependencies, nothing else", () => { // "mqtt/lib/x" and "@scope/name/x" belong to the packages "mqtt" and "@scope/name" const packageOf = (specifier) => specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/"); const imported = new Set(); - for (const file of files(DIST).filter((f) => f.endsWith(".js"))) { - const source = fs.readFileSync(path.join(DIST, file), "utf8"); - for (const [, specifier] of source.matchAll(/(?:\brequire\(|\bfrom\s+|\bimport\s+)"([^".][^"]*)"/g)) { - if (!builtinModules.includes(specifier.replace(/^node:/, ""))) imported.add(packageOf(specifier)); + for (const file of files(DIST).filter((f) => /\.(js|ts)$/.test(f))) { + // Read with the compiler's scanner: tsc keeps comments in its output, and an import in a usage example or in + // a string is not an import. + const { importedFiles } = ts.preProcessFile(fs.readFileSync(path.join(DIST, file), "utf8"), true, true); + for (const { fileName: specifier } of importedFiles) { + if (specifier.startsWith(".") || builtinModules.includes(specifier.replace(/^node:/, ""))) continue; + imported.add(packageOf(specifier)); } } assert.deepEqual([...imported].sort(), Object.keys(pkg.dependencies).sort());