X Tutup
The Wayback Machine - https://web.archive.org/web/20210814163056/https://github.com/nodejs/node/pull/37246/files
Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

module: add support for node:‑prefixed require(…) calls #37246

Merged
merged 2 commits into from Mar 19, 2021
Merged
Changes from all commits
Commits
File filter
Filter file types
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.

Always

Just for now

@@ -204,6 +204,10 @@ import _ from 'data:application/json,"world!"';
added:
- v14.13.1
- v12.20.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/37246
description: Added `node:` import support to `require(...)`.
-->

`node:` URLs are supported as an alternative means to load Node.js builtin
@@ -280,6 +280,12 @@
## Core modules

<!--type=misc-->
<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/37246
description: Added `node:` import support to `require(...)`.
-->

Node.js has several modules compiled into the binary. These modules are
described in greater detail elsewhere in this documentation.
@@ -291,6 +297,11 @@
passed to `require()`. For instance, `require('http')` will always
return the built in HTTP module, even if there is a file by that name.

Core modules can also be identified using the `node:` prefix, in which case
it bypasses the `require` cache. For instance, `require('node:http')` will
always return the built in HTTP module, even if there is `require.cache` entry
by that name.
This conversation was marked as resolved by richardlau

This comment has been minimized.

@richardlau

richardlau Mar 5, 2021
Member

This is a bit of an odd example as the non-prefixed require('http') would be treated as a built in and bypass the require cache.

This comment has been minimized.

@bmeck

bmeck Mar 5, 2021
Member

currently builtin modules go through the cache if a matching entry exists, I don't understand the comment

This comment has been minimized.

@richardlau

richardlau Mar 5, 2021
Member

If there is a cache it should always point to the built in HTTP module, shouldn't it? AFAIK it's not possible for Node.js to return anything other than the built in module for built in modules.

This comment has been minimized.

@ExE-Boss

ExE-Boss Mar 5, 2021
Author Contributor

Actually, require("http") currently checks require.cache["http"] before calling loadNativeModule("http").


## Cycles

<!--type=misc-->
@@ -642,8 +653,19 @@

Adding or replacing entries is also possible. This cache is checked before
native modules and if a name matching a native module is added to the cache,
no require call is
going to receive the native module anymore. Use with care!
only `node:`-prefixed require calls are going to receive the native module.
Use with care!

```js
const assert = require('assert');
const realFs = require('fs');
const fakeFs = {};
require.cache.fs = { exports: fakeFs };
assert.strictEqual(require('fs'), fakeFs);

Check failure on line 666 in doc/api/modules.md

GitHub Actions / lint-js

doc/api/modules.md#L666

'fs' require is duplicated

Check failure on line 666 in doc/api/modules.md

GitHub Actions / lint-md

doc/api/modules.md#L666

'fs' require is

Check failure on line 666 in doc/api/modules.md

GitHub Actions / lint-md

doc/api/modules.md#L666

'fs' require is

Check failure on line 666 in doc/api/modules.md

GitHub Actions / lint-js

doc/api/modules.md#L666

'fs' require is duplicated
assert.strictEqual(require('node:fs'), realFs);
```

#### `require.extensions`
<!-- YAML
@@ -34,8 +34,9 @@ const cjsConditions = new SafeSet(['require', 'node', ...userConditions]);

function loadNativeModule(filename, request) {
const mod = NativeModule.map.get(filename);
if (mod) {
if (mod?.canBeRequiredByUsers) {
debug('load native module %s', request);
// compileForPublicLoader() throws if mod.canBeRequiredByUsers is false:
mod.compileForPublicLoader();
return mod;
}
@@ -110,7 +110,8 @@ let hasLoadedAnyUserCJSModule = false;
const {
ERR_INVALID_ARG_VALUE,
ERR_INVALID_MODULE_SPECIFIER,
ERR_REQUIRE_ESM
ERR_REQUIRE_ESM,
ERR_UNKNOWN_BUILTIN_MODULE,
} = require('internal/errors').codes;
const { validateString } = require('internal/validators');
const pendingDeprecation = getOptionValue('--pending-deprecation');
@@ -770,6 +771,17 @@ Module._load = function(request, parent, isMain) {
}

const filename = Module._resolveFilename(request, parent, isMain);
if (StringPrototypeStartsWith(filename, 'node:')) {
// Slice 'node:' prefix
const id = StringPrototypeSlice(filename, 5);

const module = loadNativeModule(id, request);
if (!module?.canBeRequiredByUsers) {
throw new ERR_UNKNOWN_BUILTIN_MODULE(filename);
}

return module.exports;
}
Comment on lines +774 to +784

This comment has been minimized.

@guybedford

guybedford Feb 8, 2021
Contributor

What is the motivation for short-circuiting the cache here, something that has never been done previously for native modules?

I don't see why this scheme should be any different to any other in this regard.

This comment has been minimized.

@bmeck

bmeck Mar 5, 2021
Member

On this topic, should we throw or defer to cache on unknown builtins?


const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
@@ -841,7 +853,8 @@ Module._load = function(request, parent, isMain) {
};

Module._resolveFilename = function(request, parent, isMain, options) {
if (NativeModule.canBeRequiredByUsers(request)) {
if (StringPrototypeStartsWith(request, 'node:') ||
NativeModule.canBeRequiredByUsers(request)) {

This comment has been minimized.

@guybedford

guybedford Feb 8, 2021
Contributor

This bifurcation will have implications for mocking, but I can appreciate the benefit too.

I'd still prefer a more convergent path here eg, to return node:fs for both node:fs and fs inputs, and then to still populate a cache['fs'] entry in the node:fs case as the same object for backwards compatibility. Such a change would hopefully be mostly compatible but would probably need to be a separate major nontheless. Worth thinking about at least.

This comment has been minimized.

@ExE-Boss

ExE-Boss Feb 8, 2021
Author Contributor

Existing mocking tools (such as Jest) provide their own require(…) implementation, since they need to be able to bypass the mocked module using jest.requireActual(…).

return request;
}

@@ -282,7 +282,7 @@ translators.set('builtin', async function builtinStrategy(url) {
debug(`Translating BuiltinModule ${url}`);
// Slice 'node:' scheme
const id = StringPrototypeSlice(url, 5);
const module = loadNativeModule(id, url, true);
const module = loadNativeModule(id, url);
if (!StringPrototypeStartsWith(url, 'node:') || !module) {
throw new ERR_UNKNOWN_BUILTIN_MODULE(url);
}
@@ -1113,7 +1113,7 @@ REPLServer.prototype.setPrompt = function setPrompt(prompt) {
};

const importRE = /\bimport\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/;
const requireRE = /\brequire\s*\(\s*['"`](([\w@./-]+\/)?(?:[\w@./-]*))(?![^'"`])$/;
const requireRE = /\brequire\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/;
const fsAutoCompleteRE = /fs(?:\.promises)?\.\s*[a-z][a-zA-Z]+\(\s*["'](.*)/;
const simpleExpressionRE =
/(?:[a-zA-Z_$](?:\w|\$)*\??\.)*[a-zA-Z_$](?:\w|\$)*\??\.?$/;
@@ -1275,7 +1275,7 @@ function complete(line, callback) {
}

if (!subdir) {
ArrayPrototypePush(completionGroups, _builtinLibs);
ArrayPrototypePush(completionGroups, _builtinLibs, nodeSchemeBuiltinLibs);
}
} else if (RegExpPrototypeTest(importRE, line) &&
this.allowBlockingCompletions) {
@@ -51,6 +51,8 @@ function expectFsNamespace(result) {

expectModuleError(import('node:unknown'),
'ERR_UNKNOWN_BUILTIN_MODULE');
expectModuleError(import('node:internal/test/binding'),
'ERR_UNKNOWN_BUILTIN_MODULE');
expectModuleError(import('./not-an-existing-module.mjs'),
'ERR_MODULE_NOT_FOUND');
expectModuleError(import('http://example.com/foo.js'),
@@ -31,6 +31,10 @@ const assert = require('assert');
const path = require('path');
const fixtures = require('../common/fixtures');
const { builtinModules } = require('module');
const publicModules = builtinModules.filter(
(lib) => !lib.startsWith('_') && !lib.includes('/'),
);

const hasInspector = process.features.inspector;

if (!common.isMainThread)
@@ -239,9 +243,9 @@ putIn.run(['.clear']);

testMe.complete('require(\'', common.mustCall(function(error, data) {
assert.strictEqual(error, null);
builtinModules.forEach((lib) => {
publicModules.forEach((lib) => {
assert(
data[0].includes(lib) || lib.startsWith('_') || lib.includes('/'),
data[0].includes(lib) && data[0].includes(`node:${lib}`),
`${lib} not found`
);
});
@@ -258,11 +262,15 @@ testMe.complete("require\t( 'n", common.mustCall(function(error, data) {
assert.strictEqual(error, null);
assert.strictEqual(data.length, 2);
assert.strictEqual(data[1], 'n');
// require(...) completions include `node:`-prefixed modules:
publicModules.forEach((lib, index) =>
assert.strictEqual(data[0][index], `node:${lib}`));
assert.strictEqual(data[0][publicModules.length], '');
// There is only one Node.js module that starts with n:
assert.strictEqual(data[0][0], 'net');
assert.strictEqual(data[0][1], '');
assert.strictEqual(data[0][publicModules.length + 1], 'net');
assert.strictEqual(data[0][publicModules.length + 2], '');
// It's possible to pick up non-core modules too
data[0].slice(2).forEach((completion) => {
data[0].slice(publicModules.length + 3).forEach((completion) => {
assert.match(completion, /^n/);
});
}));
@@ -0,0 +1,42 @@
'use strict';

require('../common');
const assert = require('assert');
const fs = require('fs');

const errUnknownBuiltinModuleRE = /^No such built-in module: /u;

// For direct use of require expressions inside of CJS modules,
// all kinds of specifiers should work without issue.
{
assert.strictEqual(require('fs'), fs);
assert.strictEqual(require('node:fs'), fs);

assert.throws(
() => require('node:unknown'),
{
code: 'ERR_UNKNOWN_BUILTIN_MODULE',
message: errUnknownBuiltinModuleRE,
},
);

assert.throws(
() => require('node:internal/test/binding'),
{
code: 'ERR_UNKNOWN_BUILTIN_MODULE',
message: errUnknownBuiltinModuleRE,
},
);
}

// `node:`-prefixed `require(...)` calls bypass the require cache:

This comment has been minimized.

@bmeck

bmeck Feb 6, 2021
Member

this differs from other builtins, I love it, but want to be sure it is intentional.

This comment has been minimized.

@ljharb

ljharb Feb 6, 2021
Member

this actually seems like an issue - it means someone who replaces a builtin intentionally can’t replace one of these (altho presumably mutating a builtin would be visible in both). That could cause an issue where someone wants to use a package to instrument or lock down fs (and thus, policies are not an ergonomic option, unless I’m misunderstanding how policies work), but accidentally leaves a gaping security hole when an attacker requires node:fs.

This comment has been minimized.

@bmeck

bmeck Feb 6, 2021
Member

policies should intercept before any of the resolution helpers even get called

, if any module redirection is in place (including complete shutdown) then it would never call out to module.require and get here without some kind of opt-in to the behavior (like dependencies:true).

This comment has been minimized.

@benjamingr

benjamingr Feb 6, 2021
Member

At the very least this needs documentation and probably a strong indication people using policies + instrumenting fs now need to update the policy.

This comment has been minimized.

@ljharb

ljharb Feb 6, 2021
Member

My point was that policies aren’t currently required to ensure this for CJS - a package (not the app itself) can do it. Forcing policies to be the mechanism means packages are no longer capable of abstracting this for users.

This comment has been minimized.

@bmeck

bmeck Feb 6, 2021
Member

@ljharb ah, yes that is true. I just wanted to be clear that policies can prevent this intentionally and should not be considered to have support for such a workflow.

This comment has been minimized.

@ljharb

ljharb Feb 6, 2021
Member

Totally understood. Given that policies are the superior mechanism for app developers to lock things down, what's the benefit of adding special, inconsistent, cache-bypassing behavior for require with a node: prefix?

This comment has been minimized.

@bmeck

bmeck Feb 7, 2021
Member

@ljharb i have no strong opinion on if we should diverge, but cache first behavior of CJS isn't shared by ESM and isn't robust against things like core destructuring the original impls. My like is just that it isn't a confusing situation like with fs/.

This comment has been minimized.

@ExE-Boss

ExE-Boss Feb 8, 2021
Author Contributor

but cache first behavior of CJS isn't shared by ESM and isn't robust against things like core destructuring the original impls.

This is demonstrated by:

const fs = require('fs');

const fakeModule = {};
require.cache.fs = { exports: fakeModule };

assert.strictEqual((await import('fs')).default, fs);

This comment has been minimized.

@ljharb

ljharb Feb 8, 2021
Member

indeed, but that doesn’t mean it’s a good idea to make CJS inconsistent with itself.

This comment has been minimized.

@bmeck

bmeck Mar 5, 2021
Member

I don't think CJS is able to be claimed as consistent with itself since in general the natives don't normally populate the cache. I think it was just ad-hoc written together and the cache behavior is evolutionary not intentional or well understood (even by me).

{
const fakeModule = {};

require.cache.fs = { exports: fakeModule };

assert.strictEqual(require('fs'), fakeModule);
assert.strictEqual(require('node:fs'), fs);

delete require.cache.fs;
}
ProTip! Use n and p to navigate between commits in a pull request.
X Tutup