GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — module: resolve and instantiate loader pipeline hooks by guybedford · Pull Request #15445 · nodejs/node Conversation
|
|
@guybedford what might it look like to extend on the hooks available, such that a modified source-file could be stored in the cache? I'm interested in how we'll continue providing test-coverage for |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@bcoe one option might be to add a separate 'fetch' hook to such a loader object alongside the 'resolve' here which could then add instrumentation. Happy to prototype it if there's interest. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@guybedford I'm very interested. The built-in coverage in V8 doesn't seem to quite be over the finish line yet, so this would be a great transitional step for continuing to support test-coverage with Istanbul. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@guybedford @bmeck any chance we could also allow you to attach a loader in a way similar to |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
@bcoe something like import module from 'module'; import { customResolve, customFetch } from 'custom-loader'; module.registerHook('resolve', customResolve); module.registerHook('fetch', customFetch); // now kick off the dynamic import hooking the main application tree load import('./entry-point.mjs'); This was actually roughly what I had for the original API here, but after discussion with @bmeck we adopted a flag rather here to avoid some of the above issues. I'm still open to either approach. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| E('ERR_UNESCAPED_CHARACTERS', | ||
| (name) => `${name} contains unescaped characters`); | ||
| E('ERR_UNKNOWN_ENCODING', 'Unknown encoding: %s'); | ||
| E('ERR_UNKNOWN_FILE_EXTENSION', 'Uknown file extension: %s'); |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
typo: s/Uknown/Unknown
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Maybe this could print out the file too?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Sure, instead of just outputting the extension we can output the full path.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| (name) => `${name} contains unescaped characters`); | ||
| E('ERR_UNKNOWN_ENCODING', 'Unknown encoding: %s'); | ||
| E('ERR_UNKNOWN_FILE_EXTENSION', 'Uknown file extension: %s'); | ||
| E('ERR_UNKNOWN_LOADER', 'Uknown module loader: %s'); |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
here too :-)
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
The message here seems a bit vague - what does this mean?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| "--napi-modules", | ||
| "--expose-http2", | ||
| "--experimental-modules", | ||
| "--loader", |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Perhaps --module-loader would be more descriptive?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
@jasnell what might it be confused by?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| if (config_experimental_modules) | ||
| if (config_experimental_modules) { | ||
| READONLY_BOOLEAN_PROPERTY("experimentalModules"); | ||
| if (!config_userland_loader.empty()) { |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
thank you for using node_config to expose this :-)
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
That is all @bmeck!
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Awesome to see this @guybedford ... left a few initial review comments but will need to play with this a bit more before signing off. Thank you for working on this |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I'd like to leave things as declarative instead of API based if possible for now. As for a fetch transform, I have several ideas on how to accomplish similar ideas without introducing that transform:
These can be worked through in the browser if done carefully; I am not convinced we should introduce fetch hooks in Node without creating a story for network backed modules and security of them. I also would like some involvement from browser people who are creating multiple different execution workers like ServiceWorker in order to do these tasks. For code coverage I know V8 now has that instrumented in the codegen, so perhaps using that and adding missing features would be easier over time than trying to have hooks. Especially since we don't have a composition mechanism for these hooks currently. This PR looks good even though I think we have more than enough hook area already and should not expand the scope of the PR. Overall, I think minimal hooks is the best approach in the beginning as I still think these hooks will change as we use them / won't look exactly the same by the time they are released.
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
I'm not sure when this is targeted to land, but this seems very early. I would hope this doesn't end up in Node core at least until the ecosystem has had some time to settle down and explore alternatives and prove that this particular API shape is the best, and that the use cases it addresses are actually important enough to add such a powerful, fragmentation-encouraging primitive which cannot be taken back. General API comment: please never return or accept URL objects; see https://url.spec.whatwg.org/#url-apis-elsewhere . On the actual capabilities here, I think the resolve hook is straightforward enough (although may end up mismatching browsers if they need something more declarative, as is currently the thinking). The loader object is much scarier and is nowhere near what we would currently contemplate exposing in browsers (as essentially we are trying to stick with Source Text Module Records, so any hook I can imagine would be just a fetch hook, i.e. service workers). |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
left some comments :)
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| (name) => `${name} contains unescaped characters`); | ||
| E('ERR_UNKNOWN_ENCODING', 'Unknown encoding: %s'); | ||
| E('ERR_UNKNOWN_FILE_EXTENSION', 'Uknown file extension: %s'); | ||
| E('ERR_UNKNOWN_LOADER', 'Uknown module loader: %s'); |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
The message here seems a bit vague - what does this mean?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| E('ERR_UNESCAPED_CHARACTERS', | ||
| (name) => `${name} contains unescaped characters`); | ||
| E('ERR_UNKNOWN_ENCODING', 'Unknown encoding: %s'); | ||
| E('ERR_UNKNOWN_FILE_EXTENSION', 'Uknown file extension: %s'); |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Maybe this could print out the file too?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
|
||
| // Strategy for loading a standard JavaScript module | ||
| loaders.set('esm', async (url) => { | ||
| const source = `${await asyncReadFile(url)}`; |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
What happens if the readFile throws?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| return { url, loader: 'esm' }; | ||
| case '.json': | ||
| return { url, loader: 'cjs' }; | ||
| case '.node': |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Aren't .node files native addon C++?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
they use the CJS interop
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
It might be nice to add a binary format now - suggestions welcome for how to load a file as just a Node binary, throwing otherwise.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| case '.js': | ||
| return { url, loader: 'cjs' }; | ||
| default: | ||
| throw new errors.Error('ERR_UNKNOWN_FILE_EXTENSION', ext); |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this default to 'cjs'? (Though I suppose this is not required for importing CJS via ESM...)
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
This is for future proofing, just like #15365 was talking about.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| "--napi-modules", | ||
| "--expose-http2", | ||
| "--experimental-modules", | ||
| "--loader", |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
@jasnell what might it be confused by?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Thanks so much for all feedback so far. I've updated the description above to match the latest updates:
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
|
||
| Module.import = function(request, parent) { | ||
| return ESMLoader.import(request, parent); | ||
| }; |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Why not make this Module.prototype.import so you can get rid of the parent parameter (and use this instead)?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
there eventually could be multiple loaders / I would like to see this fn removed when import() lands
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Module.import doesn't appear to be used anywhere else in this PR or usage examples, is it dead code?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
used as stop gap til import() in [email protected] lands
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
used as stop gap til import() in [email protected] lands
Is there a rough idea of when that'll be?
Would Module.import be intended as public consumable API?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Sometime in Node@9
no, i'd remove it once 6.2 lands.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
We should guard this by checking for experimentalModules
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
Ok, so @bcoe asked me to chime in: My primary use case for a custom loader is mocks. I currently maintain require-inject which fiddles with the cache to allow tests to replace modules with their own code. How it does this is not something I'd want to see replicated as it's very much a hack, but the functionality is something I don't want to lose with ESM. It looks like a loader API + An interpreter flag might be workable (because these are tests) but would be awkward, and some version of |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
I'd assume |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| parentURLOrString = new URL(parentURLOrString); | ||
| } else if (parentURLOrString instanceof URL === false) { | ||
| throw new errors.TypeError('ERR_INVALID_ARG_TYPE', | ||
| 'parentURLOrString', 'URL'); |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
This doesn't really address Domenic's concern of a mutable URL object being passed around. Instead you can do if (typeof parentURLOrString !== 'string' && !(parentURLOrString instanceof URL)) throw ...; const url = new URL(parentURLOrString);.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Or just do ToString on any incoming argument like every ES and web API do.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I think the Node code precedent here is to use template strings to coerce:
`${url}`
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
This resolve isn't exposed publicly, it's the this.resolver call just below that represents the hookable API. Will add a guard to that.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
This becomes a bit awkward for writing testing tools like This is a similar concern I have with trying to facilitate test-coverage functionality using V8's built-in coverage. Which is even more of a Rube Goldberg machine, as you need to spawn a process with tldr; it's a lot easier if there's a programatic way hook into this functionality in a vanilla Node process . |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
I don't have a ton of experience with all that. I know that using tools like AVA, which forks processes, I was able to enable |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
nyc already does a lot of this, passing around state so that subprocesses can when spawned have their My concern is that this flow is already very complex and bug prone. As it stands right now the new module system makes writing a development tool like this even more difficult. EDIT: tldr; it feels like there's an opportunity to make the API for building features like this less painful, since the loader implementation is a greenfield right now. So, I don't see it as a question of whether it's possible, it's a question of can we reduce the burden placed on folks developing tools for Node.js. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
You can use the
This seems like we can improve that workflow rather than making a more complex hook system.
This is exactly why I want to work on a minimal API and iterate rather than trying to recreate the old workflows. If we focus on specific aspects instead of trying to solve all the use cases at once I think we will not suffer the previous problems with standardizing loaders. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
That's a really thoughtful point of view; mainly I just want to make sure we have examples of how we'd facilitate use-cases like dependency injection, coverage, and transpilation, before ES modules aren't behind a flag... I think if we can nail these three categories of common developer tools, we'd know the API changes are looking really healthy. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@bcoe Dependency injection won't work like in other languages where it can change over time due to idempotency of ESM for any given specifier. The spec does not allow things like: await import('thingFactory') != await import('thingFactory'); You can pass in data via query params as needed and that will give you a clean local module map that can change what I'm not going to block unflagging on transpilation as they can be done ahead of time. However, various workflows like needing a way to get coverage is certainly blocking. If that is done ahead of time and works I would not see it as a blocker. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
|
||
| Module.import = function(request, parent) { | ||
| return ESMLoader.import(request, parent); | ||
| }; |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
We should guard this by checking for experimentalModules
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Oh, wait, I'm totally wrong, it had CI, even passing CI. (I searched for OK, so wonder what changed to cause the rash of failures all of a sudden... |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@bmeck where would be a good place to start (or continue) a discussion about |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
Talking with @refack in IRC. The issue is indeed with this change but CI didn't catch it because of a bug in Makefile that caused the test to be omitted on CI. Most relevant bit:
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Ref: #16155 (comment) |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
For reference the |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Proposed fix in #16174 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
This enables a --loader flag for Node, which can provide custom "resolve" and "dynamicInstantiate" methods for custom ES module loading. In the process, module providers have been converted from classes into functions and the module APIs have been made to pass URL strings over objects. PR-URL: #15445 Reviewed-By: Bradley Farias <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Jeremiah Senkpiel <[email protected]> Reviewed-By: Michaël Zasso <[email protected]> Reviewed-By: Timothy Gu <[email protected]>
This enables a --loader flag for Node, which can provide custom "resolve" and "dynamicInstantiate" methods for custom ES module loading. In the process, module providers have been converted from classes into functions and the module APIs have been made to pass URL strings over objects. PR-URL: #15445 Reviewed-By: Bradley Farias <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Jeremiah Senkpiel <[email protected]> Reviewed-By: Michaël Zasso <[email protected]> Reviewed-By: Timothy Gu <[email protected]>
Add `"dynamic"` to the list of supported `format`s returned by a custom resolve hook. Add a table describing the meaning of each `format`. Refs: nodejs#15445
Notable Changes:
* crypto:
- expose ECDH class
#8188
* http2:
- http2 is now exposed by defualt without the need for a flag
#15685
- a new environment varible NODE\_NO\_HTTP2 has been added to allow
userland http2 to be required
#15685
- support has been added for generic `Duplex` streams
#16269
* module:
- resolve and instantiate loader pipeline hooks have been added to
the ESM lifecycle
#15445
* zlib:
- CVE-2017-14919 - In zlib v1.2.9, a change was made that causes an
error to be raised when a raw deflate stream is initialized with
windowBits set to 8. On some versions this crashes Node and you
cannot recover from it, while on some versions it throws an
exception. Node.js will now gracefully set windowBits to 9
replicating the legacy behavior to avoid a DOS vector.
https://github.com/nodejs-private/node-private/pull/95
PR-URL: https://github.com/nodejs-private/node-private/pull/98
Notable Changes:
* crypto:
- expose ECDH class
nodejs/node#8188
* http2:
- http2 is now exposed by defualt without the need for a flag
nodejs/node#15685
- a new environment varible NODE\_NO\_HTTP2 has been added to allow
userland http2 to be required
nodejs/node#15685
- support has been added for generic `Duplex` streams
nodejs/node#16269
* module:
- resolve and instantiate loader pipeline hooks have been added to
the ESM lifecycle
nodejs/node#15445
* zlib:
- CVE-2017-14919 - In zlib v1.2.9, a change was made that causes an
error to be raised when a raw deflate stream is initialized with
windowBits set to 8. On some versions this crashes Node and you
cannot recover from it, while on some versions it throws an
exception. Node.js will now gracefully set windowBits to 9
replicating the legacy behavior to avoid a DOS vector.
nodejs-private/node-private#95
PR-URL: nodejs-private/node-private#98
Add `"dynamic"` to the list of supported `format`s returned by a custom resolve hook. Add a table describing the meaning of each `format`. PR-URL: #16375 Refs: #15445 Reviewed-By: Vse Mozhet Byt <[email protected]> Reviewed-By: Yuta Hiroto <[email protected]> Reviewed-By: Ruben Bridgewater <[email protected]> Reviewed-By: Michaël Zasso <[email protected]>
Notable Changes:
* crypto:
- expose ECDH class
nodejs/node#8188
* http2:
- http2 is now exposed by defualt without the need for a flag
nodejs/node#15685
- a new environment varible NODE\_NO\_HTTP2 has been added to allow
userland http2 to be required
nodejs/node#15685
- support has been added for generic `Duplex` streams
nodejs/node#16269
* module:
- resolve and instantiate loader pipeline hooks have been added to
the ESM lifecycle
nodejs/node#15445
* zlib:
- CVE-2017-14919 - In zlib v1.2.9, a change was made that causes an
error to be raised when a raw deflate stream is initialized with
windowBits set to 8. On some versions this crashes Node and you
cannot recover from it, while on some versions it throws an
exception. Node.js will now gracefully set windowBits to 9
replicating the legacy behavior to avoid a DOS vector.
nodejs-private/node-private#95
PR-URL: nodejs-private/node-private#98
Add `"dynamic"` to the list of supported `format`s returned by a custom resolve hook. Add a table describing the meaning of each `format`. PR-URL: #16375 Refs: #15445 Reviewed-By: Vse Mozhet Byt <[email protected]> Reviewed-By: Yuta Hiroto <[email protected]> Reviewed-By: Ruben Bridgewater <[email protected]> Reviewed-By: Michaël Zasso <[email protected]>
Add `"dynamic"` to the list of supported `format`s returned by a custom resolve hook. Add a table describing the meaning of each `format`. PR-URL: #16375 Refs: #15445 Reviewed-By: Vse Mozhet Byt <[email protected]> Reviewed-By: Yuta Hiroto <[email protected]> Reviewed-By: Ruben Bridgewater <[email protected]> Reviewed-By: Michaël Zasso <[email protected]>
Add `"dynamic"` to the list of supported `format`s returned by a custom resolve hook. Add a table describing the meaning of each `format`. PR-URL: #16375 Refs: #15445 Reviewed-By: Vse Mozhet Byt <[email protected]> Reviewed-By: Yuta Hiroto <[email protected]> Reviewed-By: Ruben Bridgewater <[email protected]> Reviewed-By: Michaël Zasso <[email protected]>
Add `"dynamic"` to the list of supported `format`s returned by a custom resolve hook. Add a table describing the meaning of each `format`. PR-URL: #16375 Refs: #15445 Reviewed-By: Vse Mozhet Byt <[email protected]> Reviewed-By: Yuta Hiroto <[email protected]> Reviewed-By: Ruben Bridgewater <[email protected]> Reviewed-By: Michaël Zasso <[email protected]>
Notable Changes:
* crypto:
- expose ECDH class
nodejs/node#8188
* http2:
- http2 is now exposed by defualt without the need for a flag
nodejs/node#15685
- a new environment varible NODE\_NO\_HTTP2 has been added to allow
userland http2 to be required
nodejs/node#15685
- support has been added for generic `Duplex` streams
nodejs/node#16269
* module:
- resolve and instantiate loader pipeline hooks have been added to
the ESM lifecycle
nodejs/node#15445
* zlib:
- CVE-2017-14919 - In zlib v1.2.9, a change was made that causes an
error to be raised when a raw deflate stream is initialized with
windowBits set to 8. On some versions this crashes Node and you
cannot recover from it, while on some versions it throws an
exception. Node.js will now gracefully set windowBits to 9
replicating the legacy behavior to avoid a DOS vector.
https://github.com/nodejs-private/node-private/pull/95
PR-URL: https://github.com/nodejs-private/node-private/pull/98
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.