Core JavaScript Package Workflows
Modern JavaScript package management demands deterministic resolution, strict module boundaries, and reproducible build pipelines. This guide establishes production-grade workflows for library authors, frontend engineers, and platform teams managing monorepos and dual-packaged distributions. Every section below maps to a deeper topic page, and the diagram that follows shows how those topics fit together into one end-to-end flow — from the manifest a developer edits, through resolution and lockfile integrity, to the dual-format artifacts published to a registry.
These stages correspond directly to the topic pages on this site. Manifest design is covered in Understanding package.json Fields, with the dual-output recipe broken out in How to Configure package.json for Dual Modules. Graph mechanics live in Dependency Resolution Explained. Reproducibility is the subject of Lockfile Management Strategies. Multi-package orchestration spans Workspace Configuration Deep Dive and Root-Level vs Package-Level Scripts. Module-format correctness is handled in ESM and CJS Interoperability, and shipping type definitions that survive both formats is the focus of TypeScript Declaration Publishing.
Package Initialization and Manifest Architecture
Initialize every project with explicit engine constraints and package manager declarations to prevent environment drift. Use Corepack to pin the exact package manager version, and declare it via the packageManager field in the root package.json. This guarantees that every developer and CI runner executes the identical resolution algorithm rather than whichever version happens to be globally installed. A mismatched package manager is one of the most common sources of "works on my machine" lockfile churn, because each major version of npm, pnpm, and Yarn ships subtly different hoisting and resolution rules.
Replace legacy main and module fields with conditional exports maps. The Node.js module resolver evaluates exports strictly and in declaration order, prioritizing type definitions, then ESM, then CJS. Configure type: "module" at the package root, set sideEffects: false to enable aggressive tree-shaking, and restrict published artifacts using the files array so internal scaffolding, tests, and source maps never leak into the tarball. The exports map also acts as an encapsulation boundary: any subpath you do not explicitly export becomes unreachable to consumers, which prevents downstream code from depending on internal file layout you may want to refactor later.
{
"name": "@scope/core-lib",
"type": "module",
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
},
"files": ["dist"],
"engines": { "node": ">=18.0.0" }
}
Validate the manifest before every publish. A misordered exports key or a missing types condition will not throw locally yet will silently break consumers, so treat the manifest as a contract that warrants automated checks. The field-by-field reference, including the publish-blocking private flag and the difference between files and an .npmignore, is laid out in Understanding package.json Fields.
Dependency Classification and Graph Management
Categorize dependencies rigorously, because the bucket a package lands in determines whether it ships to consumers, whether it duplicates, and whether installs fail. Use dependencies for runtime requirements that must be installed alongside your package, devDependencies for tooling that never reaches production, and peerDependencies for framework integrations that the consumer must provide. Misclassifying a framework plugin as a direct dependency causes duplicate module instantiation, breaking React context providers and inflating bundle sizes — the canonical example being two copies of React in one tree. The decision boundary between these buckets is examined in detail in When to Use peerDependencies vs devDependencies.
Patch vulnerable transitive dependencies using overrides (npm v8.3+), pnpm.overrides (pnpm), or resolutions (Yarn v1). Avoid blanket overrides; they mask architectural misconfigurations and can introduce breaking changes during minor version bumps. pnpm's strict, content-addressed node_modules structure naturally prevents duplicate instantiation by isolating packages unless explicitly hoisted, which is why peer-dependency conflicts surface earlier and more loudly there than under npm's flat hoisting. When a peer conflict does block an install, the resolution steps are documented in Fixing npm ERESOLVE Peer Dependency Conflicts, and the specific case of a doubled framework is covered in Deduplicating Duplicate React Versions. For the full picture of how ranges flatten into an installed tree, see Dependency Resolution Explained.
{
"dependencies": { "lodash-es": "^4.17.21" },
"devDependencies": { "typescript": "^5.7.0", "tsup": "^8.0.0" },
"peerDependencies": { "react": ">=18.0.0" },
"peerDependenciesMeta": { "react": { "optional": true } },
"overrides": { "semver": "^7.5.4" }
}
Lockfile Integrity and CI/CD Enforcement
Guarantee reproducible builds by enforcing frozen lockfile installs across all CI pipelines. Never run npm install, pnpm install, or yarn install in CI without strict flags, because an unguarded install will happily mutate the lockfile to satisfy a floating range, producing builds that differ from what was reviewed. Use npm ci --ignore-scripts, pnpm install --frozen-lockfile, or yarn install --immutable to block lockfile mutations and fail fast on drift. The --ignore-scripts flag additionally neutralizes arbitrary postinstall code from transitive dependencies, closing a common supply-chain vector.
Automate conflict resolution by routing lockfile updates through dedicated dependency-bump PRs rather than letting them piggyback on feature branches. A regenerated lockfile is nearly impossible to review by eye, so the review should focus on the manifest diff while CI verifies that the lockfile resolves cleanly. Validate registry integrity using lockfile-lint to restrict allowed hosts and detect typosquatting or unexpected registries injected into the dependency graph. For cross-platform hash validation, merge-conflict recovery, and cache reuse, apply Lockfile Management Strategies; the most common merge failure has a dedicated walkthrough in Fixing pnpm-lock.yaml Merge Conflicts.
jobs:
verify-lockfile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- run: pnpm install --frozen-lockfile --ignore-scripts
- run: pnpm exec lockfile-lint --path pnpm-lock.yaml --allowed-hosts npm
ESM/CJS Module Boundaries and Dual Packaging
Enforce strict ESM/CJS boundaries using conditional export maps and explicit file extensions. Node.js throws ERR_REQUIRE_ESM when a CommonJS environment attempts to require() a file marked as ESM via type: "module", and it throws a "named export not found" SyntaxError when a consumer destructures named bindings from a CJS module that Node only exposes as a default object. Configure bundlers like tsup or Rollup to emit separate .mjs and .cjs outputs, ensuring each receives matching .d.ts declarations so type checkers resolve the correct file under each condition.
Handle dynamic import() versus synchronous require() by isolating runtime evaluation. Use createRequire from node:module when CJS code must load a CJS dependency by path, and prefer dynamic import() when CJS must reach an ESM-only module, since a static require() of ESM cannot work. Avoid top-level await in any code path that may be consumed as CJS. The full compatibility matrix, including interop edge cases and fallback strategies, lives in ESM and CJS Interoperability; the two errors above have targeted fixes in Fixing ERR_REQUIRE_ESM in Node.js and Resolving 'Named Export Not Found' in ESM. When you ship two JavaScript formats you must also ship two sets of declarations, which is the entire subject of Generating Dual CJS/ESM Type Definitions.
TypeScript Declarations as a First-Class Artifact
Treat type declarations with the same rigor as your JavaScript output. A .d.ts file that is generated against the ESM build but served to a require consumer will produce a "cannot find module or its corresponding type declarations" error even when the runtime code resolves correctly, because TypeScript follows the same conditional exports resolution and picks the file under the matching condition. Point a types condition inside both the import and require branches at format-appropriate declarations (.d.ts for ESM, .d.cts for CJS) so editors and tsc never cross-contaminate. End-to-end declaration emission, bundling, and verification are covered in TypeScript Declaration Publishing, and the specific consumer-side failure is fixed in Fixing 'Cannot Find Module' Type Declaration Errors.
Workspace Topology and Build Orchestration
Define workspace roots explicitly to enforce strict package boundaries. npm uses the workspaces array in package.json, pnpm relies on pnpm-workspace.yaml, and Yarn Berry uses the same workspaces field but resolves via Plug'n'Play or node_modules hoisting. To prevent accidental cross-package hoisting and phantom dependencies — where a package imports something it never declared simply because a sibling hoisted it — disable broad hoisting patterns and restrict native module builds to an explicit allowlist.
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
# .npmrc
hoist-pattern[]=
public-hoist-pattern[]=
onlyBuiltDependencies[]=esbuild
onlyBuiltDependencies[]=sharp
Map internal dependencies using the workspace: protocol (e.g., "workspace:*") to guarantee symlink resolution and enforce topological execution order during builds, so a package always builds after the local packages it imports. Deploy workspace-aware task runners to manage caching, parallel execution, and topological dependencies, and separate root-level orchestration scripts from package-specific commands to prevent command collision and simplify CI matrix routing. Use prepublishOnly for final validation and artifact generation, but avoid postinstall for network-dependent operations due to security and reproducibility risks. The protocol, hoisting controls, and filtering mechanics are explored in Workspace Configuration Deep Dive, with smaller setups walked through in Setting Up npm Workspaces for Small Teams and migrations in Migrating from Yarn 1 to pnpm Workspaces. Script layering across the root and individual packages is the focus of Root-Level vs Package-Level Scripts.
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
"lint": { "dependsOn": [] },
"test": { "dependsOn": ["build"], "inputs": ["src/**/*.ts", "test/**/*.ts"] }
}
}
Common Implementation Pitfalls
| Mistake | Impact | Resolution |
|---|---|---|
Omitting the packageManager field |
Developers use mismatched package manager versions, causing inconsistent resolution and lockfile corruption. | Add "packageManager": "pnpm@10.4.1" (or equivalent) and enforce via Corepack or a CI version check. |
Using npm install instead of npm ci in CI |
CI modifies lockfiles on every run, introducing non-deterministic builds and deployment drift. | Replace with npm ci --ignore-scripts or pnpm install --frozen-lockfile. |
Relying on implicit main/module fields |
Bundlers resolve incorrect entry points, breaking ESM/CJS interop and causing runtime ERR_REQUIRE_ESM errors. |
Migrate to explicit conditional exports maps with types, import, and require keys. |
Declaring framework plugins as dependencies |
Duplicate framework instances in the dependency graph, broken context providers, larger bundles. | Move framework references to peerDependencies and set peerDependenciesMeta with optional: true where applicable. |
Emitting one .d.ts for both formats |
require consumers hit "cannot find module" or get ESM-shaped types under CJS. |
Emit .d.ts for ESM and .d.cts for CJS, wired through matching exports conditions. |
Frequently Asked Questions
How do I enforce strict lockfile synchronization across a distributed team?
Enable Corepack for version pinning, add --frozen-lockfile (or --immutable for Yarn Berry) to every CI install step, and add a pre-commit hook using lint-staged and husky to block commits that change a lockfile without a matching package.json change.
What is the recommended strategy for publishing dual ESM/CJS packages?
Use a bundler like tsup or Rollup to generate separate .mjs and .cjs outputs, map them via conditional exports, emit .d.ts and .d.cts declarations for each condition, and set "type": "module" at the package root.
When should I use overrides versus resolutions in a monorepo?
Use overrides (npm v8.3+) or pnpm.overrides for explicit, audited dependency patching. Reserve resolutions for Yarn v1 projects. Avoid global overrides unless addressing a critical CVE, as they can mask underlying dependency misconfigurations.
How can I optimize CI/CD pipeline execution for large workspaces?
Adopt a task runner like Turborepo or Nx with remote caching, configure pipeline dependencies so only affected packages rebuild, use incremental inputs for test jobs, and cache node_modules and build artifacts across runs.
Do I have to choose one package manager for the whole organization?
For a single repository, yes — the lockfile format is manager-specific, so mixing them produces conflicting state. Across separate repositories you can differ, but pinning each repo's manager via packageManager and Corepack keeps every checkout deterministic.
Related
- Understanding package.json Fields — the field-by-field manifest reference that every other workflow builds on.
- Dependency Resolution Explained — how declared ranges flatten into the installed tree and where conflicts arise.
- ESM and CJS Interoperability — the module-format rules behind dual packaging and the common runtime errors.
- Lockfile Management Strategies — keeping installs reproducible and lockfiles reviewable in CI.
- TypeScript Declaration Publishing — shipping type definitions that resolve correctly under both ESM and CJS.
← Home