tooling.report

feature

Import from `node_modules`

Can dependencies be imported from node_modules?

Person in shorts with blue hair walking left

Introduction

The most common way to distribute and consume front-end packages is through the npm registry. Projects are generally set up such that bare import specifiers (imports that aren't file paths) are resolved to installed packages by looking up the corresponding package name and path within a directory like node_modules. The specifics of how resolution occurs tends to vary between different configurations, tools and runtimes, however most are based on the Node.js module resolution algorithm. In its simplest form, bare specifiers are resolved using one or more entry mappings provided by a package.json description file generally located at the root of each module.

A number of community standards for denoting entry module mappings have evolved over the past 10 years, making the details of this process a point of confusion for many developers. Many packages specify multiple source variations in package.json fields like "module" or "browser" in order to allow tools to select the most appropriate version for a given target environment. More recently, Node.js formalized Export Maps to address these use-cases, and support is now making its way into build tooling. Export Maps require package authors to explicitly declare all importable paths in an "exports" field within package.json, avoiding the need for filesystem traversal during module resolution.

The ability to resolve modules installed from a centralized registry is at the heart of JavaScript's dependency-centric ecosystem. It has been suggested that installed dependencies make up more than 50% of the compiled code in modern JavaScript applications. This means a build tool's ability to optimize for imported third party modules is important for ensuring high-quality output.

The Test

In this test, a dependency called idb is installed using npm, resulting in a node_modules/idb directory. The module's package.json includes "main" and "module" fields, defining its CommonJS and ECMAScript Modules entry files. Since the dependency is imported using modern syntax, the corresponding ECMAScript Modules source for idb should be used.

index.js

import { openDB } from 'idb';
console.log(openDB('lol', 1));

The output of building the above file with its "idb" import should be a bundle containing both the authored code and the imported dependency module. Since the idb package exposes ECMAScript Modules source under the "module" field, the resulting bundle should ideally be tree-shaken to exclude its unused exports.

Conclusion

browserify

Browserify is designed for this usecase. In this test, browserified result is passed to gulp's dest method to write to build/ directory.

parcel

Parcel supports bundling dependencies from node_modules by default.

rollup

Rollup doesn't support node_modules by default, but the official plugin @rollup/plugin-node-resolve adds support.

webpack

Webpack uses the Node.js module resolution algorithm to resolve dependencies, which means it supports resolving dependencies from node_modules out-of-the-box. It also respects the widely adopted "module" field in package.json files by default, giving it precedence over the "main" field when defined.

Webpack does not currently support Package Exports or Conditional Exports.