webcomponents-cg / community-protocols

Cross-component coordination protocols
179 stars 12 forks source link

Support versioning of Web Components #29

Open bahrus opened 2 years ago

bahrus commented 2 years ago

This could be the simplest request to fulfill, ever, assuming it makes sense to anyone.

The issue of name clashes between web components has been addressed here, and progress has been frustratingly slow. Here's to hoping it sees some progress soon.

This proposal is highly related to that proposal.

Let me give this proposal a tentative name: Custom Element Weak Map Lookup, or CEWML for short, and I'll refer to Scoped Custom Element Proposal as SCEP.

CEWML is not meant to supplant SCEP, and I think might continue to be useful even if SCEP was fully implemented by the browsers. However, I haven't wrapped my brain around that proposal enough to know if that is the case or not.

But even if it renders this proposal useless, this could, I think, be used in the interim.

The key is we create a single npm package with a single file exported JS module, with either this signature:

export const scopedVersions = new WeakMap<ShadowRoot | Document, WeakMap<{new(): HTMLElement}, string>>();

or more simply, this:

export const versions = new WeakMap<{new(): HTMLElement}, string>();

Maybe it includes both?

We version this package 1.0.0 and never, ever modify it. So as long as everyone imports it via the same mechanism (import maps or bundling), there shouldn't be an issue of multiple versions of this one or two line JS file running around.

Each web component provider could provide its own way of facilitating how to do this registration / lookup.

Here's what I'm following, but this proposal in no way requires this approach. I just think it's helpful to provide a concrete example of how this could work with one implementation.

When I register a web component, say "my-component", I first check if my-component is already defined. If not, great. I happily register it with that name.

But if it is in use, I quietly register it by appending the first number I can find that hasn't been registered: for example my-component-1, or my-component-2, etc. Kind of like starting a web server, and searching for an available port.

I adopt the Polymer convention of adding the "is" static property to my custom element constructors. I set this to the canonical name. But when I find an available name before calling define, I set another static property on the constructor: "isReally".

So now if I generate the html using JavaScript, I can dynamically substitute the name in with this admittedly ugly code:

html`<${myComponentImport.isReally}>...</${myComponentImport.isReally}>`

However, this is a very specific implementation, and other groups may have no interest in adopting this exact approach.

But the key is each web component provider that wishes to partake in this solution would need to provide some way to associate the import with a specific name guaranteed to not clash with any other import. Really, if everyone could guarantee a unique global name, we really don't need the outer ShadowDOM WeakMap key, I don't think. But maybe that's asking too much?

What's always bothered me about this, is this wouldn't work for HTML-first solutions (like server rendered code). But if there is a common lookup mechanism like we have above, it would be possible to create a little JS code for that:

  1. Wraps all web components that may have clashes with other web components inside a template:
<template>
  <my-canonical-name>
   <my-light-children></my-light-children>
 </my-canonical-name
</template>

Yes, this means the SSR wouldn't show anything until the name resolution is complete, which isn't ideal, but this is the best I can come up with.

  1. The JavaScript would, after importing all the custom element references, perform the lookup, search for such templates inside its ShadowDOM, and rewrite the outer tag name to match the lookup while instantiating the template.

The lookup would look like:

const finalName = scopedVersions .get(shadowDomRoot).get(myComponentClass);

or, if in addition, we insist that this protocol works with some way of avoiding name clashes, just do:

const finalName = versions .get(myComponentClass);

I think the first lookup would be useful for scenarios where the web component provider provides a way of automatically subclassing the base element for each ShadowDOM with a specific name in mind.

I know this proposal isn't quite 100% solid, but I wanted to throw it out there to see if there's something like this we could do.

sashafirsov commented 2 years ago

The semver on web component is quite valuable. On npm or semver CDN it has been solved, but in the light of Declarative Web Application it would go to another layer.

The first comes to mind Custom Elements Manifest which would supply own version and potentially could hold the dependency tree in similar to package.json manner. It would require the module manager to do the roundtrip, pick the dependency version, load versioned code. The single manifest file for a whole application seems to be a most popular pattern as of now. With CE manifest it goes a bit closer to WC, which logically fall into proposal ^^:

<my-component@^4.3>

Which is quite trivial to implement as long as module manager is aware of manifest and dependencies. The scoped custom element registry could simplify DOM appearance but not really needed if CE is exposed as a class within manifest. Trouble would come when the CE code would try to register itself with fixed tag for different versions. At the moment such attempt would throw an exception.

Build-time or SSR could shortcut the module loader by alligning the tag with version and registration code to something like

<my-component--v-4-3-1>

Run-time in-browser implementation would need a Web Components Module Loader. I.e. this proposal need to be paired with such.

At the moment we have a JS module loader enhanced with weak concept of import maps, with a mix of CSS and HTML module loading capabilities as proposals. Could not see the WC loader fit into this stack, rather it would require own, preferably declarative, convention and implementation.

PS. Unregistering Custom Element is a mechanism needed to support versioning swap in runtime.

chr1se commented 2 years ago

Would it be better to use attributes to hold the version e.g

<my-component ver="4.3">

in a real project there could be external code making calls like document.querySelector('my-component') that will break if the tag name is always changing.

sashafirsov commented 2 years ago
<my-component ver="4.3">

definitely would be beneficial if we would be able to define the different versions during the import. But as of now the import of JS custom components does not have registration with version.

Or we could go even further and include the version into

<link type="import" src="lib/my-component@4.3" /> <!-- the DCE would need to include semantic version, type is DCE -->
<link type="import" src="lib/my-component@5.1" /> <!-- another content-type="component-module" ? -->
sashafirsov commented 2 years ago

There is a critical part of runtime semver update: ability to de-register the component tag+version with following registration of updated revision.

The simple tag de-registration proposal been closed :(

Should the API of registration with version be coupled with register/deregister?

IMO the runtime version upgrade need to be a part of current discussion and proposal.