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

docs(publishing): update general docs on publishing #1359

Merged
merged 13 commits into from
Mar 11, 2024
2 changes: 1 addition & 1 deletion docs/framework-integration/angular.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ import { defineCustomElements } from 'stencil-library/loader';
export class ComponentLibraryModule {}
```

See the [documentation](../output-targets/dist.md#distribution-options) for more information on defining custom elements using the
See the [documentation](../output-targets/dist.md) for more information on defining custom elements using the
`dist` output target, or [update the Angular output target](#do-i-have-to-use-the-dist-output-target) to use `dist-custom-elements`.

### Link Your Packages (Optional)
Expand Down
167 changes: 135 additions & 32 deletions docs/guides/publishing.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,52 +5,155 @@ description: Publishing A Component Library
slug: /publishing
---

# Publishing A Component Library

There are numerous strategies to publish and distribute your component library to be consumed by external projects. One of the benefits of Stencil is that is makes it easy to generate the various [output targets](../output-targets/01-overview.md) that are right for your use-case.

## Publishing to Node Package Manager (NPM)
## Use Cases

To use your Stencil components in other projects, there are two different output targets to consider: [`dist`](../output-targets/dist.md) and [`dist-custom-elements`](../output-targets/custom-elements.md). Both export your components for different use cases. Luckily, both can be generated at the same time, using the same source code, and shipped in the same distribution. It would be up to the consumer of your component library to decide which build to use.

The first step and highly recommended step is to
[publish the component library to NPM](https://docs.npmjs.com/getting-started/publishing-npm-packages). NPM is an online software registry for sharing libraries, tools, utilities, packages, etc. Once the library is published to NPM, other projects are able to add your component library as a dependency and use the components within their own projects.
### Lazy Loading

If you prefer to have your components automatically loaded when used in your application, we recommend enabling the [`dist`](../output-targets/dist.md) output target. The bundle gives you a small entry file that registers all your components and defers loading the full component logic until it is rendered in your application. It doesn't matter if the actual application is written in HTML or created with vanilla JavaScript, jQuery, React, etc.

## `package.json`
Your users can import your component library, e.g. called `my-design-system`, either via a `script` tag:

The purpose of the `package.json` file is to give other tools instructions on how to find the package's files, and to provide information about the package. For example, bundlers such as [Rollup](https://rollupjs.org/) and [Webpack](https://webpack.js.org/) use this configuration to locate the project's entry files.
```html
<script type="module" src="https://unpkg.com/my-design-system"></script>
```

An advantage to using the compiler is that it is able to provide help on how to best set up the project for distribution. Below is a common setup found within a project's `package.json` file:
or by importing it in the bootstrap script of your application:

```ts
import 'my-design-system';
```

To ensure that the right entry file is loaded when importing the project, define the following fields in your `package.json`:

```json
{
"main": "dist/index.cjs.js",
"module": "dist/index.js",
"es2015": "dist/esm/index.mjs",
"es2017": "dist/esm/index.mjs",
"types": "dist/types/components.d.ts",
"unpkg": "dist/my-project-name/my-project-name.esm.js",
"collection:main": "dist/collection/index.js",
"collection": "dist/collection/collection-manifest.json",
"files": [
"dist/",
"css/",
"loader/"
]
"exports": "./dist/esm/my-design-system.js",
"main": "./dist/cjs/my-design-system.js",
"unpkg": "dist/my-design-system/my-design-system.esm.js",
}
```

| Property | Description | Recommended |
|----------|-----------------------------------------------------------------------------------------------------|-----------------------------------|
| `main` | Entry file in the CommonJS module format. | `dist/index.cjs.js` |
| `module` | Entry file in the ES module format. ES modules is the standardized and recommended format. | `dist/index.js` |
| `es2015` | Commonly used by framework bundling. | `dist/esm/index.mjs` |
| `es2017` | Commonly used by framework bundling. | `dist/esm/index.mjs` |
| `types` | Entry file to the project's types. | `dist/types/components.d.ts` |
| `unpkg` | Entry file for requests to the projects [unpkg](https://unpkg.com/) CDN. | `dist/{NAMESPACE}/{NAMESPACE}.js` |
| `files` | Array of files that should be included in a npm release. | `["dist/", "loader/"]` |
Read more about various options when it comes to configuring your project's components for lazy loading in the [`dist`](../output-targets/dist.md) output target section.

#### Considerations

To start, Stencil was designed to lazy-load itself only when the component was actually used on a page. There are many benefits to this approach, such as simply adding a script tag to any page and the entire library is available for use, yet only the components actually used are downloaded. For example, [`@ionic/core`](https://www.npmjs.com/package/@ionic/core) comes with over 100 components, but a webpage may only need `ion-toggle`. Instead of requesting the entire component library, or generating a custom bundle for just `ion-toggle`, the `dist` output target is able to generate a tiny entry build ready to load any of its components on-demand.

However be aware that this approach is not ideal in all cases. It requires your application to ship the bundled components as static assets in order for them to load properly. Furthermore, having many nested component dependencies can have an impact on the performance of your application. For example, given you have a component `CmpA` which uses a Stencil component `CmpB` which itself uses another Stencil component `CmpC`. In order to fully render `CmpA` the browser has to load 3 scripts sequentially which can result in undesired rendering delays.

### Standalone

The [`dist-custom-elements`](../output-targets/custom-elements.md) output target builds each component as a stand-alone class that extends `HTMLElement`. The output is a standardized custom element with the styles already attached and without any of Stencil's lazy-loading. This may be preferred for projects that are already handling bundling, lazy-loading and defining the custom elements themselves.

The generated files will each export a component class and will already have the styles bundled. However, this build does not define the custom elements or apply any polyfills. Static assets referenced within components will need to be set using `setAssetPath` (see [Making Assets Available](../output-targets/custom-elements.md#making-assets-available)).

You can use these standalone components by importing them via:

```ts
import { MyComponent, defineCustomElementMyComponent } from 'my-design-system'

// register to CustomElementRegistry
defineCustomElementMyComponent()

// or extend custom element via
class MyCustomComponent extends MyComponent {
// ...
Comment on lines +62 to +64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a use case we support? Extending a component built by Stencil?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, for example, let's say we publish a create-stencil component, you can change functionality by extending the class and overwriting component methods, e.g.:

<script type="module">
  import { MyComponent } from 'https://unpkg.com/[email protected]/dist/components/index.js'
  class ChangedComponent extends MyComponent {
    getText() {
      return 'a new component!';
    }
  }

  customElements.define('my-component', ChangedComponent);
  customElements.define('my-component-orig', MyComponent);
</script>
<my-component first="foo" last="bar"></my-component>
<my-component-orig first="foo" last="bar"></my-component-orig>

This will result in:

Screenshot 2024-02-28 at 10 16 25 AM

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow I didn't know that worked! I guess I hadn't tried it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah extending the compiled class output will work, but obviously wouldn't be able to add any additional reactivity (states, props, watchers, etc.) at this time.

}
define('my-custom-component', MyCustomComponent)
```

To ensure that the right entry file is loaded when importing the project, define different [exports fields](https://nodejs.org/api/packages.html#exports) in your `package.json`:

```json
{
"exports": {
".": {
"import": "./dist/components/index.js",
"types": "./dist/components/index.d.ts"
},
"./my-component": {
"import": "./dist/components/my-component.js",
"types": "./dist/components/my-component.d.ts"
}
},
Comment on lines +73 to +82
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not something we should do now, but it would be nice for supporting this usage of exports with DCE if we could auto-generate a JSON blob for all the components, so that users didn't have to automatically keep it in sync - imagine the nightmare of doing that if you had 200 components or something like that

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, sounds like a great idea!

"types": "dist/components/index.d.ts",
}
```

This allows us to map certain import paths to specific components within our project and allows users to only import the component code they are interested in and reduce the amount of code that needs to downloaded by the browser, e.g.:

```js
// this import loads all compiled components
import { MyComponent } from 'my-design-system'
// only import compiled code for MyComponent
import { MyComponent } from 'my-design-system/my-component'
```

The `collection` properties are used to allow lazy loading in other Stencil applications.
If you define exports targets for all your components as shown above and by using [`customElementsExportBehavior: 'auto-define-custom-elements'`](../output-targets/custom-elements.md#customelementsexportbehavior) as output target option, you can skip the `defineCustomElement` call and directly import the component where you need it:

```ts
import 'my-design-system/my-component'
```

:::note
If you are distributing both the `dist` and `dist-custom-elements`, then it's best to pick one of them as the main entry depending on which use case is more prominent.
:::
Comment on lines +102 to +104
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By this you mean the file path in the main field in package.json? There's a main field shown above for dist but it doesn't show one for DCE, might be helpful to have that more explanation of what setting the main field would look like for DCE

(possible I'm also misunderstanding something here haha)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left this out intentionally as I don't know if it is a good idea to define a main entry point DCE. main is usually only used for CJS environments. We don't export DCE as CJS module. I am sure this can be configured but don't think anyone would want to import CJS to then re-transpile back to ESM which is what is supported in the browser.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok that sounds fine, I guess if the user is going to distribute both then we can rely on them a bit to figure out how to do that correctly


Read more about various options when it comes to distributing your components as standalone components in the [`dist-custom-elements`](../output-targets/custom-elements.md) output target section.

The output directory will also contain an `index.js` file which exports some helper methods by default. The contents of the file will look something like:

```js
export { setAssetPath, setPlatformOptions } from '@stencil/core/internal/client';
```

:::note
The contents may look different if [`customElementsExportBehavior`](../output-targets/custom-elements.md#customelementsexportbehavior) is specified!
:::

#### Considerations

The `dist-custom-elements` is a direct build of the custom element that extends `HTMLElement`, without any lazy-loading. This distribution strategy may be preferred for projects that use an external bundler such as [Vite](https://vitejs.dev/), [WebPack](https://webpack.js.org/) or [Rollup](https://rollupjs.org) to compile the application. They ensure that only the components used within your application are bundled into compilation.

#### Usage in TypeScript

If you plan to support consuming your component library in TypeScript you'll need to set `generateTypeDeclarations: true` on the output target in your `stencil.config.ts`, like so:

```tsx title="stencil.config.ts"
import { Config } from '@stencil/core';

export const config: Config = {
outputTargets: [
{
type: 'dist-custom-elements',
generateTypeDeclarations: true,
},
// ...
],
// ...
};
```

Then you can set the `types` property in `package.json` so that consumers of your package can find the type definitions, like so:

```json title="package.json"
{
"types": "dist/components/index.d.ts",
"dependencies": {
"@stencil/core": "latest"
},
...
}
```

:::note
If you are distributing both the `dist` and `dist-custom-elements`, then it's best to pick one of them as the main entry.
If you set the `dir` property on the output target config, replace `dist/components` in the above snippet with the path set in the config.
:::

## Publishing to NPM

[NPM](https://www.npmjs.com/) is an online software registry for sharing libraries, tools, utilities, packages, etc. To make your Stencil project widely available to be consumed, it's recommended to [publish the component library to NPM](https://docs.npmjs.com/getting-started/publishing-npm-packages). Once the library is published to NPM, other projects are able to add your component library as a dependency and use the components within their own projects.
116 changes: 3 additions & 113 deletions docs/output-targets/custom-elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const config: Config = {
| `default` | No additional re-export or auto-definition behavior will be performed.<br/><br/>This value will be used if no explicit value is set in the config, or if a given value is not a valid option. |
| `auto-define-custom-elements` | A component and its children will be automatically defined with the `CustomElementRegistry` when the component's module is imported. |
| `bundle` | A utility `defineCustomElements()` function is exported from the `index.js` file of the output directory. This function can be used to quickly define all Stencil components in a project on the custom elements registry. |
| `single-export-module` | All component and custom element definition helper functions will be exported from the `index.js` file in the output directory. This file can be used as the root module when distributing your component library, see [below](#distributing-custom-elements) for more details. |
| `single-export-module` | All component and custom element definition helper functions will be exported from the `index.js` file in the output directory. This file can be used as the root module when distributing your component library, see [Publishing](/publishing) for more details. |

:::note
At this time, components that do not use JSX cannot be automatically
Expand Down Expand Up @@ -97,6 +97,8 @@ Setting this flag to `true` results in the following behaviors:
2. Filenames will not be hashed.
3. All imports from packages under `@stencil/core/*` will be marked as external and therefore not included in the generated Rollup bundle.

Ensure that `@stencil/core` is included in your list of dependencies if you set this option to `true`. This is crucial to prevent any runtime errors.

### generateTypeDeclarations

_default: `true`_
Expand Down Expand Up @@ -128,32 +130,6 @@ _default: `false`_

Setting this flag to `true` will cause file minification to follow what is specified in the [Stencil config](../config/01-overview.md#minifyjs). _However_, if [`externalRuntime`](#externalruntime) is enabled, it will override this option and always result in minification being disabled.

## Consuming Custom Elements

By default, the custom elements files will be written to `dist/components/`. This directory can be configured using the output target's [`dir`](#dir) config.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we lost some of this information in the transition, can we integrate this back into the docs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some sections back in Publishing > Use Cases > Standalone


The generated files will each export a component class and will already have the styles bundled. However, this build does not define the custom elements or apply any polyfills.
Static assets referenced within components will need to be set using `setAssetPath` (see [Making Assets Available](#making-assets-available)).

Below is an example of defining a custom element:

```tsx
import { defineCustomElement } from 'my-library/dist/components/hello-world';

defineCustomElement(); // Same as manually calling: customElements.define('hello-world', HelloWorld);
```

The output directory will also contain an `index.js` file which exports some helper methods by default. The contents of the file
will look something like:

```js
export { setAssetPath, setPlatformOptions } from '@stencil/core/internal/client';
```

:::note
The contents may look different if [`customElementsExportBehavior`](#customelementsexportbehavior) is specified!
:::

## Making Assets Available

For performance reasons, the generated bundle does not include [local assets](../guides/assets.md) built within the JavaScript output,
Expand All @@ -180,84 +156,6 @@ Make sure to copy the assets over to a public directory in your app. This config
bundling, and where your assets can be loaded from. How the files are copied to the production build directory depends on the bundler or tooling.
The configs below provide examples of how to do this automatically with popular bundlers.

## Distributing Custom Elements

See our docs on [publishing a component library](../guides/publishing.md) for information on setting up the library's `package.json` file and publishing to a package manager.

By default, custom elements will need to be imported from the [output directory](#dir) specified on the output target config:

```tsx
import { MyComponent } from 'best-web-components/dist/components/my-component';
```

However, the `module` property in the `package.json` can be modified to point to the custom element output:

```tsx title="package.json"
{
"module": "dist/components/index.js",
"dependencies": {
"@stencil/core": "latest"
},
...
}
```

:::note
Be sure to set `@stencil/core` as a dependency of the package as well.
:::

As a result, components can alternatively be imported from the root of the published package:

```tsx
import { MyComponent } from 'best-web-components';
```

:::note
If you are distributing the output of both the
[`dist`](./dist.md) and `dist-custom-elements` targets, then
it's up to you to choose which one of them should be available in the
`module` entry.
:::

### Usage in TypeScript

If you plan to support consuming your component library in TypeScript you'll
need to set `generateTypeDeclarations: true` on the your output target in
`stencil.config.ts`, like so:

```tsx title="stencil.config.ts"
import { Config } from '@stencil/core';

export const config: Config = {
outputTargets: [
{
type: 'dist-custom-elements',
generateTypeDeclarations: true,
},
// ...
],
// ...
};
```

Then you can set the `types` property in `package.json` so that consumers of
your package can find the type definitions, like so:

```tsx title="package.json"
{
"module": "dist/components/index.js",
"types": "dist/components/index.d.ts",
"dependencies": {
"@stencil/core": "latest"
},
...
}
```

:::note
If you set the `dir` property on the output target config, replace `dist/components` in the above snippet with the path set in the config.
:::

## Example Bundler Configs

Instructions for consuming the custom elements bundle vary depending on the bundler you're using. These examples will help your users consume your components with webpack and Rollup.
Expand Down Expand Up @@ -336,11 +234,3 @@ export default {
],
};
```

## How is this different from the "dist" output target?

The `dist-custom-elements` builds each component as a stand-alone class that extends `HTMLElement`. The output is a standardized custom element with the styles already attached and without any of Stencil's lazy-loading. This may be preferred for projects that are already handling bundling, lazy-loading and defining the custom elements themselves.

The `dist` output target, on the other hand, is more for projects that want to allow components to lazy-load themselves, without having to setup bundling configurations to do so.

Luckily, all builds can be generated at the same time, using the same source code, and shipped in the same distribution. It would be up to the consumer of your component library to decide which build to use.
Loading