Configuring Jest with Rollup and Typescript - javascript

I created an app within a monorepo (Lerna) using yarn workspace.
The architecture of the app is as follow:
my-monorepo
├── node_modules
├── packages
│   ├── package1(shared components)
│   ├── package2(other package consuming the shared components)
│   │ └── ./jest.config.js
├── package.json
The problem
the problem is jest is throwing the following error when trying to use package1 in package2 in any test, and I haven't found a way to fix it.
● Test suite failed to run
Jest encountered an unexpected token
This usually means that you are trying to import a file which Jest ca
nnot parse, e.g. it's not plain JavaScript.
By default, if Jest sees a Babel config, it will use that to transfor
m your files, ignoring "node_modules".
Here's what you can do:
• If you are trying to use ECMAScript Modules, see https://jestjs.io
/docs/en/ecmascript-modules for how to enable it.
• To have some of your "node_modules" files transformed, you can spe
cify a custom "transformIgnorePatterns" in your config.
• If you need a custom transformation specify a "transform" option i
n your config.
• If you simply want to mock your non-JS modules (e.g. binary assets
) you can stub them out with the "moduleNameMapper" config option.
You'll find more details and examples of these config options in the
docs:
https://jestjs.io/docs/en/configuration.html
Details:
C:\Users\my-user\Desktop\my-monorepo\node_module
s\antd\es\date-picker\generatePicker\index.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__fi
lename,global,jest){import _extends from "#babel/runtime/helpers/esm/exte
nds";
^^^^^^
According the error, I'm trying to import a file which Jest cannot parse, so the problem comes from the package1, so the first thing that comes to my mind is: maybe I'm doing something wrong in rollup in package1 and the final bundle comes in some format that jest doesn't understand...
Jest config
Jest config located in package2, where i want to consume package1:
// jest.config.js in package2
const config = {
roots: ['src'],
setupFilesAfterEnv: ['./jest.setup.ts'],
moduleFileExtensions: ['ts', 'tsx', 'js'],
testPathIgnorePatterns: ['node_modules/'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
testMatch: ['**/*.test.(ts|tsx)'],
moduleNameMapper: {
// Mocks out all these file formats when tests are run.
'\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'identity-obj-proxy',
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
};
export default config;
Rollup config
This is the rollup configuration in package1:
// rollup.config.js
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import resolve from '#rollup/plugin-node-resolve';
import commonjs from '#rollup/plugin-commonjs';
import typescript from 'rollup-plugin-typescript2';
import postcss from 'rollup-plugin-postcss';
import copy from 'rollup-plugin-copy';
import json from '#rollup/plugin-json';
import svgr from '#svgr/rollup';
import { babel } from '#rollup/plugin-babel';
import { visualizer } from 'rollup-plugin-visualizer';
import pkg from './package.json';
export default {
input: 'src/index.tsx',
output: [
{
file: pkg.main,
format: 'cjs',
exports: 'named',
sourcemap: true,
},
{
file: pkg.module,
format: 'esm',
exports: 'named',
sourcemap: true,
},
],
plugins: [
peerDepsExternal({
includeDependencies: true,
}),
json(),
svgr(),
resolve({ extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'] }),
commonjs({
include: /\**node_modules\**/,
}),
// UPDATE 3: Add babel
babel({
babelHelpers: 'bundled',
}),
typescript({
useTsconfigDeclarationDir: true,
exclude: ['*.d.ts', '**/*.d.ts', '**/*.test.tsx'],
rollupCommonJSResolveHack: true,
clean: true,
}),
postcss({
extensions: ['.css', '.less'],
use: {
less: { javascriptEnabled: true, modifyVars: {} },
},
}),
visualizer({ filename: 'stats-visualizer.html', gzipSize: true }),
copy({
targets: [
{
src: 'src/styles/themes/dark-variables.less',
dest: 'dist',
rename: 'theme/dark.less',
},
{
src: 'src/styles/themes/light-variables.less',
dest: 'dist',
rename: 'theme/light.less',
},
{
src: 'src/assets/icons',
dest: 'dist',
rename: 'assets/icons',
},
],
}),
],
};
UPDATE 1:
I've tried to use transform rules in jest.config.js as mentioned by Matt Carlota to transpile antdbut this doesn't work:
// jest.config.js
const config = {
// ... other jest settings
transform: {
'^.+\\.tsx?$': 'ts-jest',
'node_modules/antd/.+\\.(j|t)sx?$': 'ts-jest',
},
// I've tried with `antd/es` pattern too and doesn't work
transformIgnorePatterns: ['node_modules/(?!antd)'],
};
UPDATE 2:
Change manually antd/es by antd/lib in package1 resolve the problem temporarily, but there is one problem and that is that we are a large group of people working and it could be put as a convention to use only antd/lib but I feel it would be error prone.
every time someone forgets to use antd/lib and uses antd/en all tests break with the original error.
UPDATE 3:
Add babel config file and plugin in rollup configuration...
// babel.config.js
module.exports = {
plugins: [['import', { libraryName: 'antd', libraryDirectory: 'lib' }, 'antd']],
};

I've been having the same issue for the longest time and finally found a way.
The missing piece in my case was adding moduleNameMapper. A guide mentioned that here.
The solution doesn't require babel-jest.
jest.config.js
const path = require('path');
const { lstatSync, readdirSync } = require('fs');
// get listing of packages in the mono repo
const basePath = path.resolve(__dirname, '..', '..', 'packages');
const packages = readdirSync(basePath).filter((name) => {
return lstatSync(path.join(basePath, name)).isDirectory();
});
module.exports = {
preset: 'ts-jest',
verbose: true,
moduleFileExtensions: ['js', 'json', 'jsx', 'node', 'ts', 'tsx'],
moduleDirectories: ['node_modules', 'src'],
moduleNameMapper: {
...packages.reduce(
(acc, name) => ({
...acc,
[`#xyz/${name}(.*)$`]: `<rootDir>/../../packages/./${name}/src/$1`,
}),
{}
),
},
rootDir: './',
testRegex: '.spec.ts$',
transform: {
'^.+\\.(t)s$': 'ts-jest',
},
testEnvironment: 'node',
setupFilesAfterEnv: ['./jest.setup.ts'],
testMatch: null,
globals: {
'ts-jest': {
tsconfig: 'tsconfig.jest.json',
},
},
roots: ['<rootDir>'],
transformIgnorePatterns: [],
collectCoverage: false,
collectCoverageFrom: ['src/**/*.{js{,x},ts{,x}}', '!src/server/index.ts'],
};
tsconfig.json
{
"compilerOptions": {
"module": "esnext",
"target": "ES2019",
"lib": ["ES2019", "DOM"],
"noEmit": true,
"types": ["node", "jest"],
"rootDir": "./"
"paths": {
"#xyz/*": ["packages/*/src"]
},
},
"include": ["test/**/*.ts", "src/**/*.ts", "**/*.spec.ts"],
"references": [
{ "path": "../../packages/package1" },
]
}
package.json
...
scripts: {
"test": "NODE_ENV=production tsc --build ./tsconfig.jest.json && jest --env=node test --watch",
}

Related

Vite: Building a SASS file as it is written

I am building a library using Vite/Vue3/Quasar and I would like to export a quasar-variables.sass file as it is written without it being compiled or anything. Just straight SASS file that I can import to my other projects.
Is this possible with Vite?
Here is my vite.config.js:
import { resolve } from 'path';
import { defineConfig } from 'vite';
import { quasar, transformAssetUrls } from '#quasar/vite-plugin';
import eslintPlugin from 'vite-plugin-eslint';
import vue from '#vitejs/plugin-vue';
// https://vitejs.dev/config/
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.js'),
name: 'AvvinueClowder',
fileName: (format) => `avvinue-clowder.${format}.js`,
},
rollupOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue',
},
},
},
},
plugins: [
vue({
template: { transformAssetUrls },
}),
eslintPlugin(),
quasar({
sassVariables: 'src/style/_quasar-variables.sass',
}),
],
resolve: {
alias: {
'#': resolve(__dirname, './src'),
},
},
});
And part of my package.json:
"version": "2.0.9",
"files": [
"dist"
],
"main": "./dist/avvinue-clowder.umd.js",
"module": "./dist/avvinue-clowder.es.js",
"exports": {
".": {
"import": "./dist/avvinue-clowder.es.js",
"require": "./dist/avvinue-clowder.umd.js"
}
},
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
And this is what gets spit out in the dist folder:
Right now everything gets converted to simple CSS and the variables seem to get lost, which is forcing me to declare multiple variable files in multiple repositories, instead of just importing it from my NPM library I'm creating.
Thank you!
For Vite specifically, its easier if you just create a /public folder in your root and add files/assets that shouldn't be altered by the build script.
You can use the rollup copy plugin to copy your file into dist folder
import copy from 'rollup-plugin-copy'
plugins: [
copy({
targets: [
{ src: 'src/style/_quasar-variables.sass', dest: 'dist/style' }
]
})
]
Another way, you can just add your file to npm by excluding it from .npmignore file
/src <- This line prevents uploading the whole src folder to npm
!src/style/_quasar-variables.sass <- This will add your file to npm package

Vue 3 on Vite.js with Eslint — Unable to resolve path to module eslint(import/no-unresolved)

I use Vue 3 on Vite.js with Eslint + Airbnb config. Airbnb config has a rule eslint(import/no-unresolved), which is good, but Eslint doesn't know how to resolve alias path.
I want to use aliases for paths — example:
import TableComponent from '#/components/table/TableComponent.vue'˙
Environment is in plain JavaScript.
I managed to set up my vite.config.js so that the app can resolve paths like this:
import path from 'path';
import { defineConfig } from 'vite';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: [{
find: "#", replacement: path.resolve(__dirname, 'src')
},],
},
});
Vue app works like that and resolves the import path correctly, but Eslint keeps reporting the error: Unable to resolve path to module eslint(import/no-unresolved)
How and where can I tell Eslint how to resolve aliases?
I have tried:
install eslint-plugin-import eslint-import-resolver-alias --save-dev
// .eslintrc.js
// ...
extends: [
'eslint:recommended',
'plugin:import/recommended',
'airbnb-base',
'plugin:vue/vue3-strongly-recommended',
],
settings: {
'import/resolver': {
alias: {
map: [
['#', 'src'],
],
},
},
},
But that doesn't work.
EDIT:
Solved the issue, see the accepted answer if you're using plain JavaScript like I do.
If you're using TypeScript, see if Seyd's answer can help you.
this solves the issue in my TypeScript project.
npm install eslint-import-resolver-typescript
After eslint-import-resolver-typescript installation
{
// other configuration are omitted for brevity
settings: {
"import/resolver": {
typescript: {} // this loads <rootdir>/tsconfig.json to eslint
},
},
}
should be added to .eslintrc.js.
my tsconfig.json (remove unwanted settings)
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"strict": false,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"types": ["vite/client", "node"],
"baseUrl": ".",
"paths": {
"#/*": ["src/*"]
},
"allowJs": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": ["node_modules"]
}
Check the discussion here:
In case someone runs into this problem, this works in my case*:
settings: {
'import/resolver': {
alias: {
map: [
['#', './src'],
],
},
},
},
*In my case, Vue's root is 'src' directory, while Eslint's is one level higher, so it needs the './src' path.
Huge thanks to #https://github.com/aladdin-add for the answer through the github question!
I had the same problem, and even I fixed the src path it still hat the issue. It did not work until I added extensions:
"settings": {
"import/resolver": {
"alias": {
"map": [
["#", "./src"]
],
"extensions": [".js",".jsx"] <--- HERE
}
}
},
1. Use & export aliases in vite.config.js
// vite.config.js
import { resolve } from 'path';
import { defineConfig } from 'vite';
export const aliases = {
'#': resolve(__dirname, './src'),
'#u': resolve(__dirname, './src/utils'),
};
export default () => defineConfig({
// ...
resolve: {
alias: aliases,
},
})
2. Create .eslintrc with ES Modules support. (Thanks to Morgan's answer)
2.1 npm i esm -D
2.2 Create sibling to .eslintrc.js — .eslintrc.esm.js.
2.3 Put your your ESLint config into .eslintrc.esm.js.
// .eslintrc.esm.js
export default {
root: true,
extends: ['#vue/airbnb', 'plugin:vue/recommended'],
// ...
}
2.4 Inside .eslintrc.js include this code:
const _require = require('esm')(module)
module.exports = _require('./.eslintrc.esm.js').default
3. Import, map & use aliases from vite.config.js in .eslintrc.esm.js
3.1 npm i eslint-import-resolver-alias -D
3.2 Inside .eslintrc.esm.js include following code:
// .eslintrc.esm.js
import { aliases } from './vite.config';
const mappedAliases = Object.entries(aliases).map((entry) => entry); // [[alias, path], [alias, path], ...]
export default {
// ...
settings: {
'import/resolver': {
alias: {
map: mappedAliases,
},
},
},
}
I can't for the life of me get this to work, I've tried everything above. Is there something else wrong with my file?
// .eslintrc.cjs
/* eslint-env node */
require("#rushstack/eslint-patch/modern-module-resolution");
module.exports = {
root: true,
extends: [
"airbnb",
"plugin:vue/vue3-essential",
"eslint:recommended",
"#vue/eslint-config-prettier",
],
parserOptions: {
ecmaVersion: "latest",
},
// Using the accepted answer
settings: {
"import/resolver": {
alias: {
map: [["#", "./src"]],
},
},
},
};
** UPDATE **
The only way I was able to get this to work was using this: eslint-config-airbnb link here, the readme was particularly helpful.
npm add --dev #vue/eslint-config-airbnb #rushstack/eslint-patch
My .eslintrc.js is now:
/* eslint-env node */
require("#rushstack/eslint-patch/modern-module-resolution");
const path = require("node:path");
const createAliasSetting = require("#vue/eslint-config-airbnb/createAliasSetting");
module.exports = {
root: true,
extends: [
"plugin:vue/vue3-essential",
"#vue/eslint-config-airbnb", // <-- added
"eslint:recommended",
"#vue/eslint-config-prettier",
],
parserOptions: {
ecmaVersion: "latest",
},
rules: {
"import/no-unresolved": "error",
},
settings: {
...createAliasSetting({
"#": `${path.resolve(__dirname, "./src")}`,
}),
},
};
Huzzah!
In the README.md of eslint-plugin-import it is said:
Currently Node and webpack resolution have been implemented, but the resolvers are just npm packages, so third party packages are supported (and encouraged!).
Here you can see the list of the third party resolvers.
One that worked for me was this: eslint-import-resolver-custom-alias, and this is how I used it:
settings:
import/resolver:
eslint-import-resolver-custom-alias:
alias:
'#': './app/javascript'
extensions:
- '.js'
- '.vue'
For me the # had been set as an alias for app/javascript inside vite.config.ts, you should change this based on your project configurations.
in .eslintrc.js add
settings: {
'import/resolver': {
alias: {
map: [['#', './src/']],
extensions: ['.js', '.vue'],
},
},
},

How to propperly build react modular library

I'm trying to create a react components library which is based on Typescript and SASS. The components library will be used in multiple other typescript projects, so type exports are needed as well. Ideally I want to mimic something like "Material-UI"/"React-Bootrap" libraries dist output solutions.
Example project structure:
|Tabs
+--Tabs.tsx
+--Tabs.scss
+--index.tsx
index.tsx
index.tsx
export { Tabs } from './Tabs/Tabs';
Tabs/index.tsx
import React from 'react';
import './Tabs.scss';
interface TabsProps {
...
}
export const Tabs: React.FC<TabsProps> = (props) => <div>...</div>
Tabs/index.tsx
export { Tabs } from './Tabs';
Expected built dist structure should mimic the src structure:
|Tabs
+--Tabs.js
+--Tabs.d.ts
+--index.js
+--index.d.ts
index.js
index.tsx
I tried analyzing open source projects and see how they are building the libraries, however I could not find libraries using the same approaches that I could reuse.
The solutions I've tried:
Webpack: While I could compile typescript and sass files the webpack would always emit only one file specified in the output section, which usually would be bundled and I would loose the ability to import single component from a specific component's module. I know I can specify multiple entry points, but the project will be having a lot of exports and manually specifying them is not an option...
Example config I tried:
const path = require('path');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = {
entry: './src/index.tsx',
module: {
rules: [
// sass-loader is not used here yet, but should be once desired structure can be reached
{
test: /\.tsx?$/,
loader: 'babel-loader',
},
// All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
{ test: /\.js$/, loader: "source-map-loader" }
]
},
// Enable sourcemaps for debugging webpack's output.
devtool: "source-map",
resolve: {
extensions: [".tsx", ".ts", ".js"],
plugins: [
new TsconfigPathsPlugin({ configFile: "./tsconfig.build.json" })
]
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
}
};
Rollup: Similar situation as webpack
Example config that I tried:
// rollup.config.js
import babel from 'rollup-plugin-babel';
import sass from 'rollup-plugin-sass';
import nodeResolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import react from 'react';
import reactDom from 'react-dom';
const babelOptions = {
exclude: /node_modules/,
// We are using #babel/plugin-transform-runtime
runtimeHelpers: true,
extensions: ['.js', '.ts', '.tsx'],
configFile: './babel.config.js',
};
const nodeOptions = {
extensions: ['.js', '.tsx', '.ts'],
};
const commonjsOptions = {
ignoreGlobal: true,
include: /node_modules/,
namedExports: {
react: Object.keys(react),
'react-dom': Object.keys(reactDom)
},
};
export default {
input: 'src/index.tsx',
output: {
name: '[name].js',
dir: 'dist',
format: 'umd',
sourcemap: true,
},
plugins: [
nodeResolve(nodeOptions),
sass(),
commonjs(commonjsOptions),
babel(babelOptions)
],
};
Babel: I managed to compile the typescript code however once I came close to transpiling SASS files I would end up with suggestions to use webpack for that...
TSC: I successfully could run the typescript compiler and it would compile all the files without problems and would maintain the same structure. However TSC does not support other transpiling options so after a lot of searches I would end up with suggestions to use webpack and "ts-loader" or "babel-loader"..
tsconfig:
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"lib": [ "es2015", "dom" ],
"outDir": "dist",
"baseUrl": ".",
"declaration": true,
"composite": true,
"module": "commonjs",
"target": "es5"
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}
Desirced solution:
I should be able after compiling the library and installing it in another project be able to run the following:
import { Tabs } from 'my-lib/Tabs';
import { Tabs } from 'my-lib';
After a lot of playing around I managed to produce the wanted result with rollup. The only downside of the current configuration is that it does not support newly added files in the --watch mode. The magic setting is under the output.preserveModules
Config:
// rollup.config.js
import commonjs from '#rollup/plugin-commonjs';
import typescript from '#rollup/plugin-typescript';
import postcss from 'rollup-plugin-postcss';
import postcssUrl from 'postcss-url';
import resolve from "#rollup/plugin-node-resolve";
import peerDepsExternal from "rollup-plugin-peer-deps-external";
export default {
input: 'src/index.tsx',
output: {
dir: 'dist',
format: 'es',
preserveModules: true,
sourcemap: true,
},
plugins: [
resolve(),
peerDepsExternal(),
commonjs(),
typescript({
tsconfig: 'tsconfig.build.json'
}),
postcss({
minimize: true,
modules: {
generateScopedName: "[hash:base64:5]"
},
plugins: [
postcssUrl({
url: "inline"
})
]
}),
],
};
I hope this config can help others as well
You can checkout this repo. I made some changes for building a lib.
https://github.com/21paradox/react-webpack-typescript-starter
To use a library like below:
import Input from 'my-custom-ui/entry/Input';
import { Input } from 'my-custom-ui';
After doing a lot of searching, I ended up writing a plugin to manually generate the webpack entry code that webpack needed (for building a ui library).
The multiple entry + manualy generated entry file seems to be working for component seperate & no redudant code. This is also very helpful if you want to build a vue based libray.

Exporting sass/css in npm module - Rollup

Overview
I'm trying to write a react based npm module. I've configured my environment using rollup and I can see the js/d.ts/css files being created in the build.
Issue
Using npm link, I was testing in a separate project. Now I'm able to import the js file, but I'm unable to import the css file.
Here is my rollup configuration (rollup.config.js)
import typescript from "rollup-plugin-typescript2";
import commonjs from "rollup-plugin-commonjs";
import external from "rollup-plugin-peer-deps-external";
import resolve from "rollup-plugin-node-resolve";
import sass from 'rollup-plugin-sass';
import pkg from "./package.json";
export default {
input: "src/index.tsx",
output: [{
file: pkg.main,
format: "cjs",
exports: "named",
sourcemap: true
},
{
file: pkg.module,
format: "es",
exports: "named",
sourcemap: true
}
],
plugins: [
external(),
resolve({
browser: true,
extensions: ['.mjs', '.js', '.jsx', '.json', '.scss', '.css']
}),
sass({
output: "autocomplete_style.css"
}),
typescript({
rollupCommonJSResolveHack: true,
exclude: "**/__tests__/**",
clean: true
}),
commonjs({
include: ["node_modules/**"],
namedExports: {
"node_modules/react/react.js": [
"Children",
"Component",
"PropTypes",
"createElement"
],
"node_modules/react-dom/index.js": ["render"]
}
})
]
};
Please note, I'm able to create a successful build. The issue is just with the import of the generated css file.

Importing local files in `jest.config.js` file

In my jest.config.js file, I need to populate globals property. For populating the globals property I need to require local modules, as shown below:
const path = require('path')
const server = require('./server/cfg')
module.exports = {
rootDir: path.resolve(__dirname),
moduleFileExtensions: [
'js',
'json',
'vue',
'ts'
],
moduleNameMapper: {
'^#/(.*)$': '<rootDir>/src/$1'
},
transform: {
".*\\.(vue)$": "<rootDir>/node_modules/vue-jest",
"^.+\\.(js|jsx)?$": "<rootDir>/node_modules/babel-jest",
"^.+\\.ts$": "<rootDir>/node_modules/ts-jest"
},
testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
snapshotSerializers: [
"jest-serializer-vue"
],
setupFiles: [
"<rootDir>/globals.js"
],
testEnvironment: "jsdom",
globals: {
server: {
server
}
}
}
Whit this configuration, I get the following error:
Error: Cannot find module './server/cfg'
This is my folder structure
server/
cfg.ts
src/
jest.config.js
webpack.config.js
However, I can require node's built-in modules. I'm not able to figure out why it is happening. Any ideas on how I can overcome this?
Jest is initiated by node, not ts-node and it's not able to resolve the .ts file by default.
Probably adding setupFilesAfterEnv will help you.
jest.config.js
module.exports = {
// ... your config
setupFilesAfterEnv: [
"<rootDir>/environmentWithServer.ts`
]
}
environmentWithServer.ts
global.server = require('./server/cfg');

Categories

Resources