Build your own js tool library from scratch typescript+rollup+karma+mocha+coverage

Posted by akrocks_extreme on Wed, 30 Oct 2019 05:09:25 +0100

Preface

With the increase of the company's product lines, there are more and more development and maintenance projects. In the process of business development, it will be found that cookie processing, array processing, throttling and anti shake functions and other tool functions are often used in many projects. In order to avoid the low operation of multiple copy and paste of a code, the author tries to build a JavaScript tool from scratch. Library typescript+rollup+karma+mocha+coverage. This article is mainly written to share with friends with the same needs for reference, hoping to help you.

The source code of the project is at the end of the article. Please check it.~

Directory structure description

├ - scripts
 │ ├ -- config.js ------------- generate the file of rollup configuration
 ├ - build.js -------------- build all the rollup configurations in config.js
 ├ - coverage report
 ├ -- dist ------------- TS output directory of compiled files
 ├ - lib -------------- output directory of files after construction
 ├ - test
 │├ - index.ts ------------ automatic unit test entry file
 │├ - xx.spec.ts -------- unit test file
 ├ - src -------- tool function source code
 │├ - entry compiler.ts -------- function entry file
 │├ - arrayUtils ------------- store utility functions related to array processing
 Array flat. TS ------------- array tiling
│   ├── xx ------------------------------ xx
│   │   ├── xxx.ts ----------------------xxx
 ├ - package.json -------- configuration file
 ├ - package-lock.json
 ├ - index.d.ts ------------ type declaration document
 ├ - karma.conf.js ------------ karma configuration file
 ├ -. Babelrc -------------- Babel configuration file
 ├ - tsconfig.json -------- TS configuration file
 ├ - tslint.json -------------- tslint configuration file
 ├. Npmignore
 ├. Gitignore

The directory structure will iterate over time. It is recommended to check library Latest directory structure on

Building packaging

Which build tool should I choose?

At present, there are many construction tools in the community. Different construction tools can be used in different scenarios. Rollup is a js module packer, which can compile small pieces of code into complex code blocks, and tends to be applied to js libraries. Excellent open-source projects such as vue, vuex, dayjs use rollup, while webpack is a static module packer of js applications, which is applicable to the scenarios involving css and HTM. L. complex code splitting and merging front-end projects, such as element UI.

To put it simply, use webback when developing applications and Rollup when developing Libraries

If you are not familiar with Rollup, it is recommended to check Rollup official website documents

How to build?

Mainly describes the construction process of config.js and script/build.js in the following project

The first step is to build a full package. After cofig.js configuration, there are two ways to package:

  1. The script field custom instruction of package.json packs the package in the specified format and exports it to lib
  2. Get config.js from build.js and export the rollup configuration. Package packages of different formats through rollup and save them to lib folder at one time.

Custom packaging

Configure umd, es and cjs formats in config.js, and full package of compressed version min. * Please move to different formats of umd/esm/cjs.[
JS modular Specification](
https://qwqaq.com/b8fd304a.html)*

......
......
const builds = {
    'm-utils': {
        entry: resolve('dist/src/entry-compiler.js'), // Entry file path
        dest: resolve('lib/m-utils.js'), // Exported file path
        format: 'umd', // format
        moduleName: 'mUtils', 
        banner,  // Default document comments after packaging
        plugins: defaultPlugins // Plug-in unit
    },
    '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
    },
}


/**
 * Get the packaging configuration of the corresponding 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);
}
...... 
......

In order to package files compatible with node side and browser side references, getConfig returns the configuration in umd format by default, returns the 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
Specify the value of process.env.TARGET, execute npm run dev:cjs m-utils-cjs.js and save to 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",
     ......
  },
build.js build script
  ......
let building = ora('building...');
if (!fs.existsSync('lib')) {
  fs.mkdirSync('lib')
}

// Get rollup configuration
let builds = require('./config').getAllBuilds()

//Pack all configuration 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 configurations from the getAllBuilds() method exposed in config.js, pass in the buildConfig method, and package all configuration files, i.e. m-utils-cjs.js, m-utils-esm.js and other files.

You can see from the source code of lodash.js that each method is an independent file, so you can import the method name corresponding to lodash + '/' + as needed, which is conducive to the subsequent implementation of on-demand loading. Referring to this idea, each method of this project is an independent file, which is packaged and saved to the lib path. The implementation is as follows:

...... 
......

//Export a single function
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.toString().replace(/require\(".{1,2}\/[\w\/]+"\)/g, (match) => {
              //match is 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)
             //Package single function roll up to the root directory of lib file
            let moduleName = firstUpperCase(fn.replace(/.js/,''));
            let config = {
              input: path.resolve(__dirname, '../', `lib/${fn}`),
              plugins: defaultPlugins,
              external: ['tslib', 'dayjs'], // Because functions are written in ts, external external references to tslib are used to reduce packaging volume.
              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);
      }
    })
  })
}
//Build packaging (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();

...... 
......

Execute npm run build, call the build method, package the files of full package and single function.

The way to package all individual files is to be optimized

unit testing

karma + mocha + coverage + chai is used for unit testing. karma automatically creates a browser environment for testing, 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({
    //Identifying ts
    mime: {
      'text/x-typescript': ['ts', 'tsx']
    },
    //If you use webpack, you don't need a karma matching file, only one entry is left for 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: {
            //Record precompiled files before packaging with webpack
              loader: 'istanbul-instrumenter-loader',
              options: { esModules: true }
            }
          }
        ]
      },
      resolve: {
        extensions: ['.tsx', '.ts', '.js', '.json']
      }
    },
    //Generate 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 frameworks used, default is []
    frameworks: ['mocha', 'chai'],
    // list of files / patterns to load in the browser
    files: [
      'test/index.ts'
    ],
    //Preprocessing
    preprocessors: {
      'test/index.ts': ['webpack', 'coverage']
    },
    //List of reporter s used
    reporters: ['mocha', 'nyan', 'coverage-istanbul'],
    // reporter options
    mochaReporter: {
      colors: {
        success: 'blue',
        info: 'bgGreen',
        warning: 'cyan',
        error: 'bgRed'
      },
      symbols: {
        success: '+',
        info: '#',
        warning: '!',
        error: 'x'
      }
    },
    //Configure the view method of coverage report, type view type, html, text, etc., and dir output directory.
    coverageReporter: {
      type: 'lcovonly',
      dir: 'coverage/'
    },
    ...
  })
}

In the configuration, the key point of webpack is to use istanbulinstrumentor loader to record the precompiled files before packaging. Because webpack will help us add a lot of its code, and the resulting code coverage is meaningless.

View the test coverage, open the html browsing under the 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 it first. Official documents of ts

In the src directory, create a new category directory or select a category, and add sub files in the sub folder. Each file is a separate function module. (as follows: src/array/arrayFlat.ts)


/**
 * @author mznorz
 * @desc Array tile  
 * @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 prompt and other functions when using the library, add the index.d.ts declaration file under the project root path, and specify the path of the declaration file in the type field of package.json.

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

export = mUtils;

Add test case

Create a new test case under the test file

import { expect } from "chai";
import _ from "../src/entry-compiler";

describe("Test array operation method", () => {
  it("Test array tiling", () => {
    const arr1 = [1,[2,3,[4,5]],[4],0];
    const arr2 = [1,2,3,4,5,4,0];
    expect(_.arrayFlat(arr1)).to.deep.equal(arr2);
  }); 
});
...... 
......

Test and pack

Execute npm run test, check whether all test cases pass, and check the code test coverage report under the / coverage file. If there is no problem, execute npm run compile to compile ts code, and then execute npm run build package.

Publish to npm private server

[1] the internal use of the company is generally the npm private service that is released to the internal. For the construction of the npm private service, I will not explain it too much here.
[2] publish the npm scope package here. Modify the name in package.json to @ m utils / m-utils.
[3] the entry file of the project. Modify the mian and module to`
lib/m-utils-min.js and lib/m-utils-esm.js`

  • main: defines the entry file of the npm package, which can be used by the browser environment and node environment.
  • module: the entry file defining the ESM specification of the npm package, which can be used by browser environment and node environment.

[4] set the published private server address and modify the publishConfig field.

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

[5] execute npm publish to publish the login account and password

Use

  1. Download the m.min.js in the lib directory directly, and import it through the < script > tag.
 <script src="m-utils-min.js"></script> 
 <script> 
  var arrayFlat = mUtils.arrayFlat() 
 </script>
  1. Install with npm
npm i @mutils/m-utils -S

The direct installation will report that the error information of the package cannot be found. You need to create the. npmrc file in the root path of the project and set the registry for the scope package.

registry=https://registry.npmjs.org

# Set a new registry for a scoped package
# https://npm-registry.xxx.cn private server address

@mutils:registry=https://npm-registry.xxx.cn  
import mUtils from '@mutils/m-utils';
import { arrayFlat } from '@mutils/m-utils';

Related links

Today's sharing is here, and will continue to improve in the future. I hope it will help you.~~

~~To be continued

Topics: Javascript npm JSON Webpack TypeScript