├── bin └── index.js ├── src ├── config │ ├── webpackConfig │ │ ├── rules │ │ │ ├── index.ts │ │ │ ├── getFontRule.ts │ │ │ ├── getSvgRule.ts │ │ │ ├── getImageRule.ts │ │ │ ├── getJsTsRule.ts │ │ │ ├── getCssRule.ts │ │ │ ├── getRule.ts │ │ │ └── getLessRule.ts │ │ ├── index.ts │ │ ├── getWebpackConfig.ts │ │ ├── getDevConfig.ts │ │ ├── baseConfig.ts │ │ ├── getUmdConfig.ts │ │ └── getBuildConfig.ts │ ├── gulpConfig │ │ ├── getNewEntryDir.ts │ │ ├── index.ts │ │ ├── buildCjs.ts │ │ ├── buildEsm.ts │ │ ├── cssInjection.ts │ │ ├── constants.ts │ │ ├── copyLess.ts │ │ ├── less2css.ts │ │ └── compileScripts.ts │ └── babelConfig │ │ ├── lib.ts │ │ └── es.ts ├── utils │ ├── index.ts │ ├── syncChainFns.ts │ ├── after.ts │ ├── compose.ts │ ├── isAddForkTsPlugin.ts │ ├── getCustomConfig.ts │ └── getProjectConfig.ts ├── buildSite │ ├── index.ts │ └── buildSite.ts ├── dev │ ├── index.ts │ └── development.ts ├── constants.ts ├── index.ts ├── interface.ts └── buildLib │ ├── index.ts │ ├── buildUmd.ts │ └── build.ts ├── example ├── components │ ├── _util │ │ └── index.ts │ ├── card │ │ ├── index.ts │ │ └── card.tsx │ ├── style │ │ ├── app.less │ │ ├── index.less │ │ ├── variables │ │ │ └── base.less │ │ ├── themes │ │ │ ├── card.less │ │ │ ├── button.less │ │ │ └── input.less │ │ └── reset.less │ ├── input │ │ ├── index.ts │ │ └── input.tsx │ ├── button │ │ ├── index.ts │ │ └── button.tsx │ └── index.ts ├── src │ ├── index.less │ ├── index.tsx │ ├── interface.tsx │ └── toast.store.ts ├── .babelrc ├── __tests__ │ └── build.spec.ts ├── public │ ├── index.html │ └── snowman.svg ├── .editorconfig ├── .gitignore ├── tsconfig.json ├── mx.config.js └── package.json ├── .prettierrc ├── .editorconfig ├── babel.config.js ├── scripts └── release.ts ├── jest.config.js ├── tsconfig.json ├── .vscode └── settings.json ├── package.json ├── .eslintrc.js ├── .gitignore ├── README.zh.md └── README.md /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require("../lib/index"); 4 | -------------------------------------------------------------------------------- /src/config/webpackConfig/rules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getRule'; 2 | -------------------------------------------------------------------------------- /example/components/_util/index.ts: -------------------------------------------------------------------------------- 1 | const PREFIX = "test"; 2 | 3 | export { PREFIX }; 4 | -------------------------------------------------------------------------------- /example/components/card/index.ts: -------------------------------------------------------------------------------- 1 | import Card from "./card"; 2 | 3 | export default Card; 4 | -------------------------------------------------------------------------------- /example/components/style/app.less: -------------------------------------------------------------------------------- 1 | @import "reset.less"; 2 | @import "variables/base.less"; -------------------------------------------------------------------------------- /example/components/input/index.ts: -------------------------------------------------------------------------------- 1 | import Input from "./input"; 2 | 3 | export default Input; 4 | -------------------------------------------------------------------------------- /example/components/button/index.ts: -------------------------------------------------------------------------------- 1 | import Button from "./button"; 2 | 3 | export default Button; 4 | -------------------------------------------------------------------------------- /example/src/index.less: -------------------------------------------------------------------------------- 1 | .test { 2 | text-align: center; 3 | h1 { 4 | color: orange; 5 | font-size: 129px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/config/webpackConfig/index.ts: -------------------------------------------------------------------------------- 1 | import { getWebpackConfig } from './getWebpackConfig'; 2 | 3 | export default getWebpackConfig; 4 | -------------------------------------------------------------------------------- /example/components/style/index.less: -------------------------------------------------------------------------------- 1 | @import "./themes/button.less"; 2 | @import "./themes/card.less"; 3 | @import "./themes/input.less"; 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "jsxSingleQuote": false, 5 | "useTabs": false, 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /example/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react"], 3 | "plugins": ["@babel/plugin-syntax-jsx", "@babel/plugin-proposal-class-properties"] 4 | } -------------------------------------------------------------------------------- /src/config/gulpConfig/getNewEntryDir.ts: -------------------------------------------------------------------------------- 1 | export const getNewEntryDir = (entryDir) => 2 | entryDir?.[entryDir.length - 1] === '/' 3 | ? entryDir.slice(0, entryDir.length - 1) 4 | : entryDir; 5 | -------------------------------------------------------------------------------- /example/components/index.ts: -------------------------------------------------------------------------------- 1 | import './style/index.less'; 2 | 3 | export { default as Button } from './button'; 4 | export { default as Card } from './card'; 5 | export { default as Input } from './input'; 6 | -------------------------------------------------------------------------------- /example/__tests__/build.spec.ts: -------------------------------------------------------------------------------- 1 | describe("buildSite", () => { 2 | describe("exec mx build", () => { 3 | it("should toBeCalled buildSite once", () => { 4 | expect(1).toBe(2); 5 | }); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getCustomConfig'; 2 | export * from './getProjectConfig'; 3 | export * from './isAddForkTsPlugin'; 4 | export * from './syncChainFns'; 5 | export * from './compose'; 6 | export * from './after'; 7 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | require.resolve('@babel/preset-env'), 4 | require.resolve('@babel/preset-typescript'), 5 | ], 6 | plugins: [require.resolve('@babel/plugin-transform-runtime')], 7 | }; 8 | -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | zswui | <%= htmlWebpackPlugin.options.title %> 6 | 7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /src/config/gulpConfig/index.ts: -------------------------------------------------------------------------------- 1 | import { buildCjs } from './buildCjs'; 2 | import { buildEsm } from './buildEsm'; 3 | import { copyLessMid } from './copyLess'; 4 | import { less2css } from './less2css'; 5 | 6 | export { copyLessMid, less2css, buildCjs, buildEsm }; 7 | -------------------------------------------------------------------------------- /example/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false -------------------------------------------------------------------------------- /src/utils/syncChainFns.ts: -------------------------------------------------------------------------------- 1 | // Synchronous function chain 2 | export const syncChainFns = (...fns) => { 3 | const [firstFn, ...otherFns] = fns; 4 | return (...args) => { 5 | if (!otherFns) return firstFn(...args); 6 | return otherFns.reduce((ret, task) => task(ret), firstFn(...args)); 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /example/components/style/variables/base.less: -------------------------------------------------------------------------------- 1 | /* less 样式基础变量 */ 2 | @zswui-prefix: zsw; // class 前缀 3 | @borderRadius: 2px; // 边框角 4 | @font-family: "Helvetica, Microsoft YaHei, Arial, sans-serif"; // 字体 5 | @font-size: 14px; // 字体大小 6 | @text-color: #333; // 文字颜色 7 | @line-height: 1.5715; // 行高 8 | @text-indent: 2px; // 输入框锁进 9 | -------------------------------------------------------------------------------- /src/utils/after.ts: -------------------------------------------------------------------------------- 1 | import type { AnyFunction } from '../interface'; 2 | 3 | export function after( 4 | fn: AnyFunction, 5 | afterfun: AnyFunction | undefined 6 | ): (...args: any[]) => T { 7 | return (...args) => { 8 | const ret = fn(args); 9 | afterfun?.(ret); 10 | return ret; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/config/gulpConfig/buildCjs.ts: -------------------------------------------------------------------------------- 1 | import { getNewEntryDir } from './getNewEntryDir'; 2 | import { compileScripts } from './compileScripts'; 3 | 4 | export const buildCjs = async ({ mode, outDirCjs, entryDir }) => { 5 | const newEntryDir = getNewEntryDir(entryDir); 6 | await compileScripts(mode, outDirCjs, newEntryDir); 7 | }; 8 | -------------------------------------------------------------------------------- /src/config/gulpConfig/buildEsm.ts: -------------------------------------------------------------------------------- 1 | import { compileScripts } from './compileScripts'; 2 | import { getNewEntryDir } from './getNewEntryDir'; 3 | 4 | export const buildEsm = async ({ mode, outDirEsm, entryDir }) => { 5 | const newEntryDir = getNewEntryDir(entryDir); 6 | await compileScripts(mode, outDirEsm, newEntryDir); 7 | }; 8 | -------------------------------------------------------------------------------- /scripts/release.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getNextVersion, 3 | gitPush, 4 | build, 5 | publishNpm, 6 | updateVersion, 7 | compose, 8 | eslint, 9 | } from '@mx-design/release'; 10 | 11 | const middle = [ 12 | eslint(), 13 | getNextVersion, 14 | updateVersion, 15 | gitPush, 16 | build, 17 | publishNpm, 18 | ]; 19 | 20 | compose(middle); 21 | -------------------------------------------------------------------------------- /src/utils/compose.ts: -------------------------------------------------------------------------------- 1 | export function compose(middleware, initOptions) { 2 | const otherOptions = initOptions || {}; 3 | function dispatch(index) { 4 | if (index === middleware.length) return; 5 | const currMiddleware = middleware[index]; 6 | return currMiddleware(() => dispatch(++index), otherOptions); 7 | } 8 | dispatch(0); 9 | } 10 | -------------------------------------------------------------------------------- /src/config/webpackConfig/rules/getFontRule.ts: -------------------------------------------------------------------------------- 1 | import { after } from '../../../utils/after'; 2 | import type { AnyFunction } from '../../../interface'; 3 | 4 | export const getFontRule = (afterFn?: AnyFunction) => 5 | after(function _() { 6 | return { 7 | test: /\.(eot|ttf|woff|woff2?)$/, 8 | type: 'asset/resource', 9 | }; 10 | }, afterFn); 11 | -------------------------------------------------------------------------------- /src/config/webpackConfig/rules/getSvgRule.ts: -------------------------------------------------------------------------------- 1 | import { after } from '../../../utils/after'; 2 | import type { AnyFunction } from '../../../interface'; 3 | 4 | export const getSvgRule = (afterFn?: AnyFunction) => 5 | after(function _() { 6 | return { 7 | test: /\.svg$/, 8 | use: ['@svgr/webpack'], 9 | exclude: /node_modules/, 10 | }; 11 | }, afterFn); 12 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const pkg = require('./package.json'); 2 | 3 | module.exports = { 4 | preset: 'ts-jest', 5 | displayName: { 6 | name: pkg.name, 7 | color: 'blue', 8 | }, 9 | testEnvironment: 'node', 10 | testPathIgnorePatterns: ['/lib/', '/node_modules/'], 11 | testMatch: ['/src/**/__tests__/**/*.[jt]s?(x)'], 12 | verbose: true, 13 | }; 14 | -------------------------------------------------------------------------------- /src/utils/isAddForkTsPlugin.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; 3 | import { getProjectPath } from '@mx-design/node-utils'; 4 | 5 | export const isAddForkTsPlugin = (config) => { 6 | if (fs.existsSync(getProjectPath('tsconfig.json'))) { 7 | config.plugins.push(new ForkTsCheckerWebpackPlugin()); 8 | } 9 | return config; 10 | }; 11 | -------------------------------------------------------------------------------- /src/buildSite/index.ts: -------------------------------------------------------------------------------- 1 | import build from './buildSite'; 2 | import { BUILD_SITE } from '../constants'; 3 | 4 | export const buildSite = (commander) => { 5 | // 打包组件展示网站的命令,这个命令还可以用在普通业务中 6 | // 这个命令实际上执行的是deploy这个文件 7 | commander 8 | .command(BUILD_SITE) 9 | .description('部署官网站点') 10 | .option('-d, --out-dir ', '输出目录', 'dist') 11 | .option('-a, --analyzer', '是否启用分析器') 12 | .action(build); 13 | }; 14 | -------------------------------------------------------------------------------- /src/utils/getCustomConfig.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import { CustomConfig } from '../interface'; 4 | 5 | /** 6 | * Get the project files 7 | * */ 8 | export const getCustomConfig = ( 9 | configFileName = 'mx.config.js' 10 | ): Partial => { 11 | const configPath = path.join(process.cwd(), configFileName); 12 | if (fs.existsSync(configPath)) { 13 | return require(configPath); 14 | } 15 | return {}; 16 | }; 17 | -------------------------------------------------------------------------------- /src/dev/index.ts: -------------------------------------------------------------------------------- 1 | import development from './development'; 2 | import { DEV } from '../constants'; 3 | 4 | export const runDev = (commander) => { 5 | // 当你输入mx build的时候,就是执行这个命令,这个命令还可以用在普通业务中 6 | // 这个命令实际上执行的是development这个文件 7 | commander 8 | .command(DEV) 9 | .description('运行开发环境') 10 | .option('-h, --host ', '站点主机地址', 'localhost') 11 | // 默认端口号3000 12 | .option('-p, --port ', '站点端口号', '3000') 13 | .action(development); 14 | }; 15 | -------------------------------------------------------------------------------- /example/components/input/input.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | class Input extends Component { 4 | props: { onChange: any }; 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | handleChange = () => { 10 | const { onChange } = this.props; 11 | if (onChange) { 12 | onChange(); 13 | } 14 | }; 15 | 16 | render() { 17 | return ; 18 | } 19 | } 20 | 21 | export default Input; 22 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEV = 'dev'; 2 | export const BUILD_SITE = 'buildSite'; 3 | export const BUILD_LIB = 'buildLib'; 4 | export const TEST = 'test'; 5 | export const isDev = (env) => env === DEV; 6 | export const basePath = 'src'; 7 | export const ESM = 'esm'; 8 | export const CJS = 'cjs'; 9 | export const LIB = 'lib'; 10 | export const UMD = 'umd'; 11 | export const UMD_UGLY = 'umdUgly'; 12 | export const LESS_2_CSS = 'less2Css'; 13 | export const COPY_LESS = 'copyLess'; 14 | -------------------------------------------------------------------------------- /src/config/webpackConfig/rules/getImageRule.ts: -------------------------------------------------------------------------------- 1 | import { after } from '../../../utils/after'; 2 | import type { AnyFunction } from '../../../interface'; 3 | 4 | export const getImageRule = (afterFn?: AnyFunction) => 5 | after(function _() { 6 | return { 7 | test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], 8 | type: 'asset', 9 | parser: { 10 | dataUrlCondition: { 11 | maxSize: 4 * 1024, 12 | }, 13 | }, 14 | }; 15 | }, afterFn); 16 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | yarn.lock 8 | /lib 9 | /esm 10 | /dist 11 | /types 12 | 13 | # testing 14 | /coverage 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | .umi 31 | -------------------------------------------------------------------------------- /example/components/button/button.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class Button extends Component { 4 | props: any; 5 | 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | handleClick = () => { 11 | const { onClick } = this.props; 12 | if (onClick) { 13 | onClick(); 14 | } 15 | }; 16 | 17 | render() { 18 | return ; 19 | } 20 | } 21 | 22 | export default Button; 23 | -------------------------------------------------------------------------------- /src/config/babelConfig/lib.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | presets: [ 3 | require.resolve('@babel/preset-env'), 4 | require.resolve('@babel/preset-react'), 5 | require.resolve('@babel/preset-typescript'), 6 | ], 7 | plugins: [ 8 | require.resolve('@babel/plugin-transform-runtime'), 9 | [require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }], 10 | [require.resolve('@babel/plugin-proposal-class-properties')], 11 | require.resolve('@babel/plugin-proposal-optional-chaining'), 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.less'; 4 | import { Button, Card, Input } from '../components/index'; 5 | 6 | ReactDOM.render( 7 |
8 |

Hello, Boy!

9 | 12 |

13 | console.log(1)} /> 14 |

15 | 16 |

17 | test 18 |

19 |
, 20 | document.getElementById('root') 21 | ); 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "target": "ES2019", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "importHelpers": true, 8 | "jsx": "react", 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true, 11 | "sourceMap": true, 12 | "allowSyntheticDefaultImports": true, 13 | "skipLibCheck": true, 14 | "types": ["webpack", "node", "jest"] 15 | }, 16 | "include": ["src", "webpack.config.js"], 17 | "exclude": ["node_modules", "lib", "es", "**/__tests__/**", "tests"] 18 | } 19 | -------------------------------------------------------------------------------- /src/config/gulpConfig/cssInjection.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 当前组件样式 import './index.less' => import './index.css' 3 | * 依赖的其他组件样式 import '../test-comp/style' => import '../test-comp/style/css.js' 4 | * 依赖的其他组件样式 import '../test-comp/style/index.js' => import '../test-comp/style/css.js' 5 | * @param {string} content 6 | */ 7 | export function cssInjection(content) { 8 | return ( 9 | content 10 | // eslint-disable-next-line quotes 11 | .replace(/\/style\/?'/g, "/style/css'") 12 | .replace(/\/style\/?"/g, '/style/css"') 13 | .replace(/\.less/g, '.css') 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/config/webpackConfig/rules/getJsTsRule.ts: -------------------------------------------------------------------------------- 1 | import type { AnyFunction } from '../../../interface'; 2 | import babelConfig from '../../babelConfig/es'; 3 | import { after } from '../../../utils/after'; 4 | 5 | export const getJsTsRule = (afterFn?: AnyFunction) => 6 | after(function _() { 7 | return { 8 | test: /\.(js|jsx|ts|tsx)$/, 9 | exclude: /node_modules/, 10 | use: [ 11 | 'thread-loader', 12 | { 13 | loader: require.resolve('babel-loader'), 14 | options: babelConfig, 15 | }, 16 | ], 17 | }; 18 | }, afterFn); 19 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "target": "ES2019", 5 | "module": "es6", 6 | "moduleResolution": "node", 7 | "lib": ["es5", "dom"], 8 | "importHelpers": true, 9 | "jsx": "react", 10 | "resolveJsonModule": true, 11 | "sourceMap": true, 12 | "allowSyntheticDefaultImports": true, 13 | "skipLibCheck": true, 14 | "types": ["webpack", "node", "jest"] 15 | }, 16 | "include": ["src", "components"], 17 | "exclude": ["node_modules", "lib", "es", "**/__tests__/**", "tests"] 18 | } 19 | -------------------------------------------------------------------------------- /example/components/style/themes/card.less: -------------------------------------------------------------------------------- 1 | @import "../app.less"; 2 | 3 | .@{zswui-prefix}-card{ 4 | color: @text-color; 5 | font-family: @font-family; 6 | font-size: @font-size; 7 | border-radius: @borderRadius; 8 | line-height: @line-height; 9 | outline: none; 10 | padding: 16px 20px; 11 | min-height: 200px; 12 | width: 300px; 13 | margin: 0 auto; 14 | border: 1px solid #d9d9d9; 15 | background-color: #fff; 16 | } 17 | 18 | .@{zswui-prefix}-card-default{ 19 | color: @text-color; 20 | } 21 | 22 | .@{zswui-prefix}-card-primary{ 23 | color: @text-color; 24 | border: 1px solid #2E94B9; 25 | } -------------------------------------------------------------------------------- /example/components/card/card.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import classNames from "classnames"; 3 | import { PREFIX } from "../_util"; 4 | 5 | console.log("card"); 6 | 7 | class Card extends Component { 8 | props: { onChange: any; children: any }; 9 | constructor(props) { 10 | super(props); 11 | } 12 | 13 | handleChange = () => { 14 | const { onChange } = this.props; 15 | if (onChange) { 16 | onChange(); 17 | } 18 | }; 19 | 20 | render() { 21 | const cls = classNames(`${PREFIX}-card`); 22 | 23 | return
{this.props.children}
; 24 | } 25 | } 26 | 27 | export default Card; 28 | -------------------------------------------------------------------------------- /src/config/babelConfig/es.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | presets: [ 3 | [ 4 | require.resolve('@babel/preset-env'), 5 | { 6 | modules: false, 7 | }, 8 | ], 9 | require.resolve('@babel/preset-react'), 10 | require.resolve('@babel/preset-typescript'), 11 | ], 12 | plugins: [ 13 | [ 14 | require.resolve('@babel/plugin-transform-runtime'), 15 | { useESModules: true }, 16 | ], 17 | [require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }], 18 | [require.resolve('@babel/plugin-proposal-class-properties')], 19 | require.resolve('@babel/plugin-proposal-optional-chaining'), 20 | ], 21 | }; 22 | -------------------------------------------------------------------------------- /src/config/gulpConfig/constants.ts: -------------------------------------------------------------------------------- 1 | import { getProjectPath } from '@mx-design/node-utils'; 2 | import { ESM, LIB } from '../../constants'; 3 | 4 | export const paths = { 5 | dest: { 6 | lib: getProjectPath(LIB), 7 | esm: getProjectPath(ESM), 8 | }, 9 | styles: (path) => getProjectPath(`${path}/**/*.less`), 10 | scripts: (path) => [ 11 | getProjectPath(`${path}/**/*.{ts,tsx,js,jsx}`), 12 | getProjectPath(`!${path}/**/__tests__/*.{ts,tsx,js,jsx}`), 13 | ], 14 | }; 15 | 16 | export const indexToCssReg = 17 | /((\/|\\)style(\/|\\)index\.js|(\/|\\)color_style(\/|\\)index\.js|(\/|\\)token_style(\/|\\)index\.js|(\/|\\)base_style(\/|\\)index\.js)/; 18 | -------------------------------------------------------------------------------- /src/config/gulpConfig/copyLess.ts: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import { getNewEntryDir } from './getNewEntryDir'; 3 | import { CJS, ESM } from '../../constants'; 4 | import { paths } from './constants'; 5 | 6 | export const copyLessMid = async ({ entryDir, outDirCjs, outDirEsm, mode }) => { 7 | return new Promise((resolve, reject) => { 8 | const newEntryDir = getNewEntryDir(entryDir); 9 | const source = gulp.src(paths.styles(newEntryDir)); 10 | if (mode === CJS) { 11 | source.pipe(gulp.dest(outDirCjs)); 12 | } 13 | if (mode === ESM) { 14 | source.pipe(gulp.dest(outDirEsm)); 15 | } 16 | source.on('end', resolve); 17 | source.on('error', reject); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /example/components/style/themes/button.less: -------------------------------------------------------------------------------- 1 | @import "../app.less"; 2 | 3 | .@{zswui-prefix}-btn{ 4 | cursor: pointer; 5 | display: inline-block; 6 | color: @text-color; 7 | font-family: @font-family; 8 | font-size: @font-size; 9 | border-radius: @borderRadius; 10 | line-height: @line-height; 11 | outline: none; 12 | border: none; 13 | text-align: center; 14 | padding: 4px 15px; 15 | background-color: #f5f5f5; 16 | border: 1px solid #d9d9d9; 17 | &:disabled{ 18 | cursor: not-allowed; 19 | color: rgba(0, 0, 0, 0.25); 20 | } 21 | } 22 | 23 | .@{zswui-prefix}-btn-default{ 24 | color: @font-size; 25 | } 26 | 27 | .@{zswui-prefix}-btn-primary{ 28 | color: #fff; 29 | background-color: #2E94B9; 30 | border-color: #2E94B9; 31 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import commander from 'commander'; 2 | 3 | import { buildLib } from './buildLib/index'; 4 | import { buildSite } from './buildSite/index'; 5 | import { runDev } from './dev/index'; 6 | import { version } from '../package.json'; 7 | 8 | commander.version(version, '-v, --version'); 9 | 10 | buildLib(commander); 11 | buildSite(commander); 12 | runDev(commander); 13 | 14 | /** 15 | * @zh commander解析命令行参数 16 | * @en parse the command-line arguments by commander 17 | */ 18 | commander.parse(process.argv); 19 | 20 | /** 21 | * @zh 如果命令行没有参数如执行mx,则会显示帮助文档 22 | * @en if there are no arguments to the command line, such as executing mx, the help documentation will be shown 23 | */ 24 | if (!commander.args[0]) { 25 | commander.help(); 26 | } 27 | -------------------------------------------------------------------------------- /example/components/style/themes/input.less: -------------------------------------------------------------------------------- 1 | @import "../app.less"; 2 | 3 | .@{zswui-prefix}-input{ 4 | cursor: text; 5 | display: inline-block; 6 | color: @text-color; 7 | font-family: @font-family; 8 | font-size: @font-size; 9 | border-radius: @borderRadius; 10 | line-height: @line-height; 11 | outline: none; 12 | padding: 4px 15px; 13 | border: 1px solid #d9d9d9; 14 | background-color: #fff; 15 | transition: border 0.3s ease-in; 16 | &:hover{ 17 | border: 1px solid #2E94B9; 18 | } 19 | &:disabled{ 20 | cursor: not-allowed; 21 | color: rgba(0, 0, 0, 0.25); 22 | } 23 | } 24 | 25 | 26 | .@{zswui-prefix}-input-default{ 27 | color: @text-color; 28 | } 29 | 30 | .@{zswui-prefix}-input-primary{ 31 | color: @text-color; 32 | border: 1px solid #2E94B9; 33 | } -------------------------------------------------------------------------------- /src/config/gulpConfig/less2css.ts: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import autoprefixer from 'gulp-autoprefixer'; 3 | import less from 'gulp-less'; 4 | import { getNewEntryDir } from './getNewEntryDir'; 5 | import { paths } from './constants'; 6 | import { CJS, ESM } from '../../constants'; 7 | 8 | export const less2css = async ({ entryDir, outDirCjs, outDirEsm, mode }) => { 9 | return new Promise((resolve, reject) => { 10 | const newEntryDir = getNewEntryDir(entryDir); 11 | 12 | const source = gulp 13 | .src(paths.styles(newEntryDir)) 14 | .pipe(less()) 15 | .pipe(autoprefixer()); 16 | 17 | if (mode === CJS) { 18 | source.pipe(gulp.dest(outDirCjs)); 19 | } 20 | if (mode === ESM) { 21 | source.pipe(gulp.dest(outDirEsm)); 22 | } 23 | source.on('end', resolve); 24 | source.on('error', reject); 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /src/config/webpackConfig/getWebpackConfig.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from 'webpack'; 2 | import { DEV, BUILD_LIB, BUILD_SITE, UMD, UMD_UGLY } from '../../constants'; 3 | import { IWebpackConfigType } from '../../interface'; 4 | import { getDevConfig } from './getDevConfig'; 5 | import { getBuildConfig } from './getBuildConfig'; 6 | import { getUmdConfig } from './getUmdConfig'; 7 | 8 | export const getWebpackConfig = (type?: IWebpackConfigType): Configuration => { 9 | switch (type) { 10 | case DEV: 11 | return getDevConfig(); 12 | 13 | case BUILD_SITE: 14 | return getBuildConfig(); 15 | 16 | case BUILD_LIB: 17 | return getBuildConfig(); 18 | 19 | case UMD: 20 | return getUmdConfig(false); 21 | 22 | case UMD_UGLY: 23 | return getUmdConfig(true); 24 | 25 | default: 26 | return getDevConfig(); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/config/webpackConfig/rules/getCssRule.ts: -------------------------------------------------------------------------------- 1 | import type { AnyFunction } from '../../../interface'; 2 | import { after } from '../../../utils/after'; 3 | 4 | export const getCssRule = (afterFn?: AnyFunction) => 5 | after(function _() { 6 | return { 7 | test: /\.(css)$/, 8 | use: [ 9 | { 10 | loader: 'css-loader', 11 | options: { 12 | importLoaders: 1, 13 | }, 14 | }, 15 | { 16 | loader: 'postcss-loader', 17 | options: { 18 | plugins: [ 19 | require('postcss-flexbugs-fixes'), 20 | require('postcss-preset-env')({ 21 | autoprefixer: { 22 | flexbox: 'no-2009', 23 | }, 24 | stage: 3, 25 | }), 26 | ], 27 | }, 28 | }, 29 | ], 30 | }; 31 | }, afterFn); 32 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "css.validate": false, 3 | "less.validate": false, 4 | "scss.validate": false, 5 | 6 | "eslint.validate": [ 7 | "javascript", 8 | "javascriptreact", 9 | "typescript", 10 | "typescriptreact" 11 | ], 12 | "typescript.tsdk": "./node_modules/typescript/lib", 13 | 14 | "search.exclude": { 15 | "**/node_modules": true, 16 | "dist": true, 17 | "build": true 18 | }, 19 | 20 | "editor.formatOnSave": true, 21 | "editor.codeActionsOnSave": { 22 | "source.fixAll.eslint": "explicit", 23 | "source.fixAll.stylelint": "explicit" 24 | }, 25 | "[javascript]": { 26 | "editor.defaultFormatter": "esbenp.prettier-vscode" 27 | }, 28 | "[javascriptreact]": { 29 | "editor.defaultFormatter": "esbenp.prettier-vscode" 30 | }, 31 | "[typescript]": { 32 | "editor.defaultFormatter": "esbenp.prettier-vscode" 33 | }, 34 | "[typescriptreact]": { 35 | "editor.defaultFormatter": "esbenp.prettier-vscode" 36 | }, 37 | "[json]": { 38 | "editor.defaultFormatter": "esbenp.prettier-vscode" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/config/webpackConfig/rules/getRule.ts: -------------------------------------------------------------------------------- 1 | import { getCssRule } from './getCssRule'; 2 | import { getFontRule } from './getFontRule'; 3 | import { getImageRule } from './getImageRule'; 4 | import { getJsTsRule } from './getJsTsRule'; 5 | import { getLessRule } from './getLessRule'; 6 | import { getSvgRule } from './getSvgRule'; 7 | import type { AnyFunction } from '../../../interface'; 8 | 9 | type IProps = { 10 | afterJsTsRule?: AnyFunction; 11 | afterSvgRule?: AnyFunction; 12 | afterCssRule?: AnyFunction; 13 | afterLessRule?: AnyFunction; 14 | afterImageRule?: AnyFunction; 15 | afterFontRule?: AnyFunction; 16 | options?: Record; 17 | }; 18 | 19 | export function getRule({ 20 | afterJsTsRule, 21 | afterSvgRule, 22 | afterCssRule, 23 | afterLessRule, 24 | afterImageRule, 25 | afterFontRule, 26 | options, 27 | }: IProps) { 28 | const result = []; 29 | result.push(getJsTsRule(afterJsTsRule)()); 30 | result.push(getLessRule(afterLessRule)(options)); 31 | result.push(getCssRule(afterCssRule)()); 32 | result.push(getImageRule(afterImageRule)()); 33 | result.push(getFontRule(afterFontRule)()); 34 | result.push(getSvgRule(afterSvgRule)()); 35 | return result; 36 | } 37 | -------------------------------------------------------------------------------- /src/interface.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import { Configuration } from 'webpack'; 3 | import { BUILD_LIB, BUILD_SITE, DEV, UMD, UMD_UGLY } from './constants'; 4 | 5 | export interface IDevelopmentConfig { 6 | host: string; 7 | port: number; 8 | } 9 | 10 | export type IWebpackConfigType = 11 | | typeof BUILD_LIB 12 | | typeof DEV 13 | | typeof BUILD_SITE 14 | | typeof UMD 15 | | typeof UMD_UGLY; 16 | 17 | export interface IDeployConfig { 18 | outDir: string; 19 | pushGh: boolean; 20 | analyzer: boolean; 21 | } 22 | 23 | export interface ITestConfig { 24 | updateSnapshot: boolean; 25 | coverage: boolean; 26 | setupFilesAfterEnv: string; 27 | watch?: boolean; 28 | } 29 | 30 | export interface CustomConfig extends Configuration { 31 | entries: object; 32 | banner: string; 33 | setBabelOptions: (options: string | { [index: string]: any }) => void; 34 | setRules: (rules: Configuration['module']['rules']) => void; 35 | setPlugins: (plugins: Configuration['plugins']) => void; 36 | setDevOptions: Record; 37 | setOutput: (outputConfig: any) => void; 38 | setConfig: (config: any) => void; 39 | } 40 | 41 | export type AnyFunction = (...args: any[]) => any; 42 | -------------------------------------------------------------------------------- /src/config/webpackConfig/rules/getLessRule.ts: -------------------------------------------------------------------------------- 1 | import { after } from '../../../utils/after'; 2 | import type { AnyFunction } from '../../../interface'; 3 | 4 | export const getLessRule = (afterFn?: AnyFunction) => 5 | after(function _() { 6 | return { 7 | test: /\.(less)$/, 8 | use: [ 9 | { 10 | loader: 'css-loader', 11 | options: { 12 | importLoaders: 2, 13 | modules: { 14 | localIdentName: '[name]__[local]--[hash:base64:5]', 15 | auto: (resourcePath) => resourcePath.endsWith('.module.less'), 16 | }, 17 | }, 18 | }, 19 | { 20 | loader: 'postcss-loader', 21 | options: { 22 | plugins: [ 23 | require('postcss-flexbugs-fixes'), 24 | require('postcss-preset-env')({ 25 | autoprefixer: { 26 | flexbox: 'no-2009', 27 | }, 28 | stage: 3, 29 | }), 30 | ], 31 | }, 32 | }, 33 | { 34 | loader: 'less-loader', 35 | options: { 36 | lessOptions: { 37 | javascriptEnabled: true, 38 | }, 39 | }, 40 | }, 41 | ], 42 | }; 43 | }, afterFn); 44 | -------------------------------------------------------------------------------- /src/config/gulpConfig/compileScripts.ts: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import babel from 'gulp-babel'; 3 | import through2 from 'through2'; 4 | import { indexToCssReg, paths } from './constants'; 5 | import babelEsConfig from '../babelConfig/es'; 6 | import babelCjsConfig from '../babelConfig/lib'; 7 | import { ESM } from '../../constants'; 8 | import { cssInjection } from './cssInjection'; 9 | 10 | export async function compileScripts(mode, destDir, newEntryDir) { 11 | return new Promise((resolve, reject) => { 12 | const { scripts } = paths; 13 | const source = gulp 14 | .src(scripts(newEntryDir)) 15 | .pipe(babel(mode === ESM ? babelEsConfig : babelCjsConfig)) 16 | .pipe( 17 | through2.obj(function z(file, encoding, next) { 18 | this.push(file.clone()); 19 | if (file.path.match(indexToCssReg)) { 20 | const content = file.contents.toString(encoding); 21 | file.contents = Buffer.from(cssInjection(content)); 22 | file.path = file.path.replace(/index\.js/, 'css.js'); 23 | this.push(file); 24 | next(); 25 | } else { 26 | next(); 27 | } 28 | }) 29 | ) 30 | .pipe(gulp.dest(destDir)); 31 | source.on('end', resolve); 32 | source.on('error', reject); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/config/webpackConfig/getDevConfig.ts: -------------------------------------------------------------------------------- 1 | import ReactRefreshPlugin from '@pmmmwh/react-refresh-webpack-plugin'; 2 | import webpackMerge from 'webpack-merge'; 3 | import webpack, { Configuration, RuleSetUseItem } from 'webpack'; 4 | import { getBaseConfig } from './baseConfig'; 5 | import { getRule } from './rules'; 6 | 7 | export const getDevConfig = (): Configuration => { 8 | const getDevRule = getRule({ 9 | afterJsTsRule: (rule) => { 10 | rule.use[1].options.plugins.push(require.resolve('react-refresh/babel')); 11 | }, 12 | afterLessRule: (rule) => { 13 | (rule.use as RuleSetUseItem[]).unshift('style-loader'); 14 | }, 15 | afterCssRule: (rule) => { 16 | (rule.use as RuleSetUseItem[]).unshift('style-loader'); 17 | }, 18 | }); 19 | 20 | const config = webpackMerge({}, getBaseConfig(getDevRule, true), { 21 | mode: 'development', 22 | devtool: 'source-map', 23 | output: { 24 | publicPath: '/', 25 | }, 26 | plugins: [ 27 | new webpack.HotModuleReplacementPlugin(), 28 | new ReactRefreshPlugin(), 29 | ], 30 | optimization: { 31 | minimize: false, 32 | }, 33 | cache: { 34 | type: 'filesystem', 35 | buildDependencies: { 36 | config: [__filename], 37 | }, 38 | }, 39 | }); 40 | 41 | return config; 42 | }; 43 | -------------------------------------------------------------------------------- /example/mx.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | entries: { 5 | index: { 6 | entry: ["./src/index.tsx"], 7 | template: "./public/index.html", 8 | // favicon: './favicon.ico', 9 | }, 10 | // demo: { 11 | // entry: ['./demo/index.js'], 12 | // template: './demo/index.html', 13 | // favicon: './favicon.ico', 14 | // }, 15 | // demo_umd: { 16 | // template: './demo/index_umd.html', 17 | // favicon: './favicon.ico', 18 | // }, 19 | }, 20 | // resolve: { 21 | // alias: { 22 | // '@': path.join(process.cwd(), '/'), 23 | // '@zarmDir': path.join(process.cwd(), '../zarm'), 24 | // zarm: path.join(process.cwd(), '../zarm/src'), 25 | // }, 26 | // }, 27 | // setBabelOptions: (options) => { 28 | // options.plugins.push(['import', { libraryName: 'zarm-web', style: 'css' }, 'zarm-web']); 29 | // options.plugins.push([ 30 | // 'prismjs', 31 | // { 32 | // languages: ['javascript', 'typescript', 'jsx', 'tsx', 'css', 'scss', 'markup', 'bash'], 33 | // theme: 'default', 34 | // css: true, 35 | // }, 36 | // ]); 37 | // }, 38 | // setRules: (rules) => { 39 | // rules.push({ 40 | // test: /\.md$/, 41 | // use: ['raw-loader'], 42 | // }); 43 | // }, 44 | }; 45 | -------------------------------------------------------------------------------- /src/buildLib/index.ts: -------------------------------------------------------------------------------- 1 | import build from './build'; 2 | import { BUILD_LIB } from '../constants'; 3 | 4 | export const buildLib = (commander) => { 5 | /** 6 | * @zh 当你输入mx buildLib的时候,就是执行这个命令 7 | * 这个命令实际上执行的是build文件 8 | * 我们会打包es和commonjs规范的两个包 9 | * @en when you input mx buildLib, command will execute 10 | * This command actually executes the build file 11 | * We will package two packages of es and commonjs specifications 12 | */ 13 | commander 14 | .command(BUILD_LIB) 15 | .description('打包编译仓库(Package and compile project)') 16 | .option( 17 | '-a, --analyzerUmd', 18 | '是否启用webpack打包分析器(Whether to enable the webpack packaging analyzer)' 19 | ) 20 | .option( 21 | '-e, --entry ', 22 | 'umd打包路径入口文件(umd mode packaging path entry file)', 23 | './src/index' 24 | ) 25 | .option('--output-name ', '打包Umd格式后对外暴露的名称') 26 | .option('--entry-dir ', 'cjs和esm打包路径入口目录', './src') 27 | .option('--out-dir-umd ', '输出umd格式的目录', './dist') 28 | .option('--out-dir-esm ', '输出esm格式的目录', './esm') 29 | .option('--out-dir-cjs ', '输出cjs格式的目录', './lib') 30 | .option('--copy-less', '拷贝不参与编译的文件') 31 | .option('--less-2-css', '是否编译组件样式') 32 | .option( 33 | '-m, --mode ', 34 | '打包模式 目前支持umd、esm和cjs' 35 | ) 36 | .action(build); 37 | }; 38 | -------------------------------------------------------------------------------- /src/buildSite/buildSite.ts: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 3 | import { getProjectPath } from '@mx-design/node-utils'; 4 | import { log } from '@mx-design/web-utils'; 5 | import getWebpackConfig from '../config/webpackConfig'; 6 | import { getProjectConfig, syncChainFns, isAddForkTsPlugin } from '../utils'; 7 | import { IDeployConfig } from '../interface'; 8 | import { BUILD_SITE } from '../constants'; 9 | 10 | export default ({ outDir, analyzer }: IDeployConfig) => { 11 | const config = syncChainFns( 12 | getWebpackConfig, 13 | getProjectConfig, 14 | isAddForkTsPlugin 15 | )(BUILD_SITE); 16 | config.output.path = config.output.path || getProjectPath(outDir); 17 | 18 | if (analyzer) { 19 | config.plugins.push( 20 | new BundleAnalyzerPlugin({ 21 | analyzerMode: 'static', 22 | generateStatsFile: true, 23 | }) 24 | ); 25 | } 26 | 27 | webpack(config).run((err, stats) => { 28 | if (err) { 29 | return log.error('get error from webpack compiler, full error:', err); 30 | } 31 | const info = stats.toJson({ 32 | all: false, 33 | errors: true, 34 | }); 35 | if (info.errors && info.errors.length) { 36 | log.error( 37 | 'get error from webpack status, full error:', 38 | JSON.stringify(info) 39 | ); 40 | } 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /src/utils/getProjectConfig.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, RuleSetRule } from 'webpack'; 2 | import webpackMerge from 'webpack-merge'; 3 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 4 | import { getCustomConfig } from './getCustomConfig'; 5 | 6 | export function getProjectConfig(config: Configuration): Configuration { 7 | const { 8 | entries, 9 | setBabelOptions, 10 | banner, 11 | setRules, 12 | setPlugins, 13 | setDevOptions, 14 | setOutput, 15 | setConfig, 16 | ...webpackConfig 17 | } = getCustomConfig(); 18 | 19 | config.entry = {}; 20 | config.plugins = config.plugins || []; 21 | setOutput?.(config.output); 22 | setBabelOptions?.((config.module.rules[0] as RuleSetRule).use[1].options); 23 | setRules?.(config.module.rules); 24 | setPlugins?.(config.plugins); 25 | setConfig?.(config); 26 | 27 | Object.keys(entries || {}).forEach((key) => { 28 | if (entries[key].entry) { 29 | config.entry[key] = entries[key].entry; 30 | } 31 | const htmlWebpackPlugin = new HtmlWebpackPlugin({ 32 | template: entries[key].template, 33 | filename: `${key}.html`, 34 | chunks: ['manifest', key], 35 | favicon: entries[key].favicon, 36 | inject: entries[key].inject !== false, 37 | minify: false, 38 | }); 39 | 40 | config.plugins.push(htmlWebpackPlugin); 41 | }); 42 | 43 | return webpackMerge(config, webpackConfig); 44 | } 45 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.0.1", 4 | "description": "React UI Library", 5 | "private": true, 6 | "scripts": { 7 | "start": "mx dev", 8 | "build:types": "rimraf types && tsc --outDir types -d --emitDeclarationOnly", 9 | "build:es": "rimraf esm && mx buildLib --mode esm --entry-dir ./components --less-2-css", 10 | "build:cjs": "rimraf lib && mx buildLib --mode cjs --entry-dir ./components --less-2-css --copy-less", 11 | "build:umd": "rimraf dist && mx buildLib --mode umd --entry ./components/index", 12 | "build": "yarn build:types && yarn build:cjs && yarn build:es && yarn build:umd", 13 | "buildSite": "mx buildSite", 14 | "buildLibHelp": "mx help buildLib", 15 | "buildSiteHelp": "mx help buildSite", 16 | "devHelp": "mx help dev" 17 | }, 18 | "directories": { 19 | "lib": "lib" 20 | }, 21 | "license": "MIT", 22 | "devDependencies": { 23 | "classnames": "^2.2.6", 24 | "clean-webpack-plugin": "^3.0.0", 25 | "css-loader": "^5.0.0", 26 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 27 | "file-loader": "^6.2.0", 28 | "gh-pages": "^3.1.0", 29 | "prop-types": "^15.7.2", 30 | "style-loader": "^2.0.0", 31 | "ts-loader": "^8.0.7", 32 | "typescript": "^4.0.5", 33 | "@mx-design/cli": "2.2.0" 34 | }, 35 | "dependencies": { 36 | "react": "^17.0.1", 37 | "react-dom": "^17.0.1" 38 | }, 39 | "sideEffects": [ 40 | "*.css", 41 | "*.less" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /src/config/webpackConfig/baseConfig.ts: -------------------------------------------------------------------------------- 1 | import webpack, { Configuration, RuleSetRule } from 'webpack'; 2 | import WebpackBar from 'webpackbar'; 3 | 4 | export function getBaseConfig( 5 | getRule: RuleSetRule[], 6 | needRuntimeChunk 7 | ): Configuration { 8 | return { 9 | target: 'web', 10 | output: { 11 | filename: 'js/[name].js', 12 | chunkFilename: 'js/[name].[chunkhash:8].js', 13 | assetModuleFilename: 'asset/[name].[contenthash:8].[ext]', 14 | }, 15 | optimization: { 16 | runtimeChunk: !!needRuntimeChunk, 17 | splitChunks: { 18 | minChunks: 2, 19 | chunks: 'all', 20 | cacheGroups: { 21 | reactBase: { 22 | name: 'reactBase', 23 | chunks: 'all', 24 | test: /[\\/]node_modules[\\/](react|react-dom|@hot-loader|react-router|react-redux|react-router-dom)[\\/]/, 25 | }, 26 | 'async-commons': { 27 | // 异步加载公共包、组件等 28 | name: 'async-commons', 29 | chunks: 'async', 30 | test: /[\\/]node_modules[\\/]/, 31 | minChunks: 2, 32 | priority: 1, 33 | }, 34 | }, 35 | }, 36 | }, 37 | module: { 38 | rules: getRule, 39 | }, 40 | resolve: { 41 | extensions: ['.ts', '.tsx', '.js', '.jsx', '.less', '.svg'], 42 | }, 43 | plugins: [ 44 | new WebpackBar({}), 45 | new webpack.DefinePlugin({ 46 | 'process.env': JSON.stringify(process.env), 47 | }), 48 | ], 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /src/config/webpackConfig/getUmdConfig.ts: -------------------------------------------------------------------------------- 1 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 2 | import webpackMerge from 'webpack-merge'; 3 | import { Configuration, RuleSetUseItem } from 'webpack'; 4 | import { getBaseConfig } from './baseConfig'; 5 | import { getRule } from './rules'; 6 | 7 | export const getUmdConfig = (isUgly: boolean): Configuration => { 8 | const getBuildRule = getRule({ 9 | afterLessRule: (rule) => { 10 | (rule.use as RuleSetUseItem[]).unshift({ 11 | loader: MiniCssExtractPlugin.loader, 12 | options: { 13 | publicPath: '../', 14 | }, 15 | }); 16 | }, 17 | afterCssRule: (rule) => { 18 | (rule.use as RuleSetUseItem[]).unshift({ 19 | loader: MiniCssExtractPlugin.loader, 20 | options: { 21 | publicPath: '../', 22 | }, 23 | }); 24 | }, 25 | }); 26 | 27 | const config: Configuration = webpackMerge( 28 | {}, 29 | getBaseConfig(getBuildRule, false), 30 | { 31 | mode: isUgly ? 'production' : 'development', 32 | devtool: 'hidden-source-map', 33 | output: { 34 | libraryTarget: 'umd', 35 | filename: isUgly ? '[name].min.js' : '[name].js', 36 | }, 37 | externals: { 38 | react: { 39 | root: 'React', 40 | commonjs2: 'react', 41 | commonjs: 'react', 42 | amd: 'react', 43 | }, 44 | 'react-dom': { 45 | root: 'ReactDOM', 46 | commonjs2: 'react-dom', 47 | commonjs: 'react-dom', 48 | amd: 'react-dom', 49 | }, 50 | }, 51 | plugins: [ 52 | new MiniCssExtractPlugin({ 53 | filename: isUgly ? '[name].min.css' : '[name].css', 54 | }), 55 | ], 56 | } 57 | ); 58 | 59 | return config; 60 | }; 61 | -------------------------------------------------------------------------------- /src/dev/development.ts: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import WebpackDevServer from 'webpack-dev-server'; 3 | import detect from 'detect-port-alt'; 4 | import getWebpackConfig from '../config/webpackConfig'; 5 | import { 6 | isAddForkTsPlugin, 7 | syncChainFns, 8 | getProjectConfig, 9 | getCustomConfig, 10 | } from '../utils'; 11 | import { DEV } from '../constants'; 12 | import { IDevelopmentConfig } from '../interface'; 13 | 14 | const isInteractive = process.stdout.isTTY; 15 | 16 | async function choosePort(port, host) { 17 | const resPort = await detect(port, host); 18 | if (resPort === Number(port)) { 19 | return resPort; 20 | } 21 | const message = `Something is already running on port ${port}.`; 22 | 23 | if (isInteractive) { 24 | console.log(message); 25 | return resPort; 26 | } 27 | console.log(message); 28 | return null; 29 | } 30 | 31 | export default ({ host, port }: IDevelopmentConfig) => { 32 | const compiler = syncChainFns( 33 | getWebpackConfig, 34 | getProjectConfig, 35 | isAddForkTsPlugin, 36 | webpack 37 | )(DEV); 38 | const { setDevOptions } = getCustomConfig(); 39 | const serverConfig = { 40 | publicPath: '/', 41 | compress: true, 42 | noInfo: true, 43 | hot: true, 44 | historyApiFallback: true, 45 | open: true, 46 | ...setDevOptions, 47 | }; 48 | const runDevServer = async (_port) => { 49 | const devServer = new WebpackDevServer(compiler, serverConfig); 50 | const resPort = await choosePort(_port, host); 51 | if (resPort !== null) { 52 | devServer.listen(resPort, host, (err) => { 53 | if (err) { 54 | return console.error(err.message); 55 | } 56 | console.warn(`http://${host}:${resPort}\n`); 57 | }); 58 | } 59 | }; 60 | runDevServer(port); 61 | }; 62 | -------------------------------------------------------------------------------- /src/config/webpackConfig/getBuildConfig.ts: -------------------------------------------------------------------------------- 1 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 2 | import webpackMerge from 'webpack-merge'; 3 | import { Configuration, RuleSetUseItem } from 'webpack'; 4 | import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; // 压缩css插件 5 | import TerserPlugin from 'terser-webpack-plugin'; // 压缩代码 6 | import { getBaseConfig } from './baseConfig'; 7 | import { getRule } from './rules'; 8 | 9 | export const getBuildConfig = (): Configuration => { 10 | const getBuildRule = getRule({ 11 | afterLessRule: (rule) => { 12 | (rule.use as RuleSetUseItem[]).unshift({ 13 | loader: MiniCssExtractPlugin.loader, 14 | options: { 15 | publicPath: '../', 16 | }, 17 | }); 18 | }, 19 | afterCssRule: (rule) => { 20 | (rule.use as RuleSetUseItem[]).unshift({ 21 | loader: MiniCssExtractPlugin.loader, 22 | options: { 23 | publicPath: '../', 24 | }, 25 | }); 26 | }, 27 | }); 28 | 29 | const config: Configuration = webpackMerge( 30 | {}, 31 | getBaseConfig(getBuildRule, true), 32 | { 33 | mode: 'production', 34 | devtool: 'hidden-source-map', 35 | output: { 36 | filename: 'js/[name].js', 37 | chunkFilename: 'js/[name].[chunkhash:8].js', 38 | publicPath: '/', 39 | }, 40 | plugins: [ 41 | new MiniCssExtractPlugin({ 42 | filename: 'stylesheet/[name].[contenthash:8].css', 43 | chunkFilename: 'stylesheet/[id].[contenthash:8].css', 44 | }), 45 | ], 46 | } 47 | ); 48 | 49 | config.optimization.minimizer = [ 50 | // 压缩js文件 51 | new TerserPlugin({ 52 | // 开启“多线程”,提高压缩效率 53 | parallel: true, 54 | exclude: /node_modules/, 55 | extractComments: false, 56 | }), 57 | // 压缩css插件 58 | new CssMinimizerPlugin({ 59 | minimizerOptions: { 60 | preset: [ 61 | 'default', 62 | { 63 | discardComments: { removeAll: true }, 64 | }, 65 | ], 66 | }, 67 | }), 68 | ]; 69 | return config; 70 | }; 71 | -------------------------------------------------------------------------------- /example/src/interface.tsx: -------------------------------------------------------------------------------- 1 | export type ToastId = string | number; 2 | export type ToastStatus = 'success' | 'error' | 'warning' | 'info' | 'loading'; 3 | export type ToastPosition = 4 | | 'top' 5 | | 'top-left' 6 | | 'top-right' 7 | | 'bottom' 8 | | 'bottom-left' 9 | | 'bottom-right'; 10 | 11 | export interface ToastMethods { 12 | /** 13 | * Function to actually create a toast and add it 14 | * to state at the specified position 15 | */ 16 | notify: (message: any, options?: any) => ToastId; 17 | /** 18 | * Close all toasts at once. 19 | * If given positions, will only close those. 20 | */ 21 | closeAll: (options?: any) => void; 22 | /** 23 | * Requests to close a toast based on its id and position 24 | */ 25 | close: (id: ToastId) => void; 26 | /** 27 | * Update a specific toast with new options based on the 28 | * passed `id` 29 | */ 30 | update: (id: ToastId, options: Omit) => void; 31 | isActive: (id: ToastId) => boolean; 32 | } 33 | 34 | export interface ToastOptions { 35 | /** 36 | * The toast's id 37 | */ 38 | id: ToastId; 39 | /** 40 | * The duration of the toast 41 | */ 42 | duration: number | null; 43 | /** 44 | * The status of the toast's alert component. 45 | */ 46 | status: ToastStatus; 47 | 48 | /** 49 | * Function that removes the toast from manager's state. 50 | */ 51 | onRequestRemove(): void; 52 | 53 | /** 54 | * The position of the toast 55 | */ 56 | position: ToastPosition; 57 | 58 | /** 59 | * Callback function to run side effects after the toast has closed. 60 | */ 61 | onCloseComplete?(): void; 62 | 63 | /** 64 | * Internally used to queue closing a toast. Should probably not be used by 65 | * anyone else, but documented regardless. 66 | */ 67 | requestClose?: boolean; 68 | /** 69 | * Optional style overrides for the toast component. 70 | */ 71 | style?: any; 72 | } 73 | 74 | export type ToastState = { 75 | [K in ToastPosition]: ToastOptions[]; 76 | }; 77 | 78 | export type ToastStore = ToastMethods & { 79 | getState: () => ToastState; 80 | subscribe: (onStoreChange: () => void) => () => void; 81 | removeToast: (id: ToastId, position: ToastPosition) => void; 82 | }; 83 | -------------------------------------------------------------------------------- /src/buildLib/buildUmd.ts: -------------------------------------------------------------------------------- 1 | import { getProjectPath, withOra } from '@mx-design/node-utils'; 2 | import { log } from '@mx-design/web-utils'; 3 | import webpack from 'webpack'; 4 | import webpackMerge from 'webpack-merge'; 5 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 6 | import getWebpackConfig from '../config/webpackConfig'; 7 | import { BUILD_LIB } from '../constants'; 8 | 9 | const { name } = require(getProjectPath('package.json')); 10 | const checkName = (outputName, name) => { 11 | if (!outputName && name?.includes('/')) { 12 | log.warn( 13 | 'The package name of package.json contains slashes, and webpack will create folders with slashes when packaging, so please pay attention to whether the file name after packaging meets your requirements' 14 | ); 15 | } 16 | }; 17 | 18 | /** 19 | * build for umd 20 | * @param analyzer Whether to enable the analysis package plugin 21 | * @param outDirUmd output directory 22 | * @param entry packaged entry file 23 | * @param outputName packaged name 24 | */ 25 | export const buildUmd = async ({ 26 | analyzerUmd, 27 | outDirUmd, 28 | entry, 29 | outputName, 30 | mode, 31 | }) => { 32 | const customizePlugins = []; 33 | const realName = outputName || name; 34 | const entryFiles = entry.split(',').map((p) => getProjectPath(p)); 35 | checkName(outputName, name); 36 | const umdTask = (mode) => { 37 | return new Promise((resolve, reject) => { 38 | const config = webpackMerge(getWebpackConfig(mode), { 39 | entry: { 40 | [realName]: entryFiles, 41 | }, 42 | output: { 43 | path: getProjectPath(outDirUmd), 44 | library: realName, 45 | }, 46 | plugins: customizePlugins, 47 | }); 48 | 49 | if (analyzerUmd) { 50 | config.plugins.push( 51 | new BundleAnalyzerPlugin({ 52 | analyzerMode: 'static', 53 | generateStatsFile: true, 54 | }) 55 | ); 56 | } 57 | return webpack(config).run((err, stats) => { 58 | if (stats.compilation.errors?.length) { 59 | console.log('webpackError: ', stats.compilation.errors); 60 | } 61 | if (err) { 62 | log.error('webpackError: ', JSON.stringify(err)); 63 | reject(err); 64 | } else { 65 | resolve(stats); 66 | } 67 | }); 68 | }); 69 | }; 70 | await withOra(() => umdTask(mode), { 71 | text: 'building umd', 72 | successText: 'umd computed', 73 | failText: 'umd failed', 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mx-design/cli", 3 | "version": "2.3.9", 4 | "repository": "https://github.com/lio-mengxiang/mx-design-cli.git", 5 | "description": "Cli Tools for Mx Design Project", 6 | "keywords": [ 7 | "mx-cli", 8 | "react cli", 9 | "webpack pack cli", 10 | "react library pack cli" 11 | ], 12 | "license": "MIT", 13 | "author": "1334196450@qq.com", 14 | "typings": "types/src/index.d.ts", 15 | "bin": { 16 | "mx": "./bin/index.js" 17 | }, 18 | "files": [ 19 | "lib", 20 | "bin", 21 | "types" 22 | ], 23 | "scripts": { 24 | "clean": "rimraf types lib coverage", 25 | "test": "jest --config ./jest.config.js", 26 | "coverage": "rimraf coverage && jest --config ./jest.config.js --coverage", 27 | "build:types": "rimraf types && tsc --outDir types -d --emitDeclarationOnly", 28 | "build:lib": "rimraf lib && babel src --extensions .ts --out-dir lib --copy-files", 29 | "build": "yarn build:types && yarn build:lib", 30 | "release": "ts-node ./scripts/release.ts" 31 | }, 32 | "dependencies": { 33 | "@babel/cli": "^7.13.10", 34 | "@babel/core": "^7.13.10", 35 | "@babel/plugin-proposal-class-properties": "^7.13.0", 36 | "@babel/plugin-proposal-decorators": "^7.13.5", 37 | "@babel/plugin-proposal-optional-chaining": "^7.13.12", 38 | "@babel/plugin-transform-runtime": "^7.13.10", 39 | "@babel/preset-env": "^7.13.12", 40 | "@babel/preset-react": "^7.12.13", 41 | "@babel/preset-typescript": "^7.13.0", 42 | "@babel/runtime": "^7.13.10", 43 | "@babel/runtime-corejs3": "^7.13.10", 44 | "@pmmmwh/react-refresh-webpack-plugin": "0.5.4", 45 | "@types/jest": "27.4.1", 46 | "babel-jest": "^26.5.2", 47 | "babel-loader": "^8.2.2", 48 | "commander": "^7.2.0", 49 | "css-loader": "^5.2.4", 50 | "css-minimizer-webpack-plugin": "3.4.1", 51 | "detect-port-alt": "1.1.6", 52 | "fork-ts-checker-webpack-plugin": "^6.2.10", 53 | "gulp": "^4.0.2", 54 | "gulp-autoprefixer": "^8.0.0", 55 | "gulp-babel": "^8.0.0", 56 | "gulp-less": "^5.0.0", 57 | "html-webpack-plugin": "^5.3.1", 58 | "jest": "^26.5.3", 59 | "less": "4.1.3", 60 | "less-loader": "11.1.0", 61 | "mini-css-extract-plugin": "^1.6.0", 62 | "mkdirp": "^0.5.1", 63 | "postcss-flexbugs-fixes": "^4.1.0", 64 | "postcss-loader": "^3.0.0", 65 | "postcss-preset-env": "^6.7.0", 66 | "process": "^0.11.10", 67 | "react-refresh": "^0.10.0", 68 | "rimraf": "^3.0.2", 69 | "style-loader": "3.3.1", 70 | "terser-webpack-plugin": "^5.1.2", 71 | "thread-loader": "3.0.4", 72 | "through2": "^3.0.1", 73 | "ts-jest": "^26.5.0", 74 | "typescript": "^4.5.4", 75 | "webpack": "5.37.0", 76 | "webpack-bundle-analyzer": "^4.4.2", 77 | "webpack-cli": "^4.7.0", 78 | "webpack-dev-server": "^3.11.2", 79 | "webpack-merge": "^5.7.3", 80 | "webpackbar": "^5.0.0-3", 81 | "@svgr/webpack": "^5.5.0", 82 | "ts-node": "10.5.0", 83 | "@mx-design/web-utils": "0.1.8", 84 | "@mx-design/node-utils": "0.1.3" 85 | }, 86 | "devDependencies": { 87 | "@mx-design/release": "2.2.22", 88 | "@types/webpack": "4.41.26" 89 | }, 90 | "browserslist": [ 91 | "chrome 60", 92 | "Firefox 45", 93 | "safari 10" 94 | ], 95 | "publishConfig": { 96 | "registry": "https://registry.npmjs.org/" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * plugins和extends的区别 3 | * 如果eslint里没有的规则需要plugin做拓展 4 | */ 5 | module.exports = { 6 | /** 7 | * node或者浏览器中的全局变量很多,如果我们一个个进行声明显得繁琐, 8 | * 因此就需要用到我们的env,这是对环境定义的一组全局变量的预设 9 | */ 10 | env: { 11 | browser: true, 12 | commonjs: true, 13 | es6: true, 14 | node: true, 15 | jest: true, 16 | }, 17 | parser: '@typescript-eslint/parser', 18 | /** 19 | * 插件是一个 npm 包,通常输出规则。一些插件也可以输出一个或多个命名的配置。要确保这个包安装在 ESLint 能请求到的目录下。 20 | * plugins属性值可以省略包名的前缀eslint-plugin-。extends属性值可以由以下组成: 21 | * plugin:包名 (省略了前缀,比如,react)配置名称 (比如recommended) 22 | * 插件一个主要的作用就是补充规则,比如eslint:recommended中没有有关react的规则,则需要另外导入规则插件eslint-plugin-react 23 | */ 24 | plugins: ['react', 'babel', '@typescript-eslint/eslint-plugin'], 25 | /** 26 | * eslint:开头的ESLint官方扩展,有两个:eslint:recommended(推荐规范)和eslint:all(所有规范)。 27 | * plugin:开头的扩展是插件类型扩展 28 | * eslint-config:开头的来自npm包,使用时可以省略eslint-config- 29 | * @:开头的扩展和eslint-config一样,是在npm包上面加了一层作用域scope 30 | * 需要注意的是:多个扩展中有相同的规则,以后面引入的扩展中规则为准。 31 | */ 32 | extends: [ 33 | 'airbnb', 34 | 'plugin:react/recommended', 35 | 'plugin:prettier/recommended', 36 | 'plugin:react-hooks/recommended', 37 | ], 38 | parserOptions: { 39 | sourceType: 'module', 40 | ecmaFeatures: { 41 | experimentalObjectRestSpread: true, 42 | jsx: true, // 启用 JSX 43 | }, 44 | }, 45 | globals: { 46 | describe: false, 47 | it: false, 48 | expect: false, 49 | jest: false, 50 | afterEach: false, 51 | beforeEach: false, 52 | }, 53 | rules: { 54 | 'prettier/prettier': ['error', { singleQuote: true }], 55 | 'react/prop-types': 0, 56 | 'react/display-name': 0, 57 | 'react/jsx-no-target-blank': 0, // 允许 target 等于 blank 58 | 'react/jsx-key': 1, // jsx 中的遍历,需要加 key 属性,没有会提示警告 59 | 'react/no-find-dom-node': 0, 60 | indent: [2, 2, { SwitchCase: 1 }], // 缩进 2 格,jquery 项目可忽略。switch 和 case 之间缩进两个 61 | 'jsx-quotes': [2, 'prefer-double'], // jsx 属性统一使用双引号 62 | 'max-len': [1, { code: 140 }], // 渐进式调整,先设置最大长度为 140,同时只是警告 63 | 'no-mixed-spaces-and-tabs': 2, 64 | 'no-tabs': 2, 65 | 'no-trailing-spaces': 2, // 语句尾部不能出现空格 66 | quotes: [2, 'single'], // 统一使用单引号 67 | 'space-before-blocks': 2, // if 和函数等,大括号前需要空格 68 | 'space-in-parens': 2, // 括号内前后不加空格 69 | 'space-infix-ops': 2, // 中缀(二元)操作符前后加空格 70 | 'spaced-comment': 2, // 注释双斜杠后保留一个空格 71 | '@typescript-eslint/explicit-function-return-type': 0, 72 | '@typescript-eslint/no-explicit-any': 0, 73 | '@typescript-eslint/no-non-null-assertion': 0, 74 | '@typescript-eslint/ban-ts-ignore': 0, 75 | '@typescript-eslint/interface-name-prefix': 0, 76 | '@typescript-eslint/no-use-before-define': 0, 77 | 'react-hooks/rules-of-hooks': 'error', 78 | 'react-hooks/exhaustive-deps': 'warn', 79 | '@typescript-eslint/explicit-module-boundary-types': 0, 80 | '@typescript-eslint/ban-ts-comment': 0, 81 | 'consistent-return': 0, 82 | 'no-underscore-dangle': 0, 83 | 'import/prefer-default-export': 0, 84 | 'import/extensions': 0, 85 | 'import/no-unresolved': 0, 86 | camelcase: 0, 87 | 'no-plusplus': 0, 88 | 'no-param-reassign': 0, 89 | 'no-console': 0, 90 | 'import/no-extraneous-dependencies': 0, 91 | 'import/no-dynamic-require': 0, 92 | 'no-shadow': 0, 93 | 'global-require': 0, 94 | 'no-unused-expressions': 0, 95 | 'no-unused-vars': 0, 96 | }, 97 | }; 98 | -------------------------------------------------------------------------------- /src/buildLib/build.ts: -------------------------------------------------------------------------------- 1 | import { withOra } from '@mx-design/node-utils'; 2 | import { compose } from '../utils/compose'; 3 | import { 4 | copyLessMid, 5 | less2css, 6 | buildCjs, 7 | buildEsm, 8 | } from '../config/gulpConfig'; 9 | import { CJS, ESM, UMD, COPY_LESS, LESS_2_CSS } from '../constants'; 10 | import { buildUmd } from './buildUmd'; 11 | 12 | const buildLib = async ({ 13 | analyzerUmd, 14 | mode, 15 | entry, 16 | outDirEsm, 17 | outDirCjs, 18 | outDirUmd, 19 | copyLess, 20 | entryDir, 21 | less2Css, 22 | cleanDir, 23 | outputName, 24 | }) => { 25 | const buildProcess = []; 26 | if (mode === UMD) { 27 | buildProcess.push(async (next, otherOptions) => { 28 | await buildUmd({ 29 | analyzerUmd: otherOptions.analyzerUmd, 30 | outDirUmd: otherOptions.outDirUmd, 31 | entry: otherOptions.entry, 32 | outputName: otherOptions.outputName, 33 | mode, 34 | }); 35 | next(); 36 | }); 37 | } 38 | if (mode === ESM) { 39 | buildProcess.push(async (next, otherOptions) => { 40 | await withOra( 41 | () => 42 | buildEsm({ 43 | mode: otherOptions.mode, 44 | outDirEsm: otherOptions.outDirEsm, 45 | entryDir: otherOptions.entryDir, 46 | }), 47 | { 48 | text: 'buildEsm ing...', 49 | successText: 'buildEsm success', 50 | failText: 'buildEsm failed', 51 | } 52 | ); 53 | next(); 54 | }); 55 | } 56 | if (mode === CJS) { 57 | buildProcess.push(async (next, otherOptions) => { 58 | await withOra( 59 | () => 60 | buildCjs({ 61 | mode: otherOptions.mode, 62 | outDirCjs: otherOptions.outDirCjs, 63 | entryDir: otherOptions.entryDir, 64 | }), 65 | { 66 | text: 'buildCjs ing...', 67 | successText: 'buildCjs success', 68 | failText: 'buildCjs failed', 69 | } 70 | ); 71 | next(); 72 | }); 73 | } 74 | if (less2Css) { 75 | less2Css = LESS_2_CSS; 76 | buildProcess.push(async (next, otherOptions) => { 77 | await withOra( 78 | () => 79 | less2css({ 80 | outDirCjs: otherOptions.outDirCjs, 81 | entryDir: otherOptions.entryDir, 82 | mode: otherOptions.mode, 83 | outDirEsm: otherOptions.outDirEsm, 84 | }), 85 | { 86 | text: 'less2Css ing...', 87 | successText: 'copyLess success', 88 | failText: 'less2css failed', 89 | } 90 | ); 91 | next(); 92 | }); 93 | } 94 | if (copyLess) { 95 | copyLess = COPY_LESS; 96 | buildProcess.push(async (next, otherOptions) => { 97 | await withOra( 98 | () => 99 | copyLessMid({ 100 | outDirCjs: otherOptions.outDirCjs, 101 | entryDir: otherOptions.entryDir, 102 | mode: otherOptions.mode, 103 | outDirEsm: otherOptions.outDirEsm, 104 | }), 105 | { 106 | text: 'copyLess ing..', 107 | successText: 'copyLess success', 108 | failText: 'copyLess failed', 109 | } 110 | ); 111 | next(); 112 | }); 113 | } 114 | 115 | compose(buildProcess, { 116 | analyzerUmd, 117 | mode, 118 | entry, 119 | outDirEsm, 120 | outDirCjs, 121 | outDirUmd, 122 | copyLess, 123 | entryDir, 124 | less2Css, 125 | cleanDir, 126 | outputName, 127 | }); 128 | }; 129 | 130 | export default buildLib; 131 | -------------------------------------------------------------------------------- /example/src/toast.store.ts: -------------------------------------------------------------------------------- 1 | import { ToastState, ToastStore } from './interface'; 2 | 3 | const initialState = { 4 | top: [], 5 | 'top-left': [], 6 | 'top-right': [], 7 | 'bottom-left': [], 8 | bottom: [], 9 | 'bottom-right': [], 10 | }; 11 | 12 | function createStore(initialState: ToastState): ToastStore { 13 | let state = initialState; 14 | const listeners = new Set<() => void>(); 15 | 16 | const setState = (setStateFn: (values: ToastState) => ToastState) => { 17 | state = setStateFn(state); 18 | listeners.forEach((l) => l()); 19 | }; 20 | 21 | return { 22 | getState: () => state, 23 | 24 | subscribe: (listener) => { 25 | listeners.add(listener); 26 | return () => { 27 | // Delete all toasts on unmount 28 | setState(() => initialState); 29 | listeners.delete(listener); 30 | }; 31 | }, 32 | 33 | /** 34 | * Delete a toast record at its position 35 | */ 36 | removeToast: (id, position) => { 37 | setState((prevState) => ({ 38 | ...prevState, 39 | // id may be string or number 40 | // eslint-disable-next-line eqeqeq 41 | [position]: prevState[position].filter((toast) => toast.id != id), 42 | })); 43 | }, 44 | 45 | notify: (message, options) => { 46 | const toast = createToast(message, options); 47 | const { position, id } = toast; 48 | 49 | setState((prevToasts) => { 50 | const isTop = position.includes('top'); 51 | 52 | /** 53 | * - If the toast is positioned at the top edges, the 54 | * recent toast stacks on top of the other toasts. 55 | * 56 | * - If the toast is positioned at the bottom edges, the recent 57 | * toast stacks below the other toasts. 58 | */ 59 | const toasts = isTop 60 | ? [toast, ...(prevToasts[position] ?? [])] 61 | : [...(prevToasts[position] ?? []), toast]; 62 | 63 | return { 64 | ...prevToasts, 65 | [position]: toasts, 66 | }; 67 | }); 68 | 69 | return id; 70 | }, 71 | 72 | update: (id, options) => { 73 | if (!id) return; 74 | 75 | setState((prevState) => { 76 | const nextState = { ...prevState }; 77 | const { position, index } = findToast(nextState, id); 78 | 79 | if (position && index !== -1) { 80 | nextState[position][index] = { 81 | ...nextState[position][index], 82 | ...options, 83 | message: createRenderToast(options), 84 | }; 85 | } 86 | 87 | return nextState; 88 | }); 89 | }, 90 | 91 | closeAll: ({ positions } = {}) => { 92 | // only one setState here for perf reasons 93 | // instead of spamming this.closeToast 94 | setState((prev) => { 95 | const allPositions: ToastPosition[] = [ 96 | 'bottom', 97 | 'bottom-right', 98 | 'bottom-left', 99 | 'top', 100 | 'top-left', 101 | 'top-right', 102 | ]; 103 | 104 | const positionsToClose = positions ?? allPositions; 105 | 106 | return positionsToClose.reduce( 107 | (acc, position) => { 108 | acc[position] = prev[position].map((toast) => ({ 109 | ...toast, 110 | requestClose: true, 111 | })); 112 | 113 | return acc; 114 | }, 115 | { ...prev } as ToastState 116 | ); 117 | }); 118 | }, 119 | 120 | close: (id) => { 121 | setState((prevState) => { 122 | const position = getToastPosition(prevState, id); 123 | 124 | if (!position) return prevState; 125 | 126 | return { 127 | ...prevState, 128 | [position]: prevState[position].map((toast) => { 129 | // id may be string or number 130 | // eslint-disable-next-line eqeqeq 131 | if (toast.id == id) { 132 | return { 133 | ...toast, 134 | requestClose: true, 135 | }; 136 | } 137 | 138 | return toast; 139 | }), 140 | }; 141 | }); 142 | }, 143 | 144 | isActive: (id) => Boolean(findToast(toastStore.getState(), id).position), 145 | }; 146 | } 147 | /** 148 | * Store to track all the toast across all positions 149 | */ 150 | export const toastStore = createStore(initialState); 151 | -------------------------------------------------------------------------------- /example/public/snowman.svg: -------------------------------------------------------------------------------- 1 | snowman_1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom ignore 2 | /build 3 | /dist 4 | /lib 5 | /esm 6 | /types 7 | .vscode 8 | yarn.lock 9 | 10 | # General 11 | .DS_Store 12 | .AppleDouble 13 | .LSOverride 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | .vscode/* 35 | !.vscode/settings.json 36 | !.vscode/tasks.json 37 | !.vscode/launch.json 38 | !.vscode/extensions.json 39 | *.code-workspace 40 | 41 | # Local History for Visual Studio Code 42 | .history/ 43 | 44 | # Logs 45 | logs 46 | *.log 47 | npm-debug.log* 48 | yarn-debug.log* 49 | yarn-error.log* 50 | lerna-debug.log* 51 | 52 | # Diagnostic reports (https://nodejs.org/api/report.html) 53 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 54 | 55 | # Runtime data 56 | pids 57 | *.pid 58 | *.seed 59 | *.pid.lock 60 | 61 | # Directory for instrumented libs generated by jscoverage/JSCover 62 | lib-cov 63 | 64 | # Coverage directory used by tools like istanbul 65 | coverage 66 | *.lcov 67 | 68 | # nyc test coverage 69 | .nyc_output 70 | 71 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 72 | .grunt 73 | 74 | # Bower dependency directory (https://bower.io/) 75 | bower_components 76 | 77 | # node-waf configuration 78 | .lock-wscript 79 | 80 | # Compiled binary addons (https://nodejs.org/api/addons.html) 81 | build/Release 82 | 83 | # Dependency directories 84 | node_modules/ 85 | jspm_packages/ 86 | 87 | # Snowpack dependency directory (https://snowpack.dev/) 88 | web_modules/ 89 | 90 | # TypeScript cache 91 | *.tsbuildinfo 92 | 93 | # Optional npm cache directory 94 | .npm 95 | 96 | # Optional eslint cache 97 | .eslintcache 98 | 99 | # Microbundle cache 100 | .rpt2_cache/ 101 | .rts2_cache_cjs/ 102 | .rts2_cache_es/ 103 | .rts2_cache_umd/ 104 | 105 | # Optional REPL history 106 | .node_repl_history 107 | 108 | # Output of 'npm pack' 109 | *.tgz 110 | 111 | # Yarn Integrity file 112 | .yarn-integrity 113 | 114 | # dotenv environment variables file 115 | .env 116 | .env.test 117 | 118 | # parcel-bundler cache (https://parceljs.org/) 119 | .cache 120 | .parcel-cache 121 | 122 | # Next.js build output 123 | .next 124 | out 125 | 126 | # Nuxt.js build / generate output 127 | .nuxt 128 | dist 129 | 130 | # Gatsby files 131 | .cache/ 132 | # Comment in the public line in if your project uses Gatsby and not Next.js 133 | # https://nextjs.org/blog/next-9-1#public-directory-support 134 | # public 135 | 136 | # vuepress build output 137 | .vuepress/dist 138 | 139 | # Serverless directories 140 | .serverless/ 141 | 142 | # FuseBox cache 143 | .fusebox/ 144 | 145 | # DynamoDB Local files 146 | .dynamodb/ 147 | 148 | # TernJS port file 149 | .tern-port 150 | 151 | # Stores VSCode versions used for testing VSCode extensions 152 | .vscode-test 153 | 154 | # yarn v2 155 | .yarn/cache 156 | .yarn/unplugged 157 | .yarn/build-state.yml 158 | .yarn/install-state.gz 159 | .pnp.* 160 | 161 | # Windows thumbnail cache files 162 | Thumbs.db 163 | Thumbs.db:encryptable 164 | ehthumbs.db 165 | ehthumbs_vista.db 166 | 167 | # Dump file 168 | *.stackdump 169 | 170 | # Folder config file 171 | [Dd]esktop.ini 172 | 173 | # Recycle Bin used on file shares 174 | $RECYCLE.BIN/ 175 | 176 | # Windows Installer files 177 | *.cab 178 | *.msi 179 | *.msix 180 | *.msm 181 | *.msp 182 | 183 | # Windows shortcuts 184 | *.lnk 185 | 186 | *~ 187 | 188 | # temporary files which can be created if a process still has a handle open of a deleted file 189 | .fuse_hidden* 190 | 191 | # KDE directory preferences 192 | .directory 193 | 194 | # Linux trash folder which might appear on any partition or disk 195 | .Trash-* 196 | 197 | # .nfs files are created when an open file is removed but is still being accessed 198 | .nfs* 199 | 200 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 201 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 202 | 203 | # User-specific stuff 204 | .idea/**/workspace.xml 205 | .idea/**/tasks.xml 206 | .idea/**/usage.statistics.xml 207 | .idea/**/dictionaries 208 | .idea/**/shelf 209 | 210 | # Generated files 211 | .idea/**/contentModel.xml 212 | 213 | # Sensitive or high-churn files 214 | .idea/**/dataSources/ 215 | .idea/**/dataSources.ids 216 | .idea/**/dataSources.local.xml 217 | .idea/**/sqlDataSources.xml 218 | .idea/**/dynamic.xml 219 | .idea/**/uiDesigner.xml 220 | .idea/**/dbnavigator.xml 221 | 222 | # Gradle 223 | .idea/**/gradle.xml 224 | .idea/**/libraries 225 | 226 | # Gradle and Maven with auto-import 227 | # When using Gradle or Maven with auto-import, you should exclude module files, 228 | # since they will be recreated, and may cause churn. Uncomment if using 229 | # auto-import. 230 | # .idea/artifacts 231 | # .idea/compiler.xml 232 | # .idea/jarRepositories.xml 233 | # .idea/modules.xml 234 | # .idea/*.iml 235 | # .idea/modules 236 | # *.iml 237 | # *.ipr 238 | 239 | # CMake 240 | cmake-build-*/ 241 | 242 | # Mongo Explorer plugin 243 | .idea/**/mongoSettings.xml 244 | 245 | # File-based project format 246 | *.iws 247 | 248 | # IntelliJ 249 | out/ 250 | 251 | # mpeltonen/sbt-idea plugin 252 | .idea_modules/ 253 | 254 | # JIRA plugin 255 | atlassian-ide-plugin.xml 256 | 257 | # Cursive Clojure plugin 258 | .idea/replstate.xml 259 | 260 | # Crashlytics plugin (for Android Studio and IntelliJ) 261 | com_crashlytics_export_strings.xml 262 | crashlytics.properties 263 | crashlytics-build.properties 264 | fabric.properties 265 | 266 | # Editor-based Rest Client 267 | .idea/httpRequests 268 | 269 | # Android studio 3.1+ serialized cache file 270 | .idea/caches/build_file_checksums.ser 271 | 272 | # Cache files for Sublime Text 273 | *.tmlanguage.cache 274 | *.tmPreferences.cache 275 | *.stTheme.cache 276 | 277 | # Workspace files are user-specific 278 | *.sublime-workspace 279 | 280 | # Project files should be checked into the repository, unless a significant 281 | # proportion of contributors will probably not be using Sublime Text 282 | # *.sublime-project 283 | 284 | # SFTP configuration file 285 | sftp-config.json 286 | sftp-config-alt*.json 287 | 288 | # Package control specific files 289 | Package Control.last-run 290 | Package Control.ca-list 291 | Package Control.ca-bundle 292 | Package Control.system-ca-bundle 293 | Package Control.cache/ 294 | Package Control.ca-certs/ 295 | Package Control.merged-ca-bundle 296 | Package Control.user-ca-bundle 297 | oscrypto-ca-bundle.crt 298 | bh_unicode_properties.cache 299 | 300 | # Sublime-github package stores a github token in this file 301 | # https://packagecontrol.io/packages/sublime-github 302 | GitHub.sublime-settings 303 | 304 | # Swap 305 | [._]*.s[a-v][a-z] 306 | !*.svg # comment out if you don't need vector files 307 | [._]*.sw[a-p] 308 | [._]s[a-rt-v][a-z] 309 | [._]ss[a-gi-z] 310 | [._]sw[a-p] 311 | 312 | # Session 313 | Session.vim 314 | Sessionx.vim 315 | 316 | # Temporary 317 | .netrwhist 318 | *~ 319 | # Auto-generated tag files 320 | tags 321 | # Persistent undo 322 | [._]*.un~ 323 | -------------------------------------------------------------------------------- /example/components/style/reset.less: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { /* 1 */ 178 | overflow: visible; 179 | } 180 | 181 | /** 182 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 183 | * 1. Remove the inheritance of text transform in Firefox. 184 | */ 185 | 186 | button, 187 | select { /* 1 */ 188 | text-transform: none; 189 | } 190 | 191 | /** 192 | * Correct the inability to style clickable types in iOS and Safari. 193 | */ 194 | 195 | button, 196 | [type="button"], 197 | [type="reset"], 198 | [type="submit"] { 199 | -webkit-appearance: button; 200 | } 201 | 202 | /** 203 | * Remove the inner border and padding in Firefox. 204 | */ 205 | 206 | button::-moz-focus-inner, 207 | [type="button"]::-moz-focus-inner, 208 | [type="reset"]::-moz-focus-inner, 209 | [type="submit"]::-moz-focus-inner { 210 | border-style: none; 211 | padding: 0; 212 | } 213 | 214 | /** 215 | * Restore the focus styles unset by the previous rule. 216 | */ 217 | 218 | button:-moz-focusring, 219 | [type="button"]:-moz-focusring, 220 | [type="reset"]:-moz-focusring, 221 | [type="submit"]:-moz-focusring { 222 | outline: 1px dotted ButtonText; 223 | } 224 | 225 | /** 226 | * Correct the padding in Firefox. 227 | */ 228 | 229 | fieldset { 230 | padding: 0.35em 0.75em 0.625em; 231 | } 232 | 233 | /** 234 | * 1. Correct the text wrapping in Edge and IE. 235 | * 2. Correct the color inheritance from `fieldset` elements in IE. 236 | * 3. Remove the padding so developers are not caught out when they zero out 237 | * `fieldset` elements in all browsers. 238 | */ 239 | 240 | legend { 241 | box-sizing: border-box; /* 1 */ 242 | color: inherit; /* 2 */ 243 | display: table; /* 1 */ 244 | max-width: 100%; /* 1 */ 245 | padding: 0; /* 3 */ 246 | white-space: normal; /* 1 */ 247 | } 248 | 249 | /** 250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 251 | */ 252 | 253 | progress { 254 | vertical-align: baseline; 255 | } 256 | 257 | /** 258 | * Remove the default vertical scrollbar in IE 10+. 259 | */ 260 | 261 | textarea { 262 | overflow: auto; 263 | } 264 | 265 | /** 266 | * 1. Add the correct box sizing in IE 10. 267 | * 2. Remove the padding in IE 10. 268 | */ 269 | 270 | [type="checkbox"], 271 | [type="radio"] { 272 | box-sizing: border-box; /* 1 */ 273 | padding: 0; /* 2 */ 274 | } 275 | 276 | /** 277 | * Correct the cursor style of increment and decrement buttons in Chrome. 278 | */ 279 | 280 | [type="number"]::-webkit-inner-spin-button, 281 | [type="number"]::-webkit-outer-spin-button { 282 | height: auto; 283 | } 284 | 285 | /** 286 | * 1. Correct the odd appearance in Chrome and Safari. 287 | * 2. Correct the outline style in Safari. 288 | */ 289 | 290 | [type="search"] { 291 | -webkit-appearance: textfield; /* 1 */ 292 | outline-offset: -2px; /* 2 */ 293 | } 294 | 295 | /** 296 | * Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | [type="search"]::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /** 304 | * 1. Correct the inability to style clickable types in iOS and Safari. 305 | * 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; /* 1 */ 310 | font: inherit; /* 2 */ 311 | } 312 | 313 | /* Interactive 314 | ========================================================================== */ 315 | 316 | /* 317 | * Add the correct display in Edge, IE 10+, and Firefox. 318 | */ 319 | 320 | details { 321 | display: block; 322 | } 323 | 324 | /* 325 | * Add the correct display in all browsers. 326 | */ 327 | 328 | summary { 329 | display: list-item; 330 | } 331 | 332 | /* Misc 333 | ========================================================================== */ 334 | 335 | /** 336 | * Add the correct display in IE 10+. 337 | */ 338 | 339 | template { 340 | display: none; 341 | } 342 | 343 | /** 344 | * Add the correct display in IE 10. 345 | */ 346 | 347 | [hidden] { 348 | display: none; 349 | } -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | ## 语言 2 | 3 | [English](./README.md) | [中文](./README.zh.md) 4 | 5 | [源码分析](#源码分析) 6 | 7 | ## 简介 8 | 9 | - [x] 方便开发:一行命令启动 react + less 业务项目 10 | - [x] 方便打包业务代码: 一行命令打包 react 业务项目,webpack5 打包,无需关注 webpack 配置和优化,我们帮你做了 11 | - [x] 方便打包 react 组件库: 一行命令打包 react 组件库,打包方式等同 ant design(打包组件库要按需加载,跟业务代码打包的配置是不一样的) 12 | 13 | ## 项目简介 14 | 15 | - 通过 @mx-deisgn/cli 可以快速启动开发环境,打包项目(案例在 example 文件夹下) 16 | 17 | ## Install 18 | 19 | use `npm` 20 | 21 | ```node 22 | npm install @mx-design/cli --save-dev 23 | ``` 24 | 25 | or use `yarn` 26 | 27 | ```node 28 | yarn add @mx-design/cli --dev 29 | ``` 30 | 31 | ## Usage 32 | 33 | ```node 34 | mx buildLib [options] 打包编译react组件库 35 | mx dev [options] 运行开发环境 36 | mx buildSite [options] 打包编译web项目 37 | mx --help 查看帮助信息 38 | mx --version 查看版本信息 39 | ``` 40 | 41 | ## 详细命令如下 42 | 43 | 在 package.json 的 devDependencies 中加入 44 | 45 | ```javascript 46 | "devDependencies": { 47 | + "@mx-design/cli": "xxx" 48 | } 49 | ``` 50 | 51 | 开发环境配置 52 | 53 | ```javascript 54 | "scripts": { 55 | "start": "mx dev", 56 | }, 57 | ``` 58 | 59 | 为了实现 dev 环境自定义配置,我们还会读取你在根目录的 mx.config.js 文件,案例如下: 60 | 61 | ```javascript 62 | // mx.config.js 63 | const path = require('path'); 64 | 65 | module.exports = { 66 | // 自定义入口文件,必填 67 | entries: { 68 | index: { 69 | entry: ['./src/index.js'], 70 | template: './public/index.html', 71 | favicon: './favicon.ico', 72 | }, 73 | // 别名配置,可省略 74 | resolve: { 75 | alias: { 76 | '@': path.join(process.cwd(), '/'), 77 | }, 78 | }, 79 | // 加入自定义Babel插件 80 | setBabelOptions: (options) => { 81 | options.plugins.push([ 82 | 'prismjs', 83 | { 84 | languages: ['javascript', 'typescript', 'jsx', 'tsx', 'css', 'scss', 'markup', 'bash'], 85 | theme: 'default', 86 | css: true, 87 | }, 88 | ]); 89 | }, 90 | // 加入自定义loader 91 | setRules: (rules) => { 92 | rules.push({ 93 | test: /\.md$/, 94 | use: ['raw-loader'], 95 | }); 96 | }, 97 | }; 98 | 99 | ``` 100 | 101 | 好了,这就配置好开发环境了,是不是很简单,目前我们用的 webpack5 启动开发环境,解放你的 webpack 配置问题。 102 | 103 | build 业务代码更简单 104 | 105 | ```javascript 106 | "scripts": { 107 | "start": "mx buildSite", 108 | } 109 | ``` 110 | 111 | 我们也会读取你根目录下 mx.config.js 文件配置,当然还有一些遍历的命令行选项,比如 112 | 113 | ```javascript 114 | "scripts": { 115 | "start": "mx buildSite --analyzer", // 启用包分析工具 116 | } 117 | 118 | "scripts": { 119 | "start": "mx buildSite --out-dir lib", // 打包后的地址目录默认是dist,这里改成了lib 120 | } 121 | ``` 122 | 123 | 打包组件库命令行如下(以下是建议的配置,命令行输入 npm/yarn run build 即可): 124 | 125 | ```javascript 126 | "scripts": { 127 | "build:types": "rimraf types && tsc --outDir types -d --emitDeclarationOnly", 128 | "build:es": "rimraf esm && mx buildLib --mode esm --entry-dir ./components --less-2-css --copy-less", 129 | "build:cjs": "rimraf lib && mx buildLib --mode cjs --entry-dir ./components --less-2-css --copy-less", 130 | "build:umd": "rimraf dist && mx buildLib --mode umd --entry ./components/index", 131 | "build": "yarn build:types && yarn build:cjs && yarn build:es && yarn build:umd", 132 | } 133 | ``` 134 | 135 | 上面命令解释如下: 136 | 137 | - `--mode cjs` 138 | - 表示打包 cjs 模式 139 | - `--mode esm` 140 | - 表示打包 esm 模式 141 | - `--mode umd` 142 | - 表示打包 umd 模式 143 | - `--mode cjs` 144 | - 表示打包 cjs 模式 145 | - `--less-2-css` 146 | - 表示将 less 转为 css 147 | - `--entry-dir` 148 | - mode 是 esm 和 cjs 生效 149 | - 传入打包时入口目录 默认是 src 150 | - `--entry` 151 | - umd 模式生效 152 | - umd 入口文件 默认是 src/index 153 | - `--copy-less` 154 | - 复制 less 文件到 155 | - `-out-dir-umd` 156 | - 在 mode 是 umd 模式生效 157 | - 输出 umd 格式的目录,默认是`./dist` 158 | - `--out-dir-esm` 159 | - 输出 esm 格式的目录, 默认是`./esm` 160 | - `--out-dir-cjs` 161 | - 输出 cjs 格式的目录,默认`./lib"` 162 | - `--analyzerUmd` 163 | - 是否 webpack 打包启用分析器 164 | 165 | 以上的命令所有详细参数可以这样查看: 166 | 167 | ```javascript 168 | "scripts": { 169 | "buildLibHelp": "mx help buildLib", // 查看所有打包组件库的命令行参数 170 | "buildSiteHelp": "mx help buildSite", // 查看所有webpack打包业务代码的命令行参数 171 | "testHelp": "mx help test", // 查看单元测试所有命令行参数 172 | "devHelp": "mx help dev", // 查看所有dev环境配置参数 173 | } 174 | ``` 175 | 176 | ## 案例 177 | 178 | 在 example 文件夹下有一个案例,安装好包的依赖就可以 npm run dev 启动了 179 | 180 | ## 源码分析 181 | 182 | ### 开始,使用 commander 来读取命令行参数 183 | 184 | 如何创建自己的 mx 命令呢? 185 | 186 | 需要在 package.json 的 bin 字段,加上 187 | 188 | ```json 189 | "bin": { 190 | "mx": "./bin/index.js" 191 | }, 192 | ``` 193 | 194 | 这样,当别人下载你的 npm 包的时候,使用 mx 命令就对应的是调用你 npm 包里 bin 目录下的 index.js,也就是说别人在 package.json 的 script 输入 mx 命令,就相当于调用了 mx-design 包里,bin 目录下的 index.js 了 195 | 196 | 我们看看 index.js 是长什么样子 197 | 198 | ```bash 199 | #!/usr/bin/env node 200 | 201 | require('../lib/index'); 202 | ``` 203 | 204 | 很简单就是调用的 lib 下的 index.ts 文件 205 | 206 | lib 目录使我们最终生成的组件库(比如需要 ts 转译成 js,babel 转译语法什么的),里面的 index.js 就是入口文件。我们看项目里实际开发的 index.ts 入口文件吧。 207 | 208 | 讲解 index.ts 文件之前,我需要介绍一下 commander 这个库的简单用法 209 | 210 | ```javascript 211 | // index.js 212 | const program = require('commander'); 213 | program.version('1.0.0').parse(process.argv); 214 | ``` 215 | 216 | 上面的代码执行`node index.js -V`  或者  `node index.js --version`会得到版本号 1.0.0 217 | program.version('1.0.0')是注册命令的意思,parse 是解析命令行的参数,这里传入 process.argv,意思是解析 process.argv 里的参数,所以我们输入`node index.js --version`,其实就是把参数 version 传给了 commander 218 | 219 | 我们只要取得 package.json 中的 version 字段,所以会得 cli 工具的版本号 220 | 221 | src 目录下的 index.ts(代码解释会写在注释里) 222 | 223 | ```typescript 224 | import commander from 'commander'; 225 | 226 | import { buildLib } from './buildLib/index'; 227 | import { buildSite } from './buildSite/index'; 228 | import { runDev } from './dev/index'; 229 | import { version } from '../package.json'; 230 | 231 | commander.version(version, '-v, --version'); 232 | 233 | buildLib(commander); 234 | buildSite(commander); 235 | runDev(commander); 236 | 237 | commander.parse(process.argv); 238 | 239 | if (!commander.args[0]) { 240 | commander.help(); 241 | } 242 | ``` 243 | 244 | ## 开发环境配置 245 | 246 | 我们先来看看执行 mx dev 时,执行了函数`runDev(commander)`,这个函数的运行流程是什么,runDev 函数如下 247 | 248 | ```javascript 249 | // 当你mx dev时,真正执行的文件是development 250 | import development from './development'; 251 | // DEV就是字符串'dev' 252 | import { DEV } from '../constants'; 253 | 254 | export const runDev = (commander) => { 255 | // commander注册'dev'这个参数的命令 256 | commander 257 | .command(DEV) 258 | .description('运行开发环境') 259 | .option('-h, --host ', '站点主机地址', 'localhost') 260 | // 默认端口号3000 261 | .option('-p, --port ', '站点端口号', '3000') 262 | // 命令最终运行的文件 263 | .action(development); 264 | }; 265 | ``` 266 | 267 | dev 环境的重点来了,development 文件里面是什么 268 | 269 | 这个 development 有 3 个重点问题: 270 | 271 | - 如何写一个 compose 函数,提高你的代码质量,不知道 compose 函数的同学请看这篇文章[终极 compose 函数封装方案](https://juejin.cn/post/6989815456416661534),或者你直接看我下面的代码就明白了 272 | 273 | - 如何启动 WebpackDevServer 274 | - 启动的时候我们会启动默认端口 3000,那如果 3000 端口已经被占用了,我们提前直到 3000 端口占用,并找到一个没有被占用的端口让 webpackDevServer 启动呢? 275 | 276 | ### 第一个问题: 如何写一个优雅的函数迭代器,将配置合并 277 | 278 | 我们这里的 compose 代码如下: 279 | 280 | ```javascript 281 | // 同步函数链 282 | export const syncChainFns = (...fns) => { 283 | const [firstFn, ...otherFns] = fns; 284 | return (...args) => { 285 | if (!otherFns) return firstFn(...args); 286 | return otherFns.reduce((ret, task) => task(ret), firstFn(...args)); 287 | }; 288 | }; 289 | ``` 290 | 291 | 我们写个简单的案例调用一下: 292 | 293 | ```javascript 294 | function add(a, b) { 295 | return a + b; 296 | } 297 | function addVersion(sum) { 298 | return `version: ${sum}.0.0`; 299 | } 300 | 301 | syncChainFns(add, addVersion)(1, 2); // 'version: 3' 302 | ``` 303 | 304 | 也就是我们函数链条就像一个工厂加工货物一样,1 号人员加工后,给后面一个人继续加工,最后得到结果,可以类比 redux 的 compose 函数实现。这样的写法就是函数编程的初步思想,组合思想。 305 | 306 | 我们后续会用这个函数来处理 webpack 配置,因为 webpack 配置可以分为 4 个函数处理 307 | 308 | - 首先有初始化的 webpack dev 配置 309 | - 然后有用户自定义的配置,比如自己建立一个 mx.config.js 文件,作为配置文件 310 | - 是否是 ts 环境,name 就要把 ForkTsCheckerWebpackPlugin 加入到 webpack 的 plugin 里,加快 ts 的编译速度 311 | - 最后交给 webpack 函数编译,这样就生成了最终交给 webpackDevServer 启动的值了 312 | 313 | ### 第二个问题:如何启动 WebpackDevServer 314 | 315 | 我刚才说到生成的最终要启动的文件,webpackDevServer 这样启动,注意,这是 webpack5 的启动方法,跟之前 4 的参数位置不一样 316 | 317 | ```javascript 318 | const serverConfig = { 319 | publicPath: '/', 320 | compress: true, 321 | noInfo: true, 322 | hot: true, 323 | }; 324 | const devServer = new WebpackDevServer(compiler, serverConfig); 325 | ``` 326 | 327 | ### 第三个问题:启动 dev 的端口号被占用了咋办 328 | 329 | 我们使用一个库,用来检测端口是否被占用的库叫 detect,这个库如果发现端口是被占用了,会返回一个没有被占用的端口号 330 | 331 | ```javascript 332 | const resPort = await detect(port, host); 333 | ``` 334 | 335 | 好了,解决了这三个问题,我们简单看下 development 文件,不懂的函数不要紧,大致思路上面已经介绍了,我们后面将里面比较重要的函数。 336 | 337 | ```javascript 338 | import webpack from 'webpack'; 339 | import WebpackDevServer from 'webpack-dev-server'; 340 | import getWebpackConfig from '../config/webpackConfig'; 341 | import { isAddForkTsPlugin, syncChainFns, getProjectConfig } from '../utils'; 342 | import { DEV } from '../constants'; 343 | import { IDevelopmentConfig } from '../interface'; 344 | import detect from 'detect-port-alt'; 345 | 346 | const isInteractive = process.stdout.isTTY; 347 | 348 | async function choosePort(port, host) { 349 | const resPort = await detect(port, host); 350 | if (resPort === Number(port)) { 351 | return resPort; 352 | } 353 | const message = `Something is already running on port ${port}.`; 354 | 355 | if (isInteractive) { 356 | console.log(message); 357 | return resPort; 358 | } 359 | console.log(message); 360 | return null; 361 | } 362 | 363 | export default ({ host, port }: IDevelopmentConfig) => { 364 | const compiler = syncChainFns( 365 | getWebpackConfig, 366 | getProjectConfig, 367 | isAddForkTsPlugin, 368 | webpack 369 | )(DEV); 370 | 371 | const serverConfig = { 372 | publicPath: '/', 373 | compress: true, 374 | noInfo: true, 375 | hot: true, 376 | }; 377 | const runDevServer = async (port) => { 378 | const devServer = new WebpackDevServer(compiler, serverConfig); 379 | const resPort = await choosePort(port, host); 380 | if (resPort !== null) { 381 | devServer.listen(resPort, host, (err) => { 382 | if (err) { 383 | return console.error(err.message); 384 | } 385 | console.warn(`http://${host}:${resPort}\n`); 386 | }); 387 | } 388 | }; 389 | runDevServer(port); 390 | }; 391 | ``` 392 | 393 | ### 打包业务代码脚本解析 394 | 395 | 首先打包业务代码和打包组件库,你知道有什么区别吗? 396 | 397 | 业务组件库,目前来说,还是用 webpack 是最合适的选择之一,因为我们业务上线的代码需要的是稳定性,webpack 生态和生态的稳定性是很多打包工具所不具备的,不需要开发环境的效率问题(webpack5 比 4 快很多了),比如有人选择开发环境用 vite。 398 | 399 | 业务代码一般使用 umd 格式打包就行了。 400 | 401 | 而组件库代码,比如 ant design,element ui,这些库不仅仅需要 umd 格式,最需要的是 esm module,导出的是 import 语法,这个 webpack 是做不了的。为啥做不了,是因为 webpack 有自己的一套 require 规则,你用的 import 最终还是要被 webpack 这套加载模块语法转译了。 402 | 403 | 所以 esm module 你可以用 roll up,但是但是,我仔细调研了一番,多入口打包 rollup 是不支持的,而且我们需要在 css 打包上苦费心思一番,后面讲,打包 css 是非常非常讲究的,rollup 不好满足,所以我们后续直接使用 gulp 来分别打包 css 和 js 了。 404 | 405 | 就是因为定制化要求很高,不得不用 glup 去定制化打包流程。 406 | 407 | 我们先看看更简单的打包业务代码脚本的入口 408 | 409 | ```javascript 410 | import build from './buildSite'; 411 | import { BUILD_SITE } from '../constants'; 412 | 413 | export const buildSite = (commander) => { 414 | // 打包业务组件 415 | // 这个命令实际上执行的是buildSite这个文件 416 | commander 417 | .command(BUILD_SITE) 418 | .description('打包业务代码') 419 | .option('-d, --out-dir ', '输出目录', 'dist') 420 | .option('-a, --analyzer', '是否启用分析器') 421 | .action(build); 422 | }; 423 | ``` 424 | 425 | 接着,我们看看 build 文件,以下主要解释的是 getWebpackConfig 文件,和 getProjectConfig 文件的代码 426 | 427 | ```javascript 428 | import webpack from 'webpack'; 429 | // webpack代码打包分析插件 430 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 431 | // 获取webpack基础配置 432 | import getWebpackConfig from '../config/webpackConfig'; 433 | // 获取webpack定制化的配置 434 | import { getProjectPath, getProjectConfig, syncChainFns } from '../utils'; 435 | // 接口配置 436 | import { IDeployConfig } from '../interface'; 437 | // 这个常量是字符串“buildSite” 438 | import { BUILD_SITE } from '../constants'; 439 | export default ({ outDir, analyzer }: IDeployConfig) => { 440 | // 这个syncChainFns函数上面已经介绍过了,就是一个函数组合的组合器 441 | const config = syncChainFns( 442 | // 这个函数后面会讲到,就是获取不同环境下webpack的配置文件 443 | getWebpackConfig, 444 | // 这个函数后面会讲到,用来获取用户自定义的webpck配置文件 445 | getProjectConfig, 446 | // 判断是否需要加入 加快ts的解析的插件 447 | isAddForkTsPlugin 448 | )(BUILD_SITE); 449 | config.output.path = getProjectPath(outDir); 450 | 451 | // 是否启用代码包体积分析插件 452 | if (analyzer) { 453 | config.plugins.push( 454 | new BundleAnalyzerPlugin({ 455 | analyzerMode: 'static', 456 | generateStatsFile: true, 457 | }) 458 | ); 459 | } 460 | 461 | webpack(config).run((err) => { 462 | if (err) { 463 | logger.error('webpackError: ', JSON.stringify(err)); 464 | } 465 | }); 466 | }; 467 | ``` 468 | 469 | 以下是 getWebpackConfig 代码,比较简单,工厂模式的运用,很简单,就是根据命令行不同的参数调用不同的函数,比如 mx dev,就调用的 getDevConfig 函数,获取 webpack 在 dev 环境的配置 470 | 471 | ```typescirpt 472 | const getWebpackConfig = (type?: IWebpackConfigType): Configuration => { 473 | switch (type) { 474 | case DEV: 475 | return getDevConfig(); 476 | 477 | case BUILD_SITE: 478 | return getBuildConfig(); 479 | 480 | case BUILD_LIB: 481 | return getBuildConfig(); 482 | 483 | default: 484 | return getDevConfig(); 485 | } 486 | }; 487 | ``` 488 | 489 | getProjectConfig 主要是提供给用户自定配置的函数,我们主要分析一下如何拿到用户的自定义配置. 490 | 491 | ```javascript 492 | export const getCustomConfig = ( 493 | configFileName = 'mx.config.js' 494 | ): Partial => { 495 | const configPath = path.join(process.cwd(), configFileName); 496 | if (fs.existsSync(configPath)) { 497 | // eslint-disable-next-line import/no-dynamic-require 498 | return require(configPath); 499 | } 500 | return {}; 501 | }; 502 | ``` 503 | 504 | 可以看到,就是读取项目下的 mx.config.js,我们看看 mx.config.js 的写法,很简单就是假如自己想要插件和 plugin,以及入口配置。 505 | 506 | ```javascript 507 | const path = require('path'); 508 | 509 | module.exports = { 510 | entries: { 511 | index: { 512 | entry: ['./web/index.js'], 513 | template: './web/index.html', 514 | favicon: './favicon.ico', 515 | }, 516 | }, 517 | resolve: { 518 | alias: { 519 | '@': path.join(process.cwd(), '/'), 520 | }, 521 | }, 522 | setBabelOptions: (options) => { 523 | options.plugins.push(['import', { libraryName: 'antd', style: 'css' }]); 524 | }, 525 | setRules: (rules) => { 526 | rules.push({ 527 | test: /\.md$/, 528 | use: ['raw-loader'], 529 | }); 530 | }, 531 | }; 532 | ``` 533 | 534 | ### 打包组件库的核心配置文件 535 | 536 | 打包组件库的代码要比之前的复杂很多! 537 | 老规矩,看下入口文件 538 | 539 | ```javascript 540 | import build from './build'; 541 | import { BUILD_LIB } from '../constants'; 542 | 543 | export const buildLib = (commander) => { 544 | // 当你输入mx buildLib的时候,就是执行这个命令 545 | // 这个命令实际上执行的是build文件 546 | // 我们会打包es和commonjs规范的两个包 547 | commander 548 | .command(BUILD_LIB) 549 | .description('打包编译仓库') 550 | .option('-a, --analyzerUmd', '是否启用webpack打包分析器') 551 | .option('-e, --entry ', 'umd打包路径入口文件', './src/index') 552 | .option('--output-name ', '打包Umd格式后对外暴露的名称') 553 | .option('--entry-dir ', 'cjs和esm打包路径入口目录', './src') 554 | .option('--out-dir-umd ', '输出umd格式的目录', './dist') 555 | .option('--out-dir-esm ', '输出esm格式的目录', './esm') 556 | .option('--out-dir-cjs ', '输出cjs格式的目录', './lib') 557 | .option('--copy-less', '拷贝不参与编译的文件') 558 | .option('--less-2-css', '是否编译组件样式') 559 | .option('-m, --mode ', '打包模式 目前支持umd和esm两种') 560 | .action(build); 561 | }; 562 | ``` 563 | 564 | 我们看下 build 文件,也就是你输入 mx buildLib 后,执行的文件,我们先看看 umd 的打包,这个简单,稍微复杂一些的是 glup 配置。 565 | 566 | ```javascript 567 | import webpack from 'webpack'; 568 | import webpackMerge from 'webpack-merge'; 569 | // gulp任务,后面会讲 570 | import { copyLess, less2css, buildCjs, buildEsm } from '../config/gulpConfig'; 571 | import getWebpackConfig from '../config/webpackConfig'; 572 | // 工具函数,后面用到就讲 573 | import { getProjectPath, logger, run, compose } from '../utils'; 574 | // 代码包体积分析插件 575 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 576 | // 环境常量 577 | import { 578 | BUILD_LIB, 579 | CJS, 580 | ESM, 581 | UMD, 582 | COPY_LESS, 583 | LESS_2_LESS, 584 | CLEAN_DIR, 585 | } from '../constants'; 586 | 587 | // package.json的name属性作为打包出来的包名,当然也可以自定义 588 | const { name } = require(getProjectPath('package.json')); 589 | // 校验name是否有斜杠,这会影响打包出来的结果 590 | const checkName = (outputName, name) => { 591 | if (!outputName && name?.includes('/')) { 592 | logger.warn( 593 | 'package.json的包名包含斜杠,webpack打包时会以斜杠来建立文件夹,所以请注意打包后文件名是否符合你的要求' 594 | ); 595 | } 596 | }; 597 | /** 598 | * build for umd 599 | * @param analyzer 是否启用分析包插件 600 | * @param outDirUmd 输出目录 601 | * @param entry 打包的入口文件 602 | * @param outputName 打包出来的名字 603 | */ 604 | const buildUmd = async ({ analyzerUmd, outDirUmd, entry, outputName }) => { 605 | const customizePlugins = []; 606 | const realName = outputName || name; 607 | checkName(outputName, name); 608 | const umdTask = (type) => { 609 | return new Promise((resolve, reject) => { 610 | const config = webpackMerge(getWebpackConfig(type), { 611 | entry: { 612 | [realName]: getProjectPath(entry), 613 | }, 614 | // 这里主要是设置libraryTarget是设置打包格式是umd 615 | // library是配置打包出来的包名的 616 | output: { 617 | path: getProjectPath(outDirUmd), 618 | library: realName, 619 | libraryTarget: 'umd', 620 | libraryExport: 'default', 621 | }, 622 | plugins: customizePlugins, 623 | }); 624 | 625 | if (analyzerUmd) { 626 | config.plugins.push( 627 | new BundleAnalyzerPlugin({ 628 | analyzerMode: 'static', 629 | generateStatsFile: true, 630 | }) 631 | ); 632 | } 633 | return webpack(config).run((err, stats) => { 634 | if (stats.compilation.errors?.length) { 635 | console.log('webpackError: ', stats.compilation.errors); 636 | } 637 | if (err) { 638 | logger.error('webpackError: ', JSON.stringify(err)); 639 | reject(err); 640 | } else { 641 | resolve(stats); 642 | } 643 | }); 644 | }); 645 | }; 646 | logger.info('building umd'); 647 | await umdTask(BUILD_LIB); 648 | logger.success('umd computed'); 649 | }; 650 | ``` 651 | 652 | 接下来讲最复杂的 gulp 配置,先看入口文件: 653 | 654 | - 之前我们先解决写一个类似 koa 的框架的 compose 函数,这个函数是一个函数执行器,把各个异步函数按顺序调用,比如说有异步函数 1,异步函数 2,异步函数 3,我需要按照顺序调用 1,2,3,并且这 1,2,3 是解耦的,类似中间件的形式加入,并共享一些数据 655 | 656 | 我们先看看函数: 657 | 658 | ```javascript 659 | export function compose(middleware, initOptions) { 660 | const otherOptions = initOptions || {}; 661 | function dispatch(index) { 662 | if (index == middleware.length) return; 663 | const currMiddleware = middleware[index]; 664 | return currMiddleware(() => dispatch(++index), otherOptions); 665 | } 666 | dispatch(0); 667 | } 668 | ``` 669 | 670 | 这个函数的意思是: 671 | 672 | - 按数组顺序拿到 middleware 函数 673 | - 然后函数调用时,第一个参数传入下一个调用的函数,主动调用才会执行 middleware 下一个函数,并且把一个去去全局共享数据 otherOptions 传入下去。 674 | 675 | 下面是利用 compose 函数执行各个函数的文件,也就是 mx buildLib 真正执行的文件,文件内容太多,我就拿一个 build esm 来解释 676 | 677 | ```javascript 678 | import webpack from 'webpack'; 679 | import webpackMerge from 'webpack-merge'; 680 | // gulp任务,后面会讲 681 | import { copyLess, less2css, buildCjs, buildEsm } from '../config/gulpConfig'; 682 | import getWebpackConfig from '../config/webpackConfig'; 683 | // 工具函数,后面用到就讲 684 | import { getProjectPath, logger, run, compose } from '../utils'; 685 | // 代码包体积分析插件 686 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 687 | // 环境常量 688 | import { 689 | BUILD_LIB, 690 | CJS, 691 | ESM, 692 | UMD, 693 | COPY_LESS, 694 | LESS_2_LESS, 695 | CLEAN_DIR, 696 | } from '../constants'; 697 | 698 | const buildLib = async ({ 699 | analyzerUmd, 700 | mode, 701 | entry, 702 | outDirEsm, 703 | outDirCjs, 704 | outDirUmd, 705 | copyLess, 706 | entryDir, 707 | less2Css, 708 | cleanDir, 709 | outputName, 710 | }) => { 711 | // 注册中间件,然后用compose函数去组合 712 | const buildProcess = [bulidLibFns[CLEAN_DIR]]; 713 | // 是否打包umd格式,是的话加入我们之前讲的umd打包函数 714 | if (mode === UMD) { 715 | buildProcess.push(bulidLibFns[UMD]); 716 | } 717 | // 是否打包esm格式,是的话加入相应打包函数, 718 | if (mode === ESM) { 719 | buildProcess.push(bulidLibFns[ESM]); 720 | } 721 | // 省略一些代码,就是来加入各种处理函数,比如有编译less到css的中间件是否加入 722 | compose(buildProcess, { 723 | analyzerUmd, 724 | mode, 725 | entry, 726 | outDirEsm, 727 | outDirCjs, 728 | outDirUmd, 729 | copyLess, 730 | entryDir, 731 | less2Css, 732 | cleanDir, 733 | outputName, 734 | }); 735 | }; 736 | 737 | export default buildLib; 738 | ``` 739 | 740 | 我们看看 gulp 配置文件 buildesm,主要执行的是 compileScripts 函数,这个函数我们接着看 741 | 742 | ```javascript 743 | const buildEsm = ({ mode, outDirEsm, entryDir }) => { 744 | const newEntryDir = getNewEntryDir(entryDir); 745 | /** 746 | * 编译esm 747 | */ 748 | gulp.task('compileESM', () => { 749 | return compileScripts(mode, outDirEsm, newEntryDir); 750 | }); 751 | 752 | return new Promise((res) => { 753 | return gulp.series('compileESM', () => { 754 | res(true); 755 | })(); 756 | }); 757 | }; 758 | ``` 759 | 760 | ```javascript 761 | /** 762 | * 编译脚本文件 763 | * @param {string} babelEnv babel环境变量 764 | * @param {string} destDir 目标目录 765 | * @param {string} newEntryDir 入口目录 766 | */ 767 | function compileScripts(mode, destDir, newEntryDir) { 768 | const { scripts } = paths; 769 | return gulp 770 | .src(scripts(newEntryDir)) // 找到入口文件 771 | .pipe(babel(mode === ESM ? babelEsConfig : babelCjsConfig)) // 使用gulp-babel处理 772 | .pipe( 773 | // 使用gulp处理css 774 | through2.obj(function z(file, encoding, next) { 775 | this.push(file.clone()); 776 | // 找到目标 777 | if (file.path.match(/(\/|\\)style(\/|\\)index\.js/)) { 778 | const content = file.contents.toString(encoding); 779 | file.contents = Buffer.from(cssInjection(content)); // 处理文件内容 780 | file.path = file.path.replace(/index\.js/, 'css.js'); // 文件重命名 781 | this.push(file); // 新增该文件 782 | next(); 783 | } else { 784 | next(); 785 | } 786 | }) 787 | ) 788 | .pipe(gulp.dest(destDir)); 789 | } 790 | ``` 791 | 792 | ## webpack 配置详解 793 | 794 | ### output 795 | 796 | ``` 797 | output: { 798 | filename: 'js/[name].js', 799 | chunkFilename: 'js/[name].[chunkhash:8].js', 800 | assetModuleFilename: 'asset/[name].[contenthash:8].[ext]', 801 | } 802 | ``` 803 | 804 | - filename 指定了打包后生成的主代码文件的名称。这里的[name]表示使用入口文件(entry)的名称作为文件名,输出到 js 文件夹下。 805 | 806 | - chunkFilename 则指定了 webpack 打包生成的非入口文件的文件名,例如按需加载(code splitting)生成的代码块。这里使用了占位符[chunkhash:8]来确保文件名的唯一性和缓存效果。 807 | 808 | - assetModuleFilename 用于指定 webpack 打包时处理资源文件(图片、字体等)的输出路径和名称。占位符[contenthash:8]用于确保资源文件的缓存效果。[ext]用于保留文件扩展名。 809 | 810 | 以上配置将会把所有的 JS 文件打包到 js 目录下,其他类型的资源文件打包到 asset 目录下。 811 | 812 | ### optimization 813 | 814 | ```javascript 815 | optimization: { 816 | runtimeChunk: true, 817 | splitChunks: { 818 | minChunks: 2, 819 | chunks: 'all', 820 | cacheGroups: { 821 | reactBase: { 822 | name: 'reactBase', 823 | chunks: 'all', 824 | test: /[\\/]node_modules[\\/](react|react-dom|@hot-loader|react-router|react-redux|react-router-dom)[\\/]/, 825 | }, 826 | 'async-commons': { 827 | // 异步加载公共包、组件等 828 | name: 'async-commons', 829 | chunks: 'async', 830 | test: /[\\/]node_modules[\\/]/, 831 | minChunks: 2, 832 | priority: 1, 833 | }, 834 | }, 835 | }, 836 | }, 837 | ``` 838 | 839 | - runtimeChunk: true 配置项用于将 Webpack 运行时代码打包成单独的文件,避免每个模块都包含重复的运行时代码。这可以提高缓存利用率并加快构建速度。 840 | 841 | 什么是运行时代码?Webpack 运行时代码是指 Webpack 在打包时生成的一些代码片段,用于处理模块加载和解析、依赖关系管理、代码分割、以及其他一些 Webpack 内部的功能。例如: 842 | 843 | ```javascript 844 | // Webpack运行时代码片段1:模块加载 845 | (function(modules) { 846 | // 模块缓存对象 847 | var installedModules = {}; 848 | 849 | // 加载模块函数 850 | function __webpack_require__(moduleId) { 851 | // 检查模块是否被缓存 852 | if(installedModules[moduleId]) { 853 | return installedModules[moduleId].exports; 854 | } 855 | 856 | // 创建一个新的模块对象,并将其缓存 857 | var module = installedModules[moduleId] = { 858 | i: moduleId, 859 | l: false, 860 | exports: {} 861 | }; 862 | 863 | // 加载模块 864 | modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 865 | 866 | // 标记模块已经加载完毕 867 | module.l = true; 868 | 869 | // 返回模块的导出对象 870 | return module.exports; 871 | } 872 | ... 873 | ``` 874 | 875 | - splitChunks 配置项用于将代码拆分成更小的块,以便更好地利用浏览器的缓存机制。具体来说,这里配置了以下内容: 876 | 877 | - minChunks: 2 表示最小的代码复用次数,即当一个模块至少被两个 Chunk 引用时,才会被拆分出来成为一个单独的 Chunk。 878 | 879 | - chunks: 'all' 表示优化所有类型的 Chunk,包括同步和异步的 Chunk。 880 | 881 | - cacheGroups 表示缓存组,可以将满足特定条件的模块打包到一个组里,以便更好地进行拆分和缓存。这里定义了两个缓存组: 882 | 883 | - reactBase 缓存组将 React 相关的模块打包到一个名为 reactBase 的 Chunk 中,并且仅包含在 node_modules 目录下的 React 相关模块。 884 | 885 | - async-commons 缓存组将其他的公共模块打包到一个名为 async-commons 的异步 Chunk 中,它仅包含在 node_modules 目录下的模块,并且至少被两个异步 Chunk 所引用。该缓存组的优先级为 1,以确保它被拆分到一个单独的 Chunk 中。 886 | 887 | ## loader 888 | 889 | 上面提到大家可以通过 mx.config.js 来拓展 loader,mx-design-cli 本身自带的 loader 有 890 | 891 | - babel-loader 892 | - test 规则: /\.(js|jsx|ts|tsx)$/ 893 | - 通过 thread-loader 来加快编译速度 894 | - css 相关 loader 895 | - css loader 896 | - postcss-loader 897 | - less-loader 898 | - 图片 899 | - webpack5 内置了 loader 900 | - test 规则: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/] 901 | - 小于 4 \* 1024 会编译为 date URL 格式 902 | - 字体 903 | - webpack5 内置了 loader 904 | - test 规则: /\.(eot|ttf|woff|woff2?)$/ 905 | - svg 906 | - @svgr/webpack loader 907 | 908 | ## resolve 909 | 910 | ```javascript 911 | resolve: { 912 | extensions: [' ', '.ts', '.tsx', '.js', '.jsx', '.less', '.svg'], 913 | }, 914 | ``` 915 | 916 | r 这个选项告诉 Webpack 解析模块时需要查找的文件扩展名。在这个例子中,Webpack 会依次查找以下文件扩展名:.ts、.tsx、.js、.jsx、.less、.svg。当我们在 import 语句中引用这些文件时,Webpack 就会根据这个选项来解析模块路径。 917 | 918 | ## plugins 919 | 920 | ```javascript 921 | plugins: [ 922 | new WebpackBar({}), 923 | new webpack.DefinePlugin({ 924 | 'process.env': JSON.stringify(process.env), 925 | }), 926 | ], 927 | ``` 928 | 929 | ,我们使用了 WebpackBar 插件和 DefinePlugin 插件。WebpackBar 插件可以在命令行中显示进度条,让我们更加直观地了解 Webpack 的构建进度。DefinePlugin 插件可以在编译时定义全局变量,这里我们将 process.env 定义为当前环境变量,使得我们的代码能够根据不同的环境变量执行不同的逻辑。 930 | 931 | 以上是基础的 webpack 配置,生产和开发环境会有不同的处理,例如 932 | 933 | ### 开发环境 934 | 935 | 开发环境比如需要热更新的 plugin,sourceMap 内容会更详细等等... 936 | 937 | ### 生产环境(业务代码打包,非组件库) 938 | 939 | 需要做好 split chunk 更好的利用缓存去存储 node_modules 下的库(因为变化较少),还比如,可以用 TerserPlugin 开启多线程打包,CssMinimizerPlugin 开启 css 压缩等等... 940 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

@mx-design/cli

2 | 3 | [English](./README.md) | [中文](./README.zh.md) 4 | 5 | [Source code analysis](#source_code_analysis) 6 | 7 | ## Introduction 8 | 9 | - ✨ easy to develop: one line command to start react + less project 10 | - ✨ easy to pack business code: one line of command to package react business projects, using webpack5, no need to pay attention to webpack configuration and optimization, we have done it for you 11 | - ✨ easy to package react component library: one line of command to package react component library, the packaging method is the same as ant design (package component library should be loaded on demand, which is different from the configuration of business code packaging) 12 | 13 | ## Project Description 14 | 15 | - Use @mx-deisgn/cli to quickly start the development environment and package the project (the case is under the example folder) 16 | 17 | ## Install 18 | 19 | use `npm` 20 | 21 | ```node 22 | npm install @mx-design/cli --save-dev 23 | ``` 24 | 25 | or use `yarn` 26 | 27 | ```node 28 | yarn add @mx-design/cli --dev 29 | ``` 30 | 31 | ## Usage 32 | 33 | ```node 34 | mx buildLib [options] package and compile react component library 35 | mx dev [options] run the development environment 36 | mx buildSite [options] package and compile web projects 37 | mx --help view help information 38 | mx --version view version information 39 | ``` 40 | 41 | ## Detailed commands are as follows 42 | 43 | Add in devDependencies of package.json 44 | 45 | ```javascript 46 | "devDependencies": { 47 | + "@mx-design/cli": "xxx" 48 | } 49 | ``` 50 | 51 | Development environment configuration 52 | 53 | ```javascript 54 | "scripts": { 55 | "start": "mx dev", 56 | }, 57 | ``` 58 | 59 | In order to realize the custom configuration of the dev environment, we will also read your mx.config.js file in the root directory, as follows: 60 | 61 | ```javascript 62 | //mx.config.js 63 | const path = require('path'); 64 | 65 | module.exports = { 66 | // Custom entry file, required 67 | entries: { 68 | index: { 69 | entry: ['./src/index.js'], 70 | template: './public/index.html', 71 | favicon: './favicon.ico', 72 | }, 73 | // Alias configuration, can be omitted 74 | resolve: { 75 | alias: { 76 | '@': path. join(process. cwd(), '/'), 77 | }, 78 | }, 79 | // Add a custom Babel plugin 80 | setBabelOptions: (options) => { 81 | options.plugins.push([ 82 | 'prismjs', 83 | { 84 | languages: ['javascript', 'typescript', 'jsx', 'tsx', 'css', 'scss', 'markup', 'bash'], 85 | theme: 'default', 86 | css: true, 87 | }, 88 | ]); 89 | }, 90 | // add custom loader 91 | setRules: (rules) => { 92 | rules. push({ 93 | test: /\.md$/, 94 | use: ['raw-loader'], 95 | }); 96 | }, 97 | }; 98 | 99 | ``` 100 | 101 | Well, this is how to configure the development environment. Isn’t it very simple? Currently, we use webpack5 to start the development environment.No need to configure webpack yourself. 102 | 103 | Build business code is simpler 104 | 105 | ```javascript 106 | "scripts": { 107 | "start": "mx buildSite", 108 | } 109 | ``` 110 | 111 | We will also read the mx.config.js file configuration in your root directory, and of course some command line options for traversal, such as 112 | 113 | ```javascript 114 | "scripts": { 115 | "start": "mx buildSite --analyzer", // enable package analyzer 116 | } 117 | 118 | "scripts": { 119 | "start": "mx buildSite --out-dir lib", // The packaged address directory defaults to dist, here it is changed to lib 120 | } 121 | ``` 122 | 123 | The command line of the packaged component library is as follows (the following are the recommended configurations, just enter npm/yarn run build on the command line): 124 | 125 | ```javascript 126 | "scripts": { 127 | "build:types": "rimraf types && tsc --outDir types -d --emitDeclarationOnly", 128 | "build:es": "rimraf esm && mx buildLib --mode esm --entry-dir ./components --less-2-css --copy-less", 129 | "build:cjs": "rimraf lib && mx buildLib --mode cjs --entry-dir ./components --less-2-css --copy-less", 130 | "build:umd": "rimraf dist && mx buildLib --mode umd --entry ./components/index", 131 | "build": "yarn build:types && yarn build:cjs && yarn build:es && yarn build:umd", 132 | } 133 | ``` 134 | 135 | The above command is explained as follows: 136 | 137 | - `--mode cjs` 138 | - pack commonjs 139 | - `--mode esm` 140 | - pack es module 141 | - `--mode umd` 142 | - pack umd 143 | - `--less-2-css` 144 | - convert less to css 145 | - `--entry-dir` 146 | - mode is valid for esm and cjs 147 | - The entry directory when passing in packaging, the default is src 148 | - `--entry` 149 | - umd mode takes effect 150 | - umd entry file default is src/index 151 | - `--copy-less` 152 | - copy the less file to 153 | - `-out-dir-umd` 154 | - It takes effect when the mode is umd mode 155 | - output directory in umd format, the default is `./dist` 156 | - `--out-dir-esm` 157 | - Output directory in esm format, the default is `./esm` 158 | - `--out-dir-cjs` 159 | - Output directory in cjs format, default `./lib'` 160 | - `--analyzerUmd` 161 | - Whether to enable analyzers for webpack packaging 162 | 163 | All the detailed parameters of the above command can be viewed as follows: 164 | 165 | ```javascript 166 | "scripts": { 167 | "buildLibHelp": "mx help buildLib", // View command line parameters of all packaged component libraries 168 | "buildSiteHelp": "mx help buildSite", // View all command line parameters of 169 | "devHelp": "mx help dev", // View all dev environment configuration parameters 170 | } 171 | ``` 172 | 173 | ## case 174 | 175 | There is a case under the example folder, after installing the dependencies of the package, you can start it with `npm run dev` 176 | 177 | ## Source_code_analysis 178 | 179 | ### use commander to read command line parameters 180 | 181 | How about creating your own mx command? 182 | 183 | In the bin field of package.json, add 184 | 185 | ```json 186 | "bin": { 187 | "mx": "./bin/index.js" 188 | }, 189 | ``` 190 | 191 | In this way, when someone downloads your npm package, and use the mx command, it will execute index.js in the bin directory of your npm package. 192 | 193 | Let's see what index.js looks like 194 | 195 | ```bash 196 | #!/usr/bin/env node 197 | 198 | require('../lib/index'); 199 | ``` 200 | 201 | It is very simple to execute the index.ts file under the lib 202 | 203 | The lib directory is our final generated component library (for example, we need to compile ts into js, babel compile syntax, etc.), and the index.js inside is the entry file. Let's look at the index.ts. 204 | 205 | Before explaining the index.ts file, I need to introduce the simple usage of the commander library 206 | 207 | ```javascript 208 | // index.js 209 | const program = require('commander'); 210 | program.version('1.0.0').parse(process.argv); 211 | ``` 212 | 213 | Executing the above code `node index.js -V` or `node index.js --version` will get the version number 1.0.0 214 | program.version('1.0.0') means to register the command, and parse means to parse the parameters of the command line, here pass in process.argv, which means to parse the parameters in process.argv, so we enter `node index.js - -version`, in fact, is to pass the parameter version to the commander 215 | 216 | We only need to get the version field in package.json, so we will get the version number of the cli tool 217 | 218 | index.ts in the src directory 219 | 220 | ```typescript 221 | import commander from 'commander'; 222 | 223 | import { buildLib } from './buildLib/index'; 224 | import { buildSite } from './buildSite/index'; 225 | import { runDev } from './dev/index'; 226 | import { version } from '../package.json'; 227 | 228 | commander.version(version, '-v, --version'); 229 | 230 | buildLib(commander); 231 | buildSite(commander); 232 | runDev(commander); 233 | 234 | commander.parse(process.argv); 235 | 236 | if (!commander.args[0]) { 237 | commander.help(); 238 | } 239 | ``` 240 | 241 | ## Development environment configuration 242 | 243 | Let's first take a look at the function `runDev(commander)` executed when mx dev is executed. What is the running process of this function? The runDev function is as follows 244 | 245 | ```javascript 246 | // When you execute mx dev, development code will execute 247 | import development from './development'; 248 | import { DEV } from '../constants'; 249 | 250 | export const runDev = (commander) => { 251 | // Commander registers the command with the parameter 'dev' 252 | commander 253 | .command(DEV) 254 | .description('Run development environment') 255 | .option('-h, --host ', 'site host address', 'localhost') 256 | // The default port number is 3000 257 | .option('-p, --port ', 'site port number', '3000') 258 | // The file that the command will eventually run on 259 | .action(development); 260 | }; 261 | ``` 262 | 263 | This development has 3 key issues: 264 | 265 | - How to write a compose function to improve your code quality, students who do not know the compose function, please read this article [Ultimate Compose Function Encapsulation Solution](https://juejin.cn/post/6989815456416661534), or you can just look at me The following code will understand 266 | 267 | - How to start WebpackDevServer 268 | - When starting, we will start the default port 3000, so if port 3000 is already occupied, we will advance until port 3000 is occupied, and find an unoccupied port for webpackDevServer to start? 269 | 270 | ### The first question: How to write an elegant function iterator to merge configurations 271 | 272 | Our compose code here is as follows: 273 | 274 | ```javascript 275 | // synchronous function chain 276 | export const syncChainFns = (...fns) => { 277 | const [firstFn, ...otherFns] = fns; 278 | return (...args) => { 279 | if (!otherFns) return firstFn(...args); 280 | return otherFns.reduce((ret, task) => task(ret), firstFn(...args)); 281 | }; 282 | }; 283 | ``` 284 | 285 | Let's write a simple case call: 286 | 287 | ```javascript 288 | function add(a, b) { 289 | return a + b; 290 | } 291 | function addVersion(sum) { 292 | return `version: ${sum}.0.0`; 293 | } 294 | 295 | syncChainFns(add, addVersion)(1, 2); // 'version: 3' 296 | ``` 297 | 298 | That is to say, our function chain is like a factory processing goods. After No. 1 person processes it, he will continue to process it for the next person, and finally get the result, which can be realized by analogy with redux's compose function. This is the idea of functional programming. 299 | 300 | We will use this function to process webpack configuration later, because webpack configuration can be divided into 4 functions for processing 301 | 302 | - First there is an initial webpack dev configuration 303 | - Then there are user-defined configurations, such as creating a mx.config.js file yourself as a configuration file 304 | - Whether it is a ts environment, the name must add ForkTsCheckerWebpackPlugin to the webpack plugin to speed up the compilation of ts 305 | - Finally, it is handed over to the webpack function for compilation, so that the value that is finally handed over to webpackDevServer to start is generated 306 | 307 | ### The second question: How to start WebpackDevServer 308 | 309 | note that this is the starting method of webpack5, which is different from the parameter position of the previous 4 310 | 311 | ```javascript 312 | const serverConfig = { 313 | publicPath: '/', 314 | compress: true, 315 | noInfo: true, 316 | hot: true, 317 | }; 318 | const devServer = new WebpackDevServer(compiler, serverConfig); 319 | ``` 320 | 321 | ### The third question: What should I do if the port number used to start dev is occupied? 322 | 323 | We use a library called detect to detect whether the port is occupied. If the library finds that the port is occupied, it will return an unoccupied port number 324 | 325 | ```javascript 326 | const resPort = await detect(port, host); 327 | ``` 328 | 329 | Well, these three problems have been solved. Let’s take a brief look at the development file. 330 | 331 | ```javascript 332 | import webpack from 'webpack'; 333 | import WebpackDevServer from 'webpack-dev-server'; 334 | import getWebpackConfig from '../config/webpackConfig'; 335 | import { isAddForkTsPlugin, syncChainFns, getProjectConfig } from '../utils'; 336 | import { DEV } from '../constants'; 337 | import { IDevelopmentConfig } from '../interface'; 338 | import detect from 'detect-port-alt'; 339 | 340 | const isInteractive = process.stdout.isTTY; 341 | 342 | async function choosePort(port, host) { 343 | const resPort = await detect(port, host); 344 | if (resPort === Number(port)) { 345 | return resPort; 346 | } 347 | const message = `Something is already running on port ${port}.`; 348 | 349 | if (isInteractive) { 350 | console.log(message); 351 | return resPort; 352 | } 353 | console.log(message); 354 | return null; 355 | } 356 | 357 | export default ({ host, port }: IDevelopmentConfig) => { 358 | const compiler = syncChainFns( 359 | getWebpackConfig, 360 | getProjectConfig, 361 | isAddForkTsPlugin, 362 | webpack 363 | )(DEV); 364 | 365 | const serverConfig = { 366 | publicPath: '/', 367 | compress: true, 368 | noInfo: true, 369 | hot: true, 370 | }; 371 | const runDevServer = async (port) => { 372 | const devServer = new WebpackDevServer(compiler, serverConfig); 373 | const resPort = await choosePort(port, host); 374 | if (resPort !== null) { 375 | devServer.listen(resPort, host, (err) => { 376 | if (err) { 377 | return console.error(err.message); 378 | } 379 | console.warn(`http://${host}:${resPort}\n`); 380 | }); 381 | } 382 | }; 383 | runDevServer(port); 384 | }; 385 | ``` 386 | 387 | ### Packaging business code script analysis 388 | 389 | packaging the business code and packaging the component library, do you know the difference? 390 | 391 | For the business component library, at present, using webpack is one of the most suitable choices, because the code for our business launch needs stability. The ecology and ecological stability of webpack are not available in many packaging tools, and no development is required. The efficiency of the environment (webpack5 is much faster than 4), for example, some people choose to use vite for the development environment. 392 | 393 | Business codes are generally packaged in umd format. 394 | 395 | The component library code, such as ant design, element ui, these libraries not only need the umd format, but also the esm module, which exports the import syntax, which cannot be done by webpack. The reason why it can't be done is because webpack has its own set of require rules, and the import you use will eventually be translated by webpack's set of loading module syntax. 396 | 397 | So you can use roll up for the esm module, but after careful research, I found that multi-entry packaging rollup is not supported, and we need to work hard on css packaging. Later, packaging css is very, very particular Yes, rollup is not enough, so we directly use gulp to package css and js separately. 398 | 399 | It is because of the high customization requirements that we have to use glup to customize the packaging process. 400 | 401 | Let's take a look at the entry of the simpler packaged business code script 402 | 403 | ```javascript 404 | import build from './buildSite'; 405 | import { BUILD_SITE } from '../constants'; 406 | 407 | export const buildSite = (commander) => { 408 | // package business components 409 | // This command actually executes the buildSite file 410 | commander 411 | .command(BUILD_SITE) 412 | .description('Package business code') 413 | .option('-d, --out-dir ', 'Output directory', 'dist') 414 | .option('-a, --analyzer', 'whether to enable the analyzer') 415 | .action(build); 416 | }; 417 | ``` 418 | 419 | Next, let's look at the build file. The following mainly explains the code of the getWebpackConfig file and the getProjectConfig file 420 | 421 | ```javascript 422 | import webpack from 'webpack'; 423 | // webpack code packaging analysis plugin 424 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 425 | // Get webpack basic configuration 426 | import getWebpackConfig from '../config/webpackConfig'; 427 | import { getProjectPath, getProjectConfig, syncChainFns } from '../utils'; 428 | // interface configuration 429 | import { IDeployConfig } from '../interface'; 430 | // This constant is the string "buildSite" 431 | import { BUILD_SITE } from '../constants'; 432 | export default ({ outDir, analyzer }: IDeployConfig) => { 433 | // This syncChainFns function has been introduced above, it is a combiner of function combinations 434 | const config = syncChainFns( 435 | // This function will be mentioned later, which is to obtain the configuration files of webpack in different environments 436 | getWebpackConfig, 437 | // This function will be mentioned later to obtain the user-defined webpck configuration file 438 | getProjectConfig, 439 | // Determine whether you need to add a plug-in to speed up the parsing of ts 440 | isAddForkTsPlugin 441 | )(BUILD_SITE); 442 | config.output.path = getProjectPath(outDir); 443 | 444 | // Whether to enable the code package size analysis plugin 445 | if (analyzer) { 446 | config.plugins.push( 447 | new BundleAnalyzerPlugin({ 448 | analyzerMode: 'static', 449 | generateStatsFile: true, 450 | }) 451 | ); 452 | } 453 | 454 | webpack(config).run((err) => { 455 | if (err) { 456 | logger.error('webpackError: ', JSON.stringify(err)); 457 | } 458 | }); 459 | }; 460 | ``` 461 | 462 | The following is the getWebpackConfig function, which is relatively simple. It is to call different functions according to different parameters on the command line, such as mx dev, to call the getDevConfig function to obtain the configuration of webpack in the dev environment 463 | 464 | ```javascript 465 | const getWebpackConfig = (type?: IWebpackConfigType): Configuration => { 466 | switch (type) { 467 | case DEV: 468 | return getDevConfig(); 469 | 470 | case BUILD_SITE: 471 | return getBuildConfig(); 472 | 473 | case BUILD_LIB: 474 | return getBuildConfig(); 475 | 476 | default: 477 | return getDevConfig(); 478 | } 479 | }; 480 | ``` 481 | 482 | getProjectConfig is mainly a function for user-defined configuration. We mainly analyze how to get user-defined configuration. 483 | 484 | ```javascript 485 | export const getCustomConfig = ( 486 | configFileName = 'mx.config.js' 487 | ): Partial => { 488 | const configPath = path.join(process.cwd(), configFileName); 489 | if (fs.existsSync(configPath)) { 490 | // eslint-disable-next-line import/no-dynamic-require 491 | return require(configPath); 492 | } 493 | return {}; 494 | }; 495 | ``` 496 | 497 | As you can see, it is to read mx.config.js under the project. You can configure your own webpack loaders, webpack plugins, etc 498 | 499 | ```javascript 500 | const path = require('path'); 501 | 502 | module.exports = { 503 | entries: { 504 | index: { 505 | entry: ['./web/index.js'], 506 | template: './web/index.html', 507 | favicon: './favicon.ico', 508 | }, 509 | }, 510 | resolve: { 511 | alias: { 512 | '@': path.join(process.cwd(), '/'), 513 | }, 514 | }, 515 | setBabelOptions: (options) => { 516 | options.plugins.push(['import', { libraryName: 'antd', style: 'css' }]); 517 | }, 518 | setRules: (rules) => { 519 | rules.push({ 520 | test: /\.md$/, 521 | use: ['raw-loader'], 522 | }); 523 | }, 524 | }; 525 | ``` 526 | 527 | ### the configuration of the component library 528 | 529 | The code for packaging the component library is much more complicated than before! 530 | 531 | ```javascript 532 | import build from './build'; 533 | import { BUILD_LIB } from '../constants'; 534 | 535 | export const buildLib = (commander) => { 536 | // When you enter mx buildLib, this command is executed 537 | // This command actually executes the build file 538 | // We will package two packages of es and commonjs specifications 539 | commander 540 | .command(BUILD_LIB) 541 | .description('package and compile warehouse') 542 | .option( 543 | '-a, --analyzerUmd', 544 | 'Whether to enable the webpack package analyzer' 545 | ) 546 | .option('-e, --entry ', 'umd package path entry file', './src/index') 547 | .option( 548 | '--output-name ', 549 | 'The name exposed to the outside world after packaging Umd format' 550 | ) 551 | .option( 552 | '--entry-dir ', 553 | 'cjs and esm packaging path entry directory', 554 | './src' 555 | ) 556 | .option('--out-dir-umd ', 'Output directory in umd format', './dist') 557 | .option('--out-dir-esm ', 'Output directory in esm format', './esm') 558 | .option('--out-dir-cjs ', 'output directory in cjs format', './lib') 559 | .option('--copy-less', 'Copy files that do not participate in compilation') 560 | .option('--less-2-css', 'Whether to compile component styles') 561 | .option( 562 | '-m, --mode ', 563 | 'package mode currently supports umd and esm' 564 | ) 565 | .action(build); 566 | }; 567 | ``` 568 | 569 | Let's look at the build file, which is the file that is executed after you enter mx buildLib. Let's first look at the packaging of umd. This is simple. 570 | 571 | ```javascript 572 | import webpack from 'webpack'; 573 | import webpackMerge from 'webpack-merge'; 574 | // gulp task, will be discussed later 575 | import { copyLess, less2css, buildCjs, buildEsm } from '../config/gulpConfig'; 576 | import getWebpackConfig from '../config/webpackConfig'; 577 | // Tool function, we will talk about it later 578 | import { getProjectPath, logger, run, compose } from '../utils'; 579 | // Code package size analysis plugin 580 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 581 | // environment constant 582 | import { 583 | BUILD_LIB, 584 | CJS, 585 | ESM, 586 | UMD, 587 | COPY_LESS, 588 | LESS_2_LESS, 589 | CLEAN_DIR, 590 | } from '../constants'; 591 | 592 | // The name attribute of package.json is used as the package name, of course, it can also be customized 593 | const { name } = require(getProjectPath('package.json')); 594 | // Check whether the name has a slash, which will affect the packaged result 595 | const checkName = (outputName, name) => { 596 | if (!outputName && name?.includes('/')) { 597 | logger.warn( 598 | 'The package name of package.json contains slashes, and webpack will create folders with slashes when packaging, so please pay attention to whether the file name after packaging meets your requirements' 599 | ); 600 | } 601 | }; 602 | /** 603 | * build for umd 604 | * @param analyzer Whether to enable the analysis package plugin 605 | * @param outDirUmd output directory 606 | * @param entry packaged entry file 607 | * @param outputName packaged name 608 | */ 609 | const buildUmd = async ({ analyzerUmd, outDirUmd, entry, outputName }) => { 610 | const customizePlugins = []; 611 | const realName = outputName || name; 612 | checkName(outputName, name); 613 | const umdTask = (type) => { 614 | return new Promise((resolve, reject) => { 615 | const config = webpackMerge(getWebpackConfig(type), { 616 | entry: { 617 | [realName]: getProjectPath(entry), 618 | }, 619 | // The main purpose here is to set the libraryTarget to set the packaging format to umd 620 | // library is configured package name 621 | output: { 622 | path: getProjectPath(outDirUmd), 623 | library: realName, 624 | libraryTarget: 'umd', 625 | libraryExport: 'default', 626 | }, 627 | plugins: customizePlugins, 628 | }); 629 | 630 | if (analyzerUmd) { 631 | config.plugins.push( 632 | new BundleAnalyzerPlugin({ 633 | analyzerMode: 'static', 634 | generateStatsFile: true, 635 | }) 636 | ); 637 | } 638 | return webpack(config).run((err, stats) => { 639 | if (stats.compilation.errors?.length) { 640 | console.log('webpackError: ', stats.compilation.errors); 641 | } 642 | if (err) { 643 | logger.error('webpackError: ', JSON.stringify(err)); 644 | reject(err); 645 | } else { 646 | resolve(stats); 647 | } 648 | }); 649 | }); 650 | }; 651 | logger.info('building umd'); 652 | await umdTask(BUILD_LIB); 653 | logger.success('umd computed'); 654 | }; 655 | ``` 656 | 657 | Next, let’s talk about the most complicated gulp configuration, first look at the entry file: 658 | 659 | - Before we write a compose function of a framework similar to koa, this function is a function executor that calls each asynchronous function in order, for example, there are asynchronous function 1, asynchronous function 2, asynchronous function 3, I need to follow the order Call 1, 2, 3, and these 1, 2, 3 are decoupled, joined in the form of middleware, and share some data 660 | 661 | Let's look at the function first: 662 | 663 | ```javascript 664 | export function compose(middleware, initOptions) { 665 | const otherOptions = initOptions || {}; 666 | function dispatch(index) { 667 | if (index == middleware.length) return; 668 | const currMiddleware = middleware[index]; 669 | return currMiddleware(() => dispatch(++index), otherOptions); 670 | } 671 | dispatch(0); 672 | } 673 | ``` 674 | 675 | This function means: 676 | 677 | - Get the middleware functions in array order 678 | - Then when the function is called, the first parameter is passed to the next called function, and pass in the global shared data props: otherOptions. 679 | 680 | The following is the file that uses the compose function to execute each function, that is, the file actually executed by mx buildLib. There are too many files, so I will use a build esm to explain 681 | 682 | ```javascript 683 | import webpack from 'webpack'; 684 | import webpackMerge from 'webpack-merge'; 685 | // gulp task 686 | import { copyLess, less2css, buildCjs, buildEsm } from '../config/gulpConfig'; 687 | import getWebpackConfig from '../config/webpackConfig'; 688 | // Tool function 689 | import { getProjectPath, logger, run, compose } from '../utils'; 690 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 691 | // environment constant 692 | import { 693 | BUILD_LIB, 694 | CJS, 695 | ESM, 696 | UMD, 697 | COPY_LESS, 698 | LESS_2_LESS, 699 | CLEAN_DIR, 700 | } from '../constants'; 701 | 702 | const buildLib = async ({ 703 | analyzerUmd, 704 | mode, 705 | entry, 706 | outDirEsm, 707 | outDirCjs, 708 | outDirUmd, 709 | copyLess, 710 | entryDir, 711 | less2Css, 712 | cleanDir, 713 | outputName, 714 | }) => { 715 | // Register middleware, and then use the compose function to combine 716 | const buildProcess = [bulidLibFns[CLEAN_DIR]]; 717 | // Whether to pack the umd format, if yes, add the umd packaging function we talked about before 718 | if (mode === UMD) { 719 | buildProcess.push(bulidLibFns[UMD]); 720 | } 721 | // Whether to pack the esm format, if yes, add the corresponding packing function, 722 | if (mode === ESM) { 723 | buildProcess.push(bulidLibFns[ESM]); 724 | } 725 | // Omit some codes, just to add various processing functions, such as whether to add middleware that compiles less to css 726 | compose(buildProcess, { 727 | analyzerUmd, 728 | mode, 729 | entry, 730 | outDirEsm, 731 | outDirCjs, 732 | outDirUmd, 733 | copyLess, 734 | entryDir, 735 | less2Css, 736 | cleanDir, 737 | outputName, 738 | }); 739 | }; 740 | ``` 741 | 742 | Let's take a look at the gulp configuration file buildesm, which mainly executes the compileScripts function. Let's look at this function next. 743 | 744 | ```javascript 745 | const buildEsm = ({ mode, outDirEsm, entryDir }) => { 746 | const newEntryDir = getNewEntryDir(entryDir); 747 | /** 748 | * compile esm 749 | */ 750 | gulp.task('compileESM', () => { 751 | return compileScripts(mode, outDirEsm, newEntryDir); 752 | }); 753 | 754 | return new Promise((res) => { 755 | return gulp.series('compileESM', () => { 756 | res(true); 757 | })(); 758 | }); 759 | }; 760 | ``` 761 | 762 | ```javascript 763 | /** 764 | * Compile the script file 765 | * @param {string} babelEnv babel environment variable 766 | * @param {string} destDir destination directory 767 | * @param {string} newEntryDir entry directory 768 | */ 769 | function compileScripts(mode, destDir, newEntryDir) { 770 | const { scripts } = paths; 771 | return gulp 772 | .src(scripts(newEntryDir)) // find the entry file 773 | .pipe(babel(mode === ESM ? babelEsConfig : babelCjsConfig)) // use gulp-babel processing 774 | .pipe( 775 | // use gulp to process css 776 | through2.obj(function z(file, encoding, next) { 777 | this.push(file.clone()); 778 | // find target 779 | if (file.path.match(/(\/|\\)style(\/|\\)index\.js/)) { 780 | const content = file.contents.toString(encoding); 781 | file.contents = Buffer.from(cssInjection(content)); // process file content 782 | file.path = file.path.replace(/index\.js/, 'css.js'); // file rename 783 | this.push(file); // add this file 784 | next(); 785 | } else { 786 | next(); 787 | } 788 | }) 789 | ) 790 | .pipe(gulp.dest(destDir)); 791 | } 792 | ``` 793 | 794 | ## webpack configuration details 795 | 796 | ### output 797 | 798 | ``` 799 | output: { 800 | filename: 'js/[name].js', 801 | chunkFilename: 'js/[name].[chunkhash:8].js', 802 | assetModuleFilename: 'asset/[name].[contenthash:8].[ext]', 803 | } 804 | ``` 805 | 806 | - filename specifies the name of the main code file generated after packaging. [name] here means to use the name of the entry file (entry) as the file name and output it to the js folder. 807 | 808 | - chunkFilename specifies the file name of the non-entry file generated by webpack packaging, such as the code block generated by code splitting. The placeholder [chunkhash:8] is used here to ensure the uniqueness of the file name and the caching effect. 809 | 810 | - assetModuleFilename is used to specify the output path and name of resource files (images, fonts, etc.) processed by webpack when packaging. The placeholder [contenthash:8] is used to ensure the caching effect of resource files. [ext] is used to preserve the file extension. 811 | 812 | The above configuration will package all JS files into the js directory, and package other types of resource files into the asset directory. 813 | 814 | ### optimization 815 | 816 | ```javascript 817 | optimization: { 818 | runtimeChunk: true, 819 | splitChunks: { 820 | minChunks: 2, 821 | chunks: 'all', 822 | cacheGroups: { 823 | reactBase: { 824 | name: 'reactBase', 825 | chunks: 'all', 826 | test: /[\\/]node_modules[\\/](react|react-dom|@hot-loader|react-router|react-redux|react-router-dom)[\\/]/, 827 | }, 828 | 'async-commons': { 829 | // Asynchronously load public packages, components, etc. 830 | name: 'async-commons', 831 | chunks: 'async', 832 | test: /[\\/]node_modules[\\/]/, 833 | minChunks: 2, 834 | priority: 1, 835 | }, 836 | }, 837 | }, 838 | }, 839 | ``` 840 | 841 | - runtimeChunk: true The configuration item is used to package the Webpack runtime code into a separate file, so that each module does not contain duplicate runtime code. This improves cache utilization and speeds up builds. 842 | 843 | What is runtime code? Webpack runtime code refers to some code snippets generated by Webpack when packaging to handle module loading and resolution, dependency management, code splitting, and some other internal functions of Webpack. For example: 844 | 845 | ```javascript 846 | // Webpack runtime code snippet 1: module loading 847 | (function(modules) { 848 | // module cache object 849 | var installedModules = {}; 850 | 851 | // load module function 852 | function __webpack_require__(moduleId) { 853 | // check if the module is cached 854 | if(installedModules[moduleId]) { 855 | return installedModules[moduleId].exports; 856 | } 857 | 858 | // Create a new module object and cache it 859 | var module = installedModules[moduleId] = { 860 | i: moduleId, 861 | l: false, 862 | exports: {} 863 | }; 864 | 865 | // load the module 866 | modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 867 | 868 | // mark module loaded 869 | module.l = true; 870 | 871 | // Return the exported object of the module 872 | return module. exports; 873 | } 874 | ... 875 | ``` 876 | 877 | - The splitChunks configuration item is used to split the code into smaller chunks to make better use of the browser's caching mechanism. Specifically, the following are configured here: 878 | 879 | - minChunks: 2 indicates the minimum number of code reuse, that is, when a module is referenced by at least two Chunks, it will be split into a separate Chunk. 880 | 881 | - chunks: 'all' means to optimize all types of Chunk, including synchronous and asynchronous Chunk. 882 | 883 | - cacheGroups represents a cache group, which can pack modules that meet certain conditions into a group for better splitting and caching. Two cache groups are defined here: 884 | 885 | - The reactBase cache group packs React-related modules into a Chunk named reactBase, and only includes React-related modules in the node_modules directory. 886 | 887 | - The async-commons cache group packs other common modules into an asynchronous Chunk named async-commons, which only contains modules under the node_modules directory and is referenced by at least two asynchronous Chunks. The cache group has a priority of 1 to ensure that it is split into a single chunk. 888 | 889 | ## loader 890 | 891 | As mentioned above, you can expand the loader through mx.config.js. The loader that comes with mx-design-cli itself has 892 | 893 | - babel-loader 894 | - test rule: /\.(js|jsx|ts|tsx)$/ 895 | - Speed up compilation with thread-loader 896 | - css related loader 897 | - css loader 898 | - postcss-loader 899 | -less-loader 900 | - picture related loader 901 | - webpack5 built-in loader 902 | - test rules: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/] 903 | - less than 4 \* 1024 will be compiled into date URL format 904 | - font related loader 905 | - webpack5 built-in loader 906 | - test rule: /\.(eot|ttf|woff|woff2?)$/ 907 | - svg related loader 908 | - @svgr/webpack loader 909 | 910 | ## resolve 911 | 912 | ```javascript 913 | resolve: { 914 | extensions: [' ', '.ts', '.tsx', '.js', '.jsx', '.less', '.svg'], 915 | }, 916 | ``` 917 | 918 | The resolve option tells Webpack what file extensions to look for when resolving modules. In this example, Webpack will look for the following file extensions in order: .ts, .tsx, .js, .jsx, .less, .svg. When we refer to these files in the import statement, Webpack will use this option to resolve the module path. 919 | 920 | ## plugins 921 | 922 | ```javascript 923 | plugins: [ 924 | new WebpackBar({}), 925 | new webpack. DefinePlugin({ 926 | 'process.env': JSON.stringify(process.env), 927 | }), 928 | ], 929 | ``` 930 | 931 | we use the WebpackBar plugin and the DefinePlugin plugin. 932 | 933 | The WebpackBar plugin can display a progress bar on the command line, allowing us to understand the progress of Webpack's construction more intuitively. 934 | 935 | The DefinePlugin plugin can define global variables at compile time. Here we define process.env as the current environment variable, so that our code can execute different logic according to different environment variables. 936 | 937 | The above is the basic webpack configuration, and the production and development environments will be handled differently, for example 938 | 939 | ### Development Environment 940 | 941 | The development environment, such as plugins that need hot updates, sourceMap content will be more detailed, etc... 942 | 943 | ### Production environment (business code packaging, not component library) 944 | 945 | Need to do a good split chunk to better use the cache to store the library under node_modules (because there are fewer changes), for example, you can use TerserPlugin to enable multi-threaded packaging, CssMinimizerPlugin to enable css compression, etc... 946 | --------------------------------------------------------------------------------