Allow code components to import from npm packages

Created on 19 January 2025, 3 months ago

Overview

There's a lot of great libraries out there in the React ecosystem. It would be great for (in-browser-editable) Code Components to be able to use them. For example, to use a Radix UI Accordion:

import * as Accordion from "@radix-ui/react-accordion";

export default function MyComponent({ ... }) {
  return (
    <Accordion.Root ...>
      ...
    </Accordion.Root>
  )
}

Proposed resolution

Because ES module CDNs that support HTTP/2, such as https://esm.sh/, exist, we don't need to do any bundling to support this. Instead we can use an import map. But what we need to know is what packages are needed (both the ones directly imported like @radix-ui/react-accordion above, and their dependencies) across all of the code components. I think we can use @jspm/generator in the browser within XB to figure that out. https://jspm.org/getting-started has examples on how to use the CLI, but since we'd be running it in the browser, we'd need to go via the lower-level generator API.

I think the outline of the full solution would be:

  1. Extend โœจ Config entity for storing code components Active to include some additional properties: js_imports and js_import_dependencies. I think each of these could just be a sequence of MODULE@VERSION strings. I don't think we should store paths to any specific CDN here. For example, for the accordion example above, this would be:
    js_imports:
      - @radix-ui/react-accordion@1.2.2
    js_import_dependencies:
      - @radix-ui/primitive@1.1.1
      - @radix-ui/react-collapsible@1.1.2
      - @radix-ui/react-collection@1.1.1
      ...
    
  2. When a code component's code is edited within XB, use @jspm/generator to generate the above and save that in the code component's config entity.
  3. When rendering a page, add an import map (possibly use https://www.drupal.org/project/importmaps โ†’ if that's helpful) containing all of the module specifiers across all code components.
  4. For now, we can hard-code a CDN (e.g., https://esm.sh/) to map them to. In the future, we can make that CDN configurable.
  5. For now, I recommend not adding the package versions to the generated import map, so that the latest version of each package gets used. That sidesteps the issue of what to do when different code components reference different versions of the same package. In the future, we'll need to figure out how to resolve that as well as what kind of a UI to provide to the code component author to update to later versions, but that's out of scope for this issue.

User interface changes

โœจ Feature request
Status

Active

Version

0.0

Component

Theme builder

Created by

๐Ÿ‡บ๐Ÿ‡ธUnited States effulgentsia

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

Comments & Activities

  • Issue created by @effulgentsia
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States effulgentsia
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States effulgentsia

    Up until now, the XB team has been following a pseudo-scrum/pseudo-kanban process, but we're now shifting into more conventional scrum. We started a new 2-week sprint last Thursday (Jan 16). I'm tagging our current sprint's issues for visibility. This one I'm tagging with "spike" rather than "sprint", because our goal isn't to complete the implementation of it this sprint, but to figure out if the proposed approach is viable or if we need to change it in some way.

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia larowlan ๐Ÿ‡ฆ๐Ÿ‡บ๐Ÿ.au GMT+10

    @effulgentsia do you have something in mind to parse the JS and flag the imports - e.g. an AST/parsing library?

    If not, I can look into something like babylon, which is the parser used by babel.

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States effulgentsia

    Does @jspm/generator not do that? The CLI's jspm link can be pointed to an HTML file, but not sure whether it's only the CLI that can parse or if the generator library can as well.

    Babylon seems fine if we need a separate parser. In case it helps, we'll be using https://swc.rs/docs/usage/wasm to compile. I don't know if it has any API to access the imports, but perhaps it makes sense to parse the compiled JS to find the imports, in which case something lightweight like Acorn might suffice?

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia larowlan ๐Ÿ‡ฆ๐Ÿ‡บ๐Ÿ.au GMT+10

    I have a POC of using babylon to parse out imports here

    Paste this in to the text box

    import * as Accordion from "@radix-ui/react-accordion";
    
    export default function MyComponent() {
      return (
        <Accordion.Root>
        </Accordion.Root>
      )
    }
    

    I'll continue with this tomorrow by adding in jspm/generator code to see if we can get an importmap

  • ๐Ÿ‡ง๐Ÿ‡ชBelgium wim leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ


    Ah, this is literally point 1 in the issue summary. But I'm surprised that #3499927 was created without taking this into account and this issue's title doesn't convey that it's a spike for future functionality and it doesn't link that issue. ๐Ÿ˜…

    1. For now, we can hard-code a CDN (e.g., https://esm.sh/) to map them to. In the future, we can make that CDN configurable.

    Blindly trusting a single external site is a serious risk: performance, reliability but certainly also security! Especially when they're maintained by a single person. I'd not want to do this unless we at minimum do https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implemen...

    But apparently supporting this for import maps is pretty much brand new: https://jspm.org/js-integrity-with-import-maps, published August 5, 2024, released in Chrome 127. But that's only got 78.7% global support right now; it's not supported at all in Firefox for example: https://caniuse.com/mdn-html_elements_script_type_importmap_integrity

  • ๐Ÿ‡ซ๐Ÿ‡ฎFinland lauriii Finland

    Removing the security tag because it makes this issue listed on the project page as as a publicly disclosed security issue which this isn't since the security implication has to do with what's being done in this issue.

  • ๐Ÿ‡ง๐Ÿ‡ชBelgium wim leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    Removing the security tag because it makes this issue listed on the project page as as a publicly disclosed security issue which this isn't since the security implication has to do with what's being done in this issue.

    ๐Ÿคฏ Since when is that the case?! ๐Ÿคช

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States effulgentsia

    https://github.com/mozilla/standards-positions/issues/1010 is the issue to follow for updates on Firefox's plans to support importmap integrity.

  • ๐Ÿ‡ณ๐Ÿ‡ฑNetherlands balintbrews Amsterdam, NL

    #7 โœจ Allow code components to import from npm packages Active :

    But I'm surprised that #3499927 was created without taking this into account and this issue's title doesn't convey that it's a spike for future functionality and it doesn't link that issue. ๐Ÿ˜…

    I would recommend that we land โœจ Config entity for storing code components Active with its current scope to unblock the handful of issues it blocks. Given that this issue is in the spike stage, I would also recommend waiting with creating additional issues or adding relationships to existing ones before we know what our approach will be. I'm happy to update ๐ŸŒฑ [Meta] Plan for code components Active once we're there.

  • ๐Ÿ‡ณ๐Ÿ‡ฑNetherlands balintbrews Amsterdam, NL

    The POC in #6 โœจ Allow code components to import from npm packages Active is really cool! ๐Ÿ‘๐Ÿป

    I have a suggestion to also explore this from a different angle. Instead of parsing imports from the code, like it's done in #6 โœจ Allow code components to import from npm packages Active , what if we maintained a list of supported packages, and designed UI to add import statements to a code component? We could even go as far as showing/hiding the import statements in the code editor, e.g. displaying them in a collapsible, non-editable area.

    What does this approach get us? Most importantly, it addresses Wim's concerns ( #7 โœจ Allow code components to import from npm packages Active ) about performance, reliability, and security. A curated list of packages that we can include in our app's bundle. (We really need to start thinking about code splitting, but let's leave that as a future problem to solve. ๐Ÿ™‚)

    Additionally, it might even be a nice UX to be able to browse a list of quality, recommended packages to use in your components. It is not the freedom to use anything from npmjs.org, but this might be fine for the target audience of code components. Radix Primitives would be a great start, they're commonly used, even tools like v0.dev generate code that (indirectly) depends on them. We could then open this up for modules, as a new extension point, to provide new sets of packages.

  • ๐Ÿ‡ง๐Ÿ‡ชBelgium wim leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    #12++ โ€ฆ and that ties in elegantly with your proposal at #3499933-18: Storage for CSS shared across in-browser code components (and PageTemplate config entities in the distant future) โ†’ to introduce config-defined in-browser code libraries โ€” that "list of quality, recommended packages" could be defined entirely as one or more in-browser asset libraries.

    P.S.: literally yesterday:

    The specification for this has landed in HTML and the implementation landed in Chromium and WebKit.

    โ€” https://github.com/mozilla/standards-positions/issues/1010#issuecomment-...

  • ๐Ÿ‡ง๐Ÿ‡ชBelgium wim leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    Related: โœจ Add an API for importmaps Active โ€” being worked on by @larowlan :)

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia larowlan ๐Ÿ‡ฆ๐Ÿ‡บ๐Ÿ.au GMT+10
  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia larowlan ๐Ÿ‡ฆ๐Ÿ‡บ๐Ÿ.au GMT+10

    I have a POC going in this commit

    The issue is we can't use this import map in the current page because you can't change the import map in the current page at run-time
    But I guess if we make the preview component use an iframe, we could inject the import map into there.

    Here's a video. I think this is enough to conclude the spike.

    Findings:

    • Parsing of HTML in @jspm/generator wasn't working because it doesn't like JSX
    • I was unable to get acorn + acorn-jsx to parse the example component (below)
    • Using babylon (the parser under the hood of babel) gave us the nice advantage of syntax checking

    Example component text I was parsing

    import * as Accordion from "@radix-ui/react-accordion";
    
    export default function MyComponent() {
      return (
        <Accordion.Root>
        </Accordion.Root>
      )
    }
    
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium wim leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    @balintbrews: AFAICT you intend this to not block โœจ Storage for global CSS Active , right? (I tried to guess your rationale over at #3499933-31: Storage for CSS shared across in-browser code components (and other use cases in the future) โ†’ .)

  • ๐Ÿ‡ฆ๐Ÿ‡บAustralia larowlan ๐Ÿ‡ฆ๐Ÿ‡บ๐Ÿ.au GMT+10

    I think there's also a world where we use jspm in the editor to build an import map but then have Drupal fetch the resultant files, validate their resource integrity and serve them locally from inside the assets:// stream wrapper (aka sites/default/files/js or equivalent).

    Could even be lazy like core's asset optimisation so that we only fetch it when it is requested

  • ๐Ÿ‡ณ๐Ÿ‡ฑNetherlands balintbrews Amsterdam, NL

    Let me try to summarize where we are, and suggest next steps.

    What's been proven so far:

    1. We can parse import statements from the user-provided code using babylon, the parser of [Babel](https://babeljs.io).
    2. By passing those import statements to @jspm/generator we can generate an import map in the browser.

    What I suggested in #12 โœจ Allow code components to import from npm packages Active is that instead of doing 1) above, we may want to consider a different UX where we would allow code component authors to pick from a predefined list, and generate the import statements based on their selection.

    As @larowlan pointed out in #18 โœจ Allow code components to import from npm packages Active , the predefined list idea can still be relevant even if we decide to parse import statements from the code. This would also nicely support the use case of โœจ CLI tool to manage code components Active .

    I think the immediate next step would be to decide on the UX:

    A) Do we parse import statements from the user-provided code? Or B) do we provide UI for adding imports?

    This could influence our decision on how we approach storing the import statements โ€” if at all.

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States effulgentsia

    Which would be better for enabling people to generate code components with AI , or by exporting from Figma via a Figma-to-React plugin?

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States effulgentsia
  • ๐Ÿ‡ณ๐Ÿ‡ฑNetherlands balintbrews Amsterdam, NL

    @effulgentsia: Can you please confirm that my understanding is correct about the proposed js_imports and js_import_dependencies properties?

    1. We get the entire import map from @jspm/generator when processing the JS source code of the code component (first with babylon so it's not JSX).
    2. Parsing the import map we produce lists of simple module names for js_imports and js_import_dependencies to store.
    3. Those are expanded into an import map again (and merged with other entries) when generating the import map on the backend, so it looks like this: https://generator.jspm.io/#Y2NhYGBkDM0rySzJSU1hcChKTMms0C3N1C9KTUwu0U1MT....
    4. We hardcode esm.sh as the CDN for now. It supports omitting package versions and entrypoint paths. (Based on a quick testing, omitting those is not supported by jspm.io's CDN, so we e.g. need https://ga.jspm.io/npm:@radix-ui/react-accordion@1.2.2/dist/index.mjs, and can't simply have https://ga.jspm.io/npm:@radix-ui/react-accordion.)
  • ๐Ÿ‡ณ๐Ÿ‡ฑNetherlands balintbrews Amsterdam, NL

    As I was thinking through this and writing #22 โœจ Allow code components to import from npm packages Active , I looked into esm.sh. I discovered that if we use esm.sh, we don't need to worry about generating the entire import map to cover the dependencies of the dependencies.

    esm.sh rewrites import specifiers in the package code it serves: they are transformed to reference the dependencies also served by esm.sh. This means that we can simply do this:

    import * as Accordion from "https://esm.sh/@radix-ui/react-accordion";
    

    Take a look at what https://esm.sh/@radix-ui/react-accordion returns: all the dependencies are there, imported from esm.sh: ๐Ÿ˜ณ

    /* esm.sh - @radix-ui/react-accordion@1.2.2 */
    import "/@radix-ui/primitive@1.1.1/es2022/primitive.mjs";
    import "/@radix-ui/react-collapsible@1.1.2/es2022/react-collapsible.mjs";
    import "/@radix-ui/react-collection@1.1.1/es2022/react-collection.mjs";
    /* ... */
    export * from "/@radix-ui/react-accordion@1.2.2/es2022/react-accordion.mjs";
    

    After seeing this, I wanted to validate our use case, so I created a POC that uses Preact, React linked to Preact (as @effulgentsia suggested before on a call), and @radix-ui/react-accordion as an example โ€” all imported from esm.sh via an import map:

    ๐Ÿงช https://github.com/balintbrews/poc-import-from-esm-sh

    It works beautifully! ๐ŸŽ‰ The AccordionDemo component was copied straight from the Radix UI docs, no changes were required to the import statements.

    What all of this means is that as long as we are happy with esm.sh only, we don't need to store anything on the config entity, we can easily parse the import specifiers in the JS source, and replace them, so e.g.:

    import * as Accordion from "@radix-ui/react-accordion";
    

    becomes:

    import * as Accordion from "https://esm.sh/@radix-ui/react-accordion?external=react,react-dom";
    

    (See why the ?external... in the README.md of my POC)

    Then we just need this import map:

    <script type="importmap">
      {
        "imports": {
          "preact": "https://esm.sh/preact",
          "preact/": "https://esm.sh/preact/",
          "react": "https://esm.sh/preact/compat",
          "react/": "https://esm.sh/preact/compat/",
          "react-dom": "https://esm.sh/preact/compat",
          "react-dom/": "https://esm.sh/preact/compat/"
        }
      }
    </script>
    
  • ๐Ÿ‡บ๐Ÿ‡ธUnited States effulgentsia

    Yes, #22 is exactly what I had mind. For #1 of that, I don't have a preference on whether to use babylon on the source or whether to compile with SWC first and then parse for imports on compiled JS. If there's no downsides to the former, then that's great.

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States effulgentsia

    I think #23 is fine as an interim step, but eventually we'll need to support other CDNs. Also, eventually, we'll need the site owner to be able to control when to update versions, so I think for that we'll need the imports and their dependencies in the config entity. We can bump all that to followups though, so long as the PoC in this issue's fork doesn't get deleted, so we can refer to it later.

  • ๐Ÿ‡ง๐Ÿ‡ชBelgium wim leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    What's next here? Is this still relevant?

    โœจ Bundle a small selection of packages in the hydration library for code components Active is not mentioned here at all, and brought in a select predefined list of importable packages โ€” AFAICT this issue is about generalizing it. If so: the issue summary needs to be updated to reflect how this changes the status quo that #3508734 created. ๐Ÿ™

Production build 0.71.5 2024