Files
scriptc/packages/compiler/test/node-fallback-modules.test.ts
T
Chris Tate f0f91ac1ff Load a Node module's fallback declarations only when the program names it
Without @types/node, every program loaded scriptc-node-fallback.d.ts whole:
the console and process globals and 47 Node modules, about 8,100 lines,
parsed, bound and checked in every cold build whether or not the program
imported them.

- The fallback file keeps the globals and the modules they name (console,
  readline, readline/promises, perf_hooks, tty). Every other module, under
  both spellings, has its own file in ambient/node-fallback/, which
  references the files of the modules its declarations name.
- A module's file joins the program when one of the program's files names
  the module in a string literal: imports, requires, import types and JSDoc
  types all name modules that way. The entry is read before the first
  program; the program-root discovery passes add modules that other files
  name. Files are read as tsgo reads them, and the TypeScript libraries and
  scriptc's own declarations are skipped.
- The checks that recognized the fallback file by its path recognize every
  fallback file.
2026-10-25 15:36:08 -05:00

44 lines
2.1 KiB
TypeScript

import { existsSync, readdirSync, readFileSync } from "node:fs";
import { basename, dirname, join } from "node:path";
import { expect, test } from "vitest";
import {
fallbackDtsPath,
fallbackModulePath,
fallbackModulesNamedIn,
} from "../src/frontend/dts-paths.js";
const moduleDirectory = join(dirname(fallbackDtsPath()), "node-fallback");
const declaredModules = (text: string): string[] =>
[...text.matchAll(/^declare module "([^"]+)" \{/gm)].map((m) => m[1]!);
test("every fallback module file declares the specifiers that map to it, and only those", () => {
const files = readdirSync(moduleDirectory).filter((name) => name.endsWith(".d.ts"));
expect(files.length).toBeGreaterThan(0);
for (const file of files) {
const text = readFileSync(join(moduleDirectory, file), "utf8");
const declared = declaredModules(text);
expect(declared.length, file).toBeGreaterThan(0);
for (const specifier of declared) expect(basename(fallbackModulePath(specifier)!)).toBe(file);
// Every reference names another module file.
for (const [, reference] of text.matchAll(/^\/\/\/ <reference path="\.\/([^"]+)" \/>$/gm))
expect(existsSync(join(moduleDirectory, reference!)), `${file} -> ${reference}`).toBe(true);
}
});
test("the core fallback file declares no module that has a file of its own", () => {
for (const specifier of declaredModules(readFileSync(fallbackDtsPath(), "utf8")))
expect(fallbackModulePath(specifier), specifier).toBeNull();
});
test("a module named in a string literal selects its file, under either spelling", () => {
const files = fallbackModulesNamedIn(
`import { readFileSync } from "node:fs";\nconst os = require('os');\ntype T = import(\`node:stream/web\`);\nconst label = "not-a-module";`,
).map((file) => basename(file));
expect(files.sort()).toEqual(["fs.d.ts", "os.d.ts", "stream-web.d.ts"]);
expect(fallbackModulesNamedIn('console.log("hi")')).toEqual([]);
// Module names with digits.
expect(
fallbackModulesNamedIn(`require("http2"); import("node:v8");`).map((file) => basename(file)),
).toEqual(["http2.d.ts", "v8.d.ts"]);
});