From 6708fcc38688b80d4e052f755f02efb09a2071d1 Mon Sep 17 00:00:00 2001 From: "Kevin (Kun) \"Kassimo\" Qian" Date: Tue, 19 Nov 2019 13:44:59 -0800 Subject: [PATCH] std/node: add some Node.js polyfill to require() (#3382) --- std/node/README.md | 17 +++++++ std/node/global.ts | 1 + std/node/{require.ts => module.ts} | 78 ++++++++++++++++++++++-------- std/node/module_test.ts | 44 +++++++++++++++++ std/node/path.ts | 1 + std/node/require_test.ts | 27 ----------- std/node/tests/cjs/cjs_builtin.js | 10 ++++ std/node/tests/cjs/index.js | 1 + 8 files changed, 132 insertions(+), 47 deletions(-) create mode 100644 std/node/global.ts rename std/node/{require.ts => module.ts} (94%) create mode 100644 std/node/module_test.ts create mode 100644 std/node/path.ts delete mode 100644 std/node/require_test.ts create mode 100644 std/node/tests/cjs/cjs_builtin.js create mode 100644 std/node/tests/cjs/index.js diff --git a/std/node/README.md b/std/node/README.md index 6d363a11dd55b5..14c245809c7f2c 100644 --- a/std/node/README.md +++ b/std/node/README.md @@ -5,3 +5,20 @@ This module is meant to have a compatibility layer for the **Warning** : Any function of this module should not be referred anywhere in the deno standard library as it's a compatiblity module. + +## CommonJS Module Loading + +`createRequire(...)` is provided to create a `require` function for loading CJS +modules. + +```ts +import { createRequire } from "https://deno.land/std/node/module.ts"; + +const require_ = createRequire(import.meta.url); +// Loads native module polyfill. +const path = require_("path"); +// Loads extensionless module. +const cjsModule = require_("./my_mod"); +// Visits node_modules. +const leftPad = require_("left-pad"); +``` diff --git a/std/node/global.ts b/std/node/global.ts new file mode 100644 index 00000000000000..c21b0b6596903f --- /dev/null +++ b/std/node/global.ts @@ -0,0 +1 @@ +window["global"] = window; diff --git a/std/node/require.ts b/std/node/module.ts similarity index 94% rename from std/node/require.ts rename to std/node/module.ts index 22393a418462aa..3d51bf6414e224 100644 --- a/std/node/require.ts +++ b/std/node/module.ts @@ -19,6 +19,12 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. +import "./global.ts"; + +import * as nodeFS from "./fs.ts"; +import * as nodeUtil from "./util.ts"; +import * as nodePath from "./path.ts"; + import * as path from "../path/mod.ts"; import { assert } from "../testing/asserts.ts"; @@ -80,8 +86,7 @@ class Module { this.paths = []; this.path = path.dirname(id); } - // TODO: populate this with polyfills! - static builtinModules: Module[] = []; + static builtinModules: string[] = []; static _extensions: { // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: (module: Module, filename: string) => any; @@ -191,7 +196,11 @@ class Module { isMain: boolean, options?: { paths: string[] } ): string { - // Native module code removed + // Polyfills. + if (nativeModuleCanBeRequiredByUsers(request)) { + return request; + } + let paths: string[]; if (typeof options === "object" && options !== null) { @@ -355,7 +364,9 @@ class Module { return cachedModule.exports; } - // Native module NOT supported. + // Native module polyfills + const mod = loadNativeModule(filename, request); + if (mod) return mod.exports; // Don't call updateChildren(), Module constructor already does. const module = new Module(filename, parent); @@ -475,6 +486,20 @@ class Module { } } + /** + * Create a `require` function that can be used to import CJS modules. + * Follows CommonJS resolution similar to that of Node.js, + * with `node_modules` lookup and `index.js` lookup support. + * Also injects available Node.js builtin module polyfills. + * + * const require_ = createRequire(import.meta.url); + * const fs = require_("fs"); + * const leftPad = require_("left-pad"); + * const cjsModule = require_("./cjs_mod"); + * + * @param filename path or URL to current module + * @return Require function to import CJS modules + */ static createRequire(filename: string | URL): RequireFunction { let filepath: string; if ( @@ -540,6 +565,32 @@ class Module { } } +// Polyfills. +const nativeModulePolyfill = new Map(); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function createNativeModule(id: string, exports: any): Module { + const mod = new Module(id); + mod.exports = exports; + mod.loaded = true; + return mod; +} +nativeModulePolyfill.set("fs", createNativeModule("fs", nodeFS)); +nativeModulePolyfill.set("util", createNativeModule("util", nodeUtil)); +nativeModulePolyfill.set("path", createNativeModule("path", nodePath)); +function loadNativeModule( + _filename: string, + request: string +): Module | undefined { + return nativeModulePolyfill.get(request); +} +function nativeModuleCanBeRequiredByUsers(request: string): boolean { + return nativeModulePolyfill.has(request); +} +// Populate with polyfill names +for (const id of nativeModulePolyfill.keys()) { + Module.builtinModules.push(id); +} + let modulePaths = []; // Given a module name, and a list of paths to test, returns the first @@ -1171,19 +1222,6 @@ function pathToFileURL(filepath: string): URL { return outURL; } -/** - * Create a `require` function that can be used to import CJS modules - * @param path path of this module - */ -function makeRequire(filePath: string): RequireFunction { - let mod: Module; - const fullPath = path.resolve(filePath); - if (fullPath in Module._cache) { - mod = Module._cache[fullPath]; - } else { - mod = new Module(fullPath); - } - return makeRequireFunction(mod); -} - -export { makeRequire }; +export const builtinModules = Module.builtinModules; +export const createRequire = Module.createRequire; +export default Module; diff --git a/std/node/module_test.ts b/std/node/module_test.ts new file mode 100644 index 00000000000000..91744d94d522ef --- /dev/null +++ b/std/node/module_test.ts @@ -0,0 +1,44 @@ +import { test } from "../testing/mod.ts"; +import { assertEquals, assert } from "../testing/asserts.ts"; +import { createRequire } from "./module.ts"; + +// TS compiler would try to resolve if function named "require" +// Thus suffixing it with require_ to fix this... +const require_ = createRequire(import.meta.url); + +test(function requireSuccess() { + // Relative to import.meta.url + const result = require_("./tests/cjs/cjs_a.js"); + assert("helloA" in result); + assert("helloB" in result); + assert("C" in result); + assert("leftPad" in result); + assertEquals(result.helloA(), "A"); + assertEquals(result.helloB(), "B"); + assertEquals(result.C, "C"); + assertEquals(result.leftPad("pad", 4), " pad"); +}); + +test(function requireCycle() { + const resultA = require_("./tests/cjs/cjs_cycle_a"); + const resultB = require_("./tests/cjs/cjs_cycle_b"); + assert(resultA); + assert(resultB); +}); + +test(function requireBuiltin() { + const fs = require_("fs"); + assert("readFileSync" in fs); + const { readFileSync, isNull, extname } = require_("./tests/cjs/cjs_builtin"); + assertEquals( + readFileSync("./node/testdata/hello.txt", { encoding: "utf8" }), + "hello world" + ); + assert(isNull(null)); + assertEquals(extname("index.html"), ".html"); +}); + +test(function requireIndexJS() { + const { isIndex } = require_("./tests/cjs"); + assert(isIndex); +}); diff --git a/std/node/path.ts b/std/node/path.ts new file mode 100644 index 00000000000000..e8295281a19448 --- /dev/null +++ b/std/node/path.ts @@ -0,0 +1 @@ +export * from "../path/mod.ts"; diff --git a/std/node/require_test.ts b/std/node/require_test.ts deleted file mode 100644 index 7cac6d6b2b37d2..00000000000000 --- a/std/node/require_test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { test } from "../testing/mod.ts"; -import { assertEquals, assert } from "../testing/asserts.ts"; -import { makeRequire } from "./require.ts"; - -const selfPath = window.unescape(import.meta.url.substring(7)); -// TS compiler would try to resolve if function named "require" -// Thus suffixing it with require_ to fix this... -const require_ = makeRequire(selfPath); - -test(function requireSuccess() { - const result = require_("./node/tests/cjs/cjs_a.js"); - assert("helloA" in result); - assert("helloB" in result); - assert("C" in result); - assert("leftPad" in result); - assertEquals(result.helloA(), "A"); - assertEquals(result.helloB(), "B"); - assertEquals(result.C, "C"); - assertEquals(result.leftPad("pad", 4), " pad"); -}); - -test(function requireCycle() { - const resultA = require_("./node/tests/cjs/cjs_cycle_a"); - const resultB = require_("./node/tests/cjs/cjs_cycle_b"); - assert(resultA); - assert(resultB); -}); diff --git a/std/node/tests/cjs/cjs_builtin.js b/std/node/tests/cjs/cjs_builtin.js new file mode 100644 index 00000000000000..3d182981aa2bf9 --- /dev/null +++ b/std/node/tests/cjs/cjs_builtin.js @@ -0,0 +1,10 @@ +/* eslint-disable */ +const fs = require("fs"); +const util = require("util"); +const path = require("path"); + +module.exports = { + readFileSync: fs.readFileSync, + isNull: util.isNull, + extname: path.extname +}; diff --git a/std/node/tests/cjs/index.js b/std/node/tests/cjs/index.js new file mode 100644 index 00000000000000..5dea52db2c705e --- /dev/null +++ b/std/node/tests/cjs/index.js @@ -0,0 +1 @@ +module.exports = { isIndex: true };