Allow code components to import from npm packages

Created on 19 January 2025, 2 days 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>
      )
    }
    
Production build 0.71.5 2024