Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[On-Demand Definitions] Initial draft #67

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

dgp1130
Copy link
Contributor

@dgp1130 dgp1130 commented Nov 27, 2024

This is an initial draft of the On-Demand Definitions proposal: A protocol for defining custom elements when needed without relying on top-level side-effects.

TL;DR:

Problems:

  1. It is very easy to forget a critical import and use a custom element before it is defined.
    document.querySelector('my-element').doSomething();
    // ^ `<my-element>` may or may not be defined.
  2. Defining elements as adjacent top-level side-effects to the class definition prevents tree shaking.
  3. Side-effectful imports are badTM.
  4. Forgetting an import on a custom element leads to a file ordering hazard.

Proposal:

Instead of writing customElements.define('my-element', MyElement) as an adjacent top-level side-effect, put it in a static define property of the class:

export class MyElement extends HTMLElement {
  static define() {
    // Check if the tag name was already defined by another class.
    const existing = customElements.get('my-element');
    if (existing) {
      if (existing === MyElement) {
        return; // Already defined as the correct class, no-op.
      } else {
        throw new Error(`Tag name \`my-element\` already defined as \`${
            existing.name}\`.`);
      }
    }

    customElements.define('my-element', MyElement);
  }
}

This removes top-level side-effects making the element tree-shakable. Any consumers of this class can call MyElement.define() to ensure it is defined before they attempt to use it. This decentralizes the process of defining a custom element and more closely couples the definition with its specific usages.

@dgp1130 dgp1130 changed the title [static `define] Initial draft [static define] Initial draft Nov 27, 2024
@keithamus
Copy link
Contributor

Might want to also have a registry arg, as scoped registries are coming.

class StopWatchElement extends HTMLElement {
  static define(tag = "stop-watch", registry = customElements) {
    registry.define(tag, this)
  }
}

@dgp1130
Copy link
Contributor Author

dgp1130 commented Nov 27, 2024

@keithamus, I haven't had much interaction with scoped registries, but I think supporting any registry via a parameter probably a reasonable thing to do. I'll have to learn more about they work to develop a stronger opinion on it.

I also see you have a tag parameter. I considered this idea under the "Built in tag name" section. I currently think that's actually an anti-pattern in this context as it allows multiple consumers of a custom element to define it with different tag names, which causes customElements.define to throw. It allows one consumer to break another in a way which actively goes against the goals of this proposal.

@EisenbergEffect
Copy link

FAST uses a very similar pattern to this, as does my Web Component Engineering course. There's some extra code to guard against the same instance with different tag names (solvable by dynamically inheriting a new base class from the original before defining the element).

You might want to also have a third parameter for options to the define call (or library/component specific options).

@dgp1130
Copy link
Contributor Author

dgp1130 commented Nov 27, 2024

FAST uses a very similar pattern to this, as does my Web Component Engineering course.

I know I've seen a similar pattern around but was struggling to remember exactly what those were. Glad to see there's some prior art here!

There's some extra code to guard against the same instance with different tag names (solvable by dynamically inheriting a new base class from the original before defining the element).

Are you thinking something like:

export class MyElement extends HTMLElement {
  static define(tagName) {
    if (customElements.get(tagName)) return;

    // Define a unique subclass for a non-default tag name.
    const clazz = tagName ? class extends MyElement {} : MyElement;

    customElements.define(tagName ?? 'my-element', clazz);
  }
}

I'm a bit hesitant to go that direction as some of the consequences seem very unintuitive:

MyElement.define('my-other-element');

customElements.getName(MyElement) === 'my-element'; // Expected `my-other-element`.

customElements.get('my-other-element') !== MyElement; // Expected `===`.

Also as mentioned in the proposal, some component authors might want static knowledge of their tag name. I'm not sure it's reasonable to restrict this proposal only to components which are willing to give up that knowledge.

Beyond that, what exactly is the benefit of having a dynamic tag name? Is the goal just to avoid name conflicts? If so, would that be better solved by scoped registries rather than dynamic tag names?

You might want to also have a third parameter for options to the define call (or library/component specific options).

Ah yes, I always forget customElements.define accepts an options object. We should definitely include that in define.

@justinfagnani
Copy link
Member

justinfagnani commented Nov 27, 2024

Once scoped registries are common I think the best pattern will be for elements to just not self register at all - or do so in a separate side-effectful module from the component class. Then the consumer can follow whatever pattern it wants for registering, and there are no more issues with tree-shaking, etc.

For example, in Lit we support adding elements to the scoped registry declaratively:

import {LitElement, html} from 'lit';
import {ScopedRegistryHost} from '@lit-labs/scoped-registry-mixin';
import {FooElement} from './foo-element.js';

class MyElement extends ScopedRegistryHost(LitElement) {
  static elementDefinitions = {
    'foo-element': FooElement,
  };

  render() {
    return html`<foo-element></foo-element>`;
  }
}

I'm not sure that the static define pattern does anything for us on top of this.

@Westbrook
Copy link
Collaborator

Network of tooling question:

Can the following in TS be done lazily?

declare global {
  interface HTMLElementTagNameMap {
    "my-element": MyElement;
  }
}
  • If not, should it?
  • If not, does that to a degree point to element registrations happening as a side-effect being a "good" thing?
  • Related, but not related: does TS need to be given a warning about scoped registries being inbound?
    • or is there TS magic around removing declare global {}?

Clean up process:

If you have this usage:

import { MyElement } from 'my-element';
// ...
MyElement.define();
// ...
const template = `<my-element></my-element>`;

When taking <my-element> out of template, it seems just as easy to accidentally forget to remove MyElement.define();, such that the element cannot be tree shaken as it is the forget to manually remove the import in:

import 'my-element/defined.js';
// ...
const template = `<my-element></my-element>`;

Does this realistically free tooling to tree shake without the wider scale pattern you outline in HydroActive?

Class extensions

Should class extension be addressed in some way in this proposal? Maybe having the protocol documented here and then the element documented as leveraging the protocol would position an extending developer for success. Other thoughts on what could support someone in this area? You begin to address something in this realm when you choose to noop on subsequent registrations, which can be problematic in it's own right (unexpected versions being registered, etc.), but directly addressing notes above around scoped registries should position a protocol with this shape to avoid those sorts of issues.

@dgp1130
Copy link
Contributor Author

dgp1130 commented Nov 28, 2024

@justinfagnani / @Westbrook, thanks for all the feedback here! Clearly this proposal would probably benefit from a deeper dive into scoped registries. From a quick look at the proposal I have some immediate thoughts, but I might just need to be educated on some of this.

Once scoped registries are common I think the best pattern will be for elements to just not self register at all - or do so in a separate side-effectful module from the component class.

@justinfagnani, I can definitely see the appeal of a world where every component gets its own registry, therefore there is no global side-effect at all (or at least minimal side-effects, entry point elements would likely still rely on them). I do have some specific concerns though:

First, scoped registries seems coupled to shadow DOM, which feels like a weird constraint to put on affected components. Light DOM components would still benefit from the tree-shaking and tooling benefits of static define in a way which scoped registries would fail to meet. HydroActive for example does not require shadow DOM, so this requirement makes scoped registries basically a non-starter as a solution. For that matter, doesn't Lit also support light DOM components? This proposal supports those components in a way scoped registries does not.

Second, scoped registries are solving a different problem with a different set of requirements. In your example with Lit, foo-element needs to be manually mapped to FooElement. That's fine in the context of creating a scoped registry where you want to decouple the tag name from the custom element. But for the defined goals of making elements tree-shakable and attributing usage, that decoupling is not necessarily needed or even desirable. An equally valid implementation for Lit might look like:

import {LitElement, html} from 'lit';
import {FooElement} from './foo-element.js';

class MyElement extends LitElement {
  // Lit could call `.define()` automatically.
  static definitions = [FooElement];

  render() {
    return html`<foo-element></foo-element>`;
  }
}

This reduces repetition of the foo-element tag name, while maintaining the benefits of this proposal, same with my <${FooElement}></${FooElement}> suggestion earlier in the draft. Neither supports scoping elements, but that's not a goal here. Basically, I'm thinking that while scoped registries may solve this problem for Lit, not every developer or library may want that solution or the baggage it brings given that the goals of this proposal are fundamentally unrelated to scoped registries. My point above about shadow DOM highlights one particular unnecessary constraint scoped registries place on this problem space.

To illustrate a related concern, I could add scoped registry support to HydroActive (and probably should at some point), but that would require HydroActive to have a similar mapping of { 'foo-element': FooElement }. In the context of a scoped registry, that mapping is exactly what we need to have. But for the goals of this proposal, it's completely unnecessary. The current DX of host.query('foo-element').access(FooElement) is fine. Forcing developers to create a registry is requiring them to solve a problem they don't actually need to solve.

Third, how do scoped registries work for pre-rendered DOM elements? The proposal mentions adding createElement to ShadowRoot, but that doesn't help for SSR'd content. Is the idea that a pre-rendered element would use the registry of its shadow root? How does that work when a registry might be added to a shadow root after a component is defined in the global registry? Is the proposal trying to say that elements inside a shadow root would be inert until they are defined inside that scoped registry?

If I'm understanding this correctly, it's probably workable as specified. I'm just not entirely sure I'm completely understanding how scoped registries interact with pre-rendered DOM.

Fourth, even in a scoped registry world, wouldn't a define-like abstraction still be useful? This naturally removes the top-level side-effect which needs to be removed anyways. If define supports a custom tag name and registry, would there still be value in this protocol for scoped registries? You could always do:

const registry = new CustomElementRegistry();
MyElement.define(registry, 'other-element');

Slight tangent: @EisenbergEffect suggested passing through options but I'm actually wondering if that's necessarily useful. Currently the only supported option is extends (not sure if there are other proposed options to be considering). Is it valid for a consumer of a component to change its extends option? I would think that if a component extends HTMLParagraphElement, it would want to enforce that customElements.define is always called with extends: 'p'. Therefore I'd want to write its define as:

export class MyParagraphElement extends HTMLParagraphElement {
  static define(registry = customElements, tagName = 'my-p-element') {
    if (registry.get(tagName)) return;

    // Hard-coded `extends: 'p'`, don't want callers to change this.
    registry.define(tagName, MyParagraphElement, { extends: 'p' });
  }
}

I'm thinking this can still provide a useful abstraction over the custom element definition, even when used in a scoped registry.

Exposing options also gets weird for cases where different consumers might disagree on the extends option:

import {MyElement} from './my-element.js';

MyElement.define();
MyElement.define(undefined, undefined, { extends: 'p' }); // No-op means it does not use `extends: 'p'`.

This again reinforces that maybe extends shouldn't be exposed, but also future additions to these options might be worth exposing to consumers, so we might want to leave that door open for now?

I'm thinking this is a problem with customElements.define today. If two different consumers call it with different options, you're going to have the same issue and need to choose a centralized location. define won't solve that problem for you, but it does mean that if we do support any options, we should probably throw if multiple define calls disagree on those options.

Can the following in TS be done lazily?

declare global {
  interface HTMLElementTagNameMap {
    "my-element": MyElement;
  }
}
  • If not, should it?

  • If not, does that to a degree point to element registrations happening as a side-effect being a "good" thing?

  • Related, but not related: does TS need to be given a warning about scoped registries being inbound?

    • or is there TS magic around removing declare global {}?

@Westbrook, I'm not entirely sure I follow what you're getting at. Can you elaborate on how HTMLElementTagNameMap relates to this proposal?

If your concern is that the type assumes the element is defined, that is an existing issue which isn't really affected by this proposal. HydroActive actually helps with that to a certain extent. How TS handles scoped registries is maybe a related problem.

When taking <my-element> out of template, it seems just as easy to accidentally forget to remove MyElement.define();, such that the element cannot be tree shaken as it is the forget to manually remove the import

Does this realistically free tooling to tree shake without the wider scale pattern you outline in HydroActive?

This is a fair concern, but I do think this pattern is still helpful for a few reasons:

  1. The module no longer relies on a side-effectful import.
  2. MyElement is tree-shakable if it is reasonably grouped with its usage.
    • Consider:
      if (import.meta.env.DEV) { // tree-shaken when `false`
        MyElement.define();
        const template = `<my-element></my-element>`;
        // ...
      }
  3. If my-element.js contains multiple custom elements using the same pattern, only MyElement would need to be retained, other elements are tree-shakable.
  4. MyElement.define() and its usage in the template are more closely associated than an import which is as far away as it possibly could be making it more likely a programmer removes it when possible (even if not guaranteed).
  5. MyElement.define() makes it clear to developers that we're relying on an element being defined as a global side-effect. This is much more intuitive than a side-effectful import which could be doing anything.

That said, I do understand that this can have relatively minimal value for simple cases like the one you mention. I think the power of this API really comes from custom element libraries and frameworks which directly integrate with it, automatically defining elements before interacting with them. This is why I called out HydroActive and Lit as motivating use cases. In those contexts, removing the element's usage also implicitly allows its import to be tree-shaken or flagged by a linter. That's where the real power of this proposal comes from.

Should class extension be addressed in some way in this proposal?

Can you define what you mean by "class extension"? What are you referring to?

You begin to address something in this realm when you choose to noop on subsequent registrations, which can be problematic in it's own right (unexpected versions being registered, etc.)

Hmm, if multiple components attempted to claim the same tag name that should probably fail like customElements.define would. Maybe it should be written as:

export class MyElement extends HTMLElement {
  static define(registry = customElements, tagName = 'my-element') {
    const existing = registry.get(tagName);
    if (existing) {
      if (existing === MyElement) {
        return; // Already defined, no-op.
      } else {
        throw new Error(`Tag name \`${tagName}\` already defined as \`${existing.name}\``);
      }
    }

    registry.define(tagName, MyElement);
  }
}

The one other issue I can think of is the one I mentioned earlier about multiple define calls with different options causing an issue when noop-ing. I think that's potentially fixable if we either don't support any options or throw when given conflicting options.

Does that address your concern? Are there other issues with noop-ing duplicate definitions I'm overlooking?

@ChrisShank
Copy link

Just noting that I also add the tagName as a static property. I like distributing elements that have a certain name, but is easily overridable. It could also be helpful to be able to read the tagName for scoped registries or other things.

export class MyElement extends HTMLElement {
  static tagName = ‘my-element’;

  static define() {
    if (customElements.get(this.tagName)) return;

    customElements.define(this.tagName, MyElement);
  }
}

dgp1130 added a commit to dgp1130/HydroActive that referenced this pull request Dec 2, 2024
This follows the initial draft of the [static `define` community protocol](webcomponents-cg/community-protocols#67).

Each component now automatically includes a static `define` property which defines the custom element. `Dehydrated.prototype.access` and `Dehydrated.prototype.hydrate` both call this `define` function for the custom element class to ensure that it is defined before the element is used.

The major effect of this is that `define*Component` no longer automatically defines the provided element in the global registry. This allows elements to be tree shaken when they are unused, but does mean users need to manually define them in the top-level scope if their application relies on upgrading pre-rendered HTML. I'm not a huge fan of this as it is quite easy for users to forget to call `define`, which was half the point of defining all components automatically. However, HydroActive should naturally do the right thing when depending on another component due to `Dehydrated` auto-defining dependencies. Defining entry point components is unfortunately not a problem HydroActive can easily solve.

The current implementation does presumably support scoped custom element registries, but this is not tested as the spec has not been implemented by any browser just yet. The polyfill can potentially be used to verify behavior.
dgp1130 added a commit to dgp1130/HydroActive that referenced this pull request Dec 4, 2024
This follows the initial draft of the [static `define` community protocol](webcomponents-cg/community-protocols#67).

Each component now automatically includes a static `define` property which defines the custom element. `Dehydrated.prototype.access` and `Dehydrated.prototype.hydrate` both call this `define` function for the custom element class to ensure that it is defined before the element is used.

The major effect of this is that `define*Component` no longer automatically defines the provided element in the global registry. This allows elements to be tree shaken when they are unused, but does mean users need to manually define them in the top-level scope if their application relies on upgrading pre-rendered HTML. I'm not a huge fan of this as it is quite easy for users to forget to call `define`, which was half the point of defining all components automatically. However, HydroActive should naturally do the right thing when depending on another component due to `Dehydrated` auto-defining dependencies. Defining entry point components is unfortunately not a problem HydroActive can easily solve.

The current implementation does presumably support scoped custom element registries, but this is not tested as the spec has not been implemented by any browser just yet. The polyfill can potentially be used to verify behavior.
dgp1130 added a commit to dgp1130/HydroActive that referenced this pull request Dec 4, 2024
This follows the initial draft of the [on-demand definitions community protocol](webcomponents-cg/community-protocols#67).

Each component now automatically includes a static `define` property which defines the custom element. `Dehydrated.prototype.access` and `Dehydrated.prototype.hydrate` both call this `define` function for the custom element class to ensure that it is defined before the element is used.

The major effect of this is that `define*Component` no longer automatically defines the provided element in the global registry. This allows elements to be tree shaken when they are unused, but does mean users need to manually define them in the top-level scope if their application relies on upgrading pre-rendered HTML. I'm not a huge fan of this as it is quite easy for users to forget to call `define`, which was half the point of defining all components automatically. However, HydroActive should naturally do the right thing when depending on another component due to `Dehydrated` auto-defining dependencies. Defining entry point components is unfortunately not a problem HydroActive can easily solve.

The current implementation does presumably support scoped custom element registries, but this is not tested as the spec has not been implemented by any browser just yet. The polyfill can potentially be used to verify behavior.
dgp1130 added a commit to dgp1130/HydroActive that referenced this pull request Dec 4, 2024
This follows the initial draft of the [on-demand definitions community protocol](webcomponents-cg/community-protocols#67).

Each component now automatically includes a static `define` property which defines the custom element. `Dehydrated.prototype.access` and `Dehydrated.prototype.hydrate` both call this `define` function for the custom element class to ensure that it is defined before the element is used.

The major effect of this is that `define*Component` no longer automatically defines the provided element in the global registry. This allows elements to be tree shaken when they are unused, but does mean users need to manually define them in the top-level scope if their application relies on upgrading pre-rendered HTML. I'm not a huge fan of this as it is quite easy for users to forget to call `define`, which was half the point of defining all components automatically. However, HydroActive should naturally do the right thing when depending on another component due to `Dehydrated` auto-defining dependencies. Defining entry point components is unfortunately not a problem HydroActive can easily solve.

The current implementation does presumably support scoped custom element registries, but this is not tested as the spec has not been implemented by any browser just yet. The polyfill can potentially be used to verify behavior.
dgp1130 added a commit to dgp1130/HydroActive that referenced this pull request Dec 4, 2024
This follows the initial draft of the [on-demand definitions community protocol](webcomponents-cg/community-protocols#67).

Each component now automatically includes a static `define` property which defines the custom element. `Dehydrated.prototype.access` and `Dehydrated.prototype.hydrate` both call this `define` function for the custom element class to ensure that it is defined before the element is used.

The major effect of this is that `define*Component` no longer automatically defines the provided element in the global registry. This allows elements to be tree shaken when they are unused, but does mean users need to manually define them in the top-level scope if their application relies on upgrading pre-rendered HTML. I'm not a huge fan of this as it is quite easy for users to forget to call `define`, which was half the point of defining all components automatically. However, HydroActive should naturally do the right thing when depending on another component due to `Dehydrated` auto-defining dependencies. Defining entry point components is unfortunately not a problem HydroActive can easily solve.

The current implementation does presumably support scoped custom element registries, but this is not tested as the spec has not been implemented by any browser just yet. The polyfill can potentially be used to verify behavior.
dgp1130 added a commit to dgp1130/HydroActive that referenced this pull request Dec 4, 2024
This follows the initial draft of the [on-demand definitions community protocol](webcomponents-cg/community-protocols#67).

Each component now automatically includes a static `define` property which defines the custom element. `Dehydrated.prototype.access` and `Dehydrated.prototype.hydrate` both call this `define` function for the custom element class to ensure that it is defined before the element is used.

The major effect of this is that `define*Component` no longer automatically defines the provided element in the global registry. This allows elements to be tree shaken when they are unused, but does mean users need to manually define them in the top-level scope if their application relies on upgrading pre-rendered HTML. I'm not a huge fan of this as it is quite easy for users to forget to call `define`, which was half the point of defining all components automatically. However, HydroActive should naturally do the right thing when depending on another component due to `Dehydrated` auto-defining dependencies. Defining entry point components is unfortunately not a problem HydroActive can easily solve.

The current implementation does support scoped custom element registries, but this will be tested in a follow up commit.
@dgp1130 dgp1130 changed the title [static define] Initial draft [On-Demand Definitions] Initial draft Dec 4, 2024
@dgp1130
Copy link
Contributor Author

dgp1130 commented Dec 4, 2024

I just pushed some changes which:

  1. Renames this proposal to "On-Demand Definitions" as I think that more clearly describes what this seeks to achieve. It allows consumers to define a component they wish to use right when they need it. Static define is an implementation detail of how that goal is achieved.
  2. Updates the example implementation to align with feedback (throws an error if the tag is already defined with a different element).
  3. Adds some more "Open Questions" sections based on my current thinking.
  4. Calls out scoped registries in particular, how they can be supported, and how they fall short as an alternative solution.

Simultaneously, I took the opportunity to implement this proposal in HydroActive and it seems to work exactly like I hoped it would. I'll need to continue playing around with it to see if any new challenges or limitations can be identified.

Please take a look and share any feedback, suggestions, or concerns you may have! 🙏

Copy link
Collaborator

@Westbrook Westbrook left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you available for the December WCCG meeting I'd be happy to make time to bring this up at that time and query the group in person about any questions/concerns for this addition.

proposals/on-demand-definitions.md Show resolved Hide resolved
@dgp1130
Copy link
Contributor Author

dgp1130 commented Dec 5, 2024

If you available for the December WCCG meeting I'd be happy to make time to bring this up at that time and query the group in person about any questions/concerns for this addition.

Sure, happy to chat about it. Is this on Dec 16th or 17th? The Discord and Google Calendar events seem to disagree.

@Westbrook
Copy link
Collaborator

@dgp1130 thanks for pointing that out! I'm not in my normal timezone and it'm made working with calendars even harder than expected. It should be Monday the 16th at 7pm ET in both places now. Let me know if I've still not quite gotten it correct. 🙇🏼‍♂️

…ntion which inlines the `define` implementation at its usage sites, and an alternative using `customElements.whenDefined`.

Also makes various minor readability improvements and acknowledges the new scoped custom element registries proposal.
@dgp1130
Copy link
Contributor Author

dgp1130 commented Dec 17, 2024

After the community group discussion today I've updated the proposal to address the oversights mentioned by @sorvell (and gave better justification for why we should prefer this over inlining define at each call site) as well as improve some of the general readability.

Summary of changes:

  1. Added a section on treating the class conflict case as a development-only check which can be omitted in production. I think this is ok to provide a small bundle size improvement, but I could probably be talked out of it if it's worth maintain behavior between development and production.
  2. Added a section about an alternative proposal for inlining the define implementation at each call site.
  3. Added a section about customElements.whenDefined and why that is insufficient.
  4. Acknowledged the new scoped custom element registry proposal. This does address my shadow DOM point, however I still believe it is the wrong abstraction for this particular problem and fails to meet the needs of components on the global registry. I don't believe you should need to avoid the global registry to gain these benefits.
  5. Updated the original comment on this PR to better highlight the problems I'm hoping this proposal will address.
  6. Split off my update to the status of the template protocol into Updates template status to "Proposal" as that is the first stage of a community protocol. #68 as that's fundamentally decoupled from this change.

Definitely interested in hearing any more feedback about this proposal! 😁

@Westbrook
Copy link
Collaborator

As discussed in today's WCCG meeting, we will be opening a month long time box for reviewing the promotion of this protocol to "Proposal" status by ay on merging this PR which will close on January 17th.

Review status levels and their requirements here.

@trusktr
Copy link

trusktr commented Dec 19, 2024

Can the following in TS be done lazily?

declare global {
  interface HTMLElementTagNameMap {
    "my-element": MyElement;
  }
}

In TS this is a single source of truth defined up front, ahead of time. It cannot be done lazily. I.e., it will not update itself based on whichever arbitrary element names are actually sent into customElements.define.

In TS JSX, it relies on a JSX namespace, and a JSX namespace can only also have a single source of truth!

So, in either case, name collisions can still happen, and this will not pair well with scoped registries.

However! For JSX specifically (out of luck with HTMLElementTagNameMap) there one trick up our sleeves: the JSX namespace to import can be specified on a per-file basis:

/* @jsxImportSource some-lib/some-file */

// The JSX type checking in this file will specifically rely on the JSX namespace from some-lib/some-file

and another file:

/* @jsxImportSource some-lib/some-file */

// The JSX type checking in this file will specifically rely on the JSX namespace from other-lib/other-file

This means that, using scoped registries, an end user does have the opportunity to define the type checking source per file to align that with whatever elements they are using in the file's JSX templates. However HTMLElementTagNameMap is global so that will still only be able to have a single definition per name. JSX users with multiple JSX sources therefore cannot also provide the multiple custom types for global DOM APIs.

Well, the HTMLElementTagNameMap problem is in fact possible to solve in a more cumbersome way by making separate folders be separate TS "projects references", as if each of those folders is a separate TS project (useful for monorepos with separate packages for example). A TS project reference can have its own unique set of global types.

@trusktr
Copy link

trusktr commented Dec 19, 2024

I coincidentally also have my own approach to this (called .defineElement() instead of .define()) without having copied from anywhere else:

https://github.com/lume/element/blob/6270241d79466f144b672bb69e8a356e79775ec3/src/LumeElement.ts#L51-L69

Seems like the sort of thing a lot of us have naturally come up with.

@Westbrook
Copy link
Collaborator

I coincidentally also have my own approach to this (called .defineElement() instead of .define()) without having copied from anywhere else:

Not something that would necessarily need to be worked out here, but @trusktr do you have thoughts on this Protocol using .define() vs .defineElement()? While you've found your way here of your own accord there would be benefit to a shared naming in that it would prepare this for later status levels.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants