/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
tests/unit_node/module_test.ts
462 строки
15 KB
nathanwhitbot
fix(node): load graph-backed TypeScript modules synchronously (#35527)
26 июн 2026, 04:16
Не верифицирован
26 июн 2026, 04:16
c5a85e9
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. import { builtinModules, createRequire, // @ts-ignore Our internal @types/node is at v18.16.19 which predates // this change. findPackageJSON, findSourceMap, isBuiltin, Module, // @ts-ignore Our internal @types/node is at v18.16.19 which predates // this change. Updating it is difficult due to different types in Node // for `import.meta.filename` and `import.meta.dirname` that Deno // provides. register, // @ts-ignore Our internal @types/node is at v18.16.19 which predates // this change. runMain, SourceMap, stripTypeScriptTypes, syncBuiltinESMExports, } from "node:module"; import { assert, assertEquals, assertThrows } from "@std/assert"; import process from "node:process"; import * as path from "node:path"; import { pathToFileURL } from "node:url"; Deno.test("[node/module _preloadModules] has internal require hook", () => { // Check if it's there // deno-lint-ignore no-explicit-any (Module as any)._preloadModules([ "./tests/unit_node/testdata/add_global_property.js", ]); // deno-lint-ignore no-explicit-any assertEquals((globalThis as any).foo, "Hello"); }); Deno.test("[node/module runMain] loads module using the current process.argv", () => { process.argv = [ process.argv[0], "./tests/unit_node/testdata/add_global_property_run_main.js", ]; // deno-lint-ignore no-explicit-any (Module as any).runMain(); // deno-lint-ignore no-explicit-any assertEquals((globalThis as any).calledViaRunMain, true); }); Deno.test("[node/module _nodeModulePaths] prevents duplicate /node_modules/node_modules suffix", () => { // deno-lint-ignore no-explicit-any const actual: string[] = (Module as any)._nodeModulePaths( path.join(process.cwd(), "testdata", "node_modules", "foo"), ); assert( !actual.some((dir) => /node_modules[/\\]node_modules/g.test(dir)), "Duplicate 'node_modules/node_modules' suffix found", ); }); Deno.test("[node/module _nodeModulePaths] prevents duplicate root /node_modules", () => { // deno-lint-ignore no-explicit-any const actual: string[] = (Module as any)._nodeModulePaths( path.join(process.cwd(), "testdata", "node_modules", "foo"), ); assert( new Set(actual).size === actual.length, "Duplicate path entries found", ); const root = path.parse(actual[0]).root; assert( actual.includes(path.join(root, "node_modules")), "Missing root 'node_modules' directory", ); }); Deno.test("Built-in Node modules have `node:` prefix", () => { let thrown = false; try { // @ts-ignore We want to explicitly test wrong call signature createRequire(); } catch (e) { thrown = true; const stackLines = (e as Error).stack!.split("\n"); // Assert that built-in node modules have `node:<mod_name>` specifiers. assert(stackLines.some((line: string) => line.includes("(node:module:"))); } assert(thrown); }); Deno.test("[node/module isBuiltin] recognizes node builtins", () => { assert(isBuiltin("node:fs")); assert(isBuiltin("node:test")); assert(isBuiltin("fs")); assert(isBuiltin("buffer")); assert(!isBuiltin("internal/errors")); assert(!isBuiltin("test")); assert(!isBuiltin("")); // deno-lint-ignore no-explicit-any assert(!isBuiltin(undefined as any)); }); // https://github.com/denoland/deno/issues/22731 Deno.test("[node/module builtinModules] has 'module' in builtins", () => { assert(builtinModules.includes("module")); }); // https://github.com/denoland/deno/issues/34812 Deno.test("[node/module runMain] is exposed as a named export", () => { assertEquals(typeof runMain, "function"); assertEquals(runMain, Module.runMain); }); // https://github.com/denoland/deno/issues/18666 Deno.test("[node/module findSourceMap] is a function", () => { assertEquals(findSourceMap("foo"), undefined); }); Deno.test("[node/module SourceMap] is exposed as a named export", () => { assertEquals(typeof SourceMap, "function"); // @ts-ignore Not in all supported @types/node versions. assertEquals(SourceMap, Module.SourceMap); }); // https://github.com/denoland/deno/issues/31039 Deno.test("[node/module findPackageJSON] is exported and finds package.json", async () => { const tempDir = await Deno.makeTempDir(); try { const packageJsonPath = path.join(tempDir, "package.json"); const entryPath = path.join(tempDir, "src", "main.js"); await Deno.mkdir(path.dirname(entryPath), { recursive: true }); await Deno.writeTextFile(packageJsonPath, '{"name":"fixture"}'); await Deno.writeTextFile(entryPath, "export default 1;"); const realPackageJsonPath = await Deno.realPath(packageJsonPath); assertEquals(typeof findPackageJSON, "function"); assertEquals( findPackageJSON(pathToFileURL(entryPath).href), realPackageJsonPath, ); assertEquals( findPackageJSON( "./src/main.js", pathToFileURL( path.join(tempDir, "mod.mjs"), ), ), realPackageJsonPath, ); } finally { await Deno.remove(tempDir, { recursive: true }); } }); Deno.test("[node/module findPackageJSON] returns package root for bare package subpaths", async () => { const tempDir = await Deno.makeTempDir(); try { const packageRoot = path.join(tempDir, "node_modules", "foo"); const packageJsonPath = path.join(packageRoot, "package.json"); await Deno.mkdir(packageRoot, { recursive: true }); await Deno.writeTextFile( packageJsonPath, '{"name":"foo","exports":{".":"./index.js"}}', ); await Deno.writeTextFile(path.join(packageRoot, "index.js"), ""); const realPackageJsonPath = await Deno.realPath(packageJsonPath); assertEquals( findPackageJSON( "foo/not-exported", pathToFileURL( path.join(tempDir, "mod.mjs"), ), ), realPackageJsonPath, ); } finally { await Deno.remove(tempDir, { recursive: true }); } }); Deno.test("[node/module findPackageJSON] absolute specifiers use closest package.json", async () => { const tempDir = await Deno.makeTempDir(); try { const packageRoot = path.join(tempDir, "node_modules", "foo"); const nestedDir = path.join(packageRoot, "lib"); const packageJsonPath = path.join(packageRoot, "package.json"); const nestedPackageJsonPath = path.join(nestedDir, "package.json"); const entryPath = path.join(nestedDir, "index.js"); await Deno.mkdir(nestedDir, { recursive: true }); await Deno.writeTextFile( packageJsonPath, '{"name":"foo","exports":"./lib/index.js"}', ); await Deno.writeTextFile(nestedPackageJsonPath, '{"name":"nested"}'); await Deno.writeTextFile(entryPath, ""); const realPackageJsonPath = await Deno.realPath(packageJsonPath); const realNestedPackageJsonPath = await Deno.realPath( nestedPackageJsonPath, ); assertEquals( findPackageJSON("foo", pathToFileURL(path.join(tempDir, "mod.mjs"))), realPackageJsonPath, ); assertEquals( findPackageJSON(pathToFileURL(entryPath).href), realNestedPackageJsonPath, ); } finally { await Deno.remove(tempDir, { recursive: true }); } }); // https://github.com/denoland/deno/issues/24902 Deno.test("[node/module register] is a function", () => { assertEquals(register("foo"), undefined); }); Deno.test("[node/module stripTypeScriptTypes] strips TypeScript types", () => { assertEquals( stripTypeScriptTypes("const a: number = 1;"), "const a = 1;", ); assertEquals( stripTypeScriptTypes("const a: number = 1;", { sourceUrl: "source.ts" }), "const a = 1;\n\n//# sourceURL=source.ts", ); }); Deno.test("[node/module stripTypeScriptTypes] transforms TypeScript syntax", () => { const output = stripTypeScriptTypes("enum Color { Red }\nColor.Red;", { mode: "transform", }); assert(output.includes("Color")); assert(output.includes("Red")); }); Deno.test("[node/module stripTypeScriptTypes] rejects unsupported strip-only syntax", () => { const err = assertThrows( () => stripTypeScriptTypes("enum Color { Red }"), SyntaxError, ); assertEquals( (err as SyntaxError & { code?: string }).code, "ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX", ); }); // Regression test for https://github.com/denoland/deno/issues/34868. // We don't implement the out-of-thread `module.register()` loader (it's a // no-op stub), so `Module.register` must NOT be exposed as a static. Tools // like Playwright and tsx feature-probe `import M from "node:module"; // M.register`; when it's present they set up an ESM loader and wait on a // `MessagePort` handshake that our stub never answers, hanging silently. // With it absent they fall back to a path that works: Playwright skips the // loader and tsx uses `Module.registerHooks` (which we do implement) now // that the emulated Node version (>= 24.11.1) clears tsx's feature gate. Deno.test("[node/module] Module.register is not exposed, registerHooks is", () => { // @ts-ignore Not in our bundled @types/node yet. assertEquals(Module.register, undefined); // @ts-ignore Not in our bundled @types/node yet. assertEquals(typeof Module.registerHooks, "function"); }); Deno.test("[node/module syncBuiltinESMExports] is exposed", () => { assertEquals(typeof syncBuiltinESMExports, "function"); assertEquals(syncBuiltinESMExports(), undefined); assertEquals(Module.syncBuiltinESMExports, syncBuiltinESMExports); }); Deno.test("[node/module] overriding Module._compile is possible and Node globals work", () => { // @ts-ignore Not documented but available const originalCompile = Module.prototype._compile; // @ts-ignore Not documented but available Module.prototype._compile = function (content: string, filename: string) { // deno-lint-ignore no-this-alias const mod = this; function require(id: string) { return mod.require(id); } require.resolve = function (request: string) { // @ts-ignore Not documented but available return Module._resolveFilename(request, mod); }; require.main = process.mainModule; // Enable support to add extra extension types // @ts-ignore Not documented but available require.extensions = Module._extensions; // @ts-ignore Not documented but available require.cache = Module._cache; const dirname = path.dirname(filename); const wrapper = Module.wrap(content); // @ts-ignore can't index by a symbol const [f, err] = Deno[Deno.internal].core.evalContext( wrapper, `file:///${filename}`, [true], ); assertEquals(err, null); const args = [ mod.exports, require, mod, filename, dirname, process, ]; f.apply(mod.exports, args); return mod; }; const src = `module.exports = { clearImmediate: typeof clearImmediate, clearTimeout: typeof clearTimeout, setImmediate: typeof setImmediate, global: typeof global };`; // @ts-ignore Not documented but available const ret = Module.prototype._compile(src, "file.js"); const exports = ret.exports; assertEquals(exports.clearImmediate, "function"); assertEquals(exports.clearTimeout, "function"); assertEquals(exports.setImmediate, "function"); assertEquals(exports.global, "object"); // @ts-ignore Not documented but available Module.prototype._compile = originalCompile; }); Deno.test("[node/module require] throws ERR_INVALID_ARG_TYPE for non-string id", () => { const require = createRequire(import.meta.url); const err = assertThrows( // @ts-expect-error testing invalid input () => require(123), TypeError, ); assertEquals( (err as TypeError & { code?: string }).code, "ERR_INVALID_ARG_TYPE", ); }); Deno.test("[node/module require] throws ERR_INVALID_ARG_VALUE for empty string id", () => { const require = createRequire(import.meta.url); const err = assertThrows( () => require(""), TypeError, "must be a non-empty string", ); assertEquals( (err as TypeError & { code?: string }).code, "ERR_INVALID_ARG_VALUE", ); }); Deno.test("[node/module require] loads .node files through CJS addon path", async () => { const tempDir = await Deno.makeTempDir(); try { const addonPath = path.join(tempDir, "addon.node"); await Deno.writeFile(addonPath, new Uint8Array()); const require = createRequire(import.meta.url); const err = assertThrows(() => require(addonPath)); assert(err instanceof Error); assert(!err.message.includes("Cannot import native Node.js addon")); } finally { await Deno.remove(tempDir, { recursive: true }); } }); Deno.test("[node/module require] loads more than 128 local TypeScript modules", async () => { const tempDir = await Deno.makeTempDir(); try { await Promise.all( Array.from({ length: 200 }, (_, i) => Deno.writeTextFile( path.join(tempDir, `mod_${i}.ts`), `export const value = ${i};\n`, )), ); const require = createRequire(import.meta.url); for (let i = 0; i < 200; i++) { const mod = require(path.join(tempDir, `mod_${i}.ts`)) as { value: number; }; assertEquals(mod.value, i); } } finally { await Deno.remove(tempDir, { recursive: true }); } }); Deno.test("[node/module Module._stat] default returns 0/1/<0 for file/dir/missing", async () => { const tmpDir = await Deno.makeTempDir(); try { const file = path.join(tmpDir, "exists.js"); const dir = path.join(tmpDir, "subdir"); const missing = path.join(tmpDir, "missing"); await Deno.writeTextFile(file, "module.exports = {};"); await Deno.mkdir(dir); // @ts-ignore Not documented but available assertEquals(Module._stat(file), 0); // @ts-ignore Not documented but available assertEquals(Module._stat(dir), 1); // @ts-ignore Not documented but available assert(Module._stat(missing) < 0); } finally { await Deno.remove(tmpDir, { recursive: true }); } }); Deno.test("[node/module Module._stat] override is honored by require resolution", async () => { const tmpDir = await Deno.makeTempDir(); try { const file = path.join(tmpDir, "patched-target.cjs"); await Deno.writeTextFile(file, "module.exports = { value: 42 };"); const require = createRequire(path.join(tmpDir, "noop.js")); // @ts-ignore Not documented but available const originalStat = Module._stat; // Patch _stat to claim the file does not exist; the CJS loader uses // the same internal stat that _stat delegates to, so require() must // observe the override. // @ts-ignore Not documented but available Module._stat = (filename: string) => { if (filename === file) return -2; return originalStat(filename); }; try { const err = assertThrows(() => require(file)); assertEquals( (err as Error & { code?: string }).code, "MODULE_NOT_FOUND", ); } finally { // @ts-ignore Not documented but available Module._stat = originalStat; } // After restoration the same require() call must succeed, proving the // failure above was driven by the override and not the file going // missing. const mod = require(file); assertEquals(mod.value, 42); } finally { await Deno.remove(tmpDir, { recursive: true }); } });