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.
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.
Replace legacy main and module fields with conditional exports maps. The Node.js module resolver evaluates exports strictly, 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. For comprehensive manifest validation and publishing prerequisites, consult Understanding package.json Fields.
{
"name": "@scope/core-lib",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
},
"files": ["dist"],
"engines": { "node": ">=18.0.0" }
}
Monorepo Topology and Workspace Isolation
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, disable global hoisting patterns and restrict native module builds to an explicit allowlist.
Map internal dependencies using the workspace: protocol (e.g., "workspace:*") to guarantee symlink resolution and enforce topological execution order during builds. For advanced workspace filtering and dependency graph optimization, review Workspace Configuration Deep Dive.
packages:
- 'packages/*'
- 'apps/*'
hoist-pattern: []
public-hoist-pattern: []
onlyBuiltDependencies:
- esbuild
- sharp
Dependency Resolution and Graph Management
Categorize dependencies rigorously. Use dependencies for runtime requirements, devDependencies for tooling, and peerDependencies for framework integrations that must be provided by the consumer. Misclassifying framework plugins as direct dependencies causes duplicate module instantiation, breaking React context providers and inflating bundle sizes.
Patch vulnerable transitive dependencies using overrides (npm v8.3+), pnpm.overrides (pnpm v8+), or resolutions (Yarn v1). Avoid blanket overrides; they mask architectural misconfigurations and can introduce breaking changes during minor version bumps. pnpm's strict node_modules structure naturally prevents duplicate instantiation by isolating packages unless explicitly hoisted. For deterministic graph flattening and hoisting mechanics, see Dependency Resolution Explained.
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. Use npm ci --ignore-scripts, pnpm install --frozen-lockfile, or yarn install --immutable to block lockfile mutations and fail fast on drift.
Automate conflict resolution by routing lockfile updates through dedicated PRs. Implement pre-commit hooks via lint-staged and husky to block commits that modify lockfiles without corresponding dependency changes. Validate registry integrity using lockfile-lint to restrict allowed hosts and detect malicious typosquatting. For cross-platform hash validation and cache optimization, apply Lockfile Management Strategies.
jobs:
verify-lockfile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 8
- run: pnpm install --frozen-lockfile
- run: pnpm exec lockfile-lint --path pnpm-lock.yaml --allowed-hosts npm
Module System 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". Configure bundlers like tsup or Rollup to emit separate .mjs and .cjs outputs, ensuring each receives matching .d.ts type declarations.
Handle dynamic import() versus synchronous require() by isolating runtime evaluation. Use createRequire from node:module when CJS must load ESM dynamically, and avoid top-level await in CJS entry points. For seamless cross-runtime compatibility and legacy fallback strategies, leverage ESM and CJS Interoperability.
Build Orchestration and Script Execution Pipelines
Deploy workspace-aware task runners to manage caching, parallel execution, and topological dependencies. 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.
Configure pipeline dependencies to run only affected packages, reducing CI execution time by 60–80%. Cache node_modules, build outputs, and remote task caches using GitHub Actions or S3-backed runners. For scalable monorepo command routing and CI matrix optimization, reference Root-Level vs Package-Level Scripts.
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"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@8.15.0" (or equivalent) and enforce via Corepack or CI validation. |
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 for strict reproducibility. |
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, causing memory leaks and broken context providers. | Move framework references to peerDependencies and set peerDependenciesMeta with optional: true where applicable. |
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) to all CI install steps, and implement a pre-commit hook using lint-staged to block commits with uncommitted lockfile changes.
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 fields, ensure .d.ts files are generated for both, and set "type": "module" at the package root.
When should I use overrides versus resolutions in a monorepo?
Use overrides (npm) or pnpm.overrides for explicit, audited dependency patching. Reserve resolutions (Yarn) for legacy Yarn v1 projects. Avoid global overrides unless addressing critical CVEs, as they can mask underlying dependency misconfigurations.
How can I optimize CI/CD pipeline execution for large workspaces?
Implement a task runner like Turborepo or Nx with remote caching. Configure pipeline dependencies to run only affected packages, use incremental inputs for test jobs, and cache node_modules and build artifacts using GitHub Actions cache or S3-backed runners.