Build react front-end component library based on rollup+typescript+gulp+less

The last article talked about our goals, and the following is how to achieve them:

Create a target style catalog

We have talked about our style directory structure earlier. Let's review:
The encoding directory looks like this:

The generated directory looks like this:

Why do coding structures and generation structures look like this? See the last article Construction of react component library (I)
This part is actually implemented by gulp:
First, create the gulp execution entry folder gulpfile JS, and then create index JS as gulp entry (build will be described later):

gulp's pipeline flow idea is very convenient for construction:

const gulp = require('gulp');
const rimraf = require('rimraf');
var minimatch = require('minimatch');
const less = require('gulp-less');
const glob = require('glob');
// const lessImport = require('gulp-less-import');
const rename = require('gulp-rename');
const concat = require('gulp-concat');
// const gulpIf = require('gulp-if');
const autoprefix = require('less-plugin-autoprefix');
const alias = require('gulp-path-alias');

const path = require('path');

const { buildScript, buildBrowser, styleScriptBuild } = require('./build');
const { getProjectPath } = require('../utils/project');

const outputDirName = './dist';
const outputDir = getProjectPath(outputDirName);
const umdDir = getProjectPath(outputDirName + '/dist');
const esDir = getProjectPath(outputDirName + '/es');
const cjsDir = getProjectPath(outputDirName + '/lib');

// less global variable file
const varsPath = getProjectPath('./src/components/style/index.less');

function globArray(patterns, options) {
    var i,
        list = [];
    if (!Array.isArray(patterns)) {
        patterns = [patterns];
    }
    patterns.forEach(function(pattern) {
        if (pattern[0] === '!') {
            i = list.length - 1;
            while (i > -1) {
                if (!minimatch(list[i], pattern)) {
                    list.splice(i, 1);
                }
                i--;
            }
        } else {
            var newList = glob.sync(pattern, options);
            newList.forEach(function(item) {
                if (list.indexOf(item) === -1) {
                    list.push(item);
                }
            });
        }
    });
    return list;
}

// Compile less
function compileLess(cb, outputCssFileName = 'ti.css') {
    gulp.src(['src/**/style/**/*.less', 'src/style/**/*.less'])
        .pipe(
            alias({
                paths: {
                    '~@': path.resolve('./src'),
                },
            }),
        )
        .pipe(gulp.dest(esDir)) // Copy a copy of less es
        .pipe(gulp.dest(cjsDir)) // Make a copy of less cjs
        .pipe(
            less({
                plugins: [autoprefix],
                globalVars: {
                    hack: `true; @import "${varsPath}"`,
                },
            }),
        )
        .pipe(
            rename(function(path) {
                return {
                    ...path,
                    extname: '.css',
                };
            }),
        )
        .pipe(gulp.dest(esDir)) // Output css es
        .pipe(gulp.dest(cjsDir)) // Output css cjs
        .pipe(concat(outputCssFileName))
        .pipe(gulp.dest(umdDir));
    cb();
}

// Compile ts
function compileTypescript(cb) {
    const source = [
        'src/**/*.tsx',
        'src/**/*.ts',
        'src/**/*.d.ts',
        '!src/**/__test__/**',
        '!src/**/style/*.ts',
    ];

    const tsFiles = globArray(source);

    buildScript(
        tsFiles,
        {
            es: esDir,
            cjs: cjsDir,
        },
        cb,
    )
        .then(() => {
            cb();
        })
        .catch(err => {
            console.log('---> build err', err);
        });
    // Single file output
    buildBrowser('src/index.ts', umdDir, cb);
    cb();
}

// Style script file processing provided for Babel import plugin
function styleScriptTask(cb) {
    const files = glob.sync('src/**/style/*.ts');
    styleScriptBuild(files, { es: esDir, cjs: cjsDir });
    cb();
}

// Empty source file
function removeDist(cb) {
    rimraf.sync(outputDir);
    cb();
}

exports.default = gulp.series(
    removeDist,
    gulp.parallel(compileLess, styleScriptTask, compileTypescript),
);

Let's split the upper part of the code into several parts. From the export, we can see gulp Series is an api for sequential execution of gulp tasks, gulp Parallel is an api for simultaneous execution of gulp tasks:

exports.default = gulp.series(
    removeDist,
    gulp.parallel(compileLess, styleScriptTask, compileTypescript),
);

removeDist knows by name that this step is to remove the file and clear the previous compiled file before compiling the file:

// Empty source file gulp task
function removeDist(cb) {
    rimraf.sync(outputDir);
    cb();
}

rimraf is a nodejs library. Use it to clean up files, and then do the following simultaneous tasks. First, look at the first two tasks related to style file processing

gulp.parallel(compileLess, styleScriptTask, compileTypescript),
// Compile less
function compileLess(cb, outputCssFileName = 'ti.css') {
    gulp.src(['src/**/style/**/*.less', 'src/style/**/*.less'])
        .pipe(
            alias({
                paths: {
                    '~@': path.resolve('./src'),
                },
            }),
        )
        .pipe(gulp.dest(esDir)) // Copy a copy of less es
        .pipe(gulp.dest(cjsDir)) // Make a copy of less cjs
        .pipe(
            less({
                plugins: [autoprefix],
                globalVars: {
                    hack: `true; @import "${varsPath}"`,
                },
            }),
        )
        .pipe(
            rename(function(path) {
                return {
                    ...path,
                    extname: '.css',
                };
            }),
        )
        .pipe(gulp.dest(esDir)) // Output css es
        .pipe(gulp.dest(cjsDir)) // Output css cjs
        .pipe(concat(outputCssFileName))
        .pipe(gulp.dest(umdDir));
    cb();
}

These two steps are copying the less file to the es and cjs output directory

.pipe(gulp.dest(esDir)) // Copy a copy of less es
        .pipe(gulp.dest(cjsDir)) // Make a copy of less cjs

This step is the variable coverage of less. globalVars does not need to kill it, because it is a variable coverage, but I mistakenly use it as a global variable. It exists globally only in the development environment. After compilation, only the coverage variable will not be injected into all files

.pipe(
            less({
                plugins: [autoprefix],
                globalVars: {
                    hack: `true; @import "${varsPath}"`,
                },
            }),
        )

This step is to change the file suffix and generate a CSS file in the same directory, that is, a less file corresponds to a file with the same name as CSS for CSS support. Path is the path in the current writing environment, such as Src / components / checkbox / style / index Less becomes Src / components / checkbox / style / index css

.pipe(
            rename(function(path) {
                return {
                    ...path,
                    extname: '.css',
                };
            }),
        )

Output css to the style directory under es and cjs, and take Src / components / checkbox / style / index Less will output the following files
dist/es/components/checkbox/style/index.css
dist/cjs/components/checkbox/style/index.css

 .pipe(gulp.dest(esDir)) // Output css es
 .pipe(gulp.dest(cjsDir)) // Output css cjs

These two steps are to output only css when generating the style file required by umd

// The gulp concat plug-in merges all the files in the pipeline into the outputCssFileName file
.pipe(concat(outputCssFileName))
        .pipe(gulp.dest(umdDir)); // Output file to umdDir directory

Merge and output all css files to the umdDir directory. What we need is that the file name in the dist/dist directory is the outputCssFileName variable. So how do we generate the style script, that is, the file provided by the following figure to Babel import Pugin

Generate style scripts in style files

// Style script file processing provided for Babel import plugin
function styleScriptTask(cb) {
    // Match to the style entry in the source code
    const files = glob.sync('src/**/style/*.ts');
    styleScriptBuild(files, { es: esDir, cjs: cjsDir });
    cb();
}

The second gulp task generates the style script file. First, through glob Sync to match all source style script entries, as shown in the following figure:

Then it is processed through the styleScriptBuild function, which is compiled and output using rollup:

const rollup = require('rollup');
const { babel } = require('@rollup/plugin-babel');
const alias = require('@rollup/plugin-alias');
const resolve = require('@rollup/plugin-node-resolve');
const replace = require('rollup-plugin-replace');
// const typescript = require('@rollup/plugin-typescript');
const typescript = require('rollup-plugin-typescript2');
const common = require('@rollup/plugin-commonjs');
const jsx = require('rollup-plugin-jsx');
const less = require('rollup-plugin-less');
const { uglify } = require('rollup-plugin-uglify');
const analyze = require('rollup-plugin-analyzer');

const { nodeResolve } = resolve;
const fs = require('fs');
const path = require('path');
const { getProjectPath } = require('../utils/project');
const varsPath = getProjectPath('./src/components/style/index.less');

function mkdirPath(pathStr) {
    let projectPath = '/';
    const pathArr = pathStr.split('/');
    for (let i = 0; i < pathArr.length; i++) {
        projectPath += (i === 0 || i === 1 ? '' : '/') + pathArr[i];
        if (!fs.existsSync(projectPath)) {
            if (
                projectPath.indexOf('ti-component/dist') >= 0 &&
                !fs.existsSync(projectPath)
            ) {
                fs.mkdirSync(projectPath);
            }
        }
    }
    return projectPath;
}

// Is it a script running in the browser
function isBrowserScriptFormat(dir) {
    return dir.indexOf('umd') >= 0;
}

// Is it a script file for exporting styles
function isStyleScript(path) {
    return (
        path.match(/(\/|\\)style(\/|\\)index\.ts/) ||
        path.match(/(\/|\\)style(\/|\\)index\.tsx/) ||
        path.match(/(\/|\\)style(\/|\\)index\.js/) ||
        path.match(/(\/|\\)style(\/|\\)index\.jsx/)
    );
}

// Handle situations that require direct use of css
function cssInjection(content) {
    return content
        .replace(/\/style\/?'/g, "/style/css'") // By default, all imported index es are converted to imported css
        .replace(/\/style\/?"/g, '/style/css"')
        .replace(/\.less/g, '.css');
}

// Replace the string with js suffix in the imported less script
function replaceLessScript(code) {
    if (code.indexOf('.less.js') >= 0) {
        return code.replace(/\.less.js/g, '.less');
    }
    return code;
}

// The script to create and import css is called css js
function createCssJs(code, filePath, dir, format) {
    if (isBrowserScriptFormat(format)) return;
    const icode = replaceLessScript(code);
    const content = cssInjection(icode);
    const cssDir = filePath
        .replace(/^.*?src\//, dir + '/')
        .replace(/index\.ts$|index\.tsx$/, '');
    const styleJsDir = filePath
        .replace(/^.*?src\//, dir + '/')
        .replace(/index\.ts$|index\.tsx$/, '');
    const cssJsPath = filePath
        .replace(/^.*?src\//, dir + '/')
        .replace(/index\.ts$|index\.tsx$/, 'css.js');
    const styleJsPath = filePath
        .replace(/^.*?src\//, dir + '/')
        .replace(/index\.ts$|index\.tsx$/, 'index.js');
    mkdirPath(cssDir);
    mkdirPath(styleJsDir);
    fs.writeFile(cssJsPath, content, function(err) {
        if (err) {
            console.log('--------->write file err', err);
        }
    });
    fs.writeFile(styleJsPath, icode, function(err) {
        if (err) {
            console.log('--------->write file err', err);
        }
    });
}

/**
 *@desc: Get rollup input packaging configuration
 *@Date: 2021-02-18 10:43:08
 *@param {Object} inputOptionOverride Override input configuration
 *@param {Array} additionalPlugins New plug-ins
 *@param {object} tsConfig
 *@return {void}
 */
function getRollUpInputOption(
    inputOptionOverride = {},
    tsConfig = {},
    additionalPlugins = [],
) {
    const external = ['react', 'react-dom'];
    const babelOptions = {
        exclude: ['**/node_modules/**'],
        babelHelpers: 'bundled',
        presets: [
            // "stage-3",
            '@babel/preset-env',
            '@babel/preset-react',
            '@babel/preset-flow',
        ],
        extensions: ['tsx', 'ts', 'js', 'jsx'],
        plugins: [
            '@babel/transform-react-jsx',
            // ['@babel/plugin-transform-runtime', { useESModules: true }],
            [
                '@babel/plugin-proposal-class-properties',
                {
                    loose: true,
                },
            ],
            [
                '@babel/plugin-proposal-decorators',
                {
                    legacy: true,
                },
            ],
        ],
    };
    const onAnalysis = ({ bundleSize }) => {
        console.log(`Bundle size bytes: ${bundleSize} bytes`);
        return;
    };
    const inputOptions = {
        external,
        plugins: [
            common(),
            nodeResolve({
                extensions: ['.js', '.jsx', '.ts', '.tsx', '.less'],
            }),
            alias({
                entries: [
                    {
                        find: '@',
                        replacement: path.resolve('./src'),
                    },
                    {
                        find: '~@',
                        replacement: path.resolve('./src'),
                    },
                ],
            }),
            replace({
                stylePre: JSON.stringify('ti'),
                'process.env.NODE_ENV': JSON.stringify('production'),
            }),
            less({
                option: {
                    globalVars: {
                        'theme-color': '#136BDE',
                        hack: `true; @import "${varsPath}"`,
                    },
                },
                output: false,
            }),
            typescript({
                tsconfigDefaults: {
                    include: ['./src/**/*.ts', './src/**/*.tsx'],
                    compilerOptions: {
                        lib: ['es5', 'es6', 'dom'],
                        // exclude: ['./src/**/style/*.ts'],
                        target: 'ES6',
                        // typeRoots: ["./types"],
                        moduleResolution: 'node',
                        module: 'ES6',
                        jsx: 'react',
                        allowSyntheticDefaultImports: true,
                        ...tsConfig,
                    },
                },
            }),
            babel(babelOptions),

            jsx({
                factory: 'React.createElement',
                extensions: ['js', 'jsx', 'tsx'],
            }),
            analyze({ onAnalysis, skipFormatted: true, stdout: true }),
            ...additionalPlugins,
        ],
        ...inputOptionOverride,
    };
    return inputOptions;
}

// Compile the style script used to generate the Babel import plugin
exports.styleScriptBuild = async function(files, outputConf) {
    const outputOptions = [
        {
            // file: outputPath,
            format: 'cjs',
            dir: outputConf.cjs,
            preserveModulesRoot: 'src',
            preserveModules: true,
            exports: 'named',
            hoistTransitiveImports: false, // Do not import other module codes
        },
        {
            // file: outputPath,
            format: 'esm',
            dir: outputConf.es,
            preserveModulesRoot: 'src',
            preserveModules: true,
            exports: 'named',
            hoistTransitiveImports: false, // Do not import other module codes
        },
    ];
    const bundle = await rollup.rollup(
        getRollUpInputOption(
            {
                input: files,
                treeshake: false,
            },
            {
                declaration: true,
            },
        ),
    );
    for (const outputOption of outputOptions) {
        const { output } = await bundle.generate(outputOption);
        for (const chunkOrAsset of output) {
            if (chunkOrAsset.type === 'chunk') {
                if (isStyleScript(chunkOrAsset.fileName)) {
                    createCssJs(
                        chunkOrAsset.code,
                        chunkOrAsset.facadeModuleId,
                        outputOption.dir,
                        outputOption.format,
                    );
                }
            }
        }
    }
    await bundle.close();
};

// Component es cjs specification compilation output
exports.buildScript = async function(inputPaths, outputConf) {
    // Output format
    const outputOptions = [
        {
            // file: outputPath,
            format: 'cjs',
            dir: outputConf.cjs,
            preserveModulesRoot: 'src',
            preserveModules: true,
            exports: 'named',
            hoistTransitiveImports: false, // Do not import other module codes
        },
        {
            // file: outputPath,
            format: 'esm',
            dir: outputConf.es,
            preserveModulesRoot: 'src',
            preserveModules: true,
            exports: 'named',
            hoistTransitiveImports: false, // Do not import other module codes
        },
    ];
    for (const outputOption of outputOptions) {
        const bundle = await rollup.rollup(
            getRollUpInputOption(
                {
                    input: inputPaths,
                    treeshake: true,
                },
                {
                    declaration: true,
                },
            ),
        );
        await bundle.generate(outputOption);
        await bundle.write(outputOption);
        await bundle.close();
    }
};

// Package into one file
exports.buildBrowser = async function(entryPath, outputDir, cb) {
    const outputOption = {
        file: outputDir + '/index.js',
        format: 'umd',
        // dir: outputDir, preserveModulesRoot: 'src', preserveModules: true,
        name: 'ti',
        exports: 'named',
        globals: {
            react: 'React', // A single package needs to expose global variables
            'react-dom': 'ReactDOM',
        },
    };
    const bundle = await rollup.rollup(
        getRollUpInputOption(
            {
                input: entryPath,
                treeshake: true,
            },
            {},
            [uglify()],
        ),
    );
    await bundle.generate(outputOption);
    await bundle.write(outputOption);
    await bundle.close();
    cb();
};

There is a lot of code. Let's just look at the style processing part first

// Compile the style script used to generate the Babel import plugin
exports.styleScriptBuild = async function(files, outputConf) {
    const outputOptions = [
        {
            // file: outputPath,
            format: 'cjs',
            dir: outputConf.cjs, // Target output directory
            preserveModulesRoot: 'src',
            preserveModules: true, // Homologous output
            exports: 'named', // Import method named import
            hoistTransitiveImports: false, // Do not import other module code, that is, do not package the imported code into a file
        },
        {
            // file: outputPath,
            format: 'esm',
            dir: outputConf.es,
            preserveModulesRoot: 'src',
            preserveModules: true,
            exports: 'named',
            hoistTransitiveImports: false, // Do not import other module codes
        },
    ];
    const bundle = await rollup.rollup(
        getRollUpInputOption(
            {
                input: files,
                treeshake: false,
            },
            {
                declaration: true,
            },
        ),
    );
    for (const outputOption of outputOptions) {
        const { output } = await bundle.generate(outputOption);
        for (const chunkOrAsset of output) {
            if (chunkOrAsset.type === 'chunk') {
                if (isStyleScript(chunkOrAsset.fileName)) {
                    createCssJs(
                        chunkOrAsset.code,
                        chunkOrAsset.facadeModuleId,
                        outputOption.dir,
                        outputOption.format,
                    );
                }
            }
        }
    }
    await bundle.close();
};

outputOptions is the output configuration of rollup. We need to output two specifications, cjs and es. Use rollup's js api, rollup Compile rollup. Look at this function:

getRollUpInputOption(
                {
                    input: inputPaths,
                    treeshake: true,
                },
                {
                    declaration: true,
                },
     )

This is the extracted function to obtain the rollup input configuration. Because the packaged components also need to be used, it is extracted. Note the treeshake here, that is, tree shake, also known as dependency tree. Students using the packaging tool should be familiar with it. They need to close the variant style script, because our style file is not imported by any file during coding. We use Babel import plugin injection. If it is not closed, rollup's dependency analysis will think that this file is not dependent and belongs to redundant file, so it does not need to be compiled and output, Then the style script you compiled is an empty file. Then the getrootupinputoption function is the rollup entry parameter configuration:

/**
 *@desc: Get rollup input packaging configuration
 *@Date: 2021-02-18 10:43:08
 *@param {Object} inputOptionOverride Override input configuration
 *@param {Array} additionalPlugins New plug-ins
 *@param {object} tsConfig
 *@return {void}
 */
function getRollUpInputOption(
    inputOptionOverride = {},
    tsConfig = {},
    additionalPlugins = [],
) {
    const external = ['react', 'react-dom'];
    const babelOptions = {
        exclude: ['**/node_modules/**'],
        babelHelpers: 'bundled',
        presets: [
            // "stage-3",
            '@babel/preset-env',
            '@babel/preset-react',
            '@babel/preset-flow',
        ],
        extensions: ['tsx', 'ts', 'js', 'jsx'],
        plugins: [
            '@babel/transform-react-jsx',
            // ['@babel/plugin-transform-runtime', { useESModules: true }],
            [
                '@babel/plugin-proposal-class-properties',
                {
                    loose: true,
                },
            ],
            [
                '@babel/plugin-proposal-decorators',
                {
                    legacy: true,
                },
            ],
        ],
    };
    const onAnalysis = ({ bundleSize }) => {
        console.log(`Bundle size bytes: ${bundleSize} bytes`);
        return;
    };
    const inputOptions = {
        external,
        plugins: [
            common(),
            nodeResolve({
                extensions: ['.js', '.jsx', '.ts', '.tsx', '.less'],
            }),
            alias({
                entries: [
                    {
                        find: '@',
                        replacement: path.resolve('./src'),
                    },
                    {
                        find: '~@',
                        replacement: path.resolve('./src'),
                    },
                ],
            }),
            replace({
                stylePre: JSON.stringify('ti'),
                'process.env.NODE_ENV': JSON.stringify('production'),
            }),
            less({
                option: {
                    globalVars: {
                        'theme-color': '#136BDE',
                        hack: `true; @import "${varsPath}"`,
                    },
                },
                output: false,
            }),
            typescript({
                tsconfigDefaults: {
                    include: ['./src/**/*.ts', './src/**/*.tsx'],
                    compilerOptions: {
                        lib: ['es5', 'es6', 'dom'],
                        // exclude: ['./src/**/style/*.ts'],
                        target: 'ES6',
                        // typeRoots: ["./types"],
                        moduleResolution: 'node',
                        module: 'ES6',
                        jsx: 'react',
                        allowSyntheticDefaultImports: true,
                        ...tsConfig,
                    },
                },
            }),
            babel(babelOptions),

            jsx({
                factory: 'React.createElement',
                extensions: ['js', 'jsx', 'tsx'],
            }),
            analyze({ onAnalysis, skipFormatted: true, stdout: true }),
            ...additionalPlugins,
        ],
        ...inputOptionOverride,
    };
    return inputOptions;
}

Students who need it can go to the rollup document. Students who have done engineering configuration should understand what they are doing, that is, the variation of ts jsx tsx less, as well as the configuration of babel, path alias, compilation entry and other columns

 await bundle.generate(outputOption);
 await bundle.write(outputOption);
 await bundle.close();

The next step is to output the compiled content. outputOption has been configured to do homologous output, and the generation of the whole style file is over

The next chapter is the configuration of rollup component compilation. In fact, the code in this chapter already has the configuration of component compilation

Keywords: Javascript Front-end React rollup

Added by clank on Fri, 10 Dec 2021 07:24:18 +0200