Here comes the big one.

Other content about the component library can be found on Github

An overview of

The host environment varies, and the source code needs to be processed and distributed to NPM.

Identify the following goals:

  1. Export the type declaration file
  2. exportumd/Commonjs module/ES moduleAnd so on 3 forms for users to introduce
  3. Supporting style filescssIntroduce, not just haveless
  4. Support loading on demand

All code for this section is available in the chapter-3 branch of the repository.

Export the type declaration file

Since this is a component library written in typescript, users should enjoy the benefits of the type system.

We can generate a type declaration file and define entries in package.json as follows:

package.json

{
  "typings": "types/index.d.ts".// Define the type entry file
  "scripts": {
    "build:types": "tsc --emitDeclarationOnly" // the TSC command generates only the declaration file}}Copy the code

Run yarn Build :types to find that the types folder (outDir field defined in tsconfig.json) has been generated in the root directory. The directory structure is the same as that of the Components folder, as follows:

types

├ ─ ─ alert │ ├ ─ ─ alert, which s │ ├ ─ ─ the index, which s │ ├ ─ ─ interface, which s │ └ ─ ─ style │ └ ─ ─ the index, which s └ ─ ─ the index, which sCopy the code

This allows consumers to be automatically prompted when importing an NPM package and to reuse the type definition of the associated component.

Next, files such as TS (x) are processed into JS files.

Note that we need to output both Commonjs Module and ES Module files (not considering UMD). CJS refers to Commonjs Module and ESm refers to ES Module. Exports: import, require, export, module.exports

Export the Commonjs module

It is perfectly possible to compile the code using Babel or TSC command-line tools (in fact, many libraries do), but given the styling and loading on demand, we use gulp to string the process together.

Babel configuration

Start by installing Babel and its associated dependencies

yarn add @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript @babel/plugin-proposal-class-properties  @babel/plugin-transform-runtime --dev
Copy the code
yarn add @babel/runtime-corejs3
Copy the code

Create a new.babelrc.js file and write the following:

.babelrc.js

module.exports = {
  presets: ['@babel/env'.'@babel/typescript'.'@babel/react'].plugins: [
    '@babel/proposal-class-properties'['@babel/plugin-transform-runtime',
      {
        corejs: 3.helpers: true,},],],};Copy the code

About @babel/plugin-transform-runtime and @babel/runtime-corejs3

  • ifhelpersOption set totrue, can be extracted from the code compilation process repeatedly generatedhelperFunction (classCallCheck.extendsEtc.), reduce the generated code volume;
  • ifcorejsSet to3, can be introduced not to pollute the global on demandpolyfill, commonly used in class library writing (I prefer: not introducedpolyfillIn turn, tell the user what needs to be introducedpolyfillTo avoid repeated introductions or conflicts, more on that later).

See the official documentation -@babel/ plugin-transform-Runtime for more information

Configuring the target Environment

To avoid translating native browser supported syntax, create a new. Browserslistrc file and write the supported browser scope to @babel/preset-env based on adaptation requirements.

.browserslistrc

>0.2%
not dead
not op_mini all
Copy the code

Unfortunately, @babel/ Runtime-corejs3 is not able to reduce the introduction of polyfill again on an on-demand basis depending on target browser support, see @babel/ Runtime for Target Environment.

This means that @babel/ Runtime-corejs3 would inject all possible polyfills even for modern engines: unnecessarily increasing the size of the final bundle.

For component libraries (which can be large in code), I recommend giving the user the option to polyfill in the host environment. If the user is compatible, it’s natural to use @babel/preset-env + core-js +.browserslistrc for global polyfills. This one-two combo introduces all polyfills that the lowest target browser doesn’t support the API.

Set @babel/preset-env’s useBuiltIns to usage and exclude node_modules from babel-loader might want this feature: “useBuiltIns: Usage “for node_modules without transpiling #9419, set useBuiltIns to Entry without supporting the content mentioned in this issue, Or do not exclude node_modules from babel-loader.

So the component library is more important than anything else (like Zent and ANTD) by introducing extra polyfills and documenting them well.

Now @babel/runtime-corejs3 is replaced with @babel/runtime, and only the helper functions are removed.

yarn remove @babel/runtime-corejs3

yarn add @babel/runtime
Copy the code

.babelrc.js

module.exports = {
  presets: ['@babel/env'.'@babel/typescript'.'@babel/react'].plugins: ['@babel/plugin-transform-runtime'.'@babel/proposal-class-properties']};Copy the code

The helper option for @babel/ transform-Runtime defaults to true.

Gulp configuration

Install gulp-related dependencies

yarn add gulp gulp-babel --dev
Copy the code

Create gulpfile.js and write the following:

gulpfile.js

const gulp = require('gulp');
const babel = require('gulp-babel');

const paths = {
  dest: {
    lib: 'lib'.// Commonjs file directory name - this block care
    esm: 'esm'.// The name of the directory where the ES module file is stored
    dist: 'dist'.// The name of the directory where the umd file is stored
  },
  styles: 'components/**/*.less'.// Style file path - do not care for now
  scripts: ['components/**/*.{ts,tsx}'.'! components/**/demo/*.{ts,tsx}'].// Script file path
};

function compileCJS() {
  const { dest, scripts } = paths;
  return gulp
    .src(scripts)
    .pipe(babel()) // Use gulp-babel
    .pipe(gulp.dest(dest.lib));
}

// Parallel tasks can be processed in parallel after the addition of style processing
const build = gulp.parallel(compileCJS);

exports.build = build;

exports.default = build;
Copy the code

Modify the package. The json

package.json

{
- "main": "index.js",
+ "main": "lib/index.js",
  "scripts": {
    ...
+ "clean": "rimraf types lib esm dist",
+ "build": "npm run clean && npm run build:types && gulp",. }},Copy the code

Run yarn build to obtain the following information:

lib

├ ─ ─ alert │ ├ ─ ─ alert. Js │ ├ ─ ─ index. The js │ ├ ─ ─ interface. The js │ └ ─ ─ style │ └ ─ ─ index. The js └ ─ ─ index, jsCopy the code

If you look at the compiled source code, you can see that many helper methods have been removed from @babel/ Runtime, and module import/export form is also the CommonJS specification.

lib/alert/alert.js

Export the ES module

Building ES Modules makes tree shaking better. Based on the Babel configuration from the previous step, update the following:

  1. configuration@babel/preset-envthemodulesOptions forfalse, close module conversion;
  2. configuration@babel/plugin-transform-runtimetheuseESModulesOptions fortrue, the use ofES moduleIn the form of introductionhelperFunction.

.babelrc.js

module.exports = {
  presets: [['@babel/env',
      {
        modules: false.// Turn off module conversion},].'@babel/typescript'.'@babel/react',].plugins: [
    '@babel/proposal-class-properties'['@babel/plugin-transform-runtime',
      {
        useESModules: true.// 使用esm形式的helper},]],};Copy the code

When the goal is reached, we use environment variables to distinguish ESM and CJS (just set the corresponding environment variables when executing the task), and the final Babel configuration is as follows:

.babelrc.js

module.exports = {
  presets: ['@babel/env'.'@babel/typescript'.'@babel/react'].plugins: ['@babel/plugin-transform-runtime'.'@babel/proposal-class-properties'].env: {
    esm: {
      presets: [['@babel/env',
          {
            modules: false,}]].plugins: [['@babel/plugin-transform-runtime',
          {
            useESModules: true,},],],},},};Copy the code

Next, modify gulp configuration to remove compileScripts task and add compileESM task.

gulpfile.js

// ...

/** * Compile the script file *@param {string} BabelEnv Babel environment variable *@param {string} DestDir Target directory */
function compileScripts(babelEnv, destDir) {
  const { scripts } = paths;
  // Set environment variables
  process.env.BABEL_ENV = babelEnv;
  return gulp
    .src(scripts)
    .pipe(babel()) // Use gulp-babel
    .pipe(gulp.dest(destDir));
}

/** * Compiles CJS */
function compileCJS() {
  const { dest } = paths;
  return compileScripts('cjs', dest.lib);
}

/** * Compile esM */
function compileESM() {
  const { dest } = paths;
  return compileScripts('esm', dest.esm);
}

// Execute compile script tasks (CJS, ESM) serially to avoid environment variables
const buildScripts = gulp.series(compileCJS, compileESM);

// Execute tasks in parallel
const build = gulp.parallel(buildScripts);

// ...
Copy the code

After running YARN Build, three folders types/lib/esm are generated. The ESM directory structure is the same as that of lib/types. Js files are imported and exported as ES Module modules.

esm/alert/alert.js

Don’t forget to add relevant entries to package.json.

package.json

{
+ "module": "esm/index.js"
}
Copy the code

Working with style files

Copying less files

We will less file contains the NPM package, the user can through the happy – UI/lib/alert/style/index. In the form of js on-demand introduced less file, here is a less directly to copy files to the target folder.

Create a new copyLess task in gulpfile.js.

gulpfile.js

// ...

/** * Copy less file */
function copyLess() {
  return gulp
    .src(paths.styles)
    .pipe(gulp.dest(paths.dest.lib))
    .pipe(gulp.dest(paths.dest.esm));
}

const build = gulp.parallel(buildScripts, copyLess);

// ...
Copy the code

If you look at the lib directory, you can see that the less file has been copied to the alert/style directory.

lib

├ ─ ─ alert │ ├ ─ ─ alert. Js │ ├ ─ ─ index. The js │ ├ ─ ─ interface. The js │ └ ─ ─ style │ ├ ─ ─ index. The js │ └ ─ ─ but less # less file └ ─ ─ index.jsCopy the code

Some of you may have noticed the problem: if the user is using sASS or even native CSS without a less preprocessor, the existing solution won’t work. After analysis, there are the following three pre-selection schemes:

  1. Inform the user to addless-loader;
  2. Wrap up a whole servingcssFile, proceedFull amountThe introduction of;
  3. Provide a separate copystyle/css.jsFile, which introduces componentscssFile dependencies, notlessDependency, component library layer to smooth out differences.

Option 1 will result in an increase in usage costs.

Scenario 2 does not have an on-demand import of the style file (we will provide this later in the UMD packaging).

Both of the above are bad options (voiceover: if you use CSS in JS, there is no such shit).

Scheme 3 is more suitable for this scenario, and ANTD also uses this scheme.

In the process of building the component library, a question puzzled me for a long time: why do I need alert/style/index.js to introduce less files or alert/style/css.js to introduce CSS files?

The answer is managing style dependencies.

Assume the following scenario: , relies on
, users need to manually import the style of the called component () and its dependent component style (
), encounter complex components extremely troublesome, so component library developers can provide such a JS file, By manually importing the JS file, the user can import the styles of the corresponding component and its dependent components.

Continue our journey.

Generating CSS Files

Install dependencies.

yarn add gulp-less gulp-autoprefixer gulp-cssnano --dev
Copy the code

Add the less2CSS task to gulpfile.js.

// ...

/** * Generate CSS file */
function less2css() {
  return gulp
    .src(paths.styles)
    .pipe(less()) // Process less files
    .pipe(autoprefixer()) // Add the prefix according to browserslistrc
    .pipe(cssnano({ zindex: false.reduceIdents: false })) / / compression
    .pipe(gulp.dest(paths.dest.lib))
    .pipe(gulp.dest(paths.dest.esm));
}

const build = gulp.parallel(buildScripts, copyLess, less2css);

// ...
Copy the code

Run yarn Build. The CSS file already exists in the component style directory.

Next we need an alert/style/css.js to help the user import the CSS file.

To generate CSS. Js

Here’s how antD-Tools works: In the scripts task, intercept style/index.js, generate style/css.js, and use the re to change the introduced less file suffix to CSS.

Install dependencies.

yarn add through2 --dev
Copy the code

gulpfile.js

// ...

/** * Compile the script file *@param {*} BabelEnv Babel environment variable *@param {*} DestDir Target directory */
function compileScripts(babelEnv, destDir) {
  const { scripts } = paths;
  process.env.BABEL_ENV = babelEnv;
  return gulp
    .src(scripts)
    .pipe(babel()) // Use gulp-babel
    .pipe(
      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)); // File content processing
          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

CssInjection implementation:

gulpfile.js

/** * Current component styles import './index.less' => import './index.css' * Dependent other component styles import '.. /test-comp/style' => import '.. /test-comp/style/css.js' * dependent other component styles import '.. /test-comp/style/index.js' => import '.. /test-comp/style/css.js' *@param {string} content* /
function cssInjection(content) {
  return content
    .replace(/\/style\/? '/g."/style/css'")
    .replace(/\/style\/?" /g.'/style/css"')
    .replace(/\.less/g.'.css');
}
Copy the code

After the package is packaged, you can see that the component style directory is generated into css.js file, which is also the CSS file converted from the previous step less.

lib/alert

├ ─ ─ alert. Js ├ ─ ─ index. The js ├ ─ ─ interface. The js └ ─ ─ style ├ ─ ─ CSS, js # introduced index. The CSS ├ ─ ─ index. The CSS ├ ─ ─ index. The js └ ─ ─ index. The lessCopy the code

According to the need to load

Add sideEffects properties to package.json to work with ES Module for tree shaking effect (mark style dependent files as Side Effects to avoid removing them by mistake).

// ...
"sideEffects": [
  "dist/*"."esm/**/style/*"."lib/**/style/*"."*.less"].// ...
Copy the code

It is possible to load the JS part on demand using the following method, but the style needs to be imported manually:

import { Alert } from 'happy-ui';
import 'happy-ui/esm/alert/style';
Copy the code

It can also be introduced in the following ways:

import Alert from 'happy-ui/esm/alert'; // or import Alert from 'happy-ui/lib/alert';
import 'happy-ui/esm/alert/style'; // or import Alert from 'happy-ui/lib/alert';
Copy the code

The above way of introducing style files is not very elegant, and introducing full style files directly is a far cry from the intent of loading on demand.

The user can use babel-plugin-import to help reduce the amount of code to be written.

import { Alert } from 'happy-ui';
Copy the code

⬇ ️

import Alert from 'happy-ui/lib/alert';
import 'happy-ui/lib/alert/style';
Copy the code

Generate the umd

True sense of the “package”, the generation of full JS files and CSS files for users to introduce the chain. Select rollUp here for packaging.

Leave the hole to be filled in.

To be Continued…