├── 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 {this.props.children} ;
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 |
alert('test')}>
10 | `test
11 |
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 |
--------------------------------------------------------------------------------