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

Implement node:module createRequire API #2636

Merged
merged 1 commit into from
Sep 3, 2024

Conversation

jasnell
Copy link
Member

@jasnell jasnell commented Aug 31, 2024

The createRequire() function is used in node.js to create a require() function that can be used from inside an ESM module. This is useful largely for porting existing CommonJS code to ESM as it allows the ESM module to keep using require() to load other modules.

This is being implemented in the runtime rather than as a polyfill because it needs to integrate tightly with the module registry implementation.

The implementation is as close to Node.js' as we can currently make it.

Note that the return value of createRequire() is a function. In Node.js, this function has additional properties like require.resolve(...), require.main, require.extensions, and require.cache. We are not currently implementing these additional properties but we could in the future if there is a need for them. (require.cache is unlikely to ever be implemented since workers does not have a module cache the way Node.js does).

Because it was extremely trivial to do so, is closely related, and we already have to maintain a list of known node builtins,
this also implements the require('module').isBuiltin(...) API.

This was implemented because the frameworks team asked for it.

Refs: https://nodejs.org/docs/latest/api/module.html#modulecreaterequirefilename

src/node/module.ts Outdated Show resolved Hide resolved
src/node/module.ts Outdated Show resolved Hide resolved
@vicb
Copy link
Contributor

vicb commented Sep 2, 2024

If we have .isBuiltin exposed, it would be nice to also have .builtinModules

Otherwise we might end up with some iconsistencies (i.e. unenv providing .builtinModules and the the workerd implementation of .isBuiltin)

@pi0
Copy link

pi0 commented Sep 2, 2024

Re unenv: I think unenv should still return all known built-ins for builtinModules and also return true for isBuiltin if it is a known built in (same behavior as Node.js). Libraries depend on both to determine if an import is Node.js built-in or not and if we only return true for the native-subset of workerd, it can break their behavior or at least make inconsistencies. I understand this is something useful for workerd itself to support but probably not for mixed compatibility.

@vicb
Copy link
Contributor

vicb commented Sep 2, 2024

Re unenv: I think unenv should still return all known built-ins for builtinModules and also return true for isBuiltin

Good point.

src/node/module.ts Outdated Show resolved Hide resolved
@jasnell
Copy link
Member Author

jasnell commented Sep 3, 2024

Re unenv: I think unenv should still return all known built-ins for builtinModules and also return true for isBuiltin if it is a known built in (same behavior as Node.js). Libraries depend on both to determine if an import is Node.js built-in or not and if we only return true for the native-subset of workerd, it can break their behavior or at least make inconsistencies. I understand this is something useful for workerd itself to support but probably not for mixed compatibility.

Not quite sure I'm following. Are you saying the runtime should not expose isBuiltin()?

src/node/internal/module.d.ts Outdated Show resolved Hide resolved
@vicb
Copy link
Contributor

vicb commented Sep 3, 2024

Not quite sure I'm following. Are you saying the runtime should not expose isBuiltin()?

I think workerd should expose it but unenv will patch it to return true for the module it polyfills.
With unenv the workerd impl is available at process.getBuiltinModule('module').isBuiltin

Is that correct @pi0 ?

@jasnell
Copy link
Member Author

jasnell commented Sep 3, 2024

@vicb ... I'm just a bit confused because module.isBuiltin(...) should return true for all of the known Node.js built-in specifiers, not just the ones that are built into the runtime. For instance, in this implementation isBuiltin('fs') would return true even tho it is not implemented in the runtime.

@vicb
Copy link
Contributor

vicb commented Sep 3, 2024

in this implementation isBuiltin('fs') would return true even tho it is not implemented in the runtime.

Now I'm confused too 🤔

@vicb
Copy link
Contributor

vicb commented Sep 3, 2024

import module from "node:module";

for (const m of modules()) {
  if (!module.isBuiltin(m)) {
    console.error(m);
  }
}

function modules() {
  // return the output of module.builtInModules as of Node.js v22.6.0 minus "internal/..." packages
}
-> % node test.mjs
test
-> % deno run test.mjs 
test
-> % bun test.mjs
test

Noe that test is (kind of) expected per the Node.js doc

@vicb
Copy link
Contributor

vicb commented Sep 3, 2024

Would this comment

If we have .isBuiltin exposed, it would be nice to also have .builtinModules

be easy to add to the PR?

@jasnell
Copy link
Member Author

jasnell commented Sep 3, 2024

... Noe that test is (kind of) expected per the Node.js doc

It's not clear what you're saying in this comment.

image

If we have .isBuiltin exposed, it would be nice to also have .builtinModules

It would be fairly trivial yes.

@vicb
Copy link
Contributor

vicb commented Sep 3, 2024

... Noe that test is (kind of) expected per the Node.js doc

It's not clear what you're saying in this comment.

The docs says "Note: the list doesn't contain prefix-only modules like node:test."

I interpreted that as test being "special", implying that isBuiltin(test) === false is expected - "kind of" because I'm unsure if my interpretation is correct.

@jasnell
Copy link
Member Author

jasnell commented Sep 3, 2024

Yes, isBuiltin('test') should be false... which is what this PR implements:

image

@jasnell jasnell force-pushed the jsnell/implement-node-compat-module-createrequire branch 2 times, most recently from 6118b4a to 89292e5 Compare September 3, 2024 16:01
@jasnell
Copy link
Member Author

jasnell commented Sep 3, 2024

require('module').builtinModules added.

@jasnell jasnell marked this pull request as ready for review September 3, 2024 16:01
@jasnell jasnell requested review from a team as code owners September 3, 2024 16:01
@jasnell
Copy link
Member Author

jasnell commented Sep 3, 2024

Marking this ready for review

@vicb
Copy link
Contributor

vicb commented Sep 3, 2024

A few comments:

Node 22.6 had 2 extras builtin modules: test/mock_loader and wasi

I got my previous cross runtime wrong as the reference was deno instead of node. By correctly using the node ref, there are missing modules in runtimes:

-> % bun test.mjs 
test/mock_loader

-> % deno run test.mjs 
_http_client
_http_common
_http_incoming
_http_server
_stream_wrap
_tls_common
_tls_wrap
inspector/promises
test/mock_loader
trace_events

Which means that .isBuiltin() behavior is not consistent across runtimes. At least deno returns what it actually implements.

test/mock_loader might not have made it to the list (see this bug).

I think adding wasi to current PR and clearly document the behavior of workerd which is isBuiltin() === implemented is Node is good to me

Copy link
Contributor

@vicb vicb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with a few minor comments

src/node/module.ts Show resolved Hide resolved
src/node/module.ts Show resolved Hide resolved
src/node/module.ts Outdated Show resolved Hide resolved
@jasnell jasnell force-pushed the jsnell/implement-node-compat-module-createrequire branch from 89292e5 to 1904a89 Compare September 3, 2024 16:42
@vicb
Copy link
Contributor

vicb commented Sep 3, 2024

Thanks for the updates James

The `createRequire()` function is used in node.js to create a `require()`
function that can be used from inside an ESM module. This is useful largely
for porting existing CommonJS code to ESM as it allows the ESM module to
keep using `require()` to load other modules.

This is being implemented in the runtime rather than as a polyfill because
it needs to integrate tightly with the module registry implementation.

The implementation is as close to Node.js' as we can currently make it.

Note that the return value of `createRequire()` is a function. In Node.js,
this function has additional properties like `require.resolve(...)`,
`require.main`, `require.extensions`, and `require.cache`. We are not
currently implementing these additional properties but we could in the
future if there is a need for them. (`require.cache` is unlikely to ever
be implemented since workers does not have a module cache the way Node.js
does).

This was implemented because the frameworks team asked for it.

Refs: https://nodejs.org/docs/latest/api/module.html#modulecreaterequirefilename
@jasnell jasnell force-pushed the jsnell/implement-node-compat-module-createrequire branch from 1904a89 to ab07d88 Compare September 3, 2024 17:11
@jasnell
Copy link
Member Author

jasnell commented Sep 3, 2024

Running an internal CI run before landing.

@jasnell jasnell merged commit 6d652db into main Sep 3, 2024
13 checks passed
@jasnell jasnell deleted the jsnell/implement-node-compat-module-createrequire branch September 3, 2024 20:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants