Problem/Motivation

Importmaps allow a browser to resolve naked ES imports.

e.g.

import React from 'react';

Ordinarily a browser doesn't understand how to resolve that, but with an import map it can.

However, you can only have one <script type="importmap"> element on a page, so therefore it makes sense to have an API in core to support this.

Adding this API will allow us to do more interesting things with JavaScript. E.g. consider two modules that rely on a JavaScript library. If both of them bundle the same library into their code, there will be two instances of this in the page.

A concrete need for this is for the React module and the Gutenberg and Decoupled layout builder modules. Both of them need React loaded into the page.

Without importmaps, both of them will need to hard-code a reference to the URL of the bundled react libraries provided by the React module.

With importmaps, both of them can configure their bundler (Vite/Webpack) to mark React as external - this will result in the bundler leaving import React from 'react' in the bundled code.

Then the React module could specify an importmap via this new API and Drupal would take care of emitting an <script type="importmap"> into the page.

Proposed resolution

Add an API to core for importmaps. Base this off the Importmaps contrib module.

At present the importmaps module uses hook_page_top to unconditionally add the <script type="importmap"> tag to the page. If we moved this to core, we could integrate with libraries.yml and only add it based on e.g. libraries dependencies.

Remaining tasks

Agree we want to do this.
Adapt the code in importmaps (e.g. YML discovery, plugin manager).
Replace the hook_page_top with an implementation that is built into the asset renderer pipeline and can detect importmap entries to add based on metadata such as libraries.yml. For example the YML file could be extended to list libraries that should trigger the entry being added to an importmap. The asset renderer could collate attached libraries and filter out the importmap plugin definitions accordingly. If any were found, it would emit the tag, if not it would not.

User interface changes

API changes

New plugin manager and YML plugin discovery for importmaps

Data model changes

Release notes snippet

CommentFileSizeAuthor
#34 3398525-nr-bot.txt90 bytesneeds-review-queue-bot

Issue fork drupal-3398525

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

larowlan created an issue. See original summary.

andy-blum’s picture

Issue tags: -JavaScript +JavaScript

I love this idea, and think an addition to the libraries.yml would be the perfect place to declare es module usage. For example from the react module's current usage of the importmaps module:

react.libraries.yml

react:
  js:
    'js/dist/react.js': { minified: true }

react-dom:
  js:
    'js/dist/react-dom.js': { minified: true }
  dependencies:
    - react/react

react.importmaps.yml

react:
  path: js/dist/react.js
react-dom:
  path: js/dist/react-dom.js
react/jsx-runtime:
  path: js/dist/react-jsx-runtime.js

To kick off some discussion here, re-imagining this as a single .libraries.yml file could become one of several options, each with their own benefits/drawbacks.

Option 1: Adding importpath info to a JS asset

react:
  js:
    'js/dist/react.js': { minified: true, esmodule: 'react' }

react-dom:
  js:
    'js/dist/react-dom.js': { minified: true, esmodule: 'react-dom' }
  dependencies:
    - react/react

Pros:

  • Minimal addition to the yaml structure
  • Tight coupling to current libraries

Cons:

  • JS files don't necessarily need to be loaded on a page for the import map to work, but this approach would add them into the DOM 100% of the time

Option 2: Adding importpath info to an individual library

react:
  js:
    'js/dist/react.js': { minified: true }
  imports:
    'react': 'js/dist/react.js' 

react-dom:
  js:
    'js/dist/react-dom.js': { minified: true }
  imports:
    'react-dom': 'js/dist/react-dom.js'
  dependencies:
    - react/react

Pros:

  • Moderate coupling to current libraries
  • Files that exist only as "sources" of modules won't get unnecessarily loaded to the page

Cons:

  • Files that both need to both run *and* operate at a module's source have to be duplicated

Option 3: Adding importpath info as a separate non-library top level item

react:
  js:
    'js/dist/react.js': { minified: true }

react-dom:
  js:
    'js/dist/react-dom.js': { minified: true }
  dependencies:
    - react/react

IMPORT_PATH:
  'react': 'js/dist/react.js'
  'react-dom': 'js/dist/react-dom.js'

Pros:

  • Offers the greatest flexibility

Cons:

  • Loosest coupling to libraries
  • Requires a "magic" library name
larowlan’s picture

Issue tags: -JavaScript +JavaScript

Option 4

react-dom:
  importmaps:
    react: 'js/dist/react-dom.js'

ie another top level entry that isn't css/js

Because we really don't want the file attached as a regular js script. We want the browser to load it on demand.

Then another module that needs react-dom

my-amazing-library:
  js:
    'js/dist/amazing-library.js': { minified: true, attributes: {type: 'module' }}
  dependencies:
    - react/react-dom
larowlan’s picture

Just realised there was no code in the importmaps contrib project, pushed that up

andy-blum’s picture

I think your option 4 is the same as my option 2, though?

larowlan’s picture

Ah, I saw you still had js: in option 2 so thought it was different. If you're saying the same think I think we should remove the js entry in it to make it clear that that the file needs to be loaded by the browser from the importmap instead of by a script tag/Drupals standard asset rendering

Do you have a preferred option? I think option 2 with the above caveat gets us closest to the intent here

longwave’s picture

Symfony's AssetMapper component provides support for importmaps, it doesn't look like we can fully leverage this component directly, but we could learn from it and perhaps use some of their code.

https://symfony.com/doc/current/frontend/asset_mapper.html

prudloff’s picture

I started a POC contrib module that implements this: https://www.drupal.org/project/importmap

It uses data attributes to avoid breaking the libraries.yml YAML schema. But if core implemented this, it could of course be with new properties in the YAML.

larowlan’s picture

Hmm, thanks, but feels odd to create a competing module one week after I made one at drupal.org/project/importmaps - we should be collaborating (I mentioned it in comment 4)

nod_’s picture

in classic drupal fashion we're jumping the gun to implementation before getting the scope cleared up :)

I'm leaning towards option 2 with the "importmap" key to match what it's called in html.

That's why I'm very satisfied we have peast in core now, we can actually look into the js files and extract stuff. Ideally I'd like to dynamically build the dependencies from what's declared in the js file itself to avoid having to double-declare dependencies.

Closed #3331393: Provide management/mechanism for JS/ES6 importmap since this one has more activity and a better issue summary.

prudloff’s picture

Hmm, thanks, but feels odd to create a competing module one week after I made one at drupal.org/project/importmaps - we should be collaborating (I mentioned it in comment 4)

Sorry, I did not see your module. I was initially following the duplicate issue and not this one.
I created an issue about merging the two modules: #3399978: Merge with importmap module

in classic drupal fashion we're jumping the gun to implementation before getting the scope cleared up :)

We had a concrete need for this and thought it would be better to publish it as a contrib module, to get ideas rolling.
But I agree that we need to think about the best way to declare imports before adding this to core.

My initial thought was that it would make sense to have this in the library definitions since these are in fact libraries. But I agree that we would almost never need to load a JS file both as an import and with a script tag, so I guess it can make sense to a have a separate top-level key (or even a separate YAML file) for this.

We also might want to support scopes.

prudloff’s picture

That's why I'm very satisfied we have peast in core now, we can actually look into the js files and extract stuff. Ideally I'd like to dynamically build the dependencies from what's declared in the js file itself to avoid having to double-declare dependencies.

I don't think this would work with dynamic imports.

But I am not entirely sure we have to care about dependencies here.
Having a module in the import map has no effect if it is not imported by some other JS module. So we could always include every declared module in the import map and don't bother managing dependencies.

catch’s picture

It is only tangentially related but there is also #3366561: Support preloading of fonts in the libraries API where the main barrier is figuring out the definition format.

catch’s picture

Regardless of the format, anything marked for import needs to be completely excluded from Drupal's asset rendering (and aggregation if it's enabled). Drupal just should not be loading it at all; instead just providing the markup so it can be loaded on demand via the browser.

A top-level entry in libraries would not hurt here, because it then wouldn't be possible to mix imported vs. not scripts in a single library definition.

Can we be certain that we won't get a mix of libraries relying on importmaps vs. libraries relying on library dependencies? That seems like it would not be good.

An issue with taking these files out of the asset rendering system is that they won't get the asset query string or any version information etc. appended unless we handle that too - so maybe we would need to re-implement/re-use at least that bit for cache busting.

larowlan’s picture

Assigned: Unassigned » larowlan
larowlan’s picture

Assigned: larowlan » Unassigned

Made a start on this, pausing till next week.

Have added some fake stuff to core.libraries for testing sake and am seeing the console.log

This doesn't work with big pipe and we'll likely have similar issues with Ajax.

In my testing, there is no API to dynamically modify the importmap as new files are loaded.

Trying to remove the import map and add a new one (e.g. in an AjaxCommand) results in

Multiple import maps are not allowed.

Trying to rewrite the contents of the existing script tag results in the browser not detecting any new additions.

So I think we're probably going to need to build the importmap based on all known importmaps and output that.

Otherwise if bigpipe or an ajax response add new JS that requires the importmap we're out of luck

I'll work on that and test coverage next week

larowlan’s picture

Got confirmation that there is no API for dynamic imports - https://github.com/WICG/import-maps/issues/92

So will continue with loading them all upfront

nod_’s picture

If we hit some limits of the import map api, An alternative solution could be reaching into the js source and rewrite import paths at process/bundle time.

I'm worried that the import map will end up being massive for something like Drupal. Maybe not, we'll see when we get there I guess.

Do we need to worry about security if we have an import map with everything? I know we don't have access rules on js assets by design so it shouldn't be an issue but you never know.

prudloff’s picture

Symfony has a FAQ page that points to this example: https://ux.symfony.com/
It is a production website with ~100 files in the import map.

christianadamski’s picture

Anybody still working on this? Maybe during Barcelona?

larowlan’s picture

Issue summary: View changes

Discussed #18 with Lauri. Import map support is a must have for future CKEditor loading changes and will likely be needed by Experience Builder to support modules adding functionality.

On that basis we felt it would be appropriate to simplify this and move away from defining these in libraries.yml as they don't need any of the features like dependencies, weights etc. We have to load all of the importmaps on the first page load - #18 forces our hand.

I'll work to refactor the existing branch and update the issue summary.

larowlan’s picture

Status: Active » Needs review

quietone made their first commit to this issue’s fork.

quietone’s picture

The change record branch and version info needs to be updated as well as the text which refers to 10.3. Should the change record introduce the new api?

brianperry’s picture

Really excited about this feature!

Would probably outside of the scope of this issue if there is any follow up, but after reviewing the in-progress implementation it got me thinking about how this would apply to single directory components.

My best guess is that you'd currently have to:

* In the component.yml for the SDC itself, override the library to add the module attribute for the js asset:

  js:
    my-component.js: { attributes: { module: true } }

* Add an importmap.yml file at the root of the module or theme that defines the single directory component. In that file you'd need to reference the library by the name auto-generated by sdc (core/components.[THEME_OR_MODULE_NAME]--[COMPONENT_NAME_WITH_DASHES])

That would at least make adding js from a component to the import map possible on the current feature branch, which is great. But it feels like there could be some syntactic sugar in the sdc module to make this easier. One of the great things (imho) about SDCs is that a developer can write a js file and not worry about defining a library. Would love to see that concept extend to import maps.

It seems like the `hook_importmap_alter` added here would make it pretty straightforward to implement this in the sdc module once it is determined what could be used to communicate that js should be included in an import map / handled as a module.

Thoughts?

larowlan’s picture

I'm not sure where SDC fits in with regards to exposing importmap entries - I guess if you had one component that had an ES module that you also needed to import in other places you might want to do that. But really only if another module or theme needed to import it. In all other cases it would be fine to let your bundler do code-splitting and have the consuming code import it from the chunks split during bundling.

The importmap functionality is really only when you have unknown consumers. E.g. React module doesn't know what themes/modules will need to load react. Experience builder doesn't know what contrib modules will want to ship React powered things for field widgets.

But I think it would be worth exploring in a follow-up for sure.

larowlan’s picture

Updated the change record and addressed the feedback. I think the only thing remaining from reviews are follow ups as follows:

* How can SDC opt in
* Is there a nicer way for a .libraries.yml file to declare itself a module - than the current attributes approach

mortona2k’s picture

There's been some discussion on asset bundling, importmap, and SDC in the Frontend Builder Initiative slack channel.

@brianperry can you elaborate a little more on the use case (In slack if not here)? I hit some things that might be relevant, but I'm not totally sure.

One example is a SDC that imports a js module from npm, like swiper. If a module uses it as a library, and my theme component bundles it, is there some way put the bundled version in the importmap and override the module using the library? I'm not sure if this even makes sense though, or if it just makes more sense to set up front end bundling in the root and override the module to use your library.

There's some overlap in manifest.json files for bundled assets and importmaps. I'm not sure if there's any benefit to using a global importmap vs individually bundled or even globally bundled assets.

Another issue is referencing images or icons in the theme directory. When I bundle in the theme dir, the paths are relative to the theme root and drupal uses the Vite module to translate them to the correct path. Since we don't have libraries defined for images, there's some extra config needed to juggle the paths that I haven't figured out yet. I'm not sure if images in the importmap are possible or would just cause more problems.

brianperry’s picture

> The importmap functionality is really only when you have unknown consumers. E.g. React module doesn't know what themes/modules will need to load react.

I get that this is the primary use case driving this issue, but I don't know that I agree with this statement broadly. Import maps also simplify the process of using esmodule imports in unbundled code. It would be possible for example to create a small utility in a vanilla js file that is exported as a module, which could then be imported in other js module files where needed. You don't necessarily need import maps to do that, but Drupal's library system / asset handling can make it tricky to negotiate module paths without something like an import map.

In practice most complex projects will use a bundler, but this pure esm approach seems like something that could grow over time.

Symfony's Asset Mapper component uses import maps extensively. In fact, they seem to recommend asset mapper as a default

Elsewhere in the FE ecosystem the Islands Architecture popularized by Astro is built on top of esm imports. Their `client:*` directives result in a custom element that loads the necessary javascript via a dynamic import.

While this issue sparked questions related to SDCs since I have been working with them lately, I'm also following this because I've been experimenting with a Drupal Islands Architecture implementation. Based on experiments thus far, I think a Drupal import map implementation would be needed to make this viable. The current POC builds its own import map. It isn't really packaged up for easy consumption at the moment, but I'll try to follow up here when that changes.

> @brianperry can you elaborate a little more on the use case (In slack if not here)? I hit some things that might be relevant, but I'm not totally sure.

@mortona2k I'll say hi in the slack channel, but I'm mostly thinking about the un-bundled use case above.

larowlan’s picture

In practice most complex projects will use a bundler, but this pure esm approach seems like something that could grow over time.

Yeah it would allow us to move away from everything being global

But in practice I think there are concerns about the resource waterfall impact on performance

christianadamski’s picture

Just in case this is in any way helpful:
Geolocation module v4 moved to almost complete ES6 modules. A lot of them. And for lack of importmap support its all handled by dynamic imports. And they spread out over 10 sub-modules.

So, this is already out there and I would be more than happy to move away from keeping track of all the relative paths and to an importmap mechanism provided by core.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new90 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

effulgentsia’s picture

However, you can only have one <script type="importmap"> element on a page, so therefore it makes sense to have an API in core to support this. In addition you cannot dynamically update this, so e.g. things like Ajax responses cannot add new importmaps. As a result we need to declare all possible import maps on the page.

A couple months ago, Multiple import maps was merged into the HTML spec. Chrome will support this in v133. TBD when other browsers will catch up.

wim leers’s picture

larowlan’s picture

With experience builder I went with a much simpler approach which is to support #attached of type 'import_maps' and then have an attachments processor that consolidated all them and made a single <script type="importmap">

Might be worth considering here too

See https://git.drupalcode.org/project/experience_builder/-/merge_requests/687

catch’s picture

@larowlan how would #37 work with big pipe and AJAX?

larowlan’s picture

Good point. Experience builder is only using it in an IFrame so sidesteps that issue

@effulgentsia pointed out that support for multiple importmao scripts is coming to browsers which would also help, but that's a way off yet

So probably worth ignoring the XB approach for now

effulgentsia’s picture

multiple importmap scripts is coming to browsers which would also help, but that's a way off yet

I don't think it's that far off. It's in Chrome 133 and Safari HEAD (Tech Preview). https://bugzilla.mozilla.org/show_bug.cgi?id=1916277 is the issue for Firefox, they're just choosing to block it on landing import map integrity verification first.

catch’s picture

We won't be able to require multiple import maps until Firefox ESR supports it, but I think if we're only adding the API for contrib to use initially, we could assume multiple import map support and just not use it in core until it's available in ESR.

larowlan’s picture

Issue summary: View changes

Was was marked as fixed today https://github.com/WICG/import-maps/issues/92

larowlan’s picture

Priority: Normal » Critical
Issue tags: +11.3.0 release priority
Related issues: +#3527914: Use UMD installation method for CKEditor5

This blocks #3527914: Use UMD installation method for CKEditor5 which is critical, so marking this as such too

xjm’s picture

prudloff’s picture

Not sure this was discussed yet: browser cache invalidation can be a challenge.
For files in the importmap, we can add a GET parameter (generated with AssetQueryString) to invalidate browser cache when the file changes:

"swiper": "\/themes\/custom\/front\/node_modules\/swiper\/swiper.mjs?sxoxj0"

However if this file then does another import with a relative path:

export {default as Virtual} from './virtual.mjs';

We have no way to invalidate browser cache when this other file changes.

catch’s picture

Firefox still doesn't support multiple import maps:

https://caniuse.com/mdn-html_elements_script_type_importmap_multiple_imp...

It looks like it's being tracked in https://bugzilla.mozilla.org/show_bug.cgi?id=1916277

However per #41 I still think we should add support for multiple import maps, and delay usage of it until Firefox ESR has support.

effulgentsia’s picture

There's a comment in that Mozilla issue that mentions https://github.com/guybedford/es-module-shims, a polyfill for multiple import maps (and other import map features) for browsers that don't support it natively.

longwave’s picture

Firefox still doesn't have support for multiple import maps, but the end of the year is getting closer and we will need this for any CKEditor release in 2026 for #3527914: Use UMD installation method for CKEditor5

Given that most other browsers do support multiple import maps I think we should go ahead and use the polyfill here.

longwave’s picture

Rebased against 11.x, the performance test changes were unmergeable and will need redoing, otherwise the only big change was the conversion of system_page_top() to OOP.

vijaycs85 made their first commit to this issue’s fork.

vijaycs85’s picture

Issue tags: +Vienna2025
vijaycs85’s picture

The core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php is failing with below error:

❯ ddev phpunit core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php
PHPUnit 11.5.22 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.3.23
Configuration: /var/www/html/ddev.phpunit.xml

.............................                                     29 / 29 (100%)

Time: 00:03.834, Memory: 66.00 MB

There was 1 PHPUnit test runner warning:

1) Test results may not be as expected because the XML configuration file did not pass validation:

  Line 22:
  - Element 'phpunit', attribute 'failOnPhpunitWarning': The attribute 'failOnPhpunitWarning' is not allowed.


OK, but there were issues!
Tests: 29, Assertions: 52, Warnings: 5, PHPUnit Deprecations: 1.
Failed to run phpunit core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php: exit status 1

This is triggered by the 11.x change to enable the PHPUnit errors. mostly it means we need to make sure all the re-rolling MRs with new tests would face this issue.

prudloff’s picture

Something else that was not discussed I think: importmaps might break with strict CSPs that don't allow unsafe-inline.
The import needs to be allowed with its hash or a nonce.

webflo’s picture

I tried to use import-maps today and experienced some incompatibility issues in Firefox. The Import-Map was defined after a script with type="module".

The error message was: "Import maps are not allowed after a module load or preload has started."

I think import-maps should move to HTML head. Maybe build it in hook_page_attachments?

catch’s picture

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

catch’s picture

Just double-checked firefox again and nothing has changed since #48 https://caniuse.com/mdn-html_elements_script_type_importmap_multiple_imp... so +1 to adding the polyfill.

nod_’s picture

Something to keep an eye on too: https://lea.verou.me/blog/2026/external-import-maps-today/ and the corresponding lib https://github.com/nudeps/nudeps

godotislate’s picture

Tagging this as a D12 release priority.

longwave’s picture

Looks like multiple import maps will be supported in Firefox 150: https://bugzilla.mozilla.org/show_bug.cgi?id=1916277

This is scheduled for release in a month from now, but will initially be marked as experimental. We have to wait for Firefox ESR anyway, where hopefully it will be enabled by default, but no guarantees. The next ESR release is Firefox 153 which will be on 21 July 2026.

effulgentsia’s picture

Looking at recent comments on that Firefox issue, it looks like even with 150, it might be considered experimental and gated behind an off-by-default preference flag. In which case, we'll still need the polyfill until Firefox releases the feature ungated.

[Edit: sorry, I see you mentioned that in #61 as well. I missed that when I first read that comment.]

effulgentsia’s picture

https://bugzilla.mozilla.org/show_bug.cgi?id=2021012 is the new Firefox ticket for enabling it by default.

catch’s picture

Priority: Critical » Major
Issue tags: -12.0.0 release priority

Now that #3527914: Use UMD installation method for CKEditor5 went in without this, I don't think it's critical, but it would still be good to support, so moving to 'major'.