How do I configure Rollup for a React Component Library? - javascript

I'm trying to configure Rollup for a React component library (using v3), and am having a real fight. I don't know exactly what I'm doing, but chasing each error just leads to another error. I'm confident that this should be pretty straight forward and is a common use case so I'm hoping someone might be able to help?
Originally I started just with:
const peerDepsExternal = require("rollup-plugin-peer-deps-external");
const resolve = require("#rollup/plugin-node-resolve");
const commonjs = require("#rollup/plugin-commonjs");
const postcss = require("rollup-plugin-postcss");
module.exports = {
input: "src/index.js",
output: {
dir: "build",
format: "esm",
preserveModules: true,
preserveModulesRoot: "src",
sourcemap: true,
},
// external: [/#babel\/runtime/],
plugins: [
resolve(),
commonjs(),
postcss(),
peerDepsExternal(),
],
};
But I hit an error where it was unable to find any JSX files:
Could not resolve "./withSVG" from "src/hoc/index.js"
So I added in the babel JSX plugin:
const jsx = require("rollup-plugin-jsx");
...
plugins: [
resolve(),
jsx({ factory: "React.createElement" }),
commonjs(),
postcss(),
peerDepsExternal(),
]
This then led to another error, where it was unable to parse the chainable operator .?:
[!] (plugin commonjs--resolver) Error: Parse Error: Line 20: Unexpected token .
src/utils/linkStores.js
Error: Parse Error: Line 20: Unexpected token .
So I assumed I needed to insert babel into here somewhere:
const { babel } = require("#rollup/plugin-babel");
...
plugins: [
resolve(),
jsx({ factory: "React.createElement" }),
commonjs(),
babel({
exclude: "node_modules/**",
extensions: [".js", ".jsx"],
}),
postcss(),
peerDepsExternal(),
]
This gave the following message:
babelHelpers: 'bundled' option was used by default. It is recommended to configure this option explicitly, read more here: https://github.com/rollup/plugins/tree/master/packages/babel#babelhelpers
Following the link gives the follow blurb:
'runtime' - you should use this especially when building libraries with Rollup. It has to be used in combination with #babel/plugin-transform-runtime and you should also specify #babel/runtime as dependency of your package. Don't forget to tell Rollup to treat the helpers imported from within the #babel/runtime module as external dependencies when bundling for cjs & es formats. This can be accomplished via regex (external: [/#babel/runtime/]) or a function (external: id => id.includes('#babel/runtime')). It's important to not only specify external: ['#babel/runtime'] since the helpers are imported from nested paths (e.g #babel/runtime/helpers/get) and Rollup will only exclude modules that match strings exactly.
So as I'm trying to create a component library I added:
const babelPluginTransformRuntime = require("#babel/plugin-transform-runtime");
...
plugins: [
babelPluginTransformRuntime,
resolve(),
jsx({ factory: "React.createElement" }),
commonjs(),
babel({
exclude: "node_modules/**",
babelHelpers: "runtime",
extensions: [".js", ".jsx"],
}),
postcss(),
peerDepsExternal(),
]
With a babel.config.js of:
module.exports = {
presets: ["#babel/preset-env", "#babel/preset-react"],
plugins: [
[
"#babel/plugin-transform-react-jsx",
{
runtime: "automatic",
},
],
],
};
But now I get the following error and I can't seem to find any way to resolve this one:
[!] (plugin commonjs--resolver) RollupError: You have declared using "runtime" babelHelpers, but transforming /home/ian/src/chart-it/packages/react/src/index.js resulted in "inline". Please check your configuration.
Can anyone suggest a working rollup config for a react component library (not TypeScript)?
Here's a link to the repository if anyone wants to test an answer: https://github.com/IPWright83/chart-it/tree/fixingPackage

Related

Why does jest throw an ERR_UNKNOWN_FILE_EXTENSION when trying to use a ts file as custom testEnvironment?

I'm switching my TypeScript project to a (pnpm) monorepo and have troubles getting tests to run properly. I have a jest.config.js that uses a custom testEnvironment that's written in TypeScript as well. However, ever since I moved the specific project into my packages directory for the monorepo restructuring, jest throws an Error and doesn't run any tests:
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for C:\workspaces\repos\the-monorepo\packages\testproject\source\testtools\jsdom-environment-global.spec.ts
I tried it with #swc/jest as well as with ts-jest, had a look at How to use TypeScript in a Custom Test Environment file in Jest? (which makes me think "why did this ever work?") and, for whatever reason, it worked fine yesterday. I cleaned jest cache and reinstalled all node_modules to no avail. I also found answers related to "type": "module" in package.json, but this doesn't apply to my package. It's not an ESM.
Here's how the jest.config.js looks like:
/** #type {import('#jest/types').Config.InitialOptions} */
const config = {
silent: true,
testEnvironment: "<rootDir>/source/testtools/jsdom-environment-global.spec.ts",
roots: [
"<rootDir>/source"
],
maxWorkers: "50%",
transform: {
"^.+\\.(t|j)s$": ["#swc/jest", {
sourceMaps: "inline",
module: {
strict: false,
strictMode: false
},
jsc: {
target: "es2021",
parser: {
syntax: "typescript",
dynamicImport: true
}
}
}]
},
transformIgnorePatterns: [
"node_modules"
],
testMatch: [
"**/*/*.spec.ts",
"**/*/*.test.ts",
"!**/playwright-tests/**",
"!**/playwright-tests-smoke/**"
],
moduleFileExtensions: ["ts", "js", "node", "json"],
reporters: [
"default"
],
globals: {
self: {},
navigator: {},
jasmine: {},
__UNIT__: true
},
coverageDirectory: "test-results",
collectCoverage: false,
collectCoverageFrom: [
"./source/**/*.ts"
],
coveragePathIgnorePatterns: [
"/\\.spec\\.ts$/i",
"/.*node_modules.*/",
"/.*testtools.*/"
],
coverageReporters: [
"lcov", "cobertura"
],
coverageProvider: "v8",
resetMocks: true,
restoreMocks: true,
resetModules: true,
setupFilesAfterEnv: [
"jest-extended/all",
"<rootDir>/source/testtools/setup.spec.ts"
],
testPathIgnorePatterns: [
"<rootDir>/source/testtools/",
"<rootDir>/source/smoke-tests/",
"<rootDir>/source/performance-tests/",
"<rooDir>/source/playwright-tests/",
"<rooDir>/source/playwright-tests-smoke/"
],
moduleNameMapper: {
"^#test-helpers": "<rootDir>/source/testtools/index.spec.ts",
"^#test-tools/(.*)": "<rootDir>/source/testtools/$1",
'^(\\.{1,2}/.*)\\.js$': '$1'
}
};
module.exports = config;
Why is jest not able to parse the testEnvironment if it's a TypeScript file?
I found the issue: there seems to be some confusion around the transformer not being applied to ESM files. In my case, the jsdom-environment-global.spec.ts imported an ESM module from a different package within my monorepo. This import triggers an exception because jest tries to import it via require(), which is caught and then again imported via dynamic imports. This dynamic import then throws the exception that ts is an unknown exception. I'm not sure why these files haven't been transformed, but as of How to use TypeScript in a Custom Test Environment file in Jest? this seems to be normal.
Bottom line: don't import from ESM files within your jest testEnvironment module.

Rollup UMD Module Reference to Named Imports is Undefined

I had a published component library (my-components) that had a specific component (Compare) in it, along w/ other components and utilities that were used in a main application. We use rollup and create several different output formats, one of which is UMD (required by one of the teams using my-components).
The component (Compare) was starting to become large, so it was moved out on its own. The rollup build was based off of what was used for my-components. After moving it out to its own repository to be built as its own component, it now had a dependency on my-components. When used in the main application (CRA app), everything works as expected. However, when the team who uses the UMD module tried to use the Compare app, they started to get errors:
Cannot read properties of undefined (reading someService)
The code in Compare is:
import {someService, somethingElse} from 'my-components';
. . .
someService.doSomething();
I saw the following warning during the build:
WARNING: { code: 'MISSING_GLOBAL_NAME', guess: 'myComponents', message: 'No name was provided for external module \'my-components\' in output.globals - guessing \'myComponents\'' }
So I explicitly added a global entry for it.
When I looked at the resulting code in the UMD module, the code looked like:
myComponents.someService.doSomething();
Why doesn't myComponents.someService resolve to the class that is exported from my-components?
My rollup.config.js looks like:
const EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.json'];
const umdGlobals = {
axios: 'axios',
lodash: '_',
react: 'React',
'react-dom': 'ReactDOM',
'my-components': 'myComponents'
};
export default [
{
input: 'src/components/index.js',
output: [
{ file: pkg.module, format: 'esm', sourcemap: true },
{ file: pkg.main, format: 'cjs', sourcemap: true },
{ file: pkg.umd, name: 'Compare', format: 'umd', sourcemap: true, globals: umdGlobals }
],
plugins: [
autoExternal(),
resolve({ extensions: EXTENSIONS, preferBuiltins: false, browser: true}),
commonjs({include: ['node_modules/**']),
babel({
babelHelpers: 'bundled',
babelrc: false,
presets: ['#babel/preset-env', '#babel/preset-react', '#babel/preset-typescript'],
plugins: ['#babel/plugin-transform-arrow-functions', '#babel/plugin-proposal-object-rest-spread', '#babel/plugin-proposal-class-properties],
extensions: EXTENSIONS,
exclude: 'node_modules/**'
}),
json(),
requireContext(),
internal(['classnames', 'pluralize'])
]
}
];
I tried to put my-components inside of internal(), and remove it from the global list, but that started to give other warnings that I think were the result of a dependency inside of it, and would throw errors when the bundle was used anyway.
Any advice on what I may be missing? Is there something wrong w/ the output from the my-components that I should be trying to fix in its rollup config, or is there something I can do in my rollup config for Compare?
Any help is greatly appreciated.

Override some Webpack 5 NodeJS modules with polyfill

I have NodeJS code that I now need to move to an embedded system. It takes far too long to simply start NodeJS ("Hello World" ~11sec on a BeagleBone Black) so we needed an alternative. The IoT.js looks promising but it does not support some of the internal NodeJS modules (e.g. url, zlib, tty)--which my code needs. I am using Webpack 5.35.0 to create a single file for my code but this is where my problems lie. I want to use Webpack with a node target since IoT.js offers most of what node offers natively. However is there a way to force Webpack to use polyfills for some of the modules? For example, browserify-zlib instead of expecting nodes zlib.
My basic Webpack configuration is simple:
{ target: 'node10.17',
entry: './index.js',
output:
{ filename: 'index.js',
path: '/work/proj/dist',
libraryTarget: 'umd' },
stats: 'errors-only',
resolve:
{ modules: [ '/work/proj/node_modules' ],
extensions: [ '.js', '.json' ],
}
}
I have done some reading where people claim adding a simple resolve.fallback.zlib = false and resolve.alias should do the trick--which is not working for me.
I tried to simply add resolve.fallback.zlib = false in the hopes to just have zlib omitted from the Webpacked output and this did not work. No matter what I do the standard Webpack boilerplate "node" zlib include code exists.
Standard Webpack boilerplate when using node target.
/***/ "zlib":
/*!***********************!*\
!*** external "zlib" ***!
\***********************/
/***/ ((module) => {
"use strict";
module.exports = require("zlib");;
/***/ })
Other things I tried were--ALL of which did not work:
I was hoping this would alias zlib and actually put in the browserify-zlib code.
resolve:
{ modules: [ '/work/proj/node_modules' ],
extensions: [ '.js', '.json' ],
alias: { zlib: '/work/proj/node_modules/browserify-zlib/lib/index.js' },
fallback: {} } }
Same as the previous example but thought by disabling the fallback the alias/polyfill would go into the output. This is what others online had success with.
resolve:
{ modules: [ '/work/proj/node_modules' ],
extensions: [ '.js', '.json' ],
alias: { zlib: '/work/proj/node_modules/browserify-zlib/lib/index.js' },
fallback: { zlib: false } } }
Here I just hoped to not include zlib to see if Webpack would omit it with a node target.
resolve:
{ modules: [ '/work/proj/node_modules' ],
extensions: [ '.js', '.json' ],
fallback: { zlib: false } } }
Lastly I tried to use the plugin node-polyfill-webpack-plugin but with the node target it does not seem to do anything. If I chose a web target the plugin seems to work as I'd expect (taken from here). Again, I'd prefer a node target so it uses native modules and the setup seems cleaner; but maybe this is the only approach. If this is the approach then how to support fs and other non-browser modules that IoT.js supports natively?
...
plugins = [ new NodePolyfillPlugin({ excludeAliases: [] }) ];
It seems that when the node target is selected there is no way to override any of the default/boilerplate code added to the output file. Does anyone have experience with IoT.js and Webpack, or overriding the default Webpack 5 code for node and use a polyfill instead? Not sure if a Webpack plugin is an approach. I am a little new to Webpack. Could this be a problem with Webpack? Any help would be appreciated.

Webpack generated library has problem being imported by project where useBuiltIns is set to 'usage'

I want to write a reusable UI component library and pack it with Webpack. However, when I import it in another project, where the babelrc has useBuiltIns: 'usage' set, the import will fail with an error:
"export 'default' (imported as 'Component') was not found in 'component'
This is part of my webpack configuration in library project:
output: {
path: path.resolve(process.cwd(), './dist'),
filename: 'component.js',
chunkFilename: '[id].js',
library: 'Component',
libraryTarget: 'commonjs2',
libraryExport: 'default'
},
...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
},
Babel configuration in library project:
module.exports = {
presets: [
[
"env",
{
modules: false,
targets: {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}
],
"stage-2"
]
}
Babel configuration in the consuming project:
module.exports = {
presets: [
'#vue/app'
]
}
Where the useBuiltIns: 'usage' is implicitly set.
While the problem could be solve by either set useBuiltIns: false or scriptType: 'unambiguous' in the consuming project. But this is not what I want. Since my goal is to provide a reusable library and it is expected to be used in different projects. I cannot force all the consuming projects to do this.
Am I missing something here?
I've found the answer in the Vue.js forum: https://forum.vuejs.org/t/export-default-imported-as-components-was-not-found-in-mylib/63905
The problem was that I was adding the dependency with a local path, namely:
$ npm install ../component
And in this case, npm is creating a symlink in node_modules. It seems that some babel plugin doesn't really like symlinks.
After I've changed to use git:
$ npm install git+file://localhost/path/to/component
Everything works fine.

Moment failed to work with webpack (typescript + babel)

Just as the demo, when i add moment to the project, bundle built by webpack failed, once i remove the moment it is ok.
Note: the module in tsconfig.json is set to es6, which is exactly what i need, when switch it to commonjs, it is ok too.
I am told you guys are acitve here, thanks very much to you~~~
After got it running and disable minify mode, I saw that problem is how you using moment. You can use this config to see your unminify code problem:
entry: './src/index.tsx',
mode: 'production',
output: {
path: path.join(__dirname, 'lib'),
filename: 'index.js',
},
module: {
rules: [
{
test: /\.(tsx|ts)$/,
exclude: /node_modules/,
use: ['babel-loader', 'ts-loader'],
},
],
},
optimization: {
// We no not want to minimize our code.
minimize: false
},
After change, I got error moment is not a function. So it's a problem when webpack bundle and how moment export their function
https://github.com/palantir/blueprint/issues/959
You can fix it by
const moment = require('moment')
to use the function as document or
moment.toString()
to just got the current time.
Or change options in tsconfig like in https://momentjs.com/docs/#/use-it/typescript/

Categories