package.json exports, jest transformIgnorePattern and vitest inline dependencies
TL;DR: Updating @stencil/core > 4.27.0 and @stencil/react-output-target > 0.7.0 can result in some issues with vite and jest.
Make sure to format the exports object in the package.json properly:
{
"exports": {
/* the ./dist/components/*.js is necessary for @stencil/react-output-target */
"./dist/components/*.js": {
"default": "./dist/components/*",
"import": "./dist/components/*.js",
"require": "./dist/components/*",
"types": "./dist/components/*.d.ts"
},
/* the css export is necessary for global styles */
"./dist/library/library.css": "./dist/library/library.css",
/* other exports recommended by stencil docs */
".": {
"import": "./dist/library/library.esm.js",
"require": "./dist/library/library.cjs.js"
},
"./polyfills/*": {
"default": "./dist/polyfills/*.js",
"import": "./dist/polyfills/*.js",
"require": "./dist/polyfills/*.js"
},
"./dist/*": {
"default": "./dist/*",
"import": "./dist/*",
"require": "./dist/*",
"types": "./dist/*"
},
"./components/*.js": {
"default": "./dist/components/*.js",
"import": "./dist/components/*.js",
"require": "./dist/components/*.js",
"types": "./dist/components/*.d.ts"
},
"./loader": {
"import": "./loader/index.js",
"require": "./loader/index.cjs",
"types": "./loader/index.d.ts"
},
"./hydrate": {
"types": "./hydrate/index.d.ts",
"import": "./hydrate/index.js",
"require": "./hydrate/index.cjs.js",
"default": "./hydrate/index.js"
}
}
}
For vite, add to your vite.config.ts
{
test: {
deps: {
inline: ['@lit/react', /@stencil/],
},
},
}
to resolve the dependencies.
For jest, add to jest.config.js
{
transformIgnorePatterns: [
'/node_modules/(?!(?:@your/library-react|@your/library-core|@lit/react)/)',
]
}
For jsdom, add some polyfills.
Longer read
I don't even know where to start.
Recently we had to update the @stencil/core and @stencil/react-output-target dependencies in our component library to enable server side rendering for our components.
We updated @stencil/core from 4.19.0 to 4.27.1 and @stencil/react-output-target from 0.5.3 to 0.8.5 (later 1.0.4).
We published our updated library with the new SSR feature enabled and called it a day.
A few days later, colleagues pinged us on the messenger. They can't run the tests any more, something with our component library is broken.
During our update, we added the exports
object to our core package.json.
I learned that the exports
object is the new entry point into our library. If we configure it wrong, node might not find the files that we want to export (see package entry points).
Module not found
The first error we got was
"Module not found: Error: Can't resolve foo.js from library-core" inside our react output target.
The thing we were missing was the correct export inside the exports object.
Our initial exports looked like this:
"./dist/components/*": {
"import": "./dist/components/*.js",
"types": "./dist/components/*.d.ts"
},
It was missing a .js
after the *
. That's it. And we had to add a default export too. So we had to replace the block above with this one:
"./dist/components/*.js": {
"default": "./dist/components/*",
"import": "./dist/components/*.js",
"require": "./dist/components/*",
"types": "./dist/components/*.d.ts"
},
Additionally, we had to export our global css file as well. So we added this line here:
"./dist/library/library.css": "./dist/library/library.css",
One error down, a few more to go.
SyntaxError: Unexpected token 'export'
The next error we faces was SyntaxError: Unexpected token 'export'
.
The reason for this was that we wrongly configured transformIgnorePatterns
.
It should look like this:
transformIgnorePatterns: [
/* other transformations */
'/node_modules/(?!(?:library-react|library-core|@lit/react)/)',
],
This makes sure that all packages within node_modules aren't transformed except those inside the regex (see Jest transformIgnorePattern). The error here was that we added library-core, library-react and @lit/react into three different lines, so they cannibalized themselves.
TypeError: sheet.replaceSync is not a function
The next issue we faced was TypeError: sheet.replaceSync is not a function
. This occurs because newer versions of @stencil/core use that function to handle CSS. Jest uses jsdom to emulate browser apis.
sheet.replaceSync()
is not implemented in jsdom, hence running tests with jest can't find this function.
This can be fixed by providing a polyfill for the CSS functions and set them up before jest runs the test:
// Mock adoptedStyleSheets property on Document and ShadowRoot
/* eslint-disable no-underscore-dangle */
/* eslint-disable lines-between-class-members */
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable no-empty-function */
/* eslint-disable class-methods-use-this */
/* eslint-disable no-useless-constructor */
/* eslint-disable no-useless-return */
/* eslint-disable compat/compat */
Object.defineProperty(Document.prototype, 'adoptedStyleSheets', {
get() {
return this._adoptedStyleSheets || [];
},
set(styleSheets) {
this._adoptedStyleSheets = styleSheets;
},
configurable: true,
});
Object.defineProperty(ShadowRoot.prototype, 'adoptedStyleSheets', {
get() {
return this._adoptedStyleSheets || [];
},
set(styleSheets) {
this._adoptedStyleSheets = styleSheets;
},
configurable: true,
});
// Mock CSSStyleSheet constructor
global.CSSStyleSheet = class CSSStyleSheet {
cssRules: any[] = [];
rules: any[] = [];
ownerRule: any = null;
ownerNode: any = null;
parentStyleSheet: any = null;
href: string | null = null;
title = '';
media: any = {
mediaText: '',
length: 0,
item: () => null,
deleteMedium: () => {},
appendMedium: () => {},
};
disabled = false;
type = 'text/css';
constructor() {}
replaceSync() {
return;
}
replace() {
return Promise.resolve(this);
}
insertRule() {
return 0;
}
deleteRule() {}
addRule() {
return -1;
}
removeRule() {}
} as any;
cssPolyfills.js
Then, inside your jest.config.js
setupFiles: ['./path/to/cssPolyfills.js'],
Bonus: Vite - Error: require() of ES Module [...] not supported.
This is due to vite transformation issues. We fixed it by adding this configuration to the vite.config.ts:
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
// ... other test options ...
// Add this deps configuration
deps: {
inline: [
'@lit/react',
// It's often a good idea to inline the whole stencil/lit ecosystem
// to prevent similar issues with other packages.
// You can use a regular expression for this:
/@stencil/,
],
},
},
});```