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 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 14:44:34 +00:00
parent d96c6b8372
commit 128ca35c2a

View file

@ -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());