-
-
Notifications
You must be signed in to change notification settings - Fork 33.2k
module: add support for node:
‑prefixed require(…)
calls
#37246
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
Conversation
8615225
to
4f61960
Compare
You read my mind, I was precisely working on this. I noticed it produces an arguably better error when trying to load internal modules: $ node -p "require('internal/test/binding')"
Error: Cannot find module 'internal/test/binding'
$ node -p "require('node:internal/test/binding')"
Error: Should not compile internal/test/binding for public use Can you add tests in |
e740b61
to
320ef5e
Compare
cc @nodejs/modules |
); | ||
} | ||
|
||
// `node:`-prefixed `require(...)` calls bypass the require cache: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this differs from other builtins, I love it, but want to be sure it is intentional.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
policies should intercept before any of the resolution helpers even get called
node/lib/internal/modules/cjs/helpers.js
Line 52 in 36cc0ee
if (redirects) { |
module.require
and get here without some kind of opt-in to the behavior (like dependencies:true
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the very least this needs documentation and probably a strong indication people using policies + instrumenting fs now need to update the policy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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/
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indeed, but that doesn’t mean it’s a good idea to make CJS inconsistent with itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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).
3634cd4
to
d196d45
Compare
@@ -837,7 +850,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)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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(…)
.
10b08bf
to
8564814
Compare
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; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On this topic, should we throw or defer to cache on unknown builtins?
8564814
to
b0fbc91
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just making sure #37246 (comment) is addressed before landing :) (namely the caveat) - I'm appreciative of the work put in, thanks!
Also would like more @nodejs/modules and @nodejs/tsc eyes on this.
(The actual changes LGTM)
Fixes: #36098 (applies only to commit 1d8c8b7)
Refs: #37178 (applies only to commit b0fbc91)