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

feat: component versioning #3138

Open
3 tasks done
danyball opened this issue Nov 10, 2021 · 24 comments
Open
3 tasks done

feat: component versioning #3138

danyball opened this issue Nov 10, 2021 · 24 comments
Labels
Feature: Want this? Upvote it! This PR or Issue may be a great consideration for a future idea.

Comments

@danyball
Copy link

danyball commented Nov 10, 2021

Prerequisites

Describe the Feature Request

It should be possible to set a suffix to the component names that are registered via customElements.define to be better hardened when 2 custom elements are "living" on the same page in different versions with the same name.

Describe the Use Case

We have our main landing page that is organized with a content management system. And we have several apps, written with angular, react, vue - all using our stencil lib. We have 2 Apps that are loaded at this main page. These 2 Apps are using different versions of our stencil lib that can cause issues on that page. Depending on which app is loaded first on that page and registered "their" customElements first. If app1 uses lib-v-1 with the old, blue button and app2 uses lib-v-2 with the new red one and app1 is loaded first, all the buttons of app2 are blue.

Describe Preferred Solution

Example for mylib version 1.1.0.

At build time:
Add a customElements.define with <mylib-button> and <mylib-button-1-1-0>.

(optional feature) At build time of a consumer of that mylib:
Add a 3. define: <mylib-button-consumername>

Describe Alternatives

Waiting for html standard to implement something like customElements.define('mylib-button', MyButton, { version: 1.1.0 }).

Related Code

No response

Additional Information

I found a solution of how it could work of a lit-element based lib:
https://github.com/axa-ch-webhub-cloud/pattern-library/blob/develop/COMPONENT_VERSIONING.md

@ionitron-bot ionitron-bot bot added the triage label Nov 10, 2021
@splitinfinities splitinfinities added Feature: Bundling Feature: Want this? Upvote it! This PR or Issue may be a great consideration for a future idea. labels Nov 13, 2021
@ionitron-bot ionitron-bot bot removed the triage label Nov 13, 2021
@danyball
Copy link
Author

Another approach could be https://open-wc.org/docs/development/scoped-elements/

@JSMike
Copy link

JSMike commented Nov 19, 2021

Hi All, I just spoke with some on the Stencil team about versioning and was pointed to this issue. My team has a similar scenario where we need to support design system components that could potentially be used in multiple applications that are present on the same page, and there's potential that each application could be using a different version of the design system's components.

Especially for the tooling that wraps Stencil components for other frameworks (Angular/React/Vue) It would be great if we were able to keep the framework selectors the same so that each version of the library wouldn't result in a breaking change for consuming applications. IMO it would be best if Stencil could keep track of a generated hash (or a value from config) during build time and append that hash to the web component selector, but keep the original selector for the wrapped framework libraries. Under this example the selector would be defined as <mylib-button>, the Angular/React/Vue components would behave as if <mylib-button> was unchanged as the selector, but would be configured to point to use <mylib-button-ba583>.

I do believe that the scoped elements option is the proper long-term solution, but until the spec is finalized and implemented in enough browsers we'll need a pattern to manage versioning. Since the listed frameworks already hook/scope onto an element this would provide a path forward for those using wrapped stencil components without having to introduce breaking changes to those framework specific libraries with every update.

@splitinfinities
Copy link
Contributor

Question, how would #3155 relate to this? I feel like, based on the behavior of the compiler's results, either this issue or that one may need to happen first. Or are they effectively accomplishing the same objectives, for the versioning feature? Super interested in forming a strategy around this versioning and bundling behavior so I want to dig deeper. Any advice is super helpful!

@danyball
Copy link
Author

danyball commented Dec 3, 2021

Question, how would #3155 relate to this?

3155 describes a solution for the "collections" folder and a use case where 2 stencil projects working together. I dont have experience with that case but it sounds that this issue here is solving 3155. If the version of a component is being reflect to its tagname at build time, another stencil project could consume that tag. But an app could also use tag version-tag.

@danyball
Copy link
Author

danyball commented Dec 3, 2021

My optional feature request could be covered by "tagNameTransform" but that comes with some points which should be noticed when writing the stencil lib (if a component uses another):

  • at templates we can not use <mylib-button> because that tagname is probably transformed at runtime
  • at css we can not use tagnames at selector like ::slotted(mylib-button)

Its possible to solve that but maybe we could have a look at that if we/you design a solution for my optional request.
This guy describes how to solve this: https://dev.to/sanderand/running-multiple-versions-of-a-stencil-design-system-without-conflicts-2f46

@danyball
Copy link
Author

danyball commented Apr 4, 2022

Referring to my last post and the optionally requested feature: There is another open issue now: #3269

@arvindanta
Copy link

Hi All, I just spoke with some on the Stencil team about versioning and was pointed to this issue. My team has a similar scenario where we need to support design system components that could potentially be used in multiple applications that are present on the same page, and there's potential that each application could be using a different version of the design system's components.

Especially for the tooling that wraps Stencil components for other frameworks (Angular/React/Vue) It would be great if we were able to keep the framework selectors the same so that each version of the library wouldn't result in a breaking change for consuming applications. IMO it would be best if Stencil could keep track of a generated hash (or a value from config) during build time and append that hash to the web component selector, but keep the original selector for the wrapped framework libraries. Under this example the selector would be defined as <mylib-button>, the Angular/React/Vue components would behave as if <mylib-button> was unchanged as the selector, but would be configured to point to use <mylib-button-ba583>.

I do believe that the scoped elements option is the proper long-term solution, but until the spec is finalized and implemented in enough browsers we'll need a pattern to manage versioning. Since the listed frameworks already hook/scope onto an element this would provide a path forward for those using wrapped stencil components without having to introduce breaking changes to those framework specific libraries with every update.

@JSMike were you able to find any work around for this usecase ?.

@JSMike
Copy link

JSMike commented May 17, 2022

@arvindanta No, I haven't had time to focused on this issue.

I believe the scoped elements that @danyball linked is a good path forward for pure web-component development, by nesting/bundling web-components in a way to ensure the expected dependency version is used, but it doesn't help for wrapping web-components for use in other frameworks (That would still require using global scope on the page).

When component scoping becomes natively available in browsers it would be great if stencil wrapped components were able to be scoped to within the context of the application they're being used with.

@arvindanta
Copy link

arvindanta commented Jul 18, 2022

Hi, I am trying to find a way to allow multiple versions of stencil components to be used in the same page.

I am thinking of a workaround where we can use a bundler like webpack, write a loader plugin to literally string replace all references to all stencil component tag names to some different tag name of your choosing during the bundle step.
fw-button to fw-button-v1 at build time.

so as Devs, you will always use fw-button in the code but at build time it gets changed to fw-button-v1.

I was able to make this work with Tree shaking. (dist/components) folder . But with lazy loading am getting the below error,

main.1464e78e.js:10335 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'isProxied')

Using Stencil 2.9.0

@jared-christensen
Copy link

Has anyone tried this https://dev.to/sanderand/running-multiple-versions-of-a-stencil-design-system-without-conflicts-2f46
I'm curious how well it works.

@FabioGimmillaro
Copy link

Hi Jared

we tried.
But refactoring the whole code base was not a viable solution for us.
Besides that we also had some css dependencies (mostly with the ::slotted selector) which we couldn't refactor without overriding the style in js/ts.
If we considered that at the beginning of development we might have given it a chance though.

@fcano-ut
Copy link

I believe Scoped Custom Elements seems like the best solution, more so considering it's proposed as a web standard.

Right now it this polyfill is adding support: https://www.npmjs.com/package/@webcomponents/scoped-custom-element-registry?activeTab=versions, but it's unclear if it could be used with Stencil. Has anyone tried this?

@mayerraphael
Copy link

mayerraphael commented Feb 27, 2024

I believe Scoped Custom Elements seems like the best solution, more so considering it's proposed as a web standard.

Right now it this polyfill is adding support: https://www.npmjs.com/package/@webcomponents/scoped-custom-element-registry?activeTab=versions, but it's unclear if it could be used with Stencil. Has anyone tried this?

Got it working by passing the custom registry as part of the the defineCustomElements method options.

// Add polyfill before.

const registryA = new CustomElementRegistry();

class HostElementA extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({
        mode: 'open',
        customElements: registryA,
      }).innerHTML = `
         <style>:host { background: red; }</style>
            Wrapper<br/>
        <stencil-button>Button inside scoped registry.</stencil-button>
      `;

     // Pass through registry as options.
      defineCustomElements(window, { registry: registryA }).catch(console.warn);
    }
}

customElements.define('el-a', HostElementA);

The registry is just passed through to the define call via the defineCustomElements options.

// Patch. Check if a custom registry is supplied.
const registry = options.registry ?? window.customElements;
if (!exclude.includes(tagName) && !registry.get(tagName)) {
    cmpTags.push(tagName);
    registry.define(tagName, proxyComponent(HostElement, cmpMeta, 1 /* PROXY_FLAGS.isElementConstructor */));
}

In html you can then test it. The button inside el-a works, the button outside does not.

<el-a>dsa</el-a>
<br/>
 This button should not work:
<stencil-button>Test</patternlib-button>

image

@rwaskiewicz Allowing a custom elements registry in the defineCustomElements options object could work?

@AndreBarr
Copy link

AndreBarr commented Mar 12, 2024

I believe Scoped Custom Elements seems like the best solution, more so considering it's proposed as a web standard.
Right now it this polyfill is adding support: https://www.npmjs.com/package/@webcomponents/scoped-custom-element-registry?activeTab=versions, but it's unclear if it could be used with Stencil. Has anyone tried this?

Got it working by passing the custom registry as part of the the defineCustomElements method options.

// Add polyfill before.

const registryA = new CustomElementRegistry();

class HostElementA extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({
        mode: 'open',
        customElements: registryA,
      }).innerHTML = `
         <style>:host { background: red; }</style>
            Wrapper<br/>
        <stencil-button>Button inside scoped registry.</stencil-button>
      `;

     // Pass through registry as options.
      defineCustomElements(window, { registry: registryA }).catch(console.warn);
    }
}

customElements.define('el-a', HostElementA);

The registry is just passed through to the define call via the defineCustomElements options.

// Patch. Check if a custom registry is supplied.
const registry = options.registry ?? window.customElements;
if (!exclude.includes(tagName) && !registry.get(tagName)) {
    cmpTags.push(tagName);
    registry.define(tagName, proxyComponent(HostElement, cmpMeta, 1 /* PROXY_FLAGS.isElementConstructor */));
}

In html you can then test it. The button inside el-a works, the button outside does not.

<el-a>dsa</el-a>
<br/>
 This button should not work:
<stencil-button>Test</patternlib-button>

image

@rwaskiewicz Allowing a custom elements registry in the defineCustomElements options object could work?

@mayerraphael How were you able to add the custom registry as an option to the defineCustomElements method? Did you make changes in a local version of stencil?

@fcano-ut
Copy link

fcano-ut commented Mar 13, 2024

For anyone looking for a workaround, we managed to make it working doing something like this:

const backup = window.customElements;

window.customElements = registry; // pass your registry here
defineCustomElements(...) // call defineCustomElements
window.customElements = backup; // restore the customElements global to what it was

That works without any change in Stencyl, but it's not pretty. I'm hoping they can add library support for this soon as @mayerraphael suggested, to stop using this kind of "hacky" workaround

@mayerraphael
Copy link

This only works in older versions of Stencil. In newer versions defineCustomElements is async.
If you have other registrations in between it breaks.

See:

s.append(`export const defineCustomElements = async (win, options) => {\n`);

@FabioGimmillaro
Copy link

FabioGimmillaro commented Mar 13, 2024

I wrote a script which manipulates the generates dist/esm sources to also accept a registry as an input parameter:

const path = require("path");
const fs = require("fs");

const distPath = path.resolve(__dirname, "../dist/");
const esmPath = `${distPath}\\esm`;

const indexFileName = fs
  .readdirSync(esmPath)
  .find((fn) => fn.startsWith("index-") && fn.endsWith(".js"));

const indexFile = `${esmPath}\\${indexFileName}`
const fileContent = fs.readFileSync(indexFile, "utf-8");

console.log("Adjust indexFile", indexFile);

return fs.promises.writeFile(indexFile, fileContent.replace(/const customElements = win.customElements;/, "const customElements = options.registry ?? win.customElements;"), "utf-8")

I wrote the script for StencilJS 2.22.3 and the "dist" output target so I don't know if this also works for StencilJS versions >= 3.

I'm currently in my testing phase so I don't know if this is a viable solution for most use-cases

The call to defineCustomElements should look like this: defineCustomElements(window, { registry: $newRegistry });

@mayerraphael
Copy link

I wrote a script which manipulates the generates dist/esm sources to also accept a registry as an input parameter:

const path = require("path");
const fs = require("fs");

const distPath = path.resolve(__dirname, "../dist/");
const esmPath = `${distPath}\\esm`;

const indexFileName = fs
  .readdirSync(esmPath)
  .find((fn) => fn.startsWith("index-") && fn.endsWith(".js"));

const indexFile = `${esmPath}\\${indexFileName}`
const fileContent = fs.readFileSync(indexFile, "utf-8");

console.log("Adjust indexFile", indexFile);

return fs.promises.writeFile(indexFile, fileContent.replace(/const customElements = win.customElements;/, "const customElements = options.registry ?? win.customElements;"), "utf-8")

I wrote the script for StencilJS 2.22.3 and the "dist" output target so I don't know if this also works for StencilJS versions >= 3.

I'm currently in my testing phase so I don't know if this is a viable solution for most use-cases

The call to defineCustomElements should look like this: defineCustomElements(window, { registry: $newRegistry });

Again, defineCustomElements is async in newer Stencil Versions.

Imagine you have multiple microfrontends each requiring a custom registry, overwriting the global window.customElements Registry in an async initialization process will be pure chaos. I highly recommend not doing that.

The only viable option is that the internal define call in defineCustomElements has the correct registry so it works nicely with the async nature of JS.

@fcano-ut
Copy link

fcano-ut commented Mar 14, 2024

@mayerraphael I agree, the only "proper" solution is support at the Stencil level, but we need workarounds in case that takes a lot...

If defineCustomElements is a promise, something like this should work and be safe I guess? We haven't tested it, but we'll figure it out when we update Stencil

const backup = window.customElements;

new Promise(resolve => {
  window.customElements = registry; // pass your registry here
  resolve(defineCustomElements(...)); // call defineCustomElements
}).then(() => {
  window.customElements = backup; // restore the customElements global to what it was
});

@mayerraphael
Copy link

mayerraphael commented Mar 14, 2024

@fcano-ut

@mayerraphael I agree, the only "proper" solution is support at the Stencil level, but we need workarounds in case that takes a lot...

If defineCustomElements is a promise, something like this should work and be safe I guess? We haven't tested it, but we'll figure it out when we update Stencil

const backup = window.customElements;

new Promise(resolve => {
  window.customElements = registry; // pass your registry here
  resolve(defineCustomElements(...)); // call defineCustomElements
}).then(() => {
  window.customElements = backup; // restore the customElements global to what it was
});

Lets say you have a shell and a microfrontend app, both having different Versions of your component library.

// Register shell on global window.customElements.
shell.defineCustomElements();

// Register app.
backup = window.customElements;
window.customElements = myAppRegistry;
microfrontend.defineCustomElements().then(() =>window.customElements = backup);

It could happen that the shell is not done regsitering while you override the global window.customElements and also registering other versioned components in between => Chaos.

The only thing helping is synchronizing, but this blocks your frontend (one after the other => slow)

// Register shell on global window.customElements.
shell.defineCustomElements().then(() => {
  // Register app.
  backup = window.customElements;
  window.customElements = myAppRegistry;
  microfrontend.defineCustomElements().then(() =>window.customElements = backup);
});

But i dont wannt to know what other Stencil processes running async this messes with. So just don't do it.

It would be easier to patch the Stencil runtime (index.js) by providing the registry in the options than guaranteeing that overriding the global registry works in a correct way.

@fcano-ut
Copy link

I get your point, @mayerraphael, you're right. This totally breaks if the defineCustomElements function has async steps inside and if you register multiple micro-frontends at once, since the order of execution will not be guaranteed. Thanks for thinking this through. We'll abstain for updating stencil for now I guess 😐

@FabioGimmillaro
Copy link

@fcano-ut
Wouldn't it be enough if StencilJS provided an API to add a new CustomElementRegistry when calling defineCustomElements?
e.g.

defineCustomElements(window, { customElements: new CustomElementRegistry() });

or return a customElementsRegistry when calling defineCustomElements?
Whether a new customElementsRegistry is created could be configured in the stencil.config.ts by adding a flag in the extras section

const { customElementsRegistry } = defineCustomElements(window);

@mayerraphael
Copy link

I believe Scoped Custom Elements seems like the best solution, more so considering it's proposed as a web standard.
Right now it this polyfill is adding support: https://www.npmjs.com/package/@webcomponents/scoped-custom-element-registry?activeTab=versions, but it's unclear if it could be used with Stencil. Has anyone tried this?

Got it working by passing the custom registry as part of the the defineCustomElements method options.

// Add polyfill before.

const registryA = new CustomElementRegistry();

class HostElementA extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({
        mode: 'open',
        customElements: registryA,
      }).innerHTML = `
         <style>:host { background: red; }</style>
            Wrapper<br/>
        <stencil-button>Button inside scoped registry.</stencil-button>
      `;

     // Pass through registry as options.
      defineCustomElements(window, { registry: registryA }).catch(console.warn);
    }
}

customElements.define('el-a', HostElementA);

The registry is just passed through to the define call via the defineCustomElements options.

// Patch. Check if a custom registry is supplied.
const registry = options.registry ?? window.customElements;
if (!exclude.includes(tagName) && !registry.get(tagName)) {
    cmpTags.push(tagName);
    registry.define(tagName, proxyComponent(HostElement, cmpMeta, 1 /* PROXY_FLAGS.isElementConstructor */));
}

In html you can then test it. The button inside el-a works, the button outside does not.

<el-a>dsa</el-a>
<br/>
 This button should not work:
<stencil-button>Test</patternlib-button>

image
@rwaskiewicz Allowing a custom elements registry in the defineCustomElements options object could work?

@mayerraphael How were you able to add the custom registry as an option to the defineCustomElements method? Did you make changes in a local version of stencil?

Exactly, i just patched the runtime.

@AndreBarr
Copy link

AndreBarr commented Mar 14, 2024

I wrote a script which manipulates the generates dist/esm sources to also accept a registry as an input parameter:

const path = require("path");
const fs = require("fs");

const distPath = path.resolve(__dirname, "../dist/");
const esmPath = `${distPath}\\esm`;

const indexFileName = fs
  .readdirSync(esmPath)
  .find((fn) => fn.startsWith("index-") && fn.endsWith(".js"));

const indexFile = `${esmPath}\\${indexFileName}`
const fileContent = fs.readFileSync(indexFile, "utf-8");

console.log("Adjust indexFile", indexFile);

return fs.promises.writeFile(indexFile, fileContent.replace(/const customElements = win.customElements;/, "const customElements = options.registry ?? win.customElements;"), "utf-8")

I wrote the script for StencilJS 2.22.3 and the "dist" output target so I don't know if this also works for StencilJS versions >= 3.
I'm currently in my testing phase so I don't know if this is a viable solution for most use-cases
The call to defineCustomElements should look like this: defineCustomElements(window, { registry: $newRegistry });

Again, defineCustomElements is async in newer Stencil Versions.

Imagine you have multiple microfrontends each requiring a custom registry, overwriting the global window.customElements Registry in an async initialization process will be pure chaos. I highly recommend not doing that.

The only viable option is that the internal define call in defineCustomElements has the correct registry so it works nicely with the async nature of JS.

@mayerraphael Isn't the script @FabioGimmillaro is running here, the same as the patch you are doing? The global window.customElements is not being overritten.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature: Want this? Upvote it! This PR or Issue may be a great consideration for a future idea.
Projects
None yet
Development

No branches or pull requests

10 participants