本地开启 Yarn
Node.js >= 16.10
$ corepack enable
Node.js < 16.10
$ npm i -g corepack
$ corepack enable
corepack 是一个试验性质的工具,它主要用来帮助用户更好的管理你所使用的包管理工具的版本,在 Node 16.10 版本之后,它内置在 Node 中,之前的版本需要进行全局安装
初始化 Lerna
在目标文件夹中运行
$ yarn dlx lerna init --independent
初始化项目
在目标文件夹中运行
$ yarn init -2
因为本项目不打算使用 Plug'n'Play 来管理依赖,因此还需要再调整下 package.json 和 yarnrc.yml 文件中的内容
// package.json
{
"version": "0.0.0",
"private": true,
"workspaces": [
"packages/*"
],
"packageManager": "yarn@3.2.0",
"name": "lerna-yarn-rollup-boilerplate",
"devDependencies": {
"lerna": "^4.0.0"
}
}
# yarnrc.yml
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.2.0.cjs
gitignore 文件中的内容可以参考这里
安装 Yarn 插件
$ yarn plugin import workspace-tools
调整 Lerna 配置
// lerna.json
{
"useWorkspaces": true,
"npmClient": "yarn",
"packages": ["packages/*"],
"version": "independent"
}
安装和配置 Husky(可跳过)
$ yarn add husky -D
$ yarn husky install
在 package.json 中添加脚本
{
...
+ "scripts": {
+ "postinstall": "husky install"
+ },
...
}
添加一个 hook
$ yarn husky add .husky/pre-commit "yarn test"
安装 commitlint
$ yarn add -D @commitlint/{config-conventional,cli}
配置使用 conventional config
$ echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
添加 husky 的 hook
$ cat <<EEE > .husky/commit-msg
#!/bin/sh
. "\$(dirname "\$0")/_/husky.sh"
yarn commitlint --edit "\${1}"
EEE
确保 hook 可执行
$ chmod a+x .husky/commit-msg
添加子项目
用于展示组件的子项目 react app
$ yarn lerna create @lyrb/react-app
$ yarn create vite ./packages/react-app --template react-ts
按钮组件子项目 ui-button
$ yarn lerna create @lyrb/ui-button
$ yarn workspace @lyrb/ui-button add react -D
调整 ui-button 项目中的目录结构和文件内容如下:
packages/ui-button
├── src
│ ├── index.ts
│ └── Button.tsx
├── package.json
└── README.md
// ui-button/src/Button.tsx
import type { PropsWithChildren } from "react";
interface Props {}
const Button = (props: PropsWithChildren<Props>) => {
return <button>{props.children}</button>;
};
export default Button;
// ui-button/src/index.ts
export { default as Button } from "./Button";
工具库子项目 ui-utils
$ yarn lerna create @lyrb/ui-utils
调整 ui-utils 项目中的目录结构和文件内容如下:
packages/ui-utils
├── src
│ └── index.ts
├── package.json
└── README.md
// ui-utils/src/index.ts
export const sayHelloTo = (name: string) => {
console.log(`hello ${name}`);
};
安装 TypeScript
$ yarn add typescript -D
$ yarn workspace @lyrb/ui-utils add typescript -D
$ yarn workspace @lyrb/ui-button add typescript -D
初始化
$ yarn tsc --init
$ yarn workspace @lyrb/ui-button tsc --init
$ yarn workspace @lyrb/ui-utils tsc --init
tsconfig.json 文件中的配置可以参考这里
我们用根目录下的 tsconfig.json 作为基础配置,调整 ui-button 和 ui-utils 项目中的 tsconfig.json 内容为:
{
"extends": "../../tsconfig.json"
}
检验一下是否生效
$ yarn workspace @lyrb/ui-button tsc
运行后会得到如下报错:
src/Button.tsx:6:10 - error TS17004: Cannot use JSX unless the '--jsx' flag is provided.
我们调整下 ui-button 中 tsconfig.json 的内容
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "preserve"
}
}
再次执行上面的命令,发现之前的报错信息已经没有了,且 ui-button 项目中生成了 ./index.js 和 ./src/Button.jsx 这2个文件,说明我们的配置已经生效了 ✌️
安装 ESLint
上面的 typescript 我们采取的策略是每个子包都会独立去安装这个依赖,这样的好处是即使在子项目的根目录下依旧可以运行 tsc 命令
$ cd ./packages/ui-button
$ yarn tsc
但是麻烦的是,我们需要给每个子项目都安装这个依赖,如果子项目总数过多,维护起来就很繁琐。
Yarn 提供了一个可以共享脚本的功能,我们可以借助它来实现只在项目根目录中安装对应依赖,在各个子项目中共享使用这个依赖
首先在根项目中安装 eslint
$ yarn add eslint -D
初始化 eslint
$ yarn create @eslint/config
根据提示选择合适的选项后会在根项目中安装额外的依赖,安装完毕后,此时在 IDE 中会看到对应的错误提示
我们再确认下命令是否生效
在根项目的 package.json 文件中添加用来被共享的脚本
{
...
"scripts": {
"postinstall": "husky install",
+ "g:eslint": "cd $INIT_CWD && eslint"
},
...
}
在 ui-utils 项目 package.json 文件中添加脚本
{
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1",
+ "eslint": "yarn g:eslint src/**/*"
},
}
执行命令
$ yarn workspace @lyrb/ui-utils run eslint
此时我们可以看到控制台会输出对应的错误信息
/.../packages/ui-utils/src/index.ts
1:1 error Prefer default export import/prefer-default-export
2:3 warning Unexpected console statement no-console
安装 Prettier
$ yarn add prettier -D
创建配置文件和需要忽略的配置文件
$ echo {}> .prettierrc.json
$ echo coverage\\n.yarn > .prettierignore
在根项目的 package.json 文件中添加脚本
{
"scripts": {
"postinstall": "husky install",
"g:eslint": "cd $INIT_CWD && eslint",
+ "prettier": "prettier --write ."
},
}
执行脚本
$ yarn run prettier
配合 ESLint 达成 1 + 1 > 2 的效果
添加 eslint-config-prettier 和 eslint-plugin-prettier
$ yarn add eslint-config-prettier eslint-plugin-prettier -D
调整 .eslintrc.js 中的配置
module.exports = {
...
extends: [
...,
+ 'plugin:prettier/recommended',
],
...
}
安装 Stylelint
$ yarn add stylelint stylelint-config-standard -D
创建配置文件 .stylelintrc.js
{
"extends": "stylelint-config-standard",
"rules": {},
}
配合 Prettier 达成 1 + 1 > 2 的效果
添加 stylelint-config-prettier 和 stylelint-prettier)
$ yarn add stylelint-config-prettier stylelint-prettier -D
调整 .stylelintrc.js 中的配置
module.exports = {
...
extends: [
...,
+ 'plugin:prettier/recommended',
],
...
}
在根项目的 package.json 文件中添加脚本
{
...
"scripts": {
...,
+ "g:stylelint": "cd $INIT_CWD && stylelint"
},
...
}
执行命令验证一下
$ yarn run g:stylelint "**/*.css"
packages/react-app/src/App.css
1:1 ✖ Expected class selector to be kebab-case selector-class-pattern
5:1 ✖ Expected class selector to be kebab-case selector-class-pattern
11:3 ✖ Expected class selector to be kebab-case selector-class-pattern
16:1 ✖ Expected class selector to be kebab-case selector-class-pattern
27:1 ✖ Expected class selector to be kebab-case selector-class-pattern
31:12 ✖ Expected keyframe name to be kebab-case keyframes-name-pattern
packages/react-app/src/index.css
3:63 ✖ Unexpected quotes around "Roboto" font-family-name-quotes
3:73 ✖ Unexpected quotes around "Oxygen" font-family-name-quotes
3:83 ✖ Unexpected quotes around "Ubuntu" font-family-name-quotes
4:5 ✖ Unexpected quotes around "Cantarell" font-family-name-quotes
安装 lintstaged
通过 Mrm 完成快速安装和初始化配置
$ yarn dlx mrm lint-staged
命令执行完后,我们需要调整下默认的配置
# ./.husky/pre-commit
...
- npx lint-staged
+ yarn lint-staged
// ./package.json
{
...,
"lint-staged": {
- "*.js": "eslint --cache --fix",
- "*.css": "stylelint --fix"
+ "./packages/**/*.{js,jsx,ts,tsx}": "eslint --fix",
+ "./packages/**/*.css": "stylelint --fix",
+ "*.{md,json}": "prettier --write"
}
}
安装 Jest
$ yarn add jest babel-jest @types/jest react-test-renderer @types/react-test-renderer -D
初始化
$ yarn jest --init
安装 Babel 相关模块
$ yarn add @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript -D
创建 Babel 配置文件 babel.config.js
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
[
'@babel/preset-react',
{
runtime: 'automatic',
},
],
],
}
测试 ui-utils 项目
在 ui-utils 项目中添加 src/index.test.ts 文件
import { sayHelloTo } from './index'
describe('test sayHelloTo function', () => {
it('should console log Hello Kevin', () => {
const logSpy = jest.spyOn(console, 'log')
sayHelloTo('Kevin')
expect(logSpy).toHaveBeenCalledWith('Hello Kevin')
})
})
运行脚本查看测试结果
$ yarn run test
console.log
Hello Kevin
at console.<anonymous> (node_modules/jest-mock/build/index.js:836:25)
PASS packages/ui-utils/src/index.test.ts
test sayHelloTo function
✓ should console log Hello Kevin (47 ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.ts | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.555 s, estimated 1 s
Ran all test suites.
测试 ui-button 项目
首先改造下 ui-button 项目的源码
安装 clsx
$ yarn workspace @lyrb/ui-button add clsx arn
添加 style.css 文件到 ui-button 项目的 src 目录下
.btn {
padding: 12px 24px;
border: 1px solid grey;
appearance: none;
}
.btn-primary {
background: blue;
}
修改 Button.tsx 文件
import type { PropsWithChildren } from 'react'
+ import clsx from 'clsx'
+ import './style.css'
interface Props {
type: 'primary' | 'default'
}
const Button = (props: PropsWithChildren<Props>) => {
const { type } = props
+ const classNames = clsx('btn', {
+ 'btn-primary': type === 'primary',
+ })
- return <button type="button">{props.children}</button>
+ return (
+ <button className={classNames} type="button">
+ {props.children}
+ </button>
+ )
}
export default Button
添加测试文件
// ui-button/src/__tests__/button.test.tsx
import renderer from 'react-test-renderer'
import Button from '../Button'
describe('test button component', () => {
it('render correctly', () => {
const component = renderer.create(<Button type="primary">123</Button>)
expect(component).toMatchSnapshot()
})
})
因为我们在 Button.tsx 文件中引入了 styles.css 文件,因此要简单改造下 Jest 的配置文件,具体原因可以参考这里
在项目根目录添加 __mocks__/styleMock.js 文件
module.exports = {}
修改 jest.config.ts 内容
export default {
...,
+ moduleNameMapper: {
+ '\\.(css|less|sass|scss)$': '<rootDir>/__mocks__/styleMock.js',
+ },
...
}
运行脚本查看测试结果
$ yarn run test
PASS packages/ui-utils/src/index.test.ts
● Console
console.log
Hello Kevin
at console.<anonymous> (node_modules/jest-mock/build/index.js:836:25)
PASS packages/ui-button/src/__tests__/button.test.tsx
› 1 snapshot written.
---------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
ui-button/src | 100 | 100 | 100 | 100 |
Button.tsx | 100 | 100 | 100 | 100 |
ui-utils/src | 100 | 100 | 100 | 100 |
index.ts | 100 | 100 | 100 | 100 |
---------------|---------|----------|---------|---------|-------------------
Snapshot Summary
› 1 snapshot written from 1 test suite.
Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 1 written, 1 total
Time: 0.645 s, estimated 1 s
Ran all test suites.
可以在 ui-button/src/__tests__/__snapshots__ 文件夹中找到刚创建的 button.test.tsx.snap 文件
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`test button component render correctly 1`] = `
<button
className="btn btn-primary"
type="button"
>
123
</button>
`;
安装 Rollup
$ yarn add rollup -D
根项目 package.json 添加公共脚本
{
"scripts": {
...,
+ "g:build": "cd $INIT_CWD && rollup",
...
},
}
构建 ui-utils 项目
安装 Rollup 插件
$ yarn add @rollup/plugin-typescript @rollup/plugin-babel rollup-plugin-node-externals @rollup/plugin-node-resolve @rollup/plugin-commonjs rollup-plugin-sizes -D
在 ui-utils 项目根目录创建 rollup.config.js 文件
import { defineConfig } from 'rollup'
import path from 'path'
import typescript from '@rollup/plugin-typescript'
import babel from '@rollup/plugin-babel'
import externals from 'rollup-plugin-node-externals'
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import sizes from 'rollup-plugin-sizes'
import packageJson from './package.json'
/**
* @type {import('rollup').RollupOptions.plugins}
*/
const publicConfig = {
input: 'src/index.ts',
plugins: [
externals(),
resolve(),
commonjs(),
babel({
babelHelpers: 'bundled',
}),
sizes(),
],
}
/**
* @type {import('rollup').RollupOptions}
*/
const cjsConfig = {
...publicConfig,
output: {
file: packageJson.main,
format: 'cjs',
sourcemap: true,
},
plugins: [
...publicConfig.plugins,
typescript({ tsconfig: path.resolve(__dirname, './tsconfig.json') }),
],
}
/**
* @type {import('rollup').RollupOptions}
*/
const esConfig = {
...publicConfig,
output: [
{
dir: 'es',
format: 'es',
sourcemap: true,
},
],
plugins: [...publicConfig.plugins, typescript({ tsconfig: './tsconfig.json', outDir: './es' })],
}
export default defineConfig([cjsConfig, esConfig])
调整 ui-utils 项目的 package.json 文件
{
...,
- "main": "lib/ui-utils.js",
+ "main": "lib/index.js",
+ "module": "es/index.js",
+ "typings": "lib/index.d.ts",
...,
+ "files": [
+ "lib",
+ "es"
+ ],
"scripts": {
...,
+ "build": "yarn g:build -c"
},
}
调整 ui-utils 项目的 tsconfig.json 文件
{
"extends": "../../tsconfig.json",
+ "exclude": ["node_modules", "**/*.test.ts", "/**/*.test.tsx", "./es/**/*", "./lib/**/*"],
+ "compilerOptions": {
+ "declaration": true,
+ "noEmit": true,
+ "outDir": "./"
+ }
}
执行构建命令
$ yarn workspace @lyrb/ui-utils run build
构建 ui-button 项目
由于该项目中引入了 css 文件,因此要额外安装一些插件
$ yarn add rollup-plugin-styles -D
在 ui-button 项目根目录创建 rollup.config.js 文件
import { defineConfig } from 'rollup'
import path from 'path'
import typescript from '@rollup/plugin-typescript'
import sizes from 'rollup-plugin-sizes'
import styles from 'rollup-plugin-styles'
import resolve from '@rollup/plugin-node-resolve'
import externals from 'rollup-plugin-node-externals'
import commonjs from '@rollup/plugin-commonjs'
import babel from '@rollup/plugin-babel'
import packageJson from './package.json'
/**
* @type {import('rollup').RollupOptions.plugins}
*/
const publicConfig = {
input: 'src/index.ts',
plugins: [
externals(),
resolve(),
commonjs(),
babel({
babelHelpers: 'bundled',
}),
styles({
mode: 'extract',
}),
sizes(),
],
}
/**
* @type {import('rollup').RollupOptions}
*/
const cjsConfig = {
...publicConfig,
output: {
file: packageJson.main,
format: 'cjs',
sourcemap: true,
assetFileNames: '[name][extname]',
},
plugins: [
...publicConfig.plugins,
typescript({ tsconfig: path.resolve(__dirname, './tsconfig.json') }),
],
}
/**
* @type {import('rollup').RollupOptions}
*/
const esConfig = {
...publicConfig,
preserveModules: true,
output: [
{
dir: 'es',
format: 'es',
sourcemap: true,
assetFileNames: '[name][extname]',
},
],
plugins: [...publicConfig.plugins, typescript({ tsconfig: './tsconfig.json', outDir: './es' })],
}
export default defineConfig([cjsConfig, esConfig])
调整 ui-button 项目的 package.json 文件
{
...,
- "main": "lib/ui-button.js,
+ "main": "lib/index.js",
+ "module": "es/index.js",
+ "typings": "lib/index.d.ts",
...,
+ "files": [
+ "lib",
+ "es"
+ ],
"scripts": {
...,
+ "build": "yarn g:build -c"
},
}
调整 ui-button 项目的 tsconfig.json 文件
{
"extends": "../../tsconfig.json",
+ "exclude": ["node_modules", "**/*.test.ts", "/**/*.test.tsx", "./es/**/*", "./lib/**/*"],
"compilerOptions": {
- "jsx": "preserve"
+ "jsx": "react-jsx",
+ "declaration": true,
+ "noEmit": true,
+ "outDir": "./"
}
}
执行构建命令
$ yarn workspace @lyrb/ui-button run build
最后,记得在根项目的 .gitignore 和 .eslintignore 中添加 es 和 lib 目录
关联各个子项目
在 ui-button 中使用 ui-utils
$ yarn workspace @lyrb/ui-button add @lyrb/ui-utils@1.0.0
调整 ui-button 项目中的 src/Button.tsx 文件内容
- import { PropsWithChildren } from 'react'
+ import { PropsWithChildren, useCallback } from 'react'
import clsx from 'clsx'
+ import { sayHelloTo } from '@lyrb/ui-utils'
import './style.css'
interface Props {
type: 'primary' | 'default'
}
const Button = (props: PropsWithChildren<Props>) => {
const { type } = props
const classNames = clsx('btn', {
'btn-primary': type === 'primary',
})
+ const handleClick = useCallback(() => {
+ sayHelloTo('John')
+ }, [])
return (
- <button className={classNames} type="button">
+ <button className={classNames} type="button" onClick={handleClick}>
{props.children}
</button>
)
}
export default Button
调整 ui-button 中的 tsconfig.json
{
...,
"compilerOptions": {
+ "moduleResolution": "node",
...
}
}
在 react-app 中使用 ui-button
$ yarn workspace @lyrb/react-app add @lyrb/ui-button@1.0.0
调整 react-app 中的 App.tsx
import { useState } from 'react'
+ import { Button } from '@lyrb/ui-button'
+ import '@lyrb/ui-button/lib/index.css'
import logo from './logo.svg'
import './App.css'
function App() {
const [count, setCount] = useState(0)
return (
<div className="app">
<header className="app-header">
<img src={logo} className="app-logo" alt="logo" />
<p>Hello Vite + React!</p>
<p>
<button type="button" onClick={() => setCount((prevCount) => prevCount + 1)}>
count is: {count}
</button>
</p>
+ <p>
+ <Button type="primary">Click Me</Button>
+ </p>
<p>
Edit <code>App.tsx</code> and save to test HMR updates.
</p>
<p>
<a
className="app-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
{' | '}
<a
className="app-link"
href="https://vitejs.dev/guide/features.html"
target="_blank"
rel="noopener noreferrer"
>
Vite Docs
</a>
</p>
</header>
</div>
)
}
export default App
调整 vite.config.ts,原因看这里
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
+ optimizeDeps: {
+ include: ['react/jsx-runtime'],
+ },
})
构建
安装 yarn.BUILD 插件
$ yarn plugin import https://yarn.build/latest
构建项目
$ yarn build
构建完毕后,我们可以运行 react-app 项目看看效果
$ yarn workspace @lyrb/react-app dev
# or
$ yarn dlx serve packages/react-app/dist
一切顺利 🎉
发布
调整 ui-button 和 ui-utils 项目中的 package.json 文件
{
...,
"publishConfig": {
+ "access": "public",
...
},
"scripts": {
+ "prepublishOnly": "yarn run build",
...
},
...
}
调整 lerna.json 里的配置
{
"useWorkspaces": true,
"npmClient": "yarn",
+ "command": {
+ "publish": {
+ "message": "chore(release): publish"
+ }
+ },
"packages": ["packages/*"],
"version": "independent"
}
发车 🚀
$ yarn lerna publish
完整的项目在这里
(完)
参考资料
- Yarn 2 (typicode.github.io)
- Document some of the tradeoffs of V8 coverage (vs Babel/Istanbul coverage) · Issue #11188 · facebook/jest (github.com)
- jest报错SyntaxError: Cannot use import statement outside a module解决方法总结 - 掘金 (juejin.cn)
- reactjs - SyntaxError with Jest and React and importing CSS files - Stack Overflow
- Introducing the New JSX Transform – React Blog (reactjs.org)
- jsx:
preservedoes not compile · Issue #72 · rollup/plugins (github.com)- Importing react/jsx-runtime breaks dev builds · Issue #6215 · vitejs/vite (github.com)