| | |

pnpm 03 📦 Initializing Projects and package.json Setup

⏱️ Estimated read: 8 min | |

The pnpm init command is the entry point for every new project, but its behavior has evolved significantly in pnpm 10. The default module system is now ESM ("type": "module"), a --bare flag strips the manifest down to only required fields, and version pinning is handled through the devEngines.packageManager field rather than the older packageManager field alone. Understanding these defaults prevents the common surprise of generating a package.json that is either too bloated for an application or too sparse for a published library.

Key point: pnpm init creates a package.json with "type": "module" by default. Use --bare for applications that will never be published, and use --init-package-manager to pin the pnpm version in a way that the lockfile records.


Why initialization behavior changed

The CommonJS-to-ESM migration problem. For years, Node.js defaulted to CommonJS. A package.json without a type field was treated as CommonJS, and import statements failed unless the file used .mjs. The ecosystem has shifted toward ESM. pnpm 10 reflects this shift: pnpm init now writes "type": "module" by default, matching the direction of modern Node.js tooling and eliminating the need for .mjs extensions . Projects that still require CommonJS can set --init-type commonjs.

The application-versus-library problem. A package.json for a published library needs name, version, description, main, keywords, author, and license. A package.json for a private application needs almost none of these. The license field, in particular, can cause legal confusion when an application unintentionally inherits the default ISC license. The --bare flag, added in pnpm 10.25, strips the manifest to only the required fields, removing the publishing metadata that applications do not need .

The version pinning problem. Pinning pnpm to a specific version ensures reproducibility. The --init-package-manager flag writes the pin to two places: devEngines.packageManager (which captures the resolved version in pnpm-lock.yaml) and packageManager (which Corepack reads) . The lookup checks the latest tag on the registry and pins only if that version is newer than the running pnpm. If the registry is unreachable, it pins the running version without failing.

The workspace manifest problem. pnpm init creates the manifest in the current directory, not in the nearest ancestor with a package.json . This matters in monorepos: running pnpm init inside packages/utils creates a manifest there, not at the repository root. For workspace root initialization, the manifest is created at the root, and pnpm-workspace.yaml declares which subdirectories are packages.

The trade-off. The default ESM output is correct for most new projects but breaks compatibility with older CommonJS-only tooling. The --bare flag is correct for applications but removes fields that npm publish requires. The choices are explicit and reversible, but they must be made deliberately.


a. Basic initialization and module system selection

The pnpm init command creates a package.json in the current directory. If a manifest already exists, the command fails with ERR_PNPM_PACKAGE_JSON_EXISTS .

# Default: ESM module system
pnpm init

# Explicit CommonJS
pnpm init --init-type commonjs

# Bare manifest for applications
pnpm init --bare

The default package.json includes name, version, and type: "module". The name defaults to the directory name, and version defaults to 1.0.0 unless initVersion is configured . The --bare flag removes description, main, keywords, author, and license, leaving only the fields required for installation and script execution.


b. Configuring defaults via init settings

The fields written by pnpm init are configurable through settings that can be placed in pnpm-workspace.yaml, a global configuration file, or environment variables . This allows teams to standardize new manifests without passing flags on every invocation.

# pnpm-workspace.yaml (or global config)
initLicense: MIT
initAuthorName: "Alex Chen"
initAuthorEmail: "alex@example.com"
initAuthorUrl: "https://alex.dev"
initVersion: "0.1.0"
initType: "module"

The author field is assembled in the Name <email> (url) format. If none of the three author settings are configured, no author field is written. The initLicense setting is particularly useful for organizations that want a consistent license across all new packages without manually editing each manifest.


c. Pinning the package manager and understanding the fields

The --init-package-manager flag pins the project to a pnpm version. The pin is written to both devEngines.packageManager and packageManager .

{
  "name": "my-project",
  "version": "1.0.0",
  "type": "module",
  "devEngines": {
    "packageManager": "pnpm@10.25.0"
  },
  "packageManager": "pnpm@10.25.0"
}

The devEngines.packageManager field is read by pnpm itself to resolve the exact version and record it in the lockfile. The packageManager field is read by Corepack, which accepts only exact versions. Writing both ensures compatibility with both mechanisms.

The engines field, separate from devEngines, specifies the Node.js and pnpm versions the project supports. During local development, pnpm fails with an error if its version does not match the engines constraint . When the package is installed as a dependency, the check is advisory unless engineStrict is enabled.

The devEngines.runtime field, added in pnpm 10.14, allows specifying JavaScript runtimes (Node.js, Deno, Bun) that pnpm installs automatically. This is useful for projects that need a specific Node.js version for scripts or CLI tools .


Complete Example Session

# ============================================
# PART 1: BASIC INIT
# ============================================

mkdir my-app && cd my-app
pnpm init
# Creates package.json with type: module
# ============================================
# PART 2: INSPECT THE DEFAULT MANIFEST
# ============================================

cat package.json
# {
#   "name": "my-app",
#   "version": "1.0.0",
#   "type": "module"
# }
# ============================================
# PART 3: BARE INITIALIZATION
# ============================================

rm package.json
pnpm init --bare
cat package.json
# {
#   "name": "my-app",
#   "version": "1.0.0"
# }
# ============================================
# PART 4: PIN PNPM VERSION
# ============================================

rm package.json
pnpm init --init-package-manager
cat package.json
# Includes devEngines.packageManager and packageManager
# ============================================
# PART 5: CONFIGURE DEFAULTS
# ============================================

# Add to pnpm-workspace.yaml or global config:
# initLicense: MIT
# initAuthorName: "Alex Chen"

rm package.json
pnpm init
# Manifest includes license and author fields.
# ============================================
# PART 6: ADD ENGINE CONSTRAINTS
# ============================================

# Edit package.json:
# "engines": { "node": ">=20", "pnpm": ">=10" }
pnpm install
# pnpm verifies its version against the constraint.

The six parts trace initialization: default ESM manifest, inspection, bare mode, version pinning, configured defaults, and engine constraints.


Quick Reference

pnpm init Flags

FlagEffect
--bareOnly required fields (name, version)
--init-type <type>module (default) or commonjs
--init-package-managerPin pnpm version in devEngines and packageManager

Init Configuration Settings

SettingDefaultPurpose
initVersion1.0.0Version written to manifest
initLicense(none)License field
initTypemoduleModule system
initAuthorName(none)Author name

Manifest Fields

FieldPurpose
typemodule (ESM) or absent (CJS)
devEngines.packageManagerpnpm version pin (lockfile-aware)
packageManagerCorepack-compatible pin
enginesNode/pnpm version constraints

Best Practices

✅ Do This:

pnpm init --bare
<!-- Applications do not need publishing metadata --> # ✅
pnpm init --init-package-manager
<!-- Pin pnpm version for reproducibility -->        # ✅
initLicense: MIT
<!-- Standardize license in new manifests -->        # ✅

❌ Don’t Do This:

# Assume default is CommonJS
<!-- pnpm 10 defaults to ESM -->                     # ❌
# Leave license field in private applications
<!-- Can imply unintended licensing -->              # ❌
# Run pnpm init in a subdirectory of a monorepo root
<!-- Creates a nested manifest unexpectedly -->      # ❌

Common Pitfalls

PitfallWhy It HappensFix
import fails after initDefault is now ESMUse require or set --init-type commonjs
Manifest already existsRunning init twiceDelete or edit existing package.json
Version pin not respectedOnly packageManager set, not devEnginesUse --init-package-manager
pnpm add failsNo package.json in directoryRun pnpm init first
Author field malformedOnly one of name/email/url setSet all three for complete format

Real-World Examples

1. Default ESM Init

pnpm init

2. CommonJS Init

pnpm init --init-type commonjs

3. Bare Init

pnpm init --bare

4. Pin pnpm

pnpm init --init-package-manager

5. Config License

initLicense: MIT

6. Config Author

initAuthorName: "Alex Chen"
initAuthorEmail: "alex@example.com"

7. Engine Constraint

{ "engines": { "node": ">=20" } }

8. Runtime Declaration

{ "devEngines": { "runtime": "node@24" } }

9. Inspect Manifest

pnpm pkg get name version

10. Fix Manifest

pnpm pkg fix

Visual

Initialization Decision Flow

┌──────────────────────────────────────────────┐
│  Will this package be published?             │
└──────────┬───────────────────────┬───────────┘
           │ YES                   │ NO
           ▼                       ▼
┌──────────────────────┐  ┌──────────────────────┐
│  pnpm init           │  │  pnpm init --bare    │
│  (full manifest)     │  │  (name, version only)│
└──────────────────────┘  └──────────────────────┘
           │
           ▼
┌──────────────────────────────────────────────┐
│  Module system?                              │
│  ├─ ESM (default) → pnpm init                │
│  └─ CommonJS → --init-type commonjs          │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Pin pnpm version?                           │
│  ├─ YES → --init-package-manager             │
│  └─ NO  → (no pin)                           │
└──────────────────────────────────────────────┘

Version Pin Placement

┌──────────────────────────────────────────────┐
│  package.json                                │
│                                              │
│  devEngines.packageManager: "pnpm@10.25.0"   │
│  └── Read by pnpm, recorded in lockfile      │
│                                              │
│  packageManager: "pnpm@10.25.0"              │
│  └── Read by Corepack                        │
└──────────────────────────────────────────────┘

Both fields are written by --init-package-manager.


Summary

ItemValue
Default module typemodule (ESM)
Bare flag--bare
CommonJS flag--init-type commonjs
Version pin flag--init-package-manager
Pin fieldsdevEngines.packageManager, packageManager
Default version1.0.0
Config filepnpm-workspace.yaml or global

Key takeaways:

  • pnpm init defaults to ESM. The type: "module" field is written automatically. Projects requiring CommonJS must pass --init-type commonjs .
  • --bare is for applications. It removes publishing metadata (license, author, description) that private applications do not need .
  • Version pinning uses two fields. devEngines.packageManager records the resolved version in the lockfile; packageManager provides Corepack compatibility .
  • Initialization is configurable. initLicense, initAuthorName, and initVersion standardize new manifests across a team .
  • pnpm init operates on the current directory. In a monorepo, running it inside a subdirectory creates a nested manifest, not a root manifest .

Remember: pnpm init is not a passive command. It writes a manifest with defaults that reflect modern Node.js conventions (ESM) and pnpm’s version management (dual pinning). Choose --bare for applications, --init-type commonjs for legacy compatibility, and --init-package-manager for reproducibility. The defaults are sensible, but they are not universal.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!