From project construction to plug-in publishing to npm

preface

In our usual development work, we can pull out many common components and methods and publish them in the form of npm plug-ins on npm or our own npm private library to achieve the reuse effect.

Taking a react plug-in as an example, this article will go through a series of steps, such as development project construction, plug-in writing, npm packaging and release, and develop an npm plug-in with my partners.

Engineering construction

The project is built with webpack5 +, react17 +, less and TypeScript as the main body.

Project structure

|-- demo
    |-- .babelrc
    |-- .gitignore
    |-- package.json
    |-- tsconfig.json
    |-- README.md
    |-- dist
    |-- types
    |-- public
    |   |-- index.html
    |-- scripts
    |   |-- webpack.base.config.js
    |   |-- webpack.dev.config.js
    |   |-- webpack.prod.config.js
    |-- src
        |-- index.less
        |-- index.tsx
        |-- component
            |-- index.less
            |-- index.tsx
            |-- message-card.tsx

Some files will be briefly described below.

package.json

Project dependencies and configurations. You can directly:

npm install

Here are two fields: files and types. These two fields may be used less in our normal development, but they are very useful in developing npm plug-ins.

The first is files, which can specify the array of folders or files that we need to upload to npm after our development. It can be said that it has the opposite effect to. npmignore.

The second is types, the entry file of typescript, where we can specify the file address where we place xx.d.ts. Without this, the npm plug-in we uploaded may report that the type file cannot be found after it is downloaded.

{
  "name": "message-card",
  "version": "1.0.1",
  "main": "dist/message-card.js",
  "scripts": {
    "build": "webpack --config ./scripts/webpack.prod.config.js",
    "start": "webpack serve --config ./scripts/webpack.dev.config.js"
  },
  "repository": "https://github.com/XmanLin/message-card",
  "keywords": [
    "react",
    "component"
  ],
  "author": "Xmanlin",
  "license": "MIT",
  "files": [
    "dist",
    "types"
  ],
  "typings": "types/index.d.ts",
  "devDependencies": {
    "@babel/cli": "^7.14.5",
    "@babel/core": "^7.14.5",
    "@babel/preset-env": "^7.14.5",
    "@babel/preset-react": "^7.14.5",
    "@babel/preset-typescript": "^7.14.5",
    "@types/react": "^17.0.11",
    "@types/react-dom": "^17.0.7",
    "babel-loader": "^8.2.2",
    "clean-webpack-plugin": "^4.0.0-alpha.0",
    "css-loader": "^5.2.6",
    "file-loader": "^6.2.0",
    "html-webpack-plugin": "^5.3.1",
    "less": "^4.1.1",
    "less-loader": "^9.1.0",
    "optimize-css-assets-webpack-plugin": "^6.0.0",
    "style-loader": "^2.0.0",
    "terser-webpack-plugin": "^5.1.3",
    "typescript": "^4.3.2",
    "url-loader": "^4.1.1",
    "webpack": "^5.38.1",
    "webpack-cli": "^4.5.0",
    "webpack-dev-server": "^3.11.2",
    "webpack-merge": "^5.8.0"
  },
  "dependencies": {
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  }
}

.babelrc

babel related configuration.

{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": [
    "@babel/plugin-proposal-class-properties"
  ]
}

.gitignore

This is not listed one by one. You may be different. If you are interested, you can see it Project source code.

tsconfig.json

This can also be done according to your own preferences.

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    },
    "strictNullChecks": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "jsx": "react",
    "noUnusedParameters": true,
    "noUnusedLocals": true,
    "noImplicitAny": true,
    "target": "es6",
    "lib": ["dom", "es2017"],
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

scripts

Here are some configurations of webpack. The configuration file is divided into three parts. One is the common basic configuration for development and production, and the other two are separate configurations for development and production. Of course, it can also be combined. This depends on yourself.

webpack.base.config.js

const webpackBaseConfig = {
    module: {
        rules: [
            {
                test: /\.(js|jsx|ts|tsx)$/,
                exclude: /node-modules/,
                loader: 'babel-loader',
                options: {
                    cacheDirectory: true,
                    cacheCompression: false,
                    presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
                },
            },
            {
                test: /\.(css|less)$/,
                use: [
                    {
                        loader: "style-loader",
                    },
                    {
                        loader: "css-loader",
                        options: {
                            importLoaders: 1,
                        },
                    },
                    {
                        loader: "less-loader"
                    }
                ]
            },
            {
                test: /\.(png|jpg|gif)$/i,
                type: 'asset/resource'
            }
        ]
    }
}

module.exports = webpackBaseConfig

webpack.dev.config.js

const path = require('path');
const webpack = require('webpack');
const webpackBaseConfig = require('./webpack.base.config');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { merge } = require('webpack-merge');

function resolve(relatedPath) {
    return path.join(__dirname, relatedPath)
}

const webpackDevConfig = {
    mode: 'development',
    
    entry: {
        app: resolve('../src/index.tsx')
    },

    output: {
        path: resolve('../dist'),
        filename: 'message-card.js',
    },

    devtool: 'eval-cheap-module-source-map',
    resolve: {
        extensions: ['.ts', '.tsx', '.js', '.jsx', '.css', '.less']
    },
    devServer: {
        contentBase: resolve('../dist'),
        hot: true,
        open: true,
        host: 'localhost',
        port: 8080,
    },
    plugins: [
        new HtmlWebpackPlugin({template: './public/index.html'}),
        new webpack.HotModuleReplacementPlugin()
    ]
}

module.exports = merge(webpackBaseConfig, webpackDevConfig) 

webpack.prod.config.js

const path = require('path');
const webpack = require('webpack');
const webpackBaseConfig = require('./webpack.base.config');
const TerserJSPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const { merge } = require('webpack-merge');

function resolve(relatedPath) {
    return path.join(__dirname, relatedPath)
}

const webpackProdConfig = {
    mode: 'production',

    entry: {
        app: [resolve('../src/component/index.tsx')]
    },

    output: {
        filename: 'message-card.js',
        path: resolve('../dist'),
        library: {
            type: 'commonjs2'
        }
    },

    resolve: {
        extensions: ['.ts', '.tsx', '.js', '.jsx', '.css', '.less']
    },

    devtool: 'source-map',
    optimization: {
        minimizer: [
            new TerserJSPlugin({
                parallel: 4,
                terserOptions: {
                    compress: {
                        drop_console: true
                    },
                },
            }),
            new OptimizeCSSAssetsPlugin()
        ],
    },
    plugins:[
        new CleanWebpackPlugin()
    ]
}

module.exports = merge(webpackBaseConfig, webpackProdConfig)

After the webpack is configured, we can cooperate with our commands in package.json:

"scripts": {
    "build": "webpack --config ./scripts/webpack.prod.config.js",
    "start": "webpack serve --config ./scripts/webpack.dev.config.js"
  }

Why should I mention it here alone, because the configurations here are slightly different from webpack5 + and webpack4 +.

In webpack4 + (this can also be configured in webpack5, but the webpack cli should be reduced to version 3 +):

"start": "webpack-dev-server --config ./scripts/webpack.dev.config.js"

At the same time, the webpack cli is reduced to version 3 +.

Plug in development

After the development project is built, we can start the development of plug-ins.

Debug file

public/index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>My components</title>
</head>
<body>
<div id="root" class="root"></div>
</body>
</html>

src/index.tsx

Here is mainly a blank page to introduce the plug-ins we are developing. We can develop while watching the effect, which is very intuitive.

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import MessageCard from './component/index';
import './index.less'

const App = () => {
    return (
        <div className="container">
            <MessageCard
                title="Card one"
                content="Here is the content"
            />
        </div>
    )
}


ReactDOM.render(<App />, document.getElementById('root'));

Plug in code

Here is the source code of our plug-in. There is not much code.

src/component/index.tsx

Entry file when packaging plug-ins

import MessageCard from './message-card';

export default MessageCard;

src/component/message-card.tsx

import React  from 'react';
import './index.less';

export interface ICard {
    title: string;
    content?: string;
}

const MessageCard: React.FC<ICard> = (props) => {

    const { title, content } = props;
    
    return (
        <div className="card">
            <div className="title">{title}</div>
            <div className="content">{content}</div>
        </div>
    )
} 

export default MessageCard

src/component/index.less

.card {
    border: 1px solid #eee;
    border-radius: 4px;
    padding: 20px 20px;
    .title {
        min-height: 50px;
        border-bottom: 1px solid #eee;
        font-size: 20px;
        font-weight: bold;
    }
    .content {
        min-height: 100px;
        font-size: 16px;
        padding: 10px 0;
    }
}

pack

After the plug-in is developed, we can execute the command to package:

npm run build

After packaging, we can get our dist folder and the message-card.js file inside.

debugging

Before we release our npm plug-in, we need to conduct local debugging:

npm link (in package dir)
npm link [<@scope>/]<pkg>[@<version>]

alias: npm ln

You can see the specific usage Official documents , you can also have a look This article , it's very clear

Publish to npm

There must be an npm account before contracting. Just register one on the npm official website.

release

Log in to npm

Log in to npm, type the command and fill in the prompt:

npm login

Release package

Enter the following command in the project root directory:

npm publish

Note here:

  1. Remember to change the npm source address to: http://registry.npmjs.org , many people will use Taobao image or private source, which can not be published on npm;
  2. Remember to log in before contracting out.

to update

Version update is very simple. Modify the version field in package.json, and then:

npm publish

delete

Delete a version:

npm unpublish [<@scope>/]<pkg>[@<version>]

For example, we want to delete a version:

npm unpublish message-card@1.0.0

Delete entire package:

npm unpublish [<@scope>/]<pkg> --force

reference resources

https://github.com/XmanLin/message-card

https://webpack.docschina.org/concepts/

https://docs.npmjs.com/

https://react.docschina.org/docs/getting-started.html

last

This paper is based on practice from project construction to actual release. I believe it is helpful to some small partners. The plug-ins we developed can not only be sent to npm, but also can be packaged and released if there are private sources of the company or built by ourselves. We just need to change the contracting address.

The article is worth improving or has problems. Welcome to discuss it together~

Keywords: Javascript node.js Front-end npm React

Added by drummerboy on Wed, 15 Sep 2021 21:55:43 +0300