preface

Along with the increase in the company product line, development and maintenance of the project more and more, in the process of business development, will find often use cookies processing, array processing, throttling stabilization function tools such as function, these tools function will use to in many projects, in order to avoid a code to copy and paste using low operation for many times, I try to build a JavaScript library from scratch, TypeScript + Rollup + Karma + Mocha + Coverage. I write this article to share it with others who have the same needs. I hope it will be helpful to you.

Project source at the end of the article, remember to check oh ~

Directory structure description

├ ─ ─ scripts -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- build files related │ ├ ─ ─ config. Js -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- to generate a rollup configuration file │ ├ ─ ─ Build. Js -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - to config. All the rollup in the js configuration to build ├ ─ ─ coverage -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- test coverage reports ├ ─ ─ dist -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ts of the compiled file output directory ├ ─ ─ lib -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- after the build output directory file after ├ ─ ─ Test -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- contains all the test file │ ├ ─ ─ but ts -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- automated unit test entry documents │ ├ ─ ─ xx. Spec. Ts -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- unit test file ├ ─ ─ the SRC -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- tools function source │ ├ ─ ─ entry - compiler. Ts -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - function entry files │ ├ ─ ─ arrayUtils -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- storage array processing related tools function │ │ ├ ─ ─ arrayFlat. Ts -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- an array of tile │ ├ ─ ─ xx -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- xx │ │ ├ ─ ─ XXX. Ts -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- XXX ├ ─ ─ Package. The json -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- profile ├ ─ ─ package - lock. Json -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- lock installation package version number ├ ─ ─ Index, which s -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- type declaration documents ├ ─ ─ karma. Conf., js -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the karma configuration file ├ ─ ─ the babelrc -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the Babel configuration file ├ ─ ─ tsconfig. Json -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ts configuration file ├ ─ ─ tslint. Json -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- tslint configuration file ├ ─ ─ the npmignore -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- NPM contract awarding ignore configuration ├ ─ ─ the gitignore ---------------------------- Git ignores configuration

The directory structure iterates over time, and it is recommended to look at the most recent directory structure on the library

Build a package

Which build tool should I use?

Rollup is a JS module packer that compiles small blocks of code into complex blocks. It is more suitable for JS libraries. Vue, Vuex, DayJS and other excellent open source projects use Rollup. Webpack is a static module wrapper for JS applications, suitable for scenarios involving CSS, HTML, and complex code splitting and merging front-end projects, such as Element-UI.

Simply put, use Webpack for application development and Rollup for library development

If you’re not familiar with Rollup, check it outRollup’s official website

How to build it?

This article mainly describes the construction process of config.js and script/build.js in the following project

The first step is to build the full package. After cofig.js is configured, there are two ways to package it:

  1. The script field of package.json custom directive packages the package in the specified format and exports it to the lib
  2. Get the config.js export rollup configuration in build.js, and package the different formats once through rollup and save them in the lib folder
Custom packaging

Configure the full package of UMD, ES, CJS formats, and the compressed version of MIN in config.js. For the difference between the different formats of UMD/ESM/CJS, please follow the JS modularization specification

......
......
const builds = {
    'm-utils': {
        entry: resolve('dist/src/entry-compiler.js'), // 入口文件路径
        dest: resolve('lib/m-utils.js'), // 导出的文件路径
        format: 'umd', // 格式
        moduleName: 'mUtils', 
        banner,  // 打包后默认的文档注释
        plugins: defaultPlugins // 插件
    },
    'm-utils-min': {
        entry: resolve('dist/src/entry-compiler.js'),
        dest: resolve('lib/m-utils-min.js'),
        format: 'umd',
        moduleName: 'mUtils',
        banner,
        plugins: [...defaultPlugins, terser()]
    },
    'm-utils-cjs': {
        entry: resolve('dist/src/entry-compiler.js'),
        dest: resolve('lib/m-utils-cjs.js'),
        format: 'cjs',
        banner,
        plugins: defaultPlugins
    },
    'm-utils-esm': {
        entry: resolve('dist/src/entry-compiler.js'),
        dest: resolve('lib/m-utils-esm.js'),
        format: 'es',
        banner,
        plugins: defaultPlugins
    },
}


/**
 * 获取对应name的打包配置
 * @param {*} name 
 */
function getConfig(name) {
    const opts = builds[name];
    const config = {
        input: opts.entry,
        external: opts.external || [],
        plugins: opts.plugins || [],
        output: {
            file: opts.dest,
            format: opts.format,
            banner: opts.banner,
            name: opts.moduleName || 'mUtils',
            globals: opts.globals,
            exports: 'named', /** Disable warning for default imports */
        },
        onwarn: (msg, warn) => {
            warn(msg);
        }
    }
    Object.defineProperty(config, '_name', {
        enumerable: false,
        value: name
    });
    return config;
}

if(process.env.TARGET) {
    module.exports = getConfig(process.env.TARGET);
}else {
    exports.defaultPlugins = defaultPlugins;
    exports.getBuild = getConfig;
    exports.getAllBuilds = () => Object.keys(builds).map(getConfig);
}
...... 
......

To package the file for compatibility with Node side and browser-side references, getConfig returns a UMD configuration by default, returns a ROLLUP configuration in the specified format according to the environment variable process.env.target and exports the ROLLUP OPTIONS configuration

In package.json, ‘–environment TARGET:m-utils’ -cjs specifies the value of process.env.TARGET, execute NPM run dev: CJS m-utils-cjs.js and save it in lib

"scripts": {
 
   ......
    "dev:umd": "rollup -w -c scripts/config.js --environment TARGET:m-utils",
    "dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:m-utils-cjs.js",
    "dev:esm": "rollup -c scripts/config.js --environment TARGET:m-utils-esm",
     ......
  },
The build.js build script
. let building = ora('building... '); if (! Fs.existsSync ('lib') {fs.mkdirSync('lib')} let Builds = require('./config').getAllBuilds() // Pack all the configured files  function buildConfig(builds) { building.start(); let built = 0; const total = builds.length; const next = () => { buildEntry(builds[built]).then(() => { built++; if (built < total) { next() } }).then(() => { building.stop() }).catch(logError) } next() } function buildEntry(config) { const output = config.output; const { file } = output; return rollup(config).then(bundle => bundle.generate(output)).then(({ output: [{ code }] }) => { return write(file, code); })}... .

Get all the configurations from the exposed getAllBuilds() method in config.js, pass in the buildConfig method, and package all the configuration files, i.e. m-utils-cjs.js, m-utils-esm.js, and so on.

As you can see from the lodash.js source code, each method is a separate file, so you can import lodash + ‘/’ + the corresponding method name if you need it, which will help with subsequent load-on-demand implementations. Referring to this idea,
Each method of this project is a separate file, and is packaged into the lib path. The implementation is as follows:

. . Function buildSingLefn () {const targetPath1 = path.resolve(__dirname, '.. / ', 'dist/src/') const dir1 = fs.readdirSync(targetPath1) dir1.map(type => { if (/entry-compiler.js/.test(type)) return; const targetPath2 = path.resolve(__dirname, '.. /', `dist/src/${type}`) const dir2 = fs.readdirSync(targetPath2) dir2.map(fn => { if (/.map/.test(fn)) return; try { const targetPath3 = path.resolve(__dirname, '.. /', `dist/src/${type}/${fn}`) fs.readFile(targetPath3, async (err, data) => { if(err) return; Const handleContent = data. The toString (). The replace (/ require \ [". {1, 2} \ [/ w \] / / + "\)/g (match) = > {/ / match for the require (".. /collection/each") => require("./each") const splitArr = match.split('/') const lastStr = splitArr[splitArr.length - 1].slice(0, -2) const handleStr = `require('./${lastStr}')` return handleStr }) const libPath = path.resolve(__dirname, '.. /', 'lib') await fs.writeFileSync(`${libPath}/${fn}`, HandleContent) // single function rollup is packed into the lib root let moduleName = firstupperCase (fn.replace(/.js/, ")); let config = { input: path.resolve(__dirname, '.. /', `lib/${fn}`), plugins: defaultPlugins, external: ['tslib', 'dayjs'], // output: {file: 'lib/${fn} ', format: 'umd', name: `${moduleName}`, globals: { tslib:'tslib', dayjs: 'dayjs', }, banner: '/*! \n' + ` * @author mzn\n` + ` * @desc ${moduleName}\n` + ' */', } } await buildEntry(config); }) } catch (e) { logError(e); }})})} // Construct (full and single) async function build() {if (! fs.existsSync(path.resolve(__dirname, '.. /', 'lib'))) { fs.mkdirSync(path.resolve(__dirname, '.. /', 'lib')) } building.start() Promise.all([ await buildConfig(builds), await buildSingleFn(), ]).then(([result1, result2]) => { building.stop() }).catch(logError) } build(); . .

perform
npm run build, calls the build method to package the full package and the file for the individual function.

The way to package all the individual files needs to be optimized

Unit testing

Unit tests use Karma + Mocha + Coverage + Chai. Karma automatically builds a testing browser environment for us, which can test operations involving DOM and other syntax.

Introduce karma, execute karma init, and generate the karma.config.js configuration file in the root path of the project. The core part is as follows:

Module. exports = function(config) {config.set({// identify ts mime: {'text/ X-TypeScript ': ['ts', 'TSX ']}, // If you use Webpack, you don't need Karma WebpackMiddleware: {noInfo: true, stats: 'errors-only' }, webpack: { mode: 'development', entry: './src/entry-compiler.ts', output: { filename: '[name].js' }, devtool: 'inline-source-map', module: { rules: [{ test: /\.tsx?$/, use: { loader: 'ts-loader', options: { configFile: path.join(__dirname, 'tsconfig.json') } }, exclude: [path.join(__dirname, 'node_modules')] }, { test: /\.tsx?$/, include: [path.join(__dirname, 'SRC ')], Enforce: 'post', Use: {// Webpack before record before compile file loader: 'istanbul-instrumenter-loader', options: { esModules: true } } } ] }, resolve: { extensions: [' benchmark 'and' ts', 'js',' json ']}}, / / generates coverage coverage report coverageIstanbulReporter: {reports: ['html', 'lcovonly', 'text-summary'], dir: path.join(__dirname, 'coverage/%browser%/'), fixWebpackSourcePaths: True, 'report-config': {HTML: {outDir: 'HTML'}}}, // Configure the list of test frames used, default is [] frameworks: ['mocha', 'chai'], // List of files/patterns to load in the browser files: [' test/index.ts'], // Preprocessors: {'test/index.ts': ['webpack', 'coverage']}, // Use a list of reporters: ['mocha', 'nyan', 'coverage-istanbul'], // reporter options mochaReporter: { colors: { success: 'blue', info: 'bgGreen', warning: 'cyan', error: 'bgRed' }, symbols: { success: '+', info: '#', warning: '! ', error: 'x'}}, // Configure coverage report to be seen,type to be seen, HTML, text, dir output directory coverageReporter: {type: 'lcovonly', dir: 'coverage/' }, ... })}

The key to the Webpack configuration is to use istanbul-instrumenter-loader to record the pre-compiled file before packaging, because Webpack will use so much of its code instrumenter-loader for us, and the code coverage is not significant.

To view test coverage, open the HTML browser under Coverage folder.

  • Line Coverage
  • Function Coverage
  • Branch Coverage
  • Statement Coverage

release

Add function

The source code of the current project is written in TypeScript. If you are not familiar with it, please check the TS official documentation first

In the SRC directory, create a new category directory or select a category and add subfiles under the subfolders. Each file is a separate function module. SRC /array/ arrayflat.ts

/** * @author mznorz * @desc * @param {Array} arr * @return {Array} */ function arrayFlat(arr: any[]) {let temp: any[] = []; for (let i = 0; i < arr.length; i++) { const item = arr[i]; if (Object.prototype.toString.call(item).slice(8, -1) === "Array") { temp = temp.concat(arrayFlat(item)); } else { temp.push(item); } } return temp; } export = arrayFlat;

Then expose ArrayFlat in SRC /entry-compiler.ts

In order to obtain the corresponding code completion, interface hints and other functions when using the library, add them in the project root path
index.d.tsDeclare the file and in
package.jsonIn the
typeField specifies the path to the declaration file.

. Declare namespace mUtils {/** * @desc * @param {Array} arr * @return {Array} */ export function arrayFlat(arr: any[]): any[]; . } export = mUtils;

Add test cases

Create a new test case under the test file

import { expect } from "chai"; import _ from ".. /src/entry-compiler"; Describe (" test operation method of array ", () it (= > {" test array tile ", () = > {const arr1 = [1, [2, 3, 4, 5]], [4], 0]; Const arr2 =,2,3,4,5,4,0 [1]; expect(_.arrayFlat(arr1)).to.deep.equal(arr2); }); }); . .

Test and package

Execute NPM run test to see if all the test cases pass, check the code test coverage report under /coverage file, if there is no problem, compile the ts code by executing NPM run compile, then execute NPM run build package

Publish to the NPM private server

[1] For internal use of the company, it is generally released to the internal NPM private server. As for the construction of the NPM private server, there will be no too much description here. [2] Release the NPM scope package here and modify the name in package.json to @mutils/m-utils [3] project entry file. Change mian and module to ‘lib/m-utils-min.js’ and’ lib/m-utils-esm.js’, respectively

  • Main: Defines the entry file for the NPM package, which is available in both the browser and node environments
  • Module: An entry file that defines the ESM specification of the NPM package. Browser and Node environments are available

[4] Set the private server address for publication and modify the PublishConfig field

"publishConfig": {
    "registry": "https://npm-registry.xxx.cn/"
  },

[5] Publish NPM publish

use

  1. Direct downloadlibDirectory m.min.js, through<script>Tags introduced
 <script src="m-utils-min.js"></script> 
 <script> 
  var arrayFlat = mUtils.arrayFlat() 
 </script>
  1. Install using NPM
npm i @mutils/m-utils -S

A direct installation will report an error message that the package cannot be found, and you will need to create an.npmrc file in the project root path and set Registry for the scoped package

registry=https://registry.npmjs.org # Set a new registry for a scoped package # https://npm-registry.xxx.cn server address @mutils:registry=https://npm-registry.xxx.cn
import mUtils from '@mutils/m-utils';
import { arrayFlat } from '@mutils/m-utils';

A link to the

  • The source address

Today’s share is here, the follow-up will continue to improve, I hope to help you ~~

~~ to be continued