Skip to content

Conversation

@johnjenkins
Copy link
Contributor

@johnjenkins johnjenkins commented Mar 24, 2025

Scoped custom element registries support is currently tiny.

As a WC compiler Stencil has an opportunity to offer devs first-class workarounds until there is greater Scoped custom element registries support.

What is the current behavior?

importing and registrering@my/wc-components/my-component v1 and @my/wc-components/my-component v2 will error unless they have a different tag name. A global custom element registry means only one class CTOR can be registered per tag-name.

GitHub Issue Number:

What is the new behavior?

2 new core functions - setTagTransformer and tagTransform - have been made available which will work across all Stencil outputs.

Examples

dist

Within your lib's src/index.ts:

// expose setTagTransformer
export { setTagTransformer } from '@stencil/core';

Within any relevant consuming project html file:

<!-- Setup a tag transformer -->

<script type="module">
    import { setTagTransformer } from '/components/build/index.esm.js';
    
   setTagTransformer((tag) => {
      if (tag.startsWith('my-')) {
        return tag.replace('my-', 'your-');
      }
      return tag;
    })
</script>

<!-- Use the lazy loader as normal -->
<script type="module" src="/components/build/myapp.esm.js"></script>
...
<your-component></your-component>

dist-custom-elements

Within your lib's src/index.ts:

// expose setTagTransformer
export { setTagTransformer } from '@stencil/core';

Within any relevant consuming project:

import { setTagTransformer } from '@my/components';
import { defineCustomElement } from @my/components/my-component.js';

// setup a tag transformer

setTagTransformer((tag) => {
    if (tag.startsWith('my-')) {
      return tag.replace('my-', 'your-');
    }
    return tag;
})

// define components as normal

defineCustomElement();

dist-hydrate-script

Within any relevant consuming project server handler:

import { setTagTransformer, renderToString } from './hydrate/index.mjs';

// setup a tag transformer

setTagTransformer((tag) => {
    if (tag.startsWith('my-')) {
      return tag.replace('my-', 'your-');
    }
    return tag;
})

// process incoming / outgoing html in the normal way

return (await renderToString(incomingHtmlString)).html;

Additional tag transformers

Any component lib that chooses to expose setTagTransformer will need to adjust their codebase; wrapping any potentially affected tags in tagTransform(...) (or in css, avoiding using tag names altogether!)

For example:

// this:
this.host.querySelector('my-tag');

// should be converted to:
import { transformTag } from '@stencil/core';

this.host.querySelector(transformTag('my-tag'));

additionalTagTransformers to the rescue (special thanks to @Cliffback !)

Setting a new config option:

extras: {
  additionalTagTransformers: true // (or 'prod' defaults to false)
}

Will additionally transform all css selectors and querySelectors (and more) within your component's code base:

CSS:

// incoming:
const css = '.something my-tag[name] { ... }'

// outgoing:
const css = `.something ${transformTag('my-tag')}[name] { ... }`

Query selectors:

// incoming:
this.host.querySelector('my-tag');

// outgoing:
this.host.querySelector(transformTag('my-tag'));

Documentation

TODO

Does this introduce a breaking change?

  • Yes
  • No

Testing

TODO

Other information

@elliottdaviesss
Copy link

elliottdaviesss commented Aug 28, 2025

Hi @johnjenkins, would these changes extend to the framework adapters OOTB/'as is', or would that require additional changes in the output-targets repo?

We're running a React MFE architecture and would really benefit from being able to transform the tags at runtime to avoid version registration clashes between feature teams running different versions of the stencil component lib - just wondering whether these changes could facilitate that in future.

Thanks!

@johnjenkins
Copy link
Contributor Author

hey @elliottdaviesss - Thanks for the interest!

Yes, it would involve some tweaks in the output adapters (wrapping tagName references in the newly exported transformTag(tagName))

On reflection, idk if what i've done here is the best approach; @Cliffback has came up with a slightly different strategy which you can use at https://github.com/Cliffback/stencil-custom-suffix-output-target (doesn't work with react output atm I think).
I like the fact it's an output target, plus it does some extra bits like transforming css selectors.

@Cliffback
Copy link
Contributor

hey @elliottdaviesss - Thanks for the interest!

Yes, it would involve some tweaks in the output adapters (wrapping tagName references in the newly exported transformTag(tagName))

On reflection, idk if what i've done here is the best approach; @Cliffback has came up with a slightly different strategy which you can use at https://github.com/Cliffback/stencil-custom-suffix-output-target (doesn't work with react output atm I think). I like the fact it's an output target, plus it does some extra bits like transforming css selectors.

@johnjenkins I could give transforming the other output targets the same way, so that it would work with react as well?

My main focus has been on angular so far, as that is what we need at work, but would be nice to expand it now that it is on npm regardless

@johnjenkins
Copy link
Contributor Author

@Cliffback - I decided to roll your work into this, tysm ❤️

@Cliffback
Copy link
Contributor

@Cliffback - I decided to roll your work into this, tysm ❤️

That's amazing to hear! Really happy that it has been of use! 🤩

@Cliffback
Copy link
Contributor

@johnjenkins btw. do you do anything with the component.d.ts declaration file? To get emitters to get the correct type I had to add a patch for that file: custom-suffix-output-target.ts#L320

Without this patch, all emitters lost their type and would have to be set to any in the consuming application.

it turns:

    interface HTMLElementTagNameMap {
        "my-component-one": HTMLMyComponentOneElement;
        "my-component-two": HTMLMyComponentTwoElement;
    }

into:

    interface HTMLElementTagNameMap {
        "my-component-one": HTMLMyComponentOneElement;
        [key: \`my-component-one--\${string}\`]: HTMLMyComponentOneElement;
        "my-component-two": HTMLMyComponentTwoElement;
        [key: \`my-component-two--\${string}\`]: HTMLMyComponentTwoElement;
    }

This made it necessary to differentiate the postfix from the tagname with --, since componenets with parts of the same tagname all would be funneled into the shortest tagname, like for example my-radio-button-group would just be caught by my-radio-button

All in all, I think this differentation also is a good thing, as it separates more clearly the acutal tagname from the transformed one.

Any thoughts on this?

@johnjenkins
Copy link
Contributor Author

johnjenkins commented Oct 24, 2025

@johnjenkins btw. do you do anything with the component.d.ts declaration file? To get emitters to get the correct type I had to add a patch for that file: custom-suffix-output-target.ts#L320

Without this patch, all emitters lost their type and would have to be set to any in the consuming application.

it turns:

    interface HTMLElementTagNameMap {
        "my-component-one": HTMLMyComponentOneElement;
        "my-component-two": HTMLMyComponentTwoElement;
    }

into:

    interface HTMLElementTagNameMap {
        "my-component-one": HTMLMyComponentOneElement;
        [key: \`my-component-one--\${string}\`]: HTMLMyComponentOneElement;
        "my-component-two": HTMLMyComponentTwoElement;
        [key: \`my-component-two--\${string}\`]: HTMLMyComponentTwoElement;
    }

This made it necessary to differentiate the postfix from the tagname with --, since componenets with parts of the same tagname all would be funneled into the shortest tagname, like for example my-radio-button-group would just be caught by my-radio-button

All in all, I think this differentation also is a good thing, as it separates more clearly the acutal tagname from the transformed one.

Any thoughts on this?

I think it's neat @Cliffback ... idk how it would work with what I have done though seeing as I make no assumptions at either

  1. build time ... whether a transformer is being set or used and...
  2. the transformed tag's format - people are free to transform tags as they see fit.

I can't see a way to get the run-time's tagTransformer result into a useable ts template literal type.

In reality, I think this would mainly affect 'vanilla' consuming applications(?) as JS-framework consumers when using wrappers continue to be typed correctly no-matter the transformer used.

Perhaps there would still be a need for a custom output target for those that need it :)

@Cliffback
Copy link
Contributor

I think it's neat @Cliffback ... idk how it would work with what I have done though seeing as I make no assumptions at either

  1. build time ... whether a transformer is being set or used and...
  2. the transformed tag's format - people are free to transform tags as they see fit.

I can't see a way to get the run-time's tagTransformer result into a useable ts template literal type.

In reality, I think this would mainly affect 'vanilla' consuming applications(?) as JS-framework consumers when using wrappers continue to be typed correctly no-matter the transformer used.

Perhaps there would still be a need for a custom output target for those that need it :)

Yes, that makes a lot of sense, especially given that my output targets main purpose has been getting it to work with angular (and that's were the emitter type issue arose).

However the element map patch is non-destructive, it just adds support for tagnames having a postfix as well, as long as it is distinguished with a --, making sure both original tagnames and tagnames with --my-postfix-example will be correctly typed. The same could be done with a prefix as well, I guess.

However for this to work with your solution, enforcement of -- between the original tagname and the added pre/postfix would be needed, which I guess might fall outside the intenteded scope of the PR.

Regardless, when this is released, I'll see how much of our setup could be migrated, and a way to solve the rest could be to change stencil-custom-suffix-output-target from doing everything to just do the rest needed to get the angular (and hopefully the react) proxy files working.

Eagerly looking forward to this getting merged! 🤩

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.

feat: transformTagName at runtime feat: component versioning

3 participants