Add an API for importmaps

Created on 1 November 2023, 8 months ago
Updated 15 January 2024, 5 months 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 less than a minute ago

Created by

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

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

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.

Production build 0.69.0 2024