Add an API for importmaps

Created on 1 November 2023, about 1 year ago

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

✨ Feature request
Status

Active

Version

11.0 πŸ”₯

Component
JavascriptΒ  β†’

Last updated about 16 hours ago

Created by

πŸ‡¦πŸ‡ΊAustralia larowlan πŸ‡¦πŸ‡ΊπŸ.au GMT+10

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @larowlan
  • πŸ‡ΊπŸ‡ΈUnited States andy-blum Ohio, USA

    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
  • πŸ‡¦πŸ‡ΊAustralia larowlan πŸ‡¦πŸ‡ΊπŸ.au GMT+10

    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
    
  • πŸ‡¦πŸ‡ΊAustralia larowlan πŸ‡¦πŸ‡ΊπŸ.au GMT+10

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

  • πŸ‡ΊπŸ‡ΈUnited States andy-blum Ohio, USA

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

  • πŸ‡¦πŸ‡ΊAustralia larowlan πŸ‡¦πŸ‡ΊπŸ.au GMT+10

    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

  • πŸ‡¬πŸ‡§United Kingdom longwave UK

    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

  • πŸ‡«πŸ‡·France prudloff Lille

    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.

  • πŸ‡¦πŸ‡ΊAustralia larowlan πŸ‡¦πŸ‡ΊπŸ.au GMT+10

    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)

  • πŸ‡«πŸ‡·France nod_ Lille

    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 ✨ Provide management/mechanism for JS/ES6 importmap Closed: duplicate since this one has more activity and a better issue summary.

  • πŸ‡«πŸ‡·France prudloff Lille

    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: ✨ Provide management/mechanism for JS/ES6 importmap Closed: duplicate

    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 YAML file for this.

  • πŸ‡«πŸ‡·France prudloff Lille

    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.

  • πŸ‡¬πŸ‡§United Kingdom catch

    It is only tangentially related but there is also ✨ Support prefetch/preload/dns-prefetch in the libraries API Active where the main barrier is figuring out the definition format.

  • πŸ‡¬πŸ‡§United Kingdom catch

    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.

  • Assigned to larowlan
  • πŸ‡¦πŸ‡ΊAustralia larowlan πŸ‡¦πŸ‡ΊπŸ.au GMT+10
  • Issue was unassigned.
  • πŸ‡¦πŸ‡ΊAustralia larowlan πŸ‡¦πŸ‡ΊπŸ.au GMT+10

    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

  • πŸ‡¦πŸ‡ΊAustralia larowlan πŸ‡¦πŸ‡ΊπŸ.au GMT+10

    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

  • πŸ‡«πŸ‡·France nod_ Lille

    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.

  • πŸ‡«πŸ‡·France prudloff Lille

    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.

  • πŸ‡©πŸ‡ͺGermany christianadamski Berlin, Germany

    Anybody still working on this? Maybe during Barcelona?

  • πŸ‡¦πŸ‡ΊAustralia larowlan πŸ‡¦πŸ‡ΊπŸ.au GMT+10

    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.

  • Merge request !9912Add an API for import maps β†’ (Open) created by larowlan
  • πŸ‡¦πŸ‡ΊAustralia larowlan πŸ‡¦πŸ‡ΊπŸ.au GMT+10
  • Pipeline finished with Failed
    3 months ago
    Total: 213s
    #317801
  • Pipeline finished with Canceled
    3 months ago
    Total: 69s
    #317810
  • Pipeline finished with Failed
    3 months ago
    Total: 732s
    #317814
  • Pipeline finished with Failed
    3 months ago
    Total: 542s
    #317825
  • First commit to issue fork.
  • πŸ‡³πŸ‡ΏNew Zealand quietone

    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?

  • πŸ‡ΊπŸ‡ΈUnited States brianperry

    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?

  • πŸ‡¦πŸ‡ΊAustralia larowlan πŸ‡¦πŸ‡ΊπŸ.au GMT+10

    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.

  • πŸ‡¦πŸ‡ΊAustralia larowlan πŸ‡¦πŸ‡ΊπŸ.au GMT+10

    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

  • πŸ‡ΊπŸ‡ΈUnited States mortona2k Seattle

    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.

  • πŸ‡ΊπŸ‡ΈUnited States brianperry

    > 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.

  • πŸ‡¦πŸ‡ΊAustralia larowlan πŸ‡¦πŸ‡ΊπŸ.au GMT+10

    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

  • πŸ‡©πŸ‡ͺGermany christianadamski Berlin, Germany

    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.

  • Pipeline finished with Failed
    3 months ago
    Total: 719s
    #326323
  • Status changed to Needs work 2 months ago
  • 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.

  • πŸ‡ΊπŸ‡ΈUnited States effulgentsia

    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.

  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί
Production build 0.71.5 2024