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

Support for optional custom element registration to support Scoped CustomElementRegistry #705

Closed
xenobytezero opened this issue Mar 16, 2022 · 34 comments
Assignees
Labels
feature Feature requests.

Comments

@xenobytezero
Copy link

What issue are you having?

My usage of Shoelace for a pseudo-microfrontend has finally hit the inevitable issue with WebComponents where two seperate bundles of JS are trying to register the Shoelace components they are using, and the global CustomElementRegistry is obviously failing.

The most prominent solution to this issue appears to be using Scoped CustomElementRegistries, which relies on elements not auto registering with customElements.define(), and instead exposing just the raw classes to be manually registered later. As with all Lit based WebComponent libraries, this registration is handled in Shoelace by the @customElement decorator which automatically executes when the JS is loaded. There is currently no way to disable this in Lit.

Describe the solution you'd like

TL;DR I spent the day making a fork where I looked at a build time solution to offering all Shoelace components as both their raw classes and their current automatically registering forms. It's very early, and is for sure not production quality at the minute, but it does appear to be working on my Lit based test app via the @lit-labs/scoped-registry-mixin

https://github.com/xenobytezero/shoelace/tree/optional-auto-registration

Intentions

  • Zero breaking changes for current (auto registering) users of Shoelace
  • Minimal to zero source code changes for each component.

Approach

My plan was to have a build time step that will offer each component as both the current auto registering form, and as a raw class allowing it to be manually registered with Scoped Element Registries.

This takes the form of two steps

1/ Generate "auto registration" shims for each of the components, similar in a way to how the React shims are generated

This is handled in the scripts/make-reg.js Node script which is called from the build script, and creates a <tag-name>.auto.js shim in the dest/components folder for each component. This shim imports the raw class, as well as the Lit @customElement decorator, then registers the element. The shim looks like this (for example for <sl-button>)

import { customElement } from 'lit/decorators.js';
import SlButton from './button.js';
customElement('sl-button')(SlButton);
export * from './button.js';

All of the information needed for this is pulled from the custom-element.json metadata in the same way as the current React shims are created.

2/ Strip out the usages of the @customElement decorator from the components at build time.

This is done with a small ESBuild plugin which looks specifically for the component files and manually strips the decorator out before passing the source to ESbuild for standard building.

Issues

  • ESBuild does not allow for the changing of output file names dynamically
    • The original plan was to have the components (with the @customElement() decorator) be output as <tag-name>.class.js, and the generated shims to be output as <tag-name>.js as the current files are. This means that there would be zero code changes required for current users of the library to continue to use the auto registering versions of the components, and developers that want to consume the raw classes would instead do
import SlButton from '@shoelace-style/shoelace/dist/components/button/button.class'

Unfortunately, I couldn't get ESBuild to output a different filename than what it was originally, at least not for only a select set of files (only the ones in src/components). This could be solved by renaming the source files to <tag-name>.class.ts, but I didnt want to go about making sweeping changes this early on.

  • Elements that rely on specific tag names could fail when registered with a different name

    • A previous discussion on this topic mentioned that there are some components that rely on components being registered with the canonical names (DOM traversal, and I assume CSS?). For this particular use case, manually registering with a name other than the canonical one isn't required, so have not considered it here. It would be relatively straightforward to also expose a static containing the "canonical" tag name of the component that could be imported and used for manual registration. Would require clear docs.
  • None of this works with the "import everything" bundle.

    • There is no reason why it couldn't, would just require a little more work. Also it's not a use case I have right now, so wanted to test on what I know.
  • No docs or tests

    • Want to make sure this is even usable or wanted before I polish it all up. Also would want to make a decision on the first issue before continuing.

I understand that an issue is required first before creating a PR, so this serves that purpose. If there is interest I can do the official thing. This would also be my first proper PR, so apologies if I'm messing up the process.

Thoughts? Feelings? Opinions?

@xenobytezero xenobytezero added the feature Feature requests. label Mar 16, 2022
@claviska
Copy link
Member

I appreciate the intent, as this is a real problem with some application architectures. Earlier this year at Microsoft, I finished working on a FAST-based solution to this very same problem (the scoped registry polyfill wasn't an option for a number of reasons) so I'm well aware of what it entails.

To be upfront, I won't introduce that kind of custom logic in Shoelace. One, because it relies heavily on key FAST features; two, because it's a runtime solution that involves more overhead than I'd like; and three, Shoelace aims to follow standards and best practices more closely, so the "correct" solution is to wait for scoped custom element registries to ship.

Observations

That said, one problem I see with this proposal is that Shoelace isn't setup for dynamic tags names, which you've touched on above. There are a number of tags hard-coded in stylesheets, templates, and logic. For example, menus require menu items and tab groups require tabs/tab panels. (This is definitely not an exhaustive list.)

Some of this can me mitigated through the clever use of role or data- attributes. That doesn't feel very good, though, and it's not as guaranteed as a tag name.

Another concern is documentation. The docs are already plenty of work, and dynamic tag names add to that burden. However, see my comment below under "A Possible Alternative" for a potential win-win solution.

It would be relatively straightforward to also expose a static containing the "canonical" tag name of the component that could be imported and used for manual registration. Would require clear docs.

If I'm understanding correctly, you're suggesting that most tags could be named arbitrarily but those with canonical names should also, by recommendation, be registered with the correct name. If that's true, it will lead to a lot of confusion. Users want a cohesive system where they change a single prefix and use that prefix everywhere. They don't want to worry about individual tag names, so this feels very hacky to me. (Or maybe I'm simply misunderstanding you.)

All that aside, I recall hearing a suggestion where the class exists in one file and the registration in another. This would simplify the build logic, possibly eliminating it altogether. The caveat is that, for CDN users, they'll be loading additional chunks of boilerplate. (That may not be a big problem, though.)

While I appreciate the use case for multiple registrations with the same underlying components, I personally feel like it usually stems from poor architectural decisions. It would be better to hoist shared components up and resolve dependencies in such a way that all pieces of the app use the same version. This means you'd always get the correct styles and behaviors — never will the "same" component be disjointed aesthetically nor behaviorally.

At the same time, I realize in an enterprise scale, this isn't always easy or possible. 😔

But if Shoelace is going to enable such a pattern, I'd prefer that it take advantage of whatever optimizations come from the official standard once it becomes available.

A Possible Alternative

I haven't looked at the code in your branch (I'm about out of time for today), but from what you've described, it sounds like a lot of magic to get this working. That doesn't give me a good feeling.

Have you considered a post-build tool that analyzes and replaces tag names? This could exist as a standalone tool — possibly an official one — that parses and updates tag names in templates, stylesheets, and code.

This means we don't have a runtime solution (i.e. it won't work via CDN) but it keeps the stopgap solution outside of the core library, making it easier for me to support scoped custom element registry support once it lands. You would just need to build the library with the desired prefix.

The same tool could be used to parse and update tags in the docs, custom event names (which use the sl- prefix), and anything else that needs to be updated.

Remember that the scoped custom element registry proposal hasn't been finalized nor accepted, requires an imperfect polyfill, and it could change drastically before browsers implement it.

I'd feel much better about doing this with external tooling than trying to wedge it in the existing code base.

@claviska
Copy link
Member

By the way, I really appreciate the thoroughness of your proposal and your desire to not make sweeping changes unannounced. As a maintainer, thank you!

@xenobytezero
Copy link
Author

xenobytezero commented Mar 17, 2022

Hey, thanks for the thorough response!

Some thoughts.

By the way, I really appreciate the thoroughness of your proposal and your desire to not make sweeping changes unannounced. As a maintainer, thank you!

Feels like the only way you can do this kind of thing initially, so not a problem!

All that aside, I recall hearing a suggestion where the class exists in one file and the registration in another. This would simplify the build logic, possibly eliminating it altogether. The caveat is that, for CDN users, they'll be loading additional chunks of boilerplate. (That may not be a big problem, though.)

This is essentially what the build step does, split the class from the registration, but at build time. For now the decision to do this at build time was to avoid changing every component file, but also my thought was for tools like VSCode extensions and the Custom Element Manifest generator that would (possibly) rely on the @customElement decorator being present. Also the registration is pretty simple and can be dynamically generated in the same way as the React wrappers are generated, hence the approach above.

Just manually splitting the files would mean none of the build steps are required, but obviously testing that the rest of the system, extensions and tools continue to work.

That said, one problem I see with this proposal is that Shoelace isn't setup for dynamic tags names, which you've touched on above. There are a number of tags hard-coded in stylesheets, templates, and logic. For example, menus require menu items and tab groups require tabs/tab panels. (This is definitely not an exhaustive list.)

I understand that the current code does not support dynamic tag names, and using the ScopedElementRegistry also will not support this. Manual registration with the Scoped registration will have to be done with the "correct" names for the system to make it work. I'm not in a position to solve that problem with this approach, and since it's not a regression I assume it wouldn't be a thing that would be implemented for this.

I've done this for similar reasons with other WebComponents in the past, but the idea I talk about above is to expose the tag name as a static, allowing people to manually register with the correct tag name.

import { SlButton, SlButtonTag } from '@shoelace-style/shoelace/dist/components/button/button;

@customElement('scoped-component')
export class ScopedRegComponent extends ScopedRegistryHost(LitElement) {

    static elementDefinitions: ElementDefinitionsMap = {
      SlButtonTag: SlButton
    };

    static override styles = styles;

    public override render() {
        return html`<sl-button>Im a button</sl-button>`;
    }
}

@michaelwarren1106
Copy link
Contributor

michaelwarren1106 commented Apr 13, 2022

if it helps, I implemented an approach to allow for multiple versions of the same component to exist on the same page at the same time via a custom webpack loader that literally changed the tag name in the source.

I ran up against the same problem @claviska described where parent components style children by their tag name. I ended up styling via data- attributes like Cory suggests.

One other solution might just be that shoelace not directly use the @customElement decorator directly on the class, but instead create new side-effectful files where the custom element definition is done and create new exports that provide just the class by itself.

If some application has a need to have two of the same component from shoelace, with access to just the shoelace component class with no sideffects, the app could just have their own registration file and do something like:

import { SomeComponentClass} from 'shoelace';

customElements.define( 'new-tag-name', class extends SomeComponentClass {});

it would require maintenance steps in the application to make sure that the new registered names didnt clash somehow, but that is easy enough to solve with namespace prefixes and such.

The tagname references in shoelace's global style would still be a problem, but maybe theres some sort of helper that could be written there that could import the css with specified tag names replaced with a custom one? If the new component class is being registered, i think technically there would be an opportunity to add some stylesheet to the existing static styles list?

import { SomeComponentClass} from 'shoelace';

customElements.define( 'new-tag-name', class extends SomeComponentClass {
  static styles = [ someNewStylesheetWithTheRightTags];
});

@austinhallock
Copy link

+1 for being able to import the class independently from the customElements.define

File structure could be
alert/
├── alert.styles.ts
├── alert.class.ts
├── alert.test.ts
├── alert.ts

Where alert.class.ts is equivalent to today's alert.ts minus the @customElement('sl-alert') decorator

And new alert.ts looks something like

import SLAlertClass from './alert.class'

@customElement('sl-alert')
class SLAlert extends SLAlertClass {}

export default SLAlert

or if the decorator isn't necessary for VSCode extensions

import SLAlert from './alert.class'

customElements.define('sl-alert', SLAlert)

export default SLAlert

At first glance it looks like this approach wouldn't break anything, since alert.ts would still be exporting the same thing

@claviska
Copy link
Member

claviska commented Jul 2, 2022

@austinhallock I like that approach. There's still a need to work around dynamic tag names, though.

One other concern is how tooling will handle dynamic tag names since they'll be interpolated. I do this at work since it's a requirement (although it's with FAST Element) and it absolutely wrecks syntax highlighting in templates.

@michaelwarren1106
Copy link
Contributor

my two cents:

imo there are two issues here. One is the need for having an importable, side-effect-free class file as an option from shoelace. The other is how to deal with dynamic tag names internally to shoelace.

Personally, I see them as separate features, because if shoelace only provides the class export as an option, nothing has to change about shoelace itself to just provide that one new export. All the existing styles that come with shoelace would still work. If a developer wants to take a shoelace class, import it, extend it, re-register it with a different name, then the technical details of how to handle that name change would be up to that developer, per se.

One caveat would be that how separate these two features are depends on how often shoelace components refer to other shoelace components. Imo, if most of the shoelace components are largely standalone and dont refer to others, there's a lot of benefit in merely providing side-effect-free class file exports in the bundle for whatever a dev wants/needs to do with them. On the other hand, if most of the shoelace components are interconnected and depend on other components, then these features become more connected and might truly need to be solved for together. @claviska knows better than I do, and I'm assuming that there's a fair bit of interconnectedness that prompted him to bring it up in the first place :)

I see a few possible solutions for custom tag names, each with pros and cons.

source code approaches
If shoelace resolved to never use tag names anywhere in its code base and switch ALL styles/js refs everywhere to some other approach, everything would still work when the tag name got eventually re-registered. IMO for css references it would have to be data- attribs or roles or something. For js references, there could be a global lookup object that shoelace expects and checks to get the tagname for whatever application, or each component class has a common standardized static prop like canonicalName set to something that doesnt change with the tag name. Like SlAlert would have a static canonicalName = 'sl-alert'; even if the tag name was my-custom-alert and internal shoelace js references always checked el.canonicalName instead of el.tagName.

Loaders for bundlers
I would suggest that loaders for bundlers could be an option, but they wont work for CDN hosted shoelace consumers. If you installed shoelace via npm, then a bundler plugin/loader could be written that has access to the source code of the app at build time and can do the tag name find/replace during the build. One benefit here too is that bundler loaders would also have access to the application source files also, and could change tag names throughout the app code also. That way, devs could write/use sl-alert everywhere in their apps and the plugin would be automatically switching out the tag name references during the build process. So devs could still refer to shoelace docs directly and see the element names they expect, write their code as if nothing funky is going on with shoelace. Then if that loader/plugin was ever removed, the app would still function completely normally with no altered tags in source code anywhere. With this option again, nothing in the shoelace source code needs to change, as the modifications are done after the fact.

Static generator tool
There could be a tool written that will statically change every reference of every tag name to a prefixed version (sl-alert => prefix-alert everywhere it appears in the final bundle) and just output that custom version. It would be a one-time deal and you'd get a truly custom shoelace instance with ALL the custom elements all changed out to some prefixed version everywhere all at once. The drawback with this approach is that the custom version isn't CDN hostable, and would have to be packaged and added locally. The pro is that nothing about shoelace current source needs to change, as this tool would be separate and an addon for those that need it.

@jaredcwhite
Copy link
Contributor

jaredcwhite commented Jul 2, 2022

@michaelwarren1106 The "static generator" approach sounds pretty good to me. If someone's needs are so specific they need custom tag names, they're long past the "just host it on a CDN" stage. Outside looking in, I feel like Shoelace should generally remain as-is as much as possible, and support for custom names would mostly require an extra layer of tooling to facilitate.

@austinhallock
Copy link

Got it. Digging through the rest of this, it sounds like you don't like [data-sl-element="alert"] approach?

What would you say are some of the most complex instances of another component being referenced from a parent component? It looks like a lot of the CSS is styling slot elements if they're of a certain type of element. What are your thoughts on using the scoped nature of CSS variables instead?

Eg. looking at sl-input, it has

.input__prefix ::slotted(sl-icon),
  .input__suffix ::slotted(sl-icon) {
    color: var(--sl-input-icon-color);
  }

Could that instead be

.input__prefix, .input__suffix {
  --sl-icon-color: var(--sl-input-icon-color);
}

And have the sl-icon component use color: var(--sl-icon-color)

Disclaimer: I'm still very new to web components :)

@austinhallock
Copy link

@michaelwarren1106 The "static generator" approach sounds pretty good to me. If someone's needs are so specific they need custom tag names, they're long past the "just host it on a CDN" stage. Outside looking in, I feel like Shoelace should generally remain as-is as much as possible, and support for custom names would mostly require an extra layer of tooling to facilitate.

I think if done right, it doesn't need to require an extra layer of tooling. I do think they're two separate (but related) issues like @michaelwarren1106 mentioned:

  1. Ability to export classes separate from the component definition (easy)
  2. Getting rid of hardcoded component tag names

Our use-case might be specific, but I imagine there are other instances where developers will want to extend off of an existing Shoelace component to add/change some functionality, without needing to fork the entire repo

@jaredcwhite
Copy link
Contributor

@austinhallock

  1. Ability to export classes separate from the component definition (easy)

Right, that seems like an easy win. That way if I need to subclass SlAlert and change something I can, and then register that as sl-alert. Boom done.

But regarding the hardcoded tag names, I'd hate to see the component source files become much more complicated or hard to read. I don't think tag name references in parts, etc. can be wholly avoided either. Seems like it should be a reasonably straightforward proposition though simply to parse compiled output templates/CSS to replace sl-button with custom-button, etc. Or…via some sort of transform step within the toolchain itself when compiling a custom build of Shoelace.

@xenobytezero
Copy link
Author

Personally, I see them as separate features, because if shoelace only provides the class export as an option, nothing has to change about shoelace itself to just provide that one new export.

I think I didn't communicate this well enough when I opened the ticket.

I 100% agree that these feel like mutually exclusive features, with my intention of the change was to support the Scoped registry. The dynamic tag issue feels like a much larger thing.

@claviska
Copy link
Member

claviska commented Jul 2, 2022

I 100% agree that these feel like mutually exclusive features

I don't consider them separate at all. The primary use case for optional registration is to change the prefix/tag names. If optional registration is officially supported, users will expect it to work no matter what they name each element. When it doesn't, that's a poor experience. And in many cases, it won't be immediately obvious that things are broken.

Such an experience cheapens users' perception of the library. How many folks will file a bug about this? I suspect it will be a lot and "that's by design" isn't an answer they're going to want to hear.

Not considering dynamic tag names as a prerequisite for optional registration is a recipe for failure on both sides.

I'd hate to see the component source files become much more complicated or hard to read.

Me too. I enjoy working on this library because it's fun for me. Mucking up the source to support a feature that [unless the tag name problem is sorted] will only serve to allow users to break things isn't a win for anyone.

Digging through the rest of this, it sounds like you don't like [data-sl-element="alert"] approach?

A tag name is static. It can't change. A data attribute, a role, a class, etc. are all subject to DOM manipulation. For performance reasons, these would be set on init rather than on each render. If the attribute gets removed or changed at some point, the component(s) that reference them will break in unexpected ways.

We can argue that users shouldn't change said attributes, but they will either accidentally (e.g. binding a class/attribute that overrides what the component sets) or through some third-party lib and I'll have to answer the support calls.

Post-build and Premium Features

I still like the idea of a post-build option. If you're customizing Shoelace at that level, you're probably picking various components for use in a design system or a similar library. It's essentially a white-label option, so an additional step to make it your own seems like a fair tradeoff, IMO.

I'd also like to float the idea of making custom prefix/tag names a premium option, as many (most?) "white-labelers" will be using this feature to bring Shoelace into their org's design system. This might seem like a big assumption, but I haven't seen any other use case for needing to change tag names. If you're using Shoelace that way, I think it's more than fair to ask for a reasonable license fee since even one component can save your company a significant amount of money in design/engineering expenses.

I appreciate that this might be disappointing to some folks, but I'd like to spend more time on Shoelace and, in order to do that, I need to be able to justify it as more than just a free side project. I have a handful of amazing sponsors, but it's not nearly enough to drop my other obligations. A premium option like this could solve that.

By the way, this month (July) marks the two year anniversary of Shoelace 2.0's release!

@xenobytezero
Copy link
Author

I don't consider them separate at all. The primary use case for optional registration is to change the prefix/tag names. If optional registration is officially supported, users will expect it to work no matter what they name each element. When it doesn't, that's a poor experience. And in many cases, it won't be immediately obvious that things are broken.

Had not considered this, totally fair assessment.

@michaelwarren1106
Copy link
Contributor

I appreciate that this might be disappointing to some folks, but I'd like to spend more time on Shoelace and, in order to do that, I need to be able to justify it as more than just a free side project. I have a handful of amazing sponsors, but it's not nearly enough to drop my other obligations. A premium option like this could solve that.

Not me. This is your baby, make your money, dude.

The post-build or white-label option is something that you and I have talked about of this thread a little bit, like a generator that generates your own custom version of shoelace specifically for design systems and component libraries that want to use shoelace as a start, but need their own tag names and are willing and able to support/bug fix on their own after the fact.

I am onboard for that approach for sure. I picture a one-time generation thing (as in you generate a point in time version of shoelace and dont get updates when shoelace updates) and after that generation is done, the consumer has their very own customized version of shoelace that they now own/maintain.

I think sticking to one-time-generation is way simpler and easier, but if a consumer wants to get an updated version of shoelace, then they would need to regenerate and manually integrate whatever source code changes they've done on their side. On the shoelace side, this could be made a little easier by offering a generator tool that can generate both the whole lib and also a single component at a time.

@claviska
Copy link
Member

claviska commented Jul 2, 2022

but need their own tag names and are willing and able to support/bug fix on their own after the fact.

If the generator approach happens, I want to emphasize that users should be able to upgrade if they don't want to be responsible for bug fixes and new features. So you'd have the option to keep things pure and regenerate easily using the latest version, or you can generate once and "fork" components to make them your own.

@michaelwarren1106
Copy link
Contributor

michaelwarren1106 commented Jul 2, 2022

Just had an idea...

If the white-labeling option is approached as a paid option, and you setup like subscriptions and accounts for consumers, then that would enable you to generate each special version of shoelace for every account according to their settings whenever shoelace changed and still be able to host those custom versions on a CDN if you wanted to.

If there was a subscription plan/membership thing, then i can imagine that each subscriber gets their own custom CDN url for shoelace that would serve up their specifically generated version in case they wanted to use shoelace from CDN.

something like
https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@<consumer-name>/dist/shoelace.js

Maybe the combination of both CDN hosted custom shoelace versions AND the shoelace generator would be a good offering to justify the subscription cost?

@michaelwarren1106
Copy link
Contributor

users should be able to upgrade if they don't want to be responsible for bug fixes and new features

for sure! But I think that is purely a function of what folks do after they get their hands on the source code. And i'm not sure we're ever going to be able to determine that? If shoelace generates a custom source, and a consumer copies that source into a new github repo then goes to town on customizations, they've effectively forked. If the only changes they make are in completely separate files/directories that just import/extend default shoelace assets, then it might be possible for them to seamlessly upgrade, but i'm not sure how shoelace would know what kind of situation folks have?

@claviska
Copy link
Member

claviska commented Jul 2, 2022

Great ideas! We're getting a bit sidetracked from the original issue, though, so I've opened up a discussion to continue the talk about premium features.

#811

@MauriceAcerta
Copy link

Why don't you temporarily replace the decorators with the following snippet:

  if (!window.customElements.get(tagName)) {
    window.customElements.define(tagName, clazz);
  }

This would solve the issue without any side effects (that I am aware of) and could be a quick solution before applying more complex solutions.

We're currently looking at using this library, but this issue is kind of a deal-breaker

@claviska
Copy link
Member

claviska commented Jul 8, 2022

Why don't you temporarily replace the decorators with the following snippet:

What are you trying to accomplish with this? Are you looking to use the Scoped Custom Element Registry polyfill?

@MauriceAcerta
Copy link

Why don't you temporarily replace the decorators with the following snippet:

What are you trying to accomplish with this? Are you looking to use the Scoped Custom Element Registry polyfill?

We're trying to avoid having 2 or more components trying to register any <sl-*> component, resulting in an error and an empty web page

@claviska
Copy link
Member

claviska commented Jul 8, 2022

Thanks for clarifying. Are you not concerned that the first loaded module will win, regardless of which version it is? For example, if Module A loads Shoelace 2.0.0-beta.48 and Module B loads Shoelace 2.0.0-beta.70, how can you be sure components that are designed to work together will in fact work together? There are also breaking changes in various beta versions, meaning the APIs can be different.

Something as simple as a button variant can break, as the type attribute was renamed to variant in beta.63:

<sl-button type="primary">Button</sl-button> <!-- only works in beta.62 and below -->
<sl-button variant="primary">Button</sl-button> <!-- only works in beta.63 and above -->

If you don't have control over which versions are being imported, you will run into issues like this. I would suggest that, even in a microfrontend architecture,component dependencies should be resolved and loaded higher up in the stack to avoid problems like this.

The suggested conditional suppresses the error, but none of the problems that will occur as a result of re-registration will be solved. It's a foot gun. And if it's not a foot gun for you due to constraints not mentioned herein, it will be for other users.

If you really want to do this, you can overload customElements.define() and ignore any tag prefixed with sl- if it's already registered...but I wouldn't recommend doing that unless you fully understand the consequences.

@MauriceAcerta
Copy link

Thanks for clarifying. Are you not concerned that the first loaded module will win, regardless of which version it is? For example, if Module A loads Shoelace 2.0.0-beta.48 and Module B loads Shoelace 2.0.0-beta.70, how can you be sure components that are designed to work together will in fact work together? There are also breaking changes in various beta versions, meaning the APIs can be different.

Something as simple as a button variant can break, as the type attribute was renamed to variant in beta.63:

<sl-button type="primary">Button</sl-button> <!-- only works in beta.62 and below -->
<sl-button variant="primary">Button</sl-button> <!-- only works in beta.63 and above -->

If you don't have control over which versions are being imported, you will run into issues like this. I would suggest that, even in a microfrontend architecture,component dependencies should be resolved and loaded higher up in the stack to avoid problems like this.

The suggested conditional suppresses the error, but none of the problems that will occur as a result of re-registration will be solved. It's a foot gun. And if it's not a foot gun for you due to constraints not mentioned herein, it will be for other users.

If you really want to do this, you can overload customElements.define() and ignore any tag prefixed with sl- if it's already registered...but I wouldn't recommend doing that unless you fully understand the consequences.

Thanks for your explanation. I did not consider that.
Ideally we would like to have each of the components have their own dependencies and not have the dependency declared higher up in the hierarchy (imo this destroys the purpose op WC's). I'm not sure if we can have both solutions today (no collisions with registration AND each their own dependencies)

@claviska
Copy link
Member

claviska commented Jul 8, 2022

The vision behind the Scoped Custom Element Registry does indeed aim to solve this problem. My personal opinion of front-end architectures aside, this is a real world problem and something I have to face myself at MS.

The solution we came up with at work was a mechanism for allowing custom prefixes, so you can register different versions of the same components under different tag names. This eliminates most of the problems except for major versions with breaking changes, which would require a new tag name to prevent collisions. But that solution isn't as elegant as I'd like and it makes using the library a bit harder.

I consider this a gap in the platform and the cleanest way to mitigate it in 2022 is to move component registration up in the stack. I realize this defeats the purpose of the micro front-end paradigm, but I'll leave my opinions on inefficiencies and disparate aesthetics/behaviors out of this thread. 😅

FWIW the concept of a generator that can produce unique Shoelace components, e.g. <my-button>, would be a reasonable workaround. If you're not sharing components, each piece of the front end could register its own version with custom tags, circumventing the problem.

It's not a proper solution, but it would work reasonably well until the platform catches up.

@michaelwarren1106
Copy link
Contributor

michaelwarren1106 commented Jul 8, 2022

@MauriceAcerta ive been where you are I think :)

Previously I was working on a design system that was being used in a large micro-front-end architecture application, where a single page was constructed from various MFEs written by completely different teams that werent at all aware of the activities of other teams, much less their code bases.

As such, each MFE could be using a different version of the same component on the same page at the same time as @claviska described. In order to make this work at all, there are two solutions. IMO you can either

A. have each MFE application maintain a list of re-registrations so that sl-alert (beta.62) gets reregistered as sl-alert-62 and the MFE app uses sl-alert-62 in their HTML everywhere. Then another MFE re-registers sl-alert as sl-alert-someotherversion.

OR

B. If your MFEs are applications with a bundler like webpack/rollup, write a common loader plugin designed to literally string replace all references to all shoelace component tag names to some different tag name of your choosing during the bundle step where you have access to the application source code.

What we ended up doing is going with the loader approach, because that enables teams to use the regular tag names in their MFE apps (ie, devs would always write sl-alert and it would just get changed during the build process). Our loader figured out which version of which components were in the node_modules folder and appended the version to the tag name, so our string replace would have been something like the above sl-alert => sl-alert-62.

The re-register solution requires that shoelace never references a component by its tag name and uses some other method. The loader solution requires access to shoelace source code AND app source code so you can do the string replace (and also makes builds take a smidge longer).

So, basically as @claviska says, if you're just trying to prevent errors upon re-registration, then you risk the first registration being the one that wins. If you really want to make each MFE's version of shoelace truly separate, then they must get registered as truly different component tag names which right now today will only work with the loader approach, since thats the only approach that can account for the fact that shoelace today refers to other shoelace components by tag name. The loader approach is afaik the only way that you'd be able to change ALL instances of a shoelace component tag name to some other tag name.

EDIT: @claviska thought of a third option which is a pre-generation option where you take shoelace's source code and string replace it ahead of time instead of via a loader which would do it at build time

@MauriceAcerta
Copy link

MauriceAcerta commented Jul 11, 2022

Thanks a ton for the extensive reply @michaelwarren1106! Would you mind elaborating on how you can enable string replacement of tags in the source code of Shoelace during build time? I'm not familiar enough with Webpack/rollup to see how that's done right now

@michaelwarren1106
Copy link
Contributor

Webpack has some docs on how to write a loader:

https://webpack.js.org/contribute/writing-a-loader/

Rollup has guides about how to write a plugin:

https://rollupjs.org/guide/en/#plugins-overview

After checking out those docs, its just a matter of writing your plugin however you see fit according to your needs

@MauriceAcerta
Copy link

Webpack has some docs on how to write a loader:

https://webpack.js.org/contribute/writing-a-loader/

Rollup has guides about how to write a plugin:

https://rollupjs.org/guide/en/#plugins-overview

After checking out those docs, its just a matter of writing your plugin however you see fit according to your needs

I more specifically mean how to actually implement that.
I'm not sure how you'll be able to dig inside the node_modules and replace the tags in the compiled code.

@michaelwarren1106
Copy link
Contributor

you dont dig into the node_modules and replace it there, the loaders/plugins get run at build time and basically pass you the source code of files that are being run through the build and you can change stuff about them during the build process via your loader...

@arvindanta
Copy link

arvindanta commented Jul 12, 2022

@michaelwarren1106 Do you have any basic sample of how you have achieved this multiple versioning of webcomponents using loader ?

@michaelwarren1106
Copy link
Contributor

unfortunately no, that was all internal proprietary code, sorry :(

@KonnorRogers
Copy link
Collaborator

Oh wow, just stumbled on this thread. I went with button.component instead of button.class.

#1450

@KonnorRogers
Copy link
Collaborator

@MauriceAcerta This has been fixed in #1450

Closed with #1450

https://shoelace.style/getting-started/installation/#avoiding-auto-registering-imports

Note: Custom tagnames are not fully supported yet.

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

No branches or pull requests

8 participants