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 import map support #50590

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft

Conversation

wesleytodd
Copy link
Member

@wesleytodd wesleytodd commented Nov 7, 2023

This PR adds experimental support for import maps behind the --experimental-import-map=<path> flag. This is a draft as I believe there are still quite a few things to discuss and add:

  • Docs
  • More test coverage
    • Test on the module tree from some of the bundled deps
    • Use web platform tests
  • Benchmarks (specifically to see how much adding an import map hits startup)
  • Bare specifier remapping: Should we have the ability to remap one bare specifier to another? foo to bar and then have default resolution on bar, for example? This might be against spec, but also seems really helpful.
  • Does it make sense to resolve the import map via esm? Or should we do a more basic file read?
  • In the future should we plan to automatically load an import map based on a well known location? (ex node_modules/importmap.json)
  • What types of values do we want for --experimental-import-map? Right now any module specifier is allowed, but do we want that? This was done to enable http import of import maps, but that is not explicitly a goal, just a starting point for discussion.
  • CommonJS support? What do we want to do about that?
    • Decided we should hold off on that and deal with it in a follow up PR if we think we should do it.
  • module: add import map support #50590 (comment) Do we need to pass the import map reference to loaders? If not, can we remove it from context first to avoid the non-transferable problem?

Helpful links:

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/loaders

@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Nov 7, 2023
lib/internal/modules/esm/import_map.js Outdated Show resolved Hide resolved
lib/internal/modules/esm/import_map.js Outdated Show resolved Hide resolved
lib/internal/modules/esm/import_map.js Outdated Show resolved Hide resolved
lib/internal/modules/esm/import_map.js Outdated Show resolved Hide resolved
lib/internal/modules/esm/import_map.js Outdated Show resolved Hide resolved
lib/internal/modules/esm/import_map.js Outdated Show resolved Hide resolved
lib/internal/modules/esm/import_map.js Outdated Show resolved Hide resolved
lib/internal/modules/run_main.js Outdated Show resolved Hide resolved
lib/internal/modules/esm/import_map.js Outdated Show resolved Hide resolved
lib/internal/modules/esm/import_map.js Outdated Show resolved Hide resolved
lib/internal/modules/esm/import_map.js Outdated Show resolved Hide resolved
lib/internal/modules/esm/import_map.js Outdated Show resolved Hide resolved
lib/internal/modules/esm/import_map.js Outdated Show resolved Hide resolved
lib/internal/modules/run_main.js Outdated Show resolved Hide resolved
src/node_options.cc Outdated Show resolved Hide resolved
test/es-module/test-importmap.mjs Outdated Show resolved Hide resolved
lib/internal/modules/run_main.js Outdated Show resolved Hide resolved
@@ -391,6 +396,7 @@ class ModuleLoader {
conditions: this.#defaultConditions,
importAttributes,
parentURL,
importMap: this.importMap,
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this is transferable, meaning this will throw as soon as any custom loader is registered. Can you add a test with a loader?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, this currently doesn't work. Frankly I am not sure how we do want it to work. Do we want to pass just the "raw" import map object and add handling inside the worker to re-hydrate the ImportMap class with it? I am struggling to decide if there is a good use case to expose this to other loaders. Would it instead make sense to remove it from context before passing context to the worker?

Copy link
Member

Choose a reason for hiding this comment

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

Yup. Policies have to xfer the source. Take note though that if Workers are allowed to override the value in your design (since they can manipulate argv)

Copy link
Member

Choose a reason for hiding this comment

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

Can you represent the import map value as a plain object that’s transferable? Like I would assume you should be able to implement toJSON and fromJSON methods to your class, and JSON is transferable. Not that I’m suggesting going as far as JSON when an object will do, but if toJSON/fromJSON is possible then some kind of transferable data structure is therefore also possible.

Copy link
Member Author

Choose a reason for hiding this comment

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

Can you represent the import map value as a plain object that’s transferable?

Yep, that is what I meant to say by "Do we want to pass just the raw import map object and add handling inside the worker to re-hydrate the ImportMap class". I just am not sure this makes sense to do. Not only would it be slow but I also am not sure I can think of a use case for accessing/changing the import map in a loader.

Copy link
Member Author

@wesleytodd wesleytodd Nov 17, 2023

Choose a reason for hiding this comment

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

Ok, so the initial non-transferable thing was actually the parent URL instance. I resolved that. Now we have a new question of if, when using the customized loader, the import map resolve should happen inside the worker or on the main thread.

I just pushed an implementation where it happens on the main thread, which means that the processing is on the main thread both when using a custom loader and not. What are the benefits of doing the import map resolution in the worker thread.

cc @JakobJingleheimer maybe we continue this conversation here instead of slack?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, in slack I think @GeoffreyBooth made a good case for this needing to be on context. I think that means I need to make it load the import map inside the worker when using custom loader hooks.

I still think I can avoid reading it twice by checking if there are customization and only loading it on the main thread when there are not.

@aduh95
Copy link
Contributor

aduh95 commented Nov 7, 2023

Could you also fix the commit message to e.g. module: add import map support please? docs is not a valid subsystem, and src and lib are way too broad for that change.

@wesleytodd
Copy link
Member Author

Could you also fix the commit message to e.g. module: add import map support please? docs is not a valid subsystem, and src and lib are way too broad for that change.

I actually did that at first but then the PR docs and other examples I saw seemed to suggest this was the right way. And the validation already caught docs and I changed it to doc just did not edit the PR title (doing that now). Maybe we need better docs on this?

@wesleytodd wesleytodd changed the title lib,src,docs: add import map support lib,src,doc: add import map support Nov 7, 2023
@wesleytodd wesleytodd changed the title lib,src,doc: add import map support module: add import map support Nov 7, 2023
Copy link
Member

@GeoffreyBooth GeoffreyBooth left a comment

Choose a reason for hiding this comment

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

Looks quite good! We also need to add documentation for this. I would add a section to https://github.com/nodejs/node/blob/main/doc/api/module.md, maybe just above “Customization Hooks.” We also need to document the flag in https://github.com/nodejs/node/blob/main/doc/api/cli.md.

lib/internal/modules/esm/import_map.js Show resolved Hide resolved
lib/internal/modules/esm/import_map.js Outdated Show resolved Hide resolved
throw new ERR_INVALID_IMPORT_MAP('module specifier values must be non-empty strings');
}
if (specifier.endsWith('/') && !mapping.endsWith('/')) {
throw new ERR_INVALID_IMPORT_MAP('module specifier values for keys ending with / must also end with /');
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
throw new ERR_INVALID_IMPORT_MAP('module specifier values for keys ending with / must also end with /');
throw new ERR_INVALID_IMPORT_MAP('module specifier keys ending with "/" must have values that end with "/"');

Copy link
Member Author

Choose a reason for hiding this comment

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

We have generic helpers like validateString, if there isn’t a validatePlainObject it might be worth creating one.

Not sure why github did not give me a comment box to directly reply to that comment, but here it is. I think that would be good idea and I can add them in this PR if they did not exist. Where would they live if I wanted to go looking for methods like this?`

lib/internal/modules/esm/import_map.js Outdated Show resolved Hide resolved
lib/internal/modules/esm/resolve.js Show resolved Hide resolved
src/node_options.h Outdated Show resolved Hide resolved
test/es-module/test-importmap.mjs Outdated Show resolved Hide resolved
import import_map from 'internal/modules/esm/import_map';
const { ImportMap } = import_map;
import binding from 'internal/test/binding';
const { primordials: { SafeMap, JSONStringify } } = binding;
Copy link
Member

Choose a reason for hiding this comment

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

In general, you don’t need to use primordials in tests.

Copy link
Member Author

Choose a reason for hiding this comment

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

The linter told me otherwise IIRC. Happy to do whatever folks recommend, just wanted to call out that I did this because the linter told me to lol.

test/es-module/test-importmap.mjs Outdated Show resolved Hide resolved
test/es-module/test-importmap.mjs Outdated Show resolved Hide resolved
@JakobJingleheimer JakobJingleheimer added module Issues and PRs related to the module subsystem. esm Issues and PRs related to the ECMAScript Modules implementation. loaders Issues and PRs related to ES module loaders labels Nov 7, 2023
@wesleytodd wesleytodd force-pushed the import-maps branch 9 times, most recently from b60b205 to 5b6df57 Compare November 11, 2023 21:40
@wesleytodd
Copy link
Member Author

wesleytodd commented Nov 11, 2023

So I looked to see if there was a good way to use the web platform tests, but all of these ones are html tests. For now I just went through and made sure that I covered the gaps I had seen in my original tests instead of using wpt directly (hence marking that complete in the OP). This led to cleaning up some bare specifier things, as well as a bunch of data uri handling stuff. It made it clear I needed to clean up the fixtures, so I did that as well. I think now the last tests I am missing are for two things

  1. bare/sub-path in imports
  2. http imports with other url parts (host, query params, etc)

lib/internal/modules/esm/import_map.js Outdated Show resolved Hide resolved
lib/internal/modules/esm/import_map.js Outdated Show resolved Hide resolved
*/
function shouldBeTreatedAsRelativeOrAbsolutePath(specifier) {
if (specifier === '') { return false; }
if (specifier[0] === '/') { return true; }
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this be path.sep, since Windows have a different path separator?

Copy link
Member Author

Choose a reason for hiding this comment

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

I moved this so I could re-use it in import_map.js, it is just as it was before I moved it. I am thinking maybe this is because imports/require statements are all using nix style, but honestly I am not certain without digging around a bit.

test/es-module/test-import-map.mjs Outdated Show resolved Hide resolved
lib/internal/modules/esm/import_map.js Outdated Show resolved Hide resolved
lib/internal/modules/esm/import_map.js Outdated Show resolved Hide resolved
lib/internal/modules/esm/import_map.js Show resolved Hide resolved
* Determines whether a specifier is a relative path.
* @param {string} specifier - The specifier to check.
*/
function isRelativeSpecifier(specifier) {
Copy link
Member

Choose a reason for hiding this comment

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

Moving to a different file should be implemented in a different commit for future observability.

Copy link
Member Author

Choose a reason for hiding this comment

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

Happy to do that, do folks typically do that as different PRs or multiple commits in the one PR?

lib/internal/modules/run_main.js Outdated Show resolved Hide resolved
@wesleytodd wesleytodd force-pushed the import-maps branch 4 times, most recently from b0a4cbb to a59b5e5 Compare November 17, 2023 17:42
@guybedford
Copy link
Contributor

Thank you @wesleytodd for pushing this again, I was hoping to do a review here when it is ready, but to comment on a couple of the recent points:

For WPT there are data-driven tests in https://github.com/web-platform-tests/wpt/tree/master/import-maps/data-driven/resources which are not at all HTML based.

Can we please unmark the WPT checkbox until all those data-driven tests pass?

If we put import maps on context will they be regarded as mutable? Mutability of import maps when exposing them to userland is an important question to answer there, since there are fundamental resolver concerns around mutability for import maps, which are hard to answer if we need to address that question now.

@wesleytodd
Copy link
Member Author

I was hoping to do a review here when it is ready

Thanks, I was going to ping you directly when I had what I considered a "ready" state, but thanks for chiming in and if there is anything I can do to ease the review let me know.

Can we please unmark the WPT checkbox until all those data-driven tests pass?

Done.

or WPT there are data-driven tests in

I will look into these the next time I have to work on this. Should be this week sometime.

will they be regarded as mutable?

Yeah, this is my concern for the loaders accessing it as well. I am not sure I understand the use case we for this, and IMO it is easier to expose it later than to try and backtrack around folks doing things we didn't expect.

which are hard to answer if we need to address that question now.

I think we do need to address them. I would rather not expose them until we had a clear understanding of why so we can write good tests of the use case. Is there a good way to avoid exposing them while also not putting it on context?

@wesleytodd
Copy link
Member Author

Wanted to make sure I captured this here, from a slack convo with @guybedford:

We want to make the imports and scopes getters return plain objects which are converted back from the safe maps into their original format. This will simplify the tests, better meet end user expectations, and also avoid any mutation issues by exposing this on context.

@mtrackeros

This comment was marked as spam.

@dburles
Copy link

dburles commented May 31, 2024

Has there been any thought to making the parser API public? I think that would make sense.

@wesleytodd
Copy link
Member Author

Has there been any thought to making the parser API public? I think that would make sense.

That was not on the initial plan, but I would be interested in understanding your use case. IMO the best way to do this would be to publish that stuff as a package and then either vendor it in or run changes back to the separate project later. That said, it is a lot more effort so I would want to know what folks want to use it for before making any decisions on it.


As a separate note, I have not been able to work on finishing up this PR for a while now. I would like to, but work got busy and so did some of my other OSS work. If anyone wants to pair a bit to get this over the finish line I am more than happy to do that. Feel free to reach out here or in the OpenJS #nodejs-loaders channel to discuss.

@dburles
Copy link

dburles commented Jul 5, 2024

I'd like to be able to use it as part of static analysis of the module graph.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
esm Issues and PRs related to the ECMAScript Modules implementation. lib / src Issues and PRs related to general changes in the lib or src directory. loaders Issues and PRs related to ES module loaders module Issues and PRs related to the module subsystem. needs-ci PRs that need a full CI run.
Projects
None yet
Development

Successfully merging this pull request may close these issues.