npm is a package management tool widely used by front-end developers. In the project, package.json is used to manage the configuration of the npm packages that the project depends on. package.json is a json file. In addition to describing the project's package dependencies, it also allows us to use "semantic versioning rules" to indicate the versions of your project's dependent packages, allowing your builds to be better shared with other developers for easy reuse. . This article mainly starts from recent practice, combined with the latest npm and node versions, to introduce some common configurations in package.json and how to write a standardized package.json
In a nodejs project, package.json is a configuration file that manages its dependencies. Usually when we initialize a nodejs project, we will pass:
npm init
and then 3 will be generated in your directory. directories/files, node_modules, package.json and package.lock.json. The content of package.json is:
{ "name": "Your project name", "version": "1.0.0", "description": "Your project description", "main": "app.js", "scripts": { "test": "echo "Error: no test specified" && exit 1", }, "author": "Author name", "license": "ISC", "dependencies": { "dependency1": "^1.4.0", "dependency2": "^1.5.2" } }
As can be seen from the above, package.json contains the metadata of the project itself, as well as the sub-dependency information of the project (such as dependicies, etc.).
We found that during npm init, not only the package.json file was generated, but also the package-lock.json file was generated. So why do we still need to generate the package-lock.json file when clearing package.json? Essentially, the package-lock.json file is for locking the version. The sub-npm package specified in package.json is such as: react: "^16.0.0". In actual installation, as long as the version is higher than react, package.json is satisfied. requirements. This means that according to the same package.json file, the sub-dependency versions installed twice cannot be guaranteed to be consistent.
The package-lock file is as shown below, and the sub-dependency dependency1 specifies its version in detail. Plays the role of lock version.
{ "name": "Your project name", "version": "1.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { "dependency1": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/dependency1/-/dependency1-1.4.0.tgz", "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" }, "dependency2": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/dependency2/-/dependency2-1.5.2.tgz", "integrity": "sha512-WOn21V8AhyE1QqVfPIVxe3tupJacq1xGkPTB4iagT6o+P2cAgEOOwIxMftr4+ZCTI6d551ij9j61DFr0nsP2uQ==" } } }
This chapter will talk about the commonly used configuration attributes in package.json. Attributes such as name, version and so on are too simple and will not be introduced one by one. This chapter mainly introduces the script, bin and workspaces properties.
uses the script tag in npm to define a script. Whenever npm run is specified, a shell script will be automatically created. What needs to be noted here is that the new shell created by npm run will store node_modules/.bin in the local directory. Add subdirectories to the PATH variable.
This means that all scripts in the node_modules/.bin subdirectory of the current directory can be called directly with the script name without adding a path. For example, if the current project's dependencies include esbuild, just write esbuild xxx directly.
{ // ... "scripts": { "build": "esbuild index.js", } }
{ // ... "scripts": { "build": "./node_modules/.bin/esbuild index.js" } }
The above two ways of writing are equivalent.
The bin attribute is used to load executable files into the global environment. Once the npm package with the bin field is specified, it will be loaded into the global environment and the file can be executed through an alias.
For example, the npm package of @bytepack/cli:
"bin": { "bytepack": "./bin/index.js" },
Once @bytepack/cli is installed globally, you can directly execute the corresponding command through bytepack, such as
bytepack -v //Show 1.11.0.
If it is not installed globally, it will automatically be connected to the node_module/.bin directory of the project. Consistent with what was said in the script tag introduced earlier, it can be used directly with aliases.
When the project is too large, monorepo has become more and more popular recently. When it comes to monorepo, we don’t look at workspaces. In the early days, we would use yarn workspaces. Now npm officially supports workspaces. Workspaces solves the problem of how to manage multiple sub-packages under a top-level root package in the local file system. In the workspaces declaration directory The package will be soft-linked to the node_modules of the top-level root package.
Let’s illustrate directly with an example from the official website:
{ "name": "my-project", "workspaces": [ "packages/a" ] }
In an npm package named my-project, there is a directory configured by workspaces.
. +-- package.json +-- index.js `-- packages +-- a | `-- package.json
and the top-level root package named my-project has a packages/a sub-package. At this time, if we npm install, then the npm package a installed in node_modules in the root package points to the local package/a.
. +-- node_modules | `-- packages/a -> ../packages/a +-- package-lock.json +-- package.json `-- packages +-- a | `-- package.json
above--
packages/a -> ../packages/a
refers to the soft link from a in node_modules to the local npm package
Common environments with package.json environment-related attributes, basically The above is divided into two categories: browser and node environment. Next, let’s take a look at the configuration properties related to the environment in package.json. The definition of environment can be simply understood as follows:
js includes commonjs, CMD, UMD, AMD and ES module. At first, only the commonjs field was supported in node. However, starting from node13.2.0, node officially supports the ES module specification. In The type field can be used in package.json to declare the modular specification that the npm package follows.
//package.json { name: "some package", type: "module"||"commonjs" }
It should be noted that
when type is not specified, the default value of type is commonjs. However, it is recommended that all npm packages specify type.
When the type field specifies the value as module, the ESModule specification is used
. When the type field is specified, all .js in the directory will
Files ending with the suffix follow the modularization specification specified by type.
In addition to type, which can specify the modularization specification, the modularization specification followed by the file is specified through the suffix of the file. Files ending with .mjs are the ESModule specifications used. The .cjs ending follows the commonjs specification
In addition to type, package.json also has three fields: main, module and browser to define the entry file of the npm package.
’s take a look at the usage scenarios of these three fields and the priorities when these three fields exist at the same time. Let's assume there is an npm package called demo1,
----- dist |-- index.browser.js |-- index.browser.mjs |-- index.js |-- index.mjs
's package.json specifies the three fields main, module and browser at the same time,
"main": "dist/index.js", // main "module": "dist/index.mjs", // module // browser can be defined as a mapping object corresponding to the main/module field, or it can be directly defined as the string "browser": { "./dist/index.js": "./dist/index.browser.js", // browser+cjs "./dist/index.mjs": "./dist/index.browser.mjs" // browser+mjs }, // "browser": "./dist/index.browser.js" // Browser
is built and used by default. For example, if we reference this npm package in the project:
import demo from 'demo'
After building the above code through the build tool, the module The loading sequence is:
browser+mjs > module > browser+cjs > main.
This loading sequence is the default loading sequence of most build tools, such as webapck, esbuild, etc. This loading order can be modified through corresponding configuration, but in most scenarios, we will still follow the default loading order.
If the exports field is defined in package.json, then the content defined in this field is the real and complete export of the npm package, and the priority will be higher than the main and file fields.
For example:
{ "name": "pkg", "exports": { ".": "./main.mjs", "./foo": "./foo.js" } }
import { something } from "pkg"; // from "pkg/main.mjs"
const { something } = require("pkg/foo"); // require("pkg/foo.js")
from the above example It seems that exports can define exports with different paths. If exports exist, the previously valid file directory will be invalid everywhere, such as require('pkg/package.json'), because it is not specified in exports, and an error will be reported.
Another biggest feature of exports is conditional reference. For example, we can specify npm packages to reference different entry files based on different reference methods or modular types.
// package.json { "name":"pkg", "main": "./main-require.cjs", "exports": { "import": "./main-module.js", "require": "./main-require.cjs" }, "type": "module" }
In the above example, if we
refer to "./main-require.cjs"
throughconst p = require('pkg').
If you pass:
import p from 'pkg',
the reference is "./main-module.js"
. The last thing to note is: if the exports attribute exists, the exports attribute will not only have a higher priority than main, but also higher than the module and browser fields.
The dependency-related configuration properties in package.json include dependencies, devDependencies, peerDependencies, peerDependenciesMeta, etc.
Dependencies are project dependencies, and devDependencies are modules required for development, so we can install them as needed during the development process to improve our development efficiency. What needs to be noted here is to try to use them as standardly as possible in your own projects. For example, webpack, babel, etc. are development dependencies, not dependencies of the project itself. Do not put them in dependencies.
dependencies In addition to dependencies and devDependencies, this article focuses on peerDependencies and peerDependenciesMeta.
peerDependencies are dependencies in package.json, which can solve the problem of the core library being downloaded multiple times and unifying the core library version.
//package/pkg -----node_modules |-- npm-a -> Depends on react, react-dom |-- npm-b -> Depends on react, react-dom |-- index.js
For example, in the above example, if the sub-npm packages a and b both come from react and react-dom, then if we declare PeerDependicies in the package.json of the sub-npm packages a and b, the corresponding Dependencies will not be reinstalled.
There are two points to note:
18
There is such a paragraph in package.json:
"peerDependencies": { "react": "^16.8.3 || ^17 || ^18" }, "peerDependenciesMeta": { "react-dom": { "optional": true }, "react-native": { "optional": true } }
"react-dom" and "react-native" are specified here in peerDependenciesMeta and are optional, so if "react-dom" and "react-native" are not installed in the project, no error will be reported.
It is worth noting that we have indeed lifted the restriction through peerDependenciesMeta, but there are often scenarios where it is either A or B. For example, in the above example, what we need is "react-dom" and "react-native" and we need to install one, but In fact, we cannot achieve this prompt through the above statement.
There are also many three-party attributes in package.json, such as types used in tsc, sideEffects used in build tools, husky used in git, and eslintIgnore used in eslint. The configuration of these extensions is for specific Development tools are meaningful and I won’t give examples here.