Overall project advantages:
This project is currently being maintained by me and a colleague, and we are also welcome to build it together. The group wechat QR code is at the end of the article, welcome to join us. Github addresses and benefits are as follows (welcome star, this article is equivalent to source code analysis).
Github.com/lio-mengxia…
Simply we want to realize the function of the is your business project or component library need not concern tedious webpack development environment, packaging configuration and optimization configuration problem, need not care about packaging component library of glup configuration problem (packaging component library and packaged business code requirements is different, especially the UI component library, due to the customization requirements are too high, Only GLup does, and rollup does not either, so configuration is more tedious).
Current status of cli tools in popular open source libraries
Now popular b side react component library, such as
- Ali-ant Design
- Byte Tiktok – Arco Design
- Zent, ZARM mobile component library of Zhongan, and so on
They have their own separate CLI packaging tools, like Ant uses @ant-Design /tools, byte arco-design/cli, and the essence of this packaging tool is, if you’re developing your project with Webpack, Your project will be configured with the dev or buil command and config file for Webpack
So what that means is that every time you start a project, you’re going to be pasting these configuration files back and forth, and if one day WebPack gets upgraded by 6 or 7, or if you want to replace WebPack with Vite, are these projects going to replace the configuration files one by one? Moreover, during the project iteration, each project will be changed and changed by the students under the corresponding project, which is a nightmare for the follow-up unified maintenance
To make matters worse, webPack does not support ESM modules. The CLI tool also distinguishes between packaged business items and component libraries, and then executes different packaging commands. Therefore, it is necessary to integrate the engineering files into a CLI toolkit separately. (Don’t think that rollup can be packaged as antDesign’s on-demand module, rollup is very difficult to make, use gulp to customize, our tools will help you to block these annoying configurations.)
Next we implement a set of CLI packaging tools that can be used in a production environment. (The first part is usage, skip this part if you want technical details.)
The target
We tentatively named the component library @mx-design/cli and published it as an NPM package. The command line is mx, which is used in the following way (it takes about a minute to read) : Add devDependencies in package.json
"devDependencies": {+"@mx-design/cli": "1.0.2"
}
Copy the code
Development Environment Configuration
"scripts": {
"start": "mx dev",},Copy the code
To customize the dev environment, we will also read your mx.config.js file in the root directory, as follows:
const path = require('path');
module.exports = {
// Custom entry file, mandatory
entries: {
index: {
entry: ['./src/index.js'].template: './public/index.html'.favicon: './favicon.ico',},// Alias configuration, can be omitted
resolve: {
alias: {
The '@': path.join(process.cwd(), '/'),}},// Add a custom Babel plugin
setBabelOptions: (options) = > {
options.plugins.push([
'prismjs',
{
languages: ['javascript'.'typescript'.'jsx'.'tsx'.'css'.'scss'.'markup'.'bash'].theme: 'default'.css: true,}]); },// Add a custom loader
setRules: (rules) = > {
rules.push({
test: /\.md$/,
use: ['raw-loader']}); }};Copy the code
Ok, this is the configuration of the development environment, is not very simple, we currently use Webpack 5 to start the development environment, free your WebPack configuration problems.
Build business code is simpler
"scripts": {
"start": "mx buildSite",}Copy the code
We’ll also read your mx.config.js configuration in your root directory, as well as some command-line options for traversal, such as
"scripts": {
"start": "mx buildSite --analyzer".// Enable the package analysis tool
}
"scripts": {
"start": "mx buildSite --out-dir lib".// The packaged address directory is dist by default, changed to lib
}
Copy the code
The command line for packaging the component library is as follows (NPM/YARN run build is recommended) :
"scripts": {
"build:types": "rimraf types && tsc --outDir types -d --emitDeclarationOnly"."build:es": "rimraf esm && mx buildLib --mode esm --entry-dir ./components --less-2-css --copy-less"."build:cjs": "rimraf lib && mx buildLib --mode cjs --entry-dir ./components --less-2-css --copy-less"."build:umd": "rimraf dist && mx buildLib --mode umd --entry ./components/index"."build": "yarn build:types && yarn build:cjs && yarn build:es && yarn build:umd",}Copy the code
The preceding command is explained as follows:
--mode cjs
- Represents packaged CJS schema
--mode esm
- Indicates the packaged ESM mode
--mode umd
- Represents the packaged UMD mode
--mode cjs
- Represents packaged CJS schema
--less-2-css
- Less is converted to CSS
--entry-dir
- Mode: EsM and CJS take effect
- The default entry directory for incoming packaging is SRC
--entry
- Umd mode takes effect
- The umD entry file defaults to SRC /index
--copy-less
- Copy less files to
-out-dir-umd
- This command takes effect in umD mode
- Output directory in UMD format, default is
./dist
--out-dir-esm
- Output esM format directory, default is
./esm
- Output esM format directory, default is
--out-dir-cjs
- Output directory in CJS format, default
./lib"
- Output directory in CJS format, default
--analyzerUmd
- Whether webpack packs enable profilers
Test Test is simpler (jest test), automatically tests js, JSX, TS, TSX ending files in __tests__ folder.
"Scripts ": {"test": "mx test --watch", // enable incremental test mode}Copy the code
You can view all the detailed parameters of the command as follows:
"scripts": {
"buildLibHelp": "mx help buildLib".// View command line arguments for all packaged component libraries
"buildSiteHelp": "mx help buildSite".// View all command line arguments to webPack business code
"testHelp": "mx help test".// View all command line arguments for unit tests
"devHelp": "mx help dev".// View all dev environment configuration parameters
}
Copy the code
Write code from zero
To start, use Commander to read command line arguments
How do I create my own MX commands?
You need to add to the bin field of package.json
"bin": {
"mx": "./bin/index.js"
},
Copy the code
So, when someone downloads your NPM package, using mx is equivalent to calling your index.js in the bin directory of your NPM package. That is to say, when someone types mx in the script of package.josn, it is equivalent to calling the mx-design package. Bin directory in index.js
What does index.js look like
#! /usr/bin/env node
require('.. /lib/index');
Copy the code
This is simply the index.ts file in lib
The lib directory makes the index.js file the entry file for the component libraries we eventually generate (e.g., ts to JS, Babel to syntax). Let’s look at the index.ts entry file actually developed in the project.
Before I get into the index.ts file, I need to explain the simple use of the COMMANDER library
// index.js
const program = require('commander');
program.version('1.0.0').parse(process.argv);
Copy the code
Js -v or node index.js –version: 1.0.0 program.version(‘1.0.0’) Argv is passed to process.argv to parse the arguments in process.argv, so we type node index.js –version to pass version to Commander
Commander has registered the version command, so the version number will be registered.
SRC directory index.ts (code explanation will be in the comments)
// a popular library for parsing command-line arguments
import commander from "commander";
// Package the entry file for the component library
import { buildLib } from "./buildLib/index";
// Package the entry file for the project code
import { buildSite } from "./buildSite/index";
// Package the dev environment entry file
import { runDev } from "./dev/index";
// Get the version field of package.json
import { version } from ".. /package.json";
// The entry file to execute the unit test
import { runTest } from "./test";
// Register version
commander.version(version, "-v, --version");
// Register the package library command, which will be explained later
buildLib(commander);
// Register the package business item command, which will be explained later
buildSite(commander);
// Register the command to start the development environment, which will be explained later
runDev(commander);
runTest(commander);
// commander parses command line arguments
commander.parse(process.argv);
// If the command line has no arguments such as executing mx, the help document is displayed
if(! commander.args[0]) {
commander.help();
}
Copy the code
Development Environment Configuration
RunDev (commander) {runDev(commander)
// When you mx dev, the file that is actually executed is development
import development from "./development";
// DEV is the string 'DEV'
import { DEV } from ".. /constants";
export const runDev = (commander) = > {
// commander Specifies the command to register the 'dev' parameter
commander
.command(DEV)
.description("Run the development environment")
.option("-h, --host <host>"."Site host address"."localhost")
// The default port is 3000
.option("-p, --port <port>"."Site port number"."3000")
// The file in which the command is finally run
.action(development);
};
Copy the code
Here’s the focus of the Dev environment: What does a development file look like?
There are three key issues in this development:
-
For those of you who are not aware of the compose function, see the “compose function package” in this article
-
How do I start WebpackDevServer
-
If port 3000 is already occupied, what if we wait until port 3000 is occupied and find an unused port for webpackDevServer to start?
First question: How do I write an elegant function iterator that merges configurations
Our compose code here looks like this:
// Synchronization function chain
export const syncChainFns = (. fns) = > {
const [firstFn, ...otherFns] = fns;
return (. args) = > {
if(! otherFns)returnfirstFn(... args);return otherFns.reduce((ret, task) = >task(ret), firstFn(... args)); }; };Copy the code
Let’s write a simple case call:
function add(a, b){
return a+b;
}
function addVersion(sum){
return `version: ${sum}0.0 `;
}
syncChainFns(add, addVersion)(1.2) // 'version: 3'
Copy the code
In other words, our function chain is like a factory processing goods. After the first person processes the goods, the next person will continue to process the goods. Finally, the result can be achieved by analogy with redux’s compose function. This is the basic idea of functional programming, the idea of composition.
We will use this function to deal with webpack configuration later, because Webpack configuration can be divided into four functions
- First there is the initial WebPack Dev configuration
- Then there is the custom configuration, such as creating an mx.config.js file as the configuration file
- Whether ts environment, the name will add ForkTsCheckerWebpackPlugin to webpack plugin, speed up the ts compilation
- Finally, the webpack function is compiled, which generates the values that are ultimately sent to the webpackDevServer to start
Second question: How do I start WebpackDevServer
I was just talking about the generated file that will eventually be launched, so webpackDevServer starts like this, notice that this is how Webpack5 starts, and it’s not in the same position as the parameter in previous 4
const serverConfig = {
publicPath: "/".compress: true.noInfo: true.hot: true};const devServer = new WebpackDevServer(compiler, serverConfig)
Copy the code
Third question: What if the port number used to start dev is occupied
We use a library called Detect to detect whether a port is occupied. The library returns a port number that is not occupied if it detects that a port is occupied
const resPort = await detect(port, host)
Copy the code
Ok, with these three problems solved, let’s take a look at the development file. It doesn’t matter if you don’t understand the functions. The general idea has been introduced above.
import webpack from "webpack";
import WebpackDevServer from "webpack-dev-server";
import getWebpackConfig from ".. /config/webpackConfig";
import { isAddForkTsPlugin, syncChainFns, getProjectConfig } from ".. /utils";
import { DEV } from ".. /constants";
import { IDevelopmentConfig } from ".. /interface";
import detect from "detect-port-alt";
const isInteractive = process.stdout.isTTY;
async function choosePort(port, host) {
const resPort = await detect(port, host);
if (resPort === Number(port)) {
return resPort;
}
const message = `Something is already running on port ${port}. `;
if (isInteractive) {
console.log(message);
return resPort;
}
console.log(message);
return null;
}
export default ({ host, port }: IDevelopmentConfig) => {
const compiler = syncChainFns(
getWebpackConfig,
getProjectConfig,
isAddForkTsPlugin,
webpack
)(DEV);
const serverConfig = {
publicPath: "/".compress: true.noInfo: true.hot: true};const runDevServer = async (port) => {
const devServer = new WebpackDevServer(compiler, serverConfig);
const resPort = await choosePort(port, host);
if(resPort ! = =null) {
devServer.listen(resPort, host, (err) = > {
if (err) {
return console.error(err.message);
}
console.warn(`http://${host}:${resPort}\n`); }); }}; runDevServer(port); };Copy the code
Package business code for script parsing
Do you know the difference between packaging the business code and the component library first?
Business component library, at present, or webpack is one of the most appropriate choice, because our business on-line code needs stability, Webpack ecology and ecological stability is many packaging tools do not have, do not need the efficiency of the development environment (Webpack 5 is much faster than 4), Let’s say someone chooses vite for their development environment.
Business code is generally packaged in UMD format.
Component library code, such as AntDesign, Element UI, these libraries not only need UMD format, the most need is ESM Module, export is import syntax, this WebPack can not do. The reason why you can’t do this is because Webpack has its own set of require rules, and the import you use will eventually be translated by webpack’s loading module syntax.
Therefore, you can use rollup for ESM Module. However, AFTER careful investigation, multi-entry packaging rollup is not supported, and we need to make painstaking efforts on CSS packaging. As I will tell you later, packaging CSS is very, very exquisite, and rollup is not satisfactory. So we will use gulp to package CSS and JS separately.
Because customization is so demanding, you have to use GLUP to customize the packaging process.
Let’s take a look at the simpler entry for packaging business code scripts
import build from "./buildSite";
import { BUILD_SITE } from ".. /constants";
export const buildSite = (commander) = > {
// Package business components
This command actually executes the buildSite file
commander
.command(BUILD_SITE)
.description("Package business code")
.option("-d, --out-dir <path>"."Output directory"."dist")
.option("-a, --analyzer"."Enable profiler or not")
.action(build);
};
Copy the code
Next, let’s look at the build file. The following will mainly explain the code for the getWebpackConfig file and the getProjectConfig file
import webpack from "webpack";
// The Webpack code packages the analysis plug-in
import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer";
// Get the basic WebPack configuration
import getWebpackConfig from ".. /config/webpackConfig";
// Get the webPack custom configuration
import { getProjectPath, getProjectConfig, syncChainFns } from ".. /utils";
// Interface configuration
import { IDeployConfig } from ".. /interface";
// This constant is the string "buildSite"
import { BUILD_SITE } from ".. /constants";
export default ({ outDir, analyzer }: IDeployConfig) => {
// The syncChainFns function, described above, is a combinator of function combinations
const config = syncChainFns(
// This function, described later, is to get the webPack configuration files for different environments
getWebpackConfig,
// This function, described later, is used to get a user - defined Webpck configuration file
getProjectConfig,
// Determine if a plugin is needed to speed up ts parsing
isAddForkTsPlugin
)(BUILD_SITE);
config.output.path = getProjectPath(outDir);
// Whether to enable the package volume analysis plug-in
if (analyzer) {
config.plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: "static".generateStatsFile: true,})); } webpack(config).run((err) = > {
if (err) {
logger.error("webpackError: ".JSON.stringify(err)); }}); };Copy the code
The following is getWebpackConfig code, quite simple, the use of factory mode, very simple, is according to the command line different parameters call different functions, such as mx dev, call getDevConfig function, get webpack in the dev environment configuration
const getWebpackConfig = (type?: IWebpackConfigType): Configuration => {
switch (type) {
case DEV:
return getDevConfig();
case BUILD_SITE:
return getBuildConfig();
case BUILD_LIB:
return getBuildConfig();
default:
return getDevConfig();
}
};
Copy the code
GetProjectConfig is a function that provides the user’s custom configuration. Let’s look at how to get the user’s custom configuration.
export const getCustomConfig = (
configFileName = "mx.config.js"
): Partial<CustomConfig> => {
const configPath = path.join(process.cwd(), configFileName);
if (fs.existsSync(configPath)) {
// eslint-disable-next-line import/no-dynamic-require
return require(configPath);
}
return {};
};
Copy the code
Mx.config. js is written in mx.config.js. It is very simple if you want plug-ins and plugins, and entry configuration.
const path = require('path');
module.exports = {
entries: {
index: {
entry: ['./web/index.js'].template: './web/index.html'.favicon: './favicon.ico',}},resolve: {
alias: {
The '@': path.join(process.cwd(), '/'),}},setBabelOptions: (options) = > {
options.plugins.push(['import', { libraryName: 'antd'.style: 'css' }]);
},
setRules: (rules) = > {
rules.push({
test: /\.md$/,
use: ['raw-loader']}); }};Copy the code
Package the core configuration file of the component library
The code to package the component library is much more complex than before! Same old rule. Check the entry file
import build from "./build";
import { BUILD_LIB } from ".. /constants";
export const buildLib = (commander) = > {
This command is executed when you type mx buildLib
// This command actually executes the build file
// We will pack both es and CommonJS specifications
commander
.command(BUILD_LIB)
.description("Package build repository")
.option("-a, --analyzerUmd"."Whether to enable the Webpack analyzer")
.option("-e, --entry <path>"."Umd package path entry file"."./src/index")
.option("--output-name <name>"."Name exposed after packaging Umd format")
.option("--entry-dir <path>"."CJS and ESM package path entry directory"."./src")
.option("--out-dir-umd <path>"."Output directory in UMD format"."./dist")
.option("--out-dir-esm <path>"."Output directory in ESM format"."./esm")
.option("--out-dir-cjs <path>"."Output directory in CJS format"."./lib")
.option("--copy-less"."Copy files not compiled")
.option("--less-2-css"."Whether to compile component styles")
.option("-m, --mode <esm|umd|cjs>"."Packaging modes currently support both UMD and ESM")
.action(build);
};
Copy the code
Let’s take a look at the build file, which is the file that you execute after you type in MX buildLib, and let’s take a look at the PACKAGING of UMD, and this simple, slightly more complicated one is the GLUP configuration.
import webpack from "webpack";
import webpackMerge from "webpack-merge";
// Gulp task, more on that later
import { copyLess, less2css, buildCjs, buildEsm } from ".. /config/gulpConfig";
import getWebpackConfig from ".. /config/webpackConfig";
// The utility function is used later
import { getProjectPath, logger, run, compose } from ".. /utils";
// Package volume analysis plug-in
import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer";
// Environment constant
import {
BUILD_LIB,
CJS,
ESM,
UMD,
COPY_LESS,
LESS_2_LESS,
CLEAN_DIR,
} from ".. /constants";
// Package. json's name property is used as the name of the packaged package, which can also be customized
const { name } = require(getProjectPath("package.json"));
// Check whether the name has a slash, which affects the packaged result
const checkName = (outputName, name) = > {
if(! outputName && name? .includes("/")) {
logger.warn(
"The package name of package.json contains slashes. Webpack will use slashes to create folders, so please note if the file name meets your requirements after packaging."); }};/**
* build for umd
* @param Analyzer whether the analysis package plug-in is enabled *@param OutDirUmd Output directory *@param Entry Package entry file *@param OutputName The packaged name */
const buildUmd = async ({ analyzerUmd, outDirUmd, entry, outputName }) => {
const customizePlugins = [];
const realName = outputName || name;
checkName(outputName, name);
const umdTask = (type) = > {
return new Promise((resolve, reject) = > {
const config = webpackMerge(getWebpackConfig(type), {
entry: {
[realName]: getProjectPath(entry),
},
// Set the libraryTarget to umD
// Library configures the packaged package name
output: {
path: getProjectPath(outDirUmd),
library: realName,
libraryTarget: "umd".libraryExport: "default",},plugins: customizePlugins,
});
if (analyzerUmd) {
config.plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: "static".generateStatsFile: true,})); }return webpack(config).run((err, stats) = > {
if (stats.compilation.errors?.length) {
console.log("webpackError: ", stats.compilation.errors);
}
if (err) {
logger.error("webpackError: ".JSON.stringify(err));
reject(err);
} else{ resolve(stats); }}); }); }; logger.info("building umd");
await umdTask(BUILD_LIB);
logger.success("umd computed");
};
Copy the code
For the most complex gulp configuration, see the entry file:
- We solve before writing a similar framework of koa compose function, this function is a function of the actuator, the various asynchronous function calls in sequence, such as asynchronous function 1, 2 asynchronous functions, an asynchronous function 3, I need to call in accordance with the order 1, 2, 3, and this is decoupling of 1, 2, 3, similar to the form of the middleware to join, And share some data
Let’s look at the function first:
export function compose(middleware, initOptions) {
const otherOptions = initOptions || {};
function dispatch(index) {
if (index == middleware.length) return;
const currMiddleware = middleware[index];
return currMiddleware(() = > dispatch(++index), otherOptions);
}
dispatch(0);
}
Copy the code
This function means:
- Get the Middleware functions in array order
- A function call passes in the first argument to the next called function, and the active call executes the next middleware function, passing in a global shared data otherOptions.
The following is the file that executes each function using the compose function, which mx buildLib actually does. The file is so full that I’ll use a Build ESM to explain it
import webpack from "webpack";
import webpackMerge from "webpack-merge";
// Gulp task, more on that later
import { copyLess, less2css, buildCjs, buildEsm } from ".. /config/gulpConfig";
import getWebpackConfig from ".. /config/webpackConfig";
// The utility function is used later
import { getProjectPath, logger, run, compose } from ".. /utils";
// Package volume analysis plug-in
import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer";
// Environment constant
import {
BUILD_LIB,
CJS,
ESM,
UMD,
COPY_LESS,
LESS_2_LESS,
CLEAN_DIR,
} from ".. /constants";
const buildLib = async({ analyzerUmd, mode, entry, outDirEsm, outDirCjs, outDirUmd, copyLess, entryDir, less2Css, cleanDir, outputName, = > {})// Register the middleware and compose it with the compose function
const buildProcess = [bulidLibFns[CLEAN_DIR]];
// Whether to package the umD format, if yes, add the umD packaging function we talked about earlier
if (mode === UMD) {
buildProcess.push(bulidLibFns[UMD]);
}
// Whether to package esM format, if yes, add corresponding packaging function,
if (mode === ESM) {
buildProcess.push(bulidLibFns[ESM]);
}
// Omit some code to add various handlers, such as middleware that compiles less to CSS
compose(buildProcess, {
analyzerUmd,
mode,
entry,
outDirEsm,
outDirCjs,
outDirUmd,
copyLess,
entryDir,
less2Css,
cleanDir,
outputName,
});
};
// Take a look at the buildEsm method in the ESM function
const bulidLibFns = {
[CLEAN_DIR]: async (next, otherOptions) => {
await run(
`rimraf ${otherOptions.outDirEsm} ${otherOptions.outDirCjs} ${otherOptions.outDirUmd}`.'Delete before packing${otherOptions.outDirEsm} ${otherOptions.outDirCjs} ${otherOptions.outDirUmd}Folder `
);
next();
},
[ESM]: async (next, otherOptions) => {
logger.info("buildESM ing...");
await buildEsm({
mode: otherOptions.mode,
outDirEsm: otherOptions.outDirEsm,
entryDir: otherOptions.entryDir,
});
logger.success("buildESM computed"); next(); }};export default buildLib;
Copy the code
Let’s look at the gulp configuration file buildesm, which mainly executes the compileScripts function, which we’ll continue with
const buildEsm = ({ mode, outDirEsm, entryDir }) = > {
const newEntryDir = getNewEntryDir(entryDir);
/**
* 编译esm
*/
gulp.task("compileESM".() = > {
return compileScripts(mode, outDirEsm, newEntryDir);
});
return new Promise((res) = > {
return gulp.series("compileESM".() = > {
res(true); }) (); }); };Copy the code
/** * Compile the script file *@param {string} BabelEnv Babel environment variable *@param {string} DestDir Target directory *@param {string} NewEntryDir Entry directory */
function compileScripts(mode, destDir, newEntryDir) {
const { scripts } = paths;
return gulp
.src(scripts(newEntryDir)) // Find the entry file
.pipe(babel(mode === ESM ? babelEsConfig : babelCjsConfig)) // Use gulp-babel
.pipe(
// Use gulp to process CSS
through2.obj(function z(file, encoding, next) {
this.push(file.clone());
// Find the target
if (file.path.match(/(\/|\\)style(\/|\\)index\.js/)) {
const content = file.contents.toString(encoding);
file.contents = Buffer.from(cssInjection(content)); // Process file contents
file.path = file.path.replace(/index\.js/."css.js"); // Rename the file
this.push(file); // Add the file
next();
} else {
next();
}
})
)
.pipe(gulp.dest(destDir));
}
Copy the code
The general content is over. There will be an automatic release script, each process is free combination configuration, so it is not bash, use node to write, to make the configuration more flexible, this is done and then have to build a component library to check the official website, finally write components one by one. Take your time!
Welcome to join the library’s maintenance and use q&A group: