In Verdaccio building NPM Private server, we introduced how to build a NPM private server; With the server set up, we will learn how to upload our own NPM package in this chapter.

This article was first published in the public number [front-end one read], more exciting content please pay attention to the latest news of the public number.

As a necessary skill of front-end, front-end modularization has been indispensable in front-end development. And modularization brings the scale of the project is getting bigger and bigger, the project depends on more and more; As the number of projects increases, if every module is copied manually, it is like drinking poison to quench thirst. We can extract modules or components with similar functions into an NPM package. It then uploads to a private NPM server and iterates through the NPM package to update the dependencies that manage all projects.

Basic understanding of NPM packages

First, let’s take a look at what it takes to implement an NPM package.

packaging

Usually, we pack some module files in a directory, easy to load uniformly; Yes, NPM packages need to be packaged, and while it is possible to write NPM package modules directly (not recommended), we often use typescript, Babel, ESLint, code compression, etc., so we need to package NPM packages before we publish them.

In an in-depth comparison of Webpack, Parcel, and Rollup packaging tools, we concluded that Rollup is better than Webpack for packaging some third-party libraries, so this article focuses on Rollup packaging.

NPM package domain level

As the number of NPM packages increases and the package names are unique, if a name is taken by someone else, you can no longer use that name; Let’s say I want to develop a utils package, but John has already published a utils package, then my package name cannot be utils; At this point we can add some concatenation or other characters to distinguish, but this will make the package name unreadable.

In NPM’s package management system, there is a scoped Packages mechanism for domain-level package management by grouping NPM packages under a namespace named @scope/package.

Domain-level packages do not need to be named the same as others’ packages, and packages with similar functions can be divided and managed uniformly. For example, our vue scaffolding project has domain level packages such as @vue/cli-plugin-babel, @vue/ cli-plugin-esLint, etc.

We can use the command line to add scope when initializing the project:

npm init --scope=username
Copy the code

Packages in the same domain-scope are installed in the same file path, such as node_modules/@username/, and can contain any number of scoped packages. Installing domain-level packages also requires specifying their scope:

npm install @username/package
Copy the code

Scope scope is also required when introduced in code:

require("@username/package")
Copy the code

Load the rules

In package.json files in NPM packages, we often see fields like main, jsNext :main, Module, browser, etc. What do these fields mean? In fact, it depends on the working environment of NPM packages. As we know, NPM packages are divided into the following types:

  • It can only be used in the browser
  • It can only be used on the server side
  • It can be used on both browser and server sides

If we develop an NPM package that supports both the browser side and the server side (e.g. Axios, LoDash, etc.), we need to load different entry files of the NPM package in different environments, and just one field is no longer sufficient.

The main field is the default nodeJS file entry and is the most widely supported. It is mainly used when referring to a dependency package. If we don’t use the main field, we might need to refer to dependencies like this:

import('some-module/dist/bundle.js')
Copy the code

So its function is to tell the packager which file the NPM package entry file is, and which file to let the packager import when packaging; The files here are typically commonJS (CJS) modular.

There are packaging tools, such as WebPack or rollup, that directly handle imported ESM modules. We can package the module file as an ESM module and specify the Module field. It is up to the user of the package to decide how to reference it.

The jsNext :main and Module fields have the same meaning, both can specify esM module files; But jsNext: Main is a community convention field, not an official one, and module is an official convention field, so we often use both fields together.

MainFields is used by Webpack to parse modules. By default, browser, Module, and main fields are parsed in order.

Sometimes we want to write an NPM package that runs on both the browser and the server (such as Axios), but the environment is slightly different. For example, the browser requests data using XMLHttpRequest and the server using HTTP or HTTPS. So how do we distinguish between different environments?

In addition to checking the environment parameters in your code (such as whether XMLHttpRequest is undefined), you can also use the browser field to replace the main field in the browser environment. Browser can be used in either of the following ways. If browser is a single string, it replaces main with the entry file of the browser environment, usually the UMD module:

{
  "browser": "./dist/bundle.umd.js"
}
Copy the code

Browser can also be an object that declares files to be replaced or ignored. This format is better for replacing parts of the file without creating a new entry. Key is the module or file name to be replaced, and to the right is the new file to be replaced. For example, this substitution is used in axios’s packages.json:

{
  "browser": {
    "./lib/adapters/http.js": "./lib/adapters/xhr.js"}}Copy the code

When the packaging tool packages to the browser environment, it replaces the imported file from./lib/ Adapters /http.js with the./lib/ Adapters /xhr.js file.

In some packages we also see the types field pointing to the types/index.d.ts file, which contains the types of variables and functions of the NPM package; For example, when we used the Lodash-es package, we could not remember the names of some functions. For example, if you type fi, you can automatically come up with a function name such as fill or findIndex in the compiler, which provides a great convenience for the package user, without having to look at the contents of the package, you can know the parameter name of the exported package, providing a better IDE support for the user.

Which files to publish

In the NPM package, we can choose which files to distribute to the server, such as only distributing the compressed code and filtering the source code; We can specify this through configuration files, which can be divided into the following cases:

  • There are.npmignoreFile to.npmignoreThe content in the file will be ignored and will not be uploaded. Even if there is.gitignoreThe document will not take effect.
  • There is no.npmignoreFile to.gitignoreThe file prevails and is generally irrelevant, for example, vscode and other environment configuration related.
  • There is no.npmignoreThere is no.gitignoreAll files will be uploaded.
  • package.jsonThere is a files field, which can be interpreted as files is a whitelist.

Ignore is a blacklist and files is a whitelist. When the two contents conflict, which one shall prevail? The answer is files, which has the highest priority.

The NPM pack command will generate a TGZ zip in the project root directory, which is the content of the file to be uploaded.

Project depend on

In package.json, all dependencies are managed in the dependencies and devDependencies fields:

  • Dependencies: Indicates the production environment dependencies, –save -s;
  • DevDependencies: devDependencies in the development environment. –save-dev -d.

The Dependencies field specifies the module that the project depends on when it goes live. For example, vue, jquery, Axios, etc., will continue to be used after the project goes online.

The devDependencies field specifies the modules required for the development of the project, and what the development environment will use. For example, webpack, ESLint, etc., we use them when we pack, but we don’t need them when we go live, so put them in devDependencies.

In addition to dependencies and devDependencies, the peerDependencies field is also found in the NPM package. The peerDependencies field is also found in the NPM plugin.

Suppose our project, MyProject, has a dependency on PackageA, and its PackageB dependency is specified in package.json, so our project structure looks like this:

MyProject
|- node_modules
   |- PackageA
      |- node_modules
         |- PackageB
Copy the code

So we can reference PackageA dependencies directly in MyProject, but if we want to use PackageB directly, sorry, we can’t; Even if PackageB is already installed, Node will only look for PackageB in MyProject/node_modules.

To solve this problem, the peerDependencies field is introduced, which translates to: If you install me, you better install the following dependencies as well. For example, if we add the following code to PackageA’s package.json:

{
    "peerDependencies": {
        "PackageB": "1.0.0"}}Copy the code

So if you install PackageA, it will automatically install PackageB, forming the following directory structure:

MyProject
|- node_modules
   |- PackageA
   |- PackageB
Copy the code

We can happily use the PackageA and PackageB dependencies in MyProject.

The familiar Element-Plus component library, for example, cannot run on its own and must rely on the VUE3 environment to run. So in its package.json we see its hosting environment requirements:

{
  "peerDependencies": {
    "vue": "^ 3.2.0"}},Copy the code

So we can see that the vUE dependencies it introduces into the component are actually vue3 dependencies provided by the host environment:

import { ref, watch, nextTick } from 'vue'
Copy the code

license

The license field allows us to define a license that applies to the code described in package.json. Again, this is important when publishing a project to the NPM registry, because the license may restrict the use of the software by some developers or organizations. Having a clear license helps to clearly define the terms that the software can use.

To explain all licenses, use a diagram from Max Law on Zhihu:

The version number

The version number of the NPM package is also required by the specification. The general version is to follow the semver semantic version specification. The version format is major.minor.patch, and each letter represents the following meaning:

  1. Major: When you make incompatible API changes
  2. Minor: When you make a backward-compatible functional addition
  3. Patch: When you make a retro-compatible problem fix

The prior version number is appended to the revision number as an extension of the version number; When a major release or core feature is to be released, but there is no guarantee that the release will be completely normal, an advance release is required.

The prior version number is in the format of the revised version number followed by a join mark (-) followed by a series of dots (.) The identifier can consist of English characters, numbers, and join numbers ([0-9A-zA-Z -]). Such as:

1.0.0 - alpha 1.0.0 - alpha. 1 1.0.0-0.3.7Copy the code

Common advanced version numbers are:

  1. Alpha: Unstable version. Generally speaking, this version has many bugs and needs to be modified. It is a test version
  2. Beta: Stable, improved over Alpha and eliminated serious bugs
  3. Rc: Basically the same as the official version, there are almost no bugs that cause errors
  4. Release: Final version

The version number of each NPM package is unique. Every time we update the NPM package, we need to update the version number. Otherwise, an error warning will be reported:

After the major version is upgraded, the minor version and revision need to be reset to 0. After the minor version is upgraded, the revised version needs to be reset to 0.

But if you had to manually update the version number every time, it would be too much trouble; Is there a command line that automatically updates the version number? Because the version number depends on the subjective action of the content decision, so it can not be fully automated update, who knows you are changing a large version or a small version, so can only achieve semi-automatic operation through the command line; The value of the command corresponds to the semantic version, and the corresponding version is incremented by 1:

We also see identifiers like ^, ~, or >= in dependent versions of package.json, or no identifiers. What does that mean?

  1. No symbols: a perfect 100% match must use the current version number
  2. Contrast symbol class: >(greater than) >=(greater than or equal to) <(less than) <=(less than or equal to)
  3. tilde~: Fixes major and minor version numbers. Revision numbers can be changed at will, for example~ 2.0.0Can use versions 2.0.0, 2.0.2, 2.0.9.
  4. The caret^: Fixed major version number, minor version number and revision number can be changed at will, e.g^ 2.0.0, you can use versions 2.0.1, 2.2.2, and 2.9.9.
  5. Any version * : There is no restriction on the version
  6. Or a symbol: | | can be used to set up multiple version number limit rules, such as > = 3.0.0 | | < = 1.0.0

NPM package development

From the introduction of package.json above, WE believe that you have a certain understanding of the NPM package. Now let’s enter the code implementation stage, develop and upload a NPM package.

Utility class package

I believe that many children will encounter repeated functions in business development, or the development of the same tool function, every time they have to go to other projects to copy the code; If the code logic in one project is optimized and needs to be synchronized to other projects, it needs to be copied from project to project again, which is time-consuming and repetitive.

We can integrate the requirements of each project and develop an NPM package suitable for our own project. The structure of the package is as follows:

Hello - NPM | - lib/documents after (for packing) | -- - SRC/(source) | -- package. Json | -- a rollup. Config. Base. Js based configuration (rollup) | -- - Rollup. Config. Dev. Js (rollup development configuration) | -- a rollup. Config. Js (rollup formal configuration) | - README. Md | -- tsconfig. JsonCopy the code

Let’s look at the package.json configuration first. Rollup distinguishes the different configurations based on the development environment:

{
  "name": "hello-npm"."version": "1.0.0"."description": "I am the description of the NPM package"."main": "lib/bundle.cjs.js"."jsnext:main": "lib/bundle.esm.js"."module": "lib/bundle.esm.js"."browser": "lib/bundle.browser.js"."types": "types/index.d.ts"."author": ""."scripts": {
    "dev": "npx rollup -wc rollup.config.dev.js"."build": "npx rollup -c rollup.config.js && npm run build:types"."build:types": "npx tsc",},"license": "ISC"
}
Copy the code

Then configure the base config file for rollup:

import typescript from "@rollup/plugin-typescript";
import pkg from "./package.json";
import json from "rollup-plugin-json";
import resolve from "rollup-plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import eslint from "@rollup/plugin-eslint";
import { babel } from '@rollup/plugin-babel'
const formatName = "hello";
export default {
  input: "./src/index.ts".output: [{file: pkg.main,
      format: "cjs"}, {file: pkg.module,
      format: "esm"}, {file: pkg.browser,
      format: "umd".name: formatName,
    },
  ],
  plugins: [
    json(),
    commonjs({
      include: /node_modules/,
    }),
    resolve({
      preferBuiltins: true.jsnext: true.main: true.brower: true,
    }),
    typescript(),
    eslint(),
    babel({ exclude: "node_modules/**"})]};Copy the code

Here we will package into commonJS, ESM and UMD module specifications, and then the production environment configuration, add Terser and filesize respectively to compress and check the package size:

import { terser } from "rollup-plugin-terser";
import filesize from "rollup-plugin-filesize";

import baseConfig from "./rollup.config.base";

export default {
  ...baseConfig,
  plugins: [...baseConfig.plugins, terser(), filesize()],
};
Copy the code

Then there is the configuration of the development environment:

import baseConfig from "./rollup.config.base";
import serve from "rollup-plugin-serve";
import livereload from "rollup-plugin-livereload";

export default {
  ...baseConfig,
  plugins: [
    ...baseConfig.plugins,
    serve({
      contentBase: "".port: 8020,
    }),
    livereload("src")]};Copy the code

With the environment configured, we can pack it up

# Test environment
npm run dev
# Production environment
npm run build
Copy the code

Global package

NPM i-g [PKG] is used to install NPM packages globally. For example, common commands such as vue create, static-server, and pm2 are installed using global commands. So how is the global NPM package developed?

Let’s implement a global command calculator, create a new project and run:

cd my-calc
npm init -y
Copy the code

Json. This is an object whose key name tells Node to define a global command globally and whose value is the path to the script file that executes the command. Multiple commands can be defined at the same time.

{
  "name": "my-calc"."version": "1.0.0"."description": ""."main": "index.js"."scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "bin": {
    "calc": "./src/calc.js",},"license": "ISC",}Copy the code

With the command defined, let’s implement the contents of calc.js:

#! /usr/bin/env node

if (process.argv.length <= 2) {
  console.log("Please enter the number of the operation");
  return;
}

let total = process.argv
  .slice(2)
  .map((el) = > {
    let parseEl = parseFloat(el);
    return !isNaN(parseEl) ? parseEl : 0;
  })
  .reduce((total, num) = > {
    total += num;
    return total;
  }, 0);

console.log('Results:${total}`);
Copy the code

Note that the #! The /usr/bin/env node is required to tell the node that it is an executable js file and will fail if it is not written; Process.argv. slice(2) is used to obtain the command parameters. The first two parameters are the node path and the executable script path respectively. The third parameter is the command line parameter, so we run the command line to see the result:

calc 1 2 3 -4
Copy the code

If our script is complex and we want to debug the script, we need to publish it to the NPM server every time, and then test it after global installation, which is time-consuming and laborious. Is there any way to run the script directly? Here we use the NPM link command, which links the debugging NPM module to the corresponding running project. We can also use this command to link the module to the global.

Run the command in our project:

npm link
Copy the code

We can see the new calc file in the global NPM directory. The calc command points to the calc.js file in the local project, and we can run debugging as much as we want. After debugging, we no longer need to point to the local project, so we need to use the following command to unbind:

npm unlink
Copy the code

After untying, NPM will delete the global calc file, so we can release the NPM package and do the real global installation.

Vue component library

In the Vue project, we also use common components in many projects. We can extract these components into a component library. We can implement our own UI component library modeled after Element-UI. Let’s start by building our project catalog:

|- lib
|- src
    |- MyButton
        |- index.js
        |- index.vue
        |- index.scss
    |- MyInput
        |- index.js
        |- index.vue
        |- index.scss
    |- main.js
|- rollup.config.js
Copy the code

We build MyButton and MyInput components. Without going into the vue file and SCSS, let’s look at the index.js of the exported component:

import MyButton from "./index.vue";

MyButton.install = function (Vue) {
  Vue.component(MyButton.name, MyButton);
};
export default MyButton;
Copy the code

Unified component registration in main.js after component export:

import MyButton from "./MyButton/index.js";
import MyInput from "./MyInput/index";
import { version } from ".. /package.json";

import "./MyButton/index.scss";
import "./MyInput/index.scss";

const components = [MyButton, MyInput];

const install = function (Vue) {
  components.forEach((component) = > {
    Vue.component(component.name, component);
  });
};
if (typeof window! = ="undefined" && window.Vue) {
  install(window.Vue);
}
export { MyButton, MyInput, install };
export default { version, install };
Copy the code

Then configure rollup.config.js:

import resolve from "rollup-plugin-node-resolve";
import vue from "rollup-plugin-vue";
import babel from "@rollup/plugin-babel";
import commonjs from "@rollup/plugin-commonjs";
import scss from "rollup-plugin-scss";
import json from "@rollup/plugin-json";

const formatName = "MyUI";
const config = {
  input: "./src/main.js".output: [{file: "./lib/bundle.cjs.js".format: "cjs".name: formatName,
      exports: "auto"}, {file: "./lib/bundle.js".format: "iife".name: formatName,
      exports: "auto"],},plugins: [
    json(),
    resolve(),
    vue({
      css: true.compileTemplate: true,
    }),
    babel({
      exclude: "**/node_modules/**",
    }),
    commonjs(),
    scss(),
  ],
};
export default config;
Copy the code

Here we package commonJS and Iife module specifications, one can be used with the packaging tool, and the other can be imported directly from script in the browser. Vue files are parsed by rollup-plugin-vue plug-in. Note that vue2 is parsed by 5.x version and vue3 is parsed by 6.x version, which is installed by default. If we were using VUe2, we would need to switch to an older version of the plug-in and install the following vUE compiler:

npm install --save-dev vue-template-compiler
Copy the code

Once packaged, we can see the files in the lib directory, and we can happily use our OWN UI components like element-UI, introducing our UI into the project:

/* Global import main.js */
import MyUI from "my-ui";
// Introduce styles
import "my-ui/lib/bundle.cjs.css";

Vue.use(MyUI);


/* Introduces */ as needed in the component
import { MyButton } from "my-ui";
export default {
  components: {
    MyButton
  }
}
Copy the code

If you want to debug locally, you can also create a link using the link command. First run NPM link in the my-UI directory to mount the component globally, and then run the following command in the vue project to introduce global my-UI:

npm link my-ui
Copy the code

We should see the following output indicating that the My-UI module in the Vue project has been linked to the My-UI project:

D:\project\vue-demo\node_modules\my-ui 
-> 
C:\Users\XXXX\AppData\Roaming\npm\node_modules\my-ui
-> 
D:\project\my-ui
Copy the code

NPM package release

Once our NPM package is complete, we are ready to publish. First we need to prepare an account, either using –registry to specify the NPM server, or using NRM directly to manage:

npm adduser
npm adduser --registry=http://example.com
Copy the code

Then log in and enter the password of your registered account email:

npm login
Copy the code

You can also use the following command to exit the current account

npm logout
Copy the code

If you do not know the current login account, run the who command to check the identity:

npm who am i
Copy the code

After logging in successfully, we can push our package to the server, execute the following command, and we will see a pile of NPM notice:

npm publish
Copy the code

If there is a problem with a version of the package, we can also withdraw it

npm unpublish [pkg]@[version]
Copy the code

If you think it’s good, check out my Nuggets page. Please visit Xie xiaofei’s blog for more articles