Skip to content

Commit

Permalink
node: add some Node.js polyfill to require() (denoland/deno#3382)
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinkassimo authored and caspervonb committed Jan 24, 2021
1 parent 73c18e7 commit 5c5ce28
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 47 deletions.
17 changes: 17 additions & 0 deletions node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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");
```
1 change: 1 addition & 0 deletions node/global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
window["global"] = window;
78 changes: 58 additions & 20 deletions node/require.ts → node/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -540,6 +565,32 @@ class Module {
}
}

// Polyfills.
const nativeModulePolyfill = new Map<string, Module>();
// 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
Expand Down Expand Up @@ -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;
44 changes: 44 additions & 0 deletions node/module_test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
1 change: 1 addition & 0 deletions node/path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "../path/mod.ts";
27 changes: 0 additions & 27 deletions node/require_test.ts

This file was deleted.

10 changes: 10 additions & 0 deletions node/tests/cjs/cjs_builtin.js
Original file line number Diff line number Diff line change
@@ -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
};
1 change: 1 addition & 0 deletions node/tests/cjs/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = { isIndex: true };

0 comments on commit 5c5ce28

Please sign in to comment.