Set code component dependencies based on import statements

Created on 9 April 2025, 12 days ago

Overview

Code components need to send their dependencies to the backend in order to support importing other code components. See the parent issue, 🌱 [Plan] First-party code component imports Active .

Proposed resolution

Before/after compiling the JavaScript source code for a code component, parse its import statements (see POC by @larowlan for parsing with babylon Allow code components to import from npm packages Active , also existing code in our codebase in the Preview.tsx component) and add any first-party components to the js_dependencies property of the code component, introduced in Store and calculate dependencies in `JavaScriptComponent` Active .

User interface changes

None.

Feature request
Status

Active

Version

0.0

Component

Page builder

Created by

🇳🇱Netherlands balintbrews Amsterdam, NL

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

Merge Requests

Comments & Activities

  • Issue created by @balintbrews
  • 🇳🇱Netherlands balintbrews Amsterdam, NL
  • First commit to issue fork.
  • Merge request !872Resolve #3518198 "Set code component" → (Merged) created by hooroomoo
  • Pipeline finished with Failed
    12 days ago
    Total: 729s
    #469576
  • 🇺🇸United States tedbow Ithaca, NY, USA
  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    @hooroomoo We're working on Store and calculate dependencies in `JavaScriptComponent` Active , and would like you to confirm that the client side can determine the JavaScriptComponent config entities' machine names based only on the JS source code of the code component.

    Is that right? 🤓

    Crediting @tedbow and @effulgentsia for the discussion that led to that!

  • Pipeline finished with Failed
    11 days ago
    Total: 1023s
    #470525
  • 🇺🇸United States hooroomoo

    @wim leers No it can't determine machine name from only the JS source code. What prompts this question?

    Here I have a component I named "My Component" and the machine name the backend sets is "my_component" but the machine name is not in the source code.

  • 🇺🇸United States hooroomoo

    Unless are you talking about if the frontend can get the machine names of imported JS components from its source code? Then the answer is yes.

    I believe there is an expectation that the author has to write imports in the JS code editor like this:

    import Whatever from "@/components/button"
    import FooWhatever from "@/components/heading"
    

    Where the import statement has to be "@/components/machineName"

  • 🇳🇱Netherlands balintbrews Amsterdam, NL

    While on a call with @hooroomoo and @tedbow, it became clear to me that one piece is missing in the plan. We need to make sure the code editor preview can also understand these imports. Here is an idea.

    1. Extend the code editor preview's import map by adding every imported component's compiled JS code in a blob URL — something like this:

      diff --git a/ui/src/features/code-editor/Preview.tsx b/ui/src/features/code-editor/Preview.tsx
      index 9f433faf..6a18e787 100644
      --- a/ui/src/features/code-editor/Preview.tsx
      +++ b/ui/src/features/code-editor/Preview.tsx
      @@ -79,6 +79,22 @@ const importMap = {
           'class-variance-authority': 'https://esm.sh/class-variance-authority',
           'tailwind-merge': 'https://esm.sh/tailwind-merge',
           '@/lib/utils': `${XB_MODULE_UI_PATH}/lib/astro-hydration/dist/utils.js`,
      +    '@/components/button': URL.createObjectURL(
      +      new Blob(
      +        [
      +          `
      +          import { jsx as _jsx } from "react/jsx-runtime";
      +          const Button = ({ children = "Button" }) => {
      +            return /*#__PURE__*/ _jsx("button", {
      +              children: children
      +            });
      +          };
      +          export default Button;
      +          `,
      +        ],
      +        { type: 'text/javascript' },
      +      ),
      +    ),
         },
       };
       

      As part of this issue, we will already know what other components are imported, and we can fetch their compiled JS code. The trick is to grab the auto-saved version if it exists. Use code from ui/src/features/code-editor/hooks/useCodeComponentData.ts as example.

    2. Also add the CSS from the imported code components to the preview iFrame, similar to how we add CSS now in ui/src/features/code-editor/Preview.tsx. This will result in a lot of duplicated CSS, but we'll rectify that in 📌 Compile Tailwind CSS globally for code components Active .

    We could extract this to another issue, but this one can't work without it anyway, so I'd suggest to include it in the scope here.

  • 🇺🇸United States effulgentsia

    For #11.1, you should be able to just do this:

    '@/components/': Drupal.url('xb/api/auto-saves/js/js_component/')
    

    And thereby not need a separate import map entry for every component.

  • 🇳🇱Netherlands balintbrews Amsterdam, NL

    #12: That's amazing, thank you! 👏🏻

    When there is no auto-saved version, that endpoint does a 307 redirect to the static JS file. I wonder if the browser will like that. But we'll find out soon! Or maybe you already know. 🙂

  • Pipeline finished with Failed
    11 days ago
    Total: 220s
    #470654
  • 🇺🇸United States hooroomoo

    @balintbrews I ended up with the same idea before I even saw your comments. That's validating to me haha

    @effulgentsia Thanks!! Was running into a security error on my very WIP-y test commit, need to give that a try

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    So then … the answer to my question at #8 is:

    import Whatever from "@/components/button"
    import Whatever2 from "@/components/asdfsdf"
    

    would result in the client sending

    imported_js_components: ["button", "asdfsdf"]
    

    to the server, and the client can compute it from only the source_code_js it received.

    Is that right?

    A definitive answer to this is blocking Store and calculate dependencies in `JavaScriptComponent` Active .

  • 🇳🇱Netherlands balintbrews Amsterdam, NL

    #16: That's right, that's what's being implemented in this issue.

  • Pipeline finished with Failed
    10 days ago
    Total: 740s
    #471629
  • 🇺🇸United States hooroomoo

    still have some todos left

  • Pipeline finished with Failed
    7 days ago
    Total: 617s
    #473391
  • Pipeline finished with Failed
    7 days ago
    Total: 948s
    #473396
  • Pipeline finished with Failed
    7 days ago
    Total: 982s
    #473413
  • 🇺🇸United States hooroomoo

  • Pipeline finished with Failed
    7 days ago
    Total: 469s
    #473426
  • 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10
  • 🇺🇸United States hooroomoo

    I grabbed a func over from @larowlan's poc

  • 🇺🇸United States hooroomoo
  • 🇺🇸United States hooroomoo
  • Pipeline finished with Failed
    7 days ago
    Total: 1063s
    #473433
  • Pipeline finished with Success
    6 days ago
    Total: 960s
    #474119
  • Pipeline finished with Canceled
    6 days ago
    Total: 1024s
    #474201
  • Pipeline finished with Success
    6 days ago
    Total: 1060s
    #474209
  • Pipeline finished with Success
    6 days ago
    Total: 1017s
    #474275
  • 🇦🇺Australia larowlan 🇦🇺🏝.au GMT+10

    Reviewed and manually tested, works great!

  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    Lee's screenshot is 🤩 — great work, @hooroomoo!

    Didn't test it, really trust @larowlan's review and wanted to go ahead and merge, but … I reviewed this MR anyway because I reviewed the back-end counterpart (#3518185), and I ended up finding a few things where it's not clear how to enter the failure mode that the code is protecting against.

    Either the protection is unnecessary, or the logic + error message can be tightened some, which would leave the codebase in a clearer state for future iteration 🙏

    (Also: Lee also had one remark to address 😇)

  • Pipeline finished with Success
    5 days ago
    Total: 1178s
    #475175
  • Pipeline finished with Success
    5 days ago
    Total: 1183s
    #475314
  • Pipeline finished with Success
    5 days ago
    Total: 1222s
    #475334
  • Pipeline finished with Skipped
    5 days ago
    #475346
    • hooroomoo committed 1ddd0aba on 0.x
      Issue #3518198 by hooroomoo, balintbrews, wim leers, effulgentsia,...
  • 🇺🇸United States hooroomoo

    GIF in https://www.drupal.org/project/experience_builder/issues/3518198#comment... Set code component dependencies based on import statements Active

  • 🇺🇸United States hooroomoo
  • 🇺🇸United States hooroomoo

    Testing steps with example components

    1. Create a new code component named 'My Button'
    2. Add this code and also add a prop named 'name' of type 'text' in the component data window

    const MyButton = ({name}) => {
      return (
        <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
          {name}
        </button>
      );
    };
    export default MyButton;
    

    3. Create a second code component named 'My Card'
    4. Add this code:

    import MyButton from '@/components/my_button'
    const MyCard = () => {
      return (
        <div className="max-w-sm rounded overflow-hidden shadow-lg bg-orange-500 p-4">
          <div className="font-bold text-xl mb-2">Card Title</div>
          <p className="text-gray-700 text-base">
            Here is a simple description of what this card is about. Add anything you like here.
          </p>
          <div className="pt-4">
            <MyButton name="FOOOO"/>
          </div>
        </div>
      );
    };
    export default MyCard;
    

    5. MyCard component imports the button component created in step 1 and you can pass whatever name into the name prop of the button component

Production build 0.71.5 2024