Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.
Sign upGitHub is where the world builds software
Millions of developers and companies build, ship, and maintain their software on GitHub — the largest and most advanced development platform in the world.
module: ESM loaders next steps #36396
Comments
|
Combining load and transform into a single hook makes me very uncomfortable. It seems like this will result in transform hooks being silently skipped by load hooks if chained in the wrong order. |
This issue is meant to be a tracking issue for where we as a team think we want ES module loaders to go. I’ll start it off by writing what I think the next steps are, and based on feedback in comments I’ll revise this top post accordingly.
I think the first priority is to finish the WIP PR that @jkrems started to slim down the main four loader hooks (
resolve,getFormat,getSource,transformSource) into two (resolveToURLandloadFromURL, or should they be calledresolveandload?). This would solve the issue discussed in #34144 / #34753.Next I’d like to add support for chained loaders. There was already a PR opened to achieve this, but as far as I can tell that PR doesn’t actually implement chaining as I understand it; it allows the
transformSourcehook to be chained but not the other hooks, if I understand it correctly, and therefore doesn’t really solve the user request.A while back I had a conversation with @jkrems to hash out a design for what we thought a chained loaders API should look like. Starting from a base where we assume #35524 has been merged in and therefore the only hooks are
resolveandloadandgetGlobalPreloadCode(which probably should be renamed to justglobalPreloadCode, as there are no longer any other hooks namedget*), we were thinking of changing the last argument of each hook fromdefault<hookName>tonext, wherenextis the next registered function for that hook. Then we hashed out some examples for how each of the two primary hooks,resolveandload, would chain.Chaining
resolvehooksSo for example say you had a chain of three loaders,
unpkg,http-to-https,cache-buster:unpkgloader resolves a specifierfooto an urlhttp://unpkg.com/foo.http-to-httpsloader rewrites that url tohttps://unpkg.com/foo.cache-busterthat takes the url and adds a timestamp to the end, so likehttps://unpkg.com/foo?ts=1234567890.These could be implemented as follows:
unpkgloaderhttp-to-httpsloadercache-busterloaderThese chain “backwards” in the same way that function calls do, along the lines of
cacheBusterResolve(httpToHttpsResolve(unpkgResolve(nodeResolve(...))))(though in this particular example, the position ofcache-busterandhttp-to-httpscan be swapped without affecting the result). The point though is that the hook functions nest: each one always just returns a string, like Node’sresolve, and the chaining happens as a result of callingnext; and if a hook doesn’t callnext, the chain short-circuits. I’m not sure if it’s preferable for the API to benode --loader unpkg --loader http-to-https --loader cache-busteror the reverse, but it would be easy to flip that if we get feedback that one way is more intuitive than the other.Chaining
loadhooksChaining
loadhooks would be similar toresolvehooks, though slightly more complicated in that instead of returning a single string, eachloadhook returns an object{ format, source }wheresourceis the loaded module’s source code/contents andformatis the name of one of Node’s ESM loader’s “translators”:commonjs,module,builtin(a Node internal module likefs),json(with--experimental-json-modules) orwasm(with--experimental-wasm-modules).Currently, Node’s internal ESM loader throws an error on unknown file types:
import('file.javascript')throws, even if the contents of that file are perfectly acceptable JavaScript. This error happens during Node’s internalresolvewhen it encounters a file extension it doesn’t recognize; hence the current CoffeeScript loader example has lots of code to tell Node to allow CoffeeScript file extensions. We should move this validation check to be after the format is determined, which is one of the return values ofload; so basically, it’s onloadto return aformatthat Node recognizes. Node’s internalloaddoesn’t know to resolve a URL ending in.coffeetomodule, so Node would continue to error like it does now; but the CoffeeScript loader under this new design no longer needs to hook intoresolveat all, since it can determine the format of CoffeeScript files withinload. In code:coffeescriptloaderAnd the other example loader in the docs, to allow
importofhttps://URLs, would similarly only need aloadhook:httpsloaderIf these two loaders are used together, where the
coffeescriptloader’snextis thehttpsloader’s hook andhttpsloader’snextis Node’s native hook, so likecoffeeScriptLoad(httpsLoad(nodeLoad(...))), then for a URL likehttps://example.com/module.coffee:httpsloader would load the source over the network, but returnformat: undefined, assuming the server supplied a correctContent-Typeheader likeapplication/vnd.coffeescriptwhich ourhttpsloader doesn’t recognize.coffescriptloader would get that{ source, format: undefined }early on from its call tonext, and setformat: 'module'based on the.coffeeat the end of the URL. It would also transpile the source into JavaScript. It then returns{ format: 'module', source }wheresourceis runnable JavaScript rather than the original CoffeeScript.Chaining
globalPreloadCodehooksFor now, I think that this wouldn’t be chained the way
resolveandloadwould be. This hook would just be called sequentially for each registered loader, in the same order as the loaders themselves are registered. If this is insufficient, for example for instrumentation use cases, we can discuss and potentially change this to follow the chaining style ofload.Next Steps
Based on the above, here are the next few PRs as I see them:
resolve,loadandglobalPreloadCode.resolveandload. Node’s internal loader already has no-ops fortransformSourceandgetGlobalPreloadCode, so all this really entails is merging the internalgetFormatandgetSourceinto one functionload.resolve(on detection of unknown extensions) to withinload(if the resolved extension has no defined translator).default<hookName>becomesnextand references the next registered hook in the chain.loadreturn value offormat: 'commonjs'to work, or at least error informatively. See #34753 (comment).This work should complete many of the major outstanding ES module feature requests, such as supporting transpilers, mocks and instrumentation. If there are other significant user stories that still wouldn’t be possible with the loaders design as described here, please let me know. cc @nodejs/modules / @nodejs/modules-active-members / @nodejs/modules-observers