Skip to content

On-Demand Definitions Support #79

@dgp1130

Description

@dgp1130

@rules_prerender should be able to use the On-Demand Definitions community protocol to automatically define custom elements rendered in the top-level scope.

This could look like a customElement function which references a client-side module and symbol of that custom element. This would render like an any other element, except that it also includes an inline script which defines the associated custom element.

// Represents a component with the tag name `my-component` defined in `./my-component.mjs` and exported as `MyComponent`.
const MyComponent = customElement('my-component', define(import.meta, './my-component.mjs', 'MyComponent'));

export function Component(): VNode {
  return <MyComponent>
    <div>Hello, World!</div>
  </MyComponent>;
}

This would effectively render:

<my-component>
  <div>Hello, World!</div>
  <script type="module">
    import {MyComponent} from './my-component.mjs';
    MyComponent?.define();
  </script>
</my-component>

This ensures the definition for <my-component> is automatically included in the page just by nature of having rendered it.

We need to do some extra work to make this bundle correctly, since we don't actually want to ship an inline script tag like that.

Users can opt-out of this behavior with defer-hydration, since that signals that the component will be a no-op until the attribute is removed. The idea is that whatever module removes defer-hydration is responsible for making sure the component is upgraded first, and that's something HydroActive or any CE framework with good on-demand definitions integration should be able to solve.

export function Component(): VNode {
  return <MyComponent>
    {/* Definition is *not* included on the page. `MyComponent` should do that at runtime before removing `defer-hydration`. */}
    <MyOtherComponent defer-hydration />
  </MyComponent>
}

I also experimented with using import attributes rather than a manually managed define function.

// `type: 'symbol-ref'` tells the runtime to generate `MyComponentDef` from the exported `MyComponent` symbol.
import { MyComponentDef } from './my-component.mjs' with { type: 'symbol-ref' };

const MyComponent = customElement('my-component', MyComponentDef);

This is theoretically better as it provides stronger type checking on the imported symbol. However it requires a type dependency on the client code, which is a bit awkward to manage and TS / IDE support is definitely not there yet. This requires a @ts-ignore right now, so it's really not much safer or more ergonomic. I think there's a potential future with this approach, but we're not there yet, and we should probably stick with a manual define call right now.

Prototype branch: ref/on-demand-definitions-prototype

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions