ionic-team / stencil

A toolchain for building scalable, enterprise-ready component systems on top of TypeScript and Web Component standards. Stencil components can be distributed natively to React, Angular, Vue, and traditional web developers from a single, framework-agnostic codebase.
https://stenciljs.com
Other
12.48k stars 783 forks source link

feat: support of external libraries/components in dist-custom-elements #3576

Open MarkChrisLevy opened 2 years ago

MarkChrisLevy commented 2 years ago

Prerequisites

Describe the Feature Request

When building components with dist-custom-elements we may need to share code with other components/apps or use external libraries. Currently every external library that is included within a component will be bundled in the compilation output.

It extends FR described in #3226 and follows the discussion in #3136.

Describe the Use Case

There are two use cases:

  1. Marking, that a library, that is used by a component should not be packed in the component's output bundle
  2. Marking, that an external lazy loaded component that is used by a component should not be packed in the component's output bundle

In the first case, lets say that we have a dependency to moment.js - right now, if you build a component and within the code of the component there are imports from moment.js, then all required code from moment.js (and its dependencies) will be put in the output bundle. That is fine and proper default behavior, but in my case moment.js is used across multiple components and within main project (which imports those components), moment.js is also directly imported so I would end up with moment.js being bundled three times. Having an "external" option in dist-custom-elements will allow to avoid that.

Second case, lets say, that we created a component, that uses \@ionic/core lazy loaded component, e.g. ion-button. If you build the component, you will have in the output bundle the ion-button code but also other ionic components (whole collection is consumed). Having an "external" option would tell rollup to treat \@ionic/core as external but also to treat stencil collections as external components.

Describe Preferred Solution

In dist-custom-elements we need to create new option "external":

export interface OutputTargetDistCustomElements extends OutputTargetBaseNext {
   ...
   external?: (string | RegExp)[];
   ...
}

Value of the external is directly passed to rollup (array of string or RegExp) - this will fully solve the first case (importing external library). Additionally, somewhere in dist-custom-elements compilation process, Stencil must check if lazy components collection is also to be treated as external.

In my Stencil fork both of those cases are covered in dist-custom-elements-bundle output target, below you may see what changes I had to make in order to make it work:

Describe Alternatives

First case (importing external library, not lazy component) can be resolved with rollup plugin defined in stencil.config.ts:

export function externalsPlugin(externals: (string | RegExp)[]) {
    return {
        id: "externalsPlugin",
        resolveId(id: string) {
            for (const e of externals) {
                if ((typeof e === "string" && id === e) || (e instanceof RegExp && e.exec(id))) {
                    return {id, external: true}
                }
            }
        }
    }
}

Related Code

No response

Additional Information

No response

alessandrodolci commented 1 year ago

Any update on this?

I'd add that the use case doesn't seem to be exclusively related to dist-custom-elements, as we're facing the same issue with different output targets, too. From my understanding, every import statement referencing a runtime object (so, not just some type declaration) that resolves to a Stencil collection forces the output bundle to always include the entire collection, with no way to avoid this behaviour.

Our situation is the second one @MarkChrisLevy is describing: we have a number of Stencil projects where we import some enums from a components library also built with Stencil. We want to keep the library as a peer dependency, so we'd need a way to exclude it from the output bundles of the consumer projects.

If you think I'm missing something or know some alternative solution for our use case, I'd be really helpful if you could point me in the right direction. Thank you!

saldomik commented 1 year ago

This problem is quite annoying, as you are not in full control of your application. The solution proposed here seems effectively to work only when a peer dependency is not a stencil project. If you have a library A made in Stencil that uses another library B marked as peer dependecy, also made with Stencil, the output of library B will always end in library A, even if this is not the desired behaviour.

tomwayson commented 1 year ago

The OP mentions 2 problems.

The first is that stencil does not offer us a way to treat dependency libraries as external, which is covered by pre-existing issues like #3226, and for which there are known workarounds like using a rollup plugin like rollup-plugin-peer-deps-external @rollup/plugin-alias to define which dependencies will be external.

The second problem is the specific case where one or more of those dependencies is a stencil library, and in that case the above workarounds won't work. The build will process the depenecy's entire collection and include all of its components in the build output. This problem is best captured by these statements:

you will have in the output bundle the ion-button code but also other ionic components (whole collection is consumed). Having an "external" option would tell rollup to treat @ionic/core as external but also to treat stencil collections as external components.

every import statement referencing a runtime object (so, not just some type declaration) that resolves to a Stencil collection forces the output bundle to always include the entire collection, with no way to avoid this behaviour.

If you have a library A made in Stencil that uses another library B marked as peer dependecy, also made with Stencil, the output of library B will always end in library A, even if this is not the desired behaviour.

This is a problem for us as well. We have come up with a very hacky workaround that seems to work, and that is we use a prebuild script to temporarily rename node_modules/stencil-library-dependency/dist/collection/collection-manifest.json and then after the build runs call a command that renames that file back to it's original name. When we do this, we see a message in the console saying that stencil can't find the collection manifest, but the build still appears to work (and it's 100x faster). This works for our use case where we want the stencil library dependency to be external b/c the consuming application of our library is going to install it anyway.

We think a better solution would be for stencil to offer a configuration option that would allow us to opt out of the default behavior of parsing the dependency's collection manifest and including all of its components in the build output.