Upgrading to Stencil v5
This guide takes a Stencil v4 project to v5. Most of the work is done by the stencil migrate command. The rest is a short list of changes to make by hand, grouped below by where they are.
Upgrade from the latest Stencil v4. If your project is on an older major version, upgrade one major version at a time.
For the reasoning behind each change, see the list of breaking changes.
Before You Start
- Use Node.js 22 or later.
- Commit your work, so that you can review what the migration changes.
- Check that your project builds on Stencil v4.
1. Install Stencil v5
- npm
- Yarn
- pnpm
npm install @stencil/core@beta --save-dev
yarn add @stencil/core@beta --dev
pnpm add @stencil/core@beta --save-dev
While Stencil v5 is in preview, it's published under the beta tag.
If your project uses Stencil's framework output targets, update them too. They support both v4 and v5:
- npm
- Yarn
- pnpm
npm install @stencil/react-output-target@latest @stencil/vue-output-target@latest @stencil/angular-output-target@latest --save-dev
yarn add @stencil/react-output-target@latest @stencil/vue-output-target@latest @stencil/angular-output-target@latest --dev
pnpm add @stencil/react-output-target@latest @stencil/vue-output-target@latest @stencil/angular-output-target@latest --save-dev
2. Run the Migration
Preview the changes first. This command lists every change and modifies nothing:
npx stencil migrate --dry-run
Then apply them:
npx stencil migrate
stencil build also detects a project that needs migrating, and offers to run the migration for you.
The migration updates your Stencil config and your component files:
| In | Change |
|---|---|
| Components | shadow, scoped, and formAssociated become the encapsulation option and @AttachInternals() |
| Components | Imports of raw .css, .svg, and text files gain a ?stencil suffix |
| Config | Output targets are renamed: dist to loader-bundle, dist-custom-elements to standalone, dist-hydrate-script to ssr |
| Config | esmLoaderPath becomes loaderPath, adjusted so that the loader stays where it was |
| Config | extras becomes compat, and its slot-fix options become lightDomPatches |
| Config | rollupConfig becomes rolldownConfig |
| Config | buildDist, buildDocs, and devMode are replaced by skipInDev on each output target |
| Config | hashFileNames and hashedFileNameLength move onto the loader-bundle and www output targets |
| Config | extras.addGlobalStyleToComponents and globalStyle become one global-style output target, with input and inject options |
| Config | Options that now match the default are removed, such as serviceWorker: null |
Review the diff before you go on.
3. Update tsconfig.json
Stencil v4 shipped its own copy of TypeScript. Stencil v5 uses the TypeScript installed in your project, and installs TypeScript 6 if your project has none. TypeScript 6 rejects two settings that Stencil v4 projects commonly have.
Change moduleResolution from node to bundler. Otherwise the build fails with Option 'moduleResolution=node10' is deprecated:
{
"compilerOptions": {
"moduleResolution": "bundler"
}
}
TypeScript 6 also turns on strict by default, and a decorated member with no initial value, such as @Prop() name: string;, then fails with Property 'name' has no initializer. To keep these members as they are, turn that one check off:
{
"compilerOptions": {
"strictPropertyInitialization": false
}
}
To stay on TypeScript 5 instead, add it to your project's devDependencies.
Set target to es2022 or later, to match the browsers Stencil v5 supports. Stencil v4 projects usually have es2017. That still builds, but newer syntax compiles to older, larger code, and Stencil warns about it on every build:
{
"compilerOptions": {
"target": "es2022"
}
}
4. Update package.json
The output targets write to new directories, and the main entry files end in .js instead of .esm.js:
| Stencil v4 | Stencil v5 |
|---|---|
dist/ (dist output target) | dist/loader-bundle/ |
dist/components/ | dist/standalone/ |
hydrate/ | dist/ssr/ |
dist/types/ | dist/types/ |
dist/collection/ | dist/collection/ |
dist/my-library/my-library.esm.js | dist/loader-bundle/my-library/my-library.js (my-library.esm.js is kept beside it, and loads my-library.js) |
Update the main, module, types, unpkg, and exports fields to match. The build warns when main or module point to a file that doesn't exist. It doesn't check unpkg or your exports entries, so check those yourself.
Stencil can also write the exports field for you. See generateExportMaps and Publishing.
Two more changes affect what you publish:
- CommonJS is opt-in. The
loader-bundleandssroutput targets write ES modules only. If your consumers still need CommonJS, setcjs: trueon the output target. Setting"type": "module"in yourpackage.jsonis recommended either way. - The browser build has moved. Pages that load your library from a CDN use a URL that ends in
dist/my-library/my-library.esm.js. To keep that URL working, setbuildDir: '../'on theloader-bundleoutput target. The browser build is then written todist/my-library/as before, andmy-library.esm.jsremains as a file that forwards tomy-library.js.
Then update your scripts:
- Remove
--prod. Production is the default build, and--devopts out of it. - Remove
--no-openfrom dev server commands. The dev server no longer opens a browser unless you pass--open. - Remove
--docs. A production build already generates your documentation output targets. To generate them in a development build, setskipInDev: falseon them, or runstencil docs. - Remove
stencil telemetrycommands. - Replace
stencil test, as described in Move your tests.
5. Update Your Code
The migration doesn't make these changes, because they depend on what your code means to do.
Components
Replace hostData() with <Host>. hostData() has been deprecated since Stencil 1, and is ignored in v5, with no warning:
- hostData() {
- return { role: 'alert' };
- }
render() {
- return <slot />;
+ return (
+ <Host role="alert">
+ <slot />
+ </Host>
+ );
}
Update componentShouldUpdate(). It now runs once for each render, and receives one argument that holds every change:
- componentShouldUpdate(newVal, oldVal, propName) {
- return !(propName === 'name' && newVal === oldVal);
- }
+ componentShouldUpdate(changes: ComponentShouldUpdateChanges<this>) {
+ return !(changes.name && changes.name.newVal === changes.name.oldVal);
+ }
A method that still takes three parameters compiles, and receives undefined for the second and third. The build warns about each one.
Check boolean props that are set to "false". A boolean prop is now true whenever its attribute is present, as with native HTML elements. <my-cmp disabled="false"> gave false in v4 and gives true in v5. Remove the attribute, or set the property instead. To keep accepting the string, see @AttrDeserialize().
Check @Watch() handlers that ran before the first render. A watcher no longer runs until its component has rendered once. If you need it to run on the initial value, use @Watch('name', { immediate: true }).
Wrap functional components that return an array. A FunctionalComponent must now return one node or null, so wrap an array in a fragment: <>{items}</>.
Config
- Remove
buildEs5and anyextras.__deprecated__*options. ES5 builds and their polyfills are gone, and v5 supports browsers that implement ES2022. - Remove the
testingsection. - If you use the
docs-custom-elements-manifestoutput without settingfile, the manifest now goes todist/custom-elements.jsoninstead of the project root. Delete the oldcustom-elements.jsonand update anything that read it, or setfile: 'custom-elements.json'to keep it where it was. Stencil also sets thecustomElementsfield in yourpackage.jsonto the manifest's path. - Add
{ type: 'docs-readme' }tooutputTargetsif you rely on generated component readme files. They're no longer generated unless you ask for them. - Set
serviceWorker: trueon awwwoutput target if your app relies on a service worker. It's no longer generated by default. - Check your copy tasks.
ignorenow adds to Stencil's default exclusions instead of replacing them, and directory copies apply them too, so__mocks__/,dist/folders and.gitignorefiles are no longer copied. Give anything you need from those its own task. A copy from outsidesrcnow needs adest. - If your project imports from another Stencil library to bundle that library's components into its own, list the library in
collections. - If you use Rollup plugins, see Module Bundling. Stencil now bundles with Rolldown, which runs most Rollup plugins and replaces
@rollup/plugin-commonjsand@rollup/plugin-node-resolve.
Code That Uses Your Library
-
Remove calls to
applyPolyfills(). The loader no longer exports it. -
If server code reads the result of the
ssroutput'sstreamToString()as a Node.js stream, update it: the function now returns a webReadableStream. See Server-Side Rendering. -
Update imports of Stencil's internal packages:
Stencil v4 Stencil v5 @stencil/core/internal@stencil/core/runtime@stencil/core/internal/client@stencil/core/runtime/client@stencil/core/internal/hydrate@stencil/core/runtime/server@stencil/core/cli@stencil/cli@stencil/core/dev-server@stencil/dev-server@stencil/core/compiler/stencil.js@stencil/core/compiler -
Use
import, notrequire(), to load any@stencil/coremodule from a Node.js script. The package is now ES modules only.
6. Move Your Tests
The stencil test command, with its Jest and Puppeteer integrations, is removed.
- For spec tests, follow the Vitest migration guide. Existing
newSpecPage()tests can keep running with few changes. - For e2e tests, use browser tests in Vitest for components, or
@stencil/playwrightfor whole pages.
7. Build and Check
Run a build, and fix any warnings it reports:
- npm
- Yarn
- pnpm
npm run build
yarn build
pnpm run build
Then check the things a build can't:
- Load your library the way your consumers do, from a
<script>tag or through a bundler. - If you publish framework wrappers, build them against the new output.
- Run
npm pack --dry-run, and confirm that every path in yourpackage.jsonis in the package.