Pipeline
Build pipeline
What pkgist does for each package, in order. Steps 4 and 9 are conditional; the rest run for every successful build.
The 10 steps
1. Load source package.json → read current version2. Resolve new version (auto-bump or explicit)3. Create build output directory (buildDir/<name>/<new-version>/)4. Snapshot source to sourcesDir/<name>/ — full copy excluding .git, node_modules, dist, .turbo, .cache (only if settings.sourcesDir is set)5. Compile with tsdown → esm/ and cjs/ subdirectories6. Clone extra files/directories listed in `clone`7. Write clean package.json for the build (no devDeps, no scripts)8. Update source package.json version in-place9. Git: add . → commit → push → tag v<version> → push tags (only if commit resolves to a non-empty string)10. npm publish --access <public|restricted> from build directory (only if publish !== false)Build, clone, write, git, and publish each happen in parallel across packages up to concurrency. Within one package, the steps are strictly sequential.
Build output structure
With preserveModules: true (default)
Each source file becomes its own output file, mirroring the source tree:
builds/└── utils/ └── 2.1.1/ ├── package.json ← clean: no devDeps, no scripts ├── README.md ← cloned ├── LICENSE ← cloned ├── skills/ ← cloned (directory) ├── llms.txt ← cloned ├── llms-full.txt ← cloned ├── esm/ │ ├── index.mjs │ ├── index.mjs.map │ ├── index.d.mts │ ├── array/ │ │ ├── chunk.mjs │ │ ├── chunk.mjs.map │ │ └── chunk.d.mts │ └── string/ │ ├── trim.mjs │ └── trim.d.mts └── cjs/ ├── index.cjs ├── index.cjs.map ├── index.d.cts └── array/ ├── chunk.cjs └── chunk.d.ctsThe generated package.json sets:
{ "main": "./cjs/index.cjs", "module": "./esm/index.mjs", "types": "./esm/index.d.mts", "exports": { ".": { "import": { "types": "./esm/index.d.mts", "default": "./esm/index.mjs" }, "require": { "types": "./esm/index.d.mts", "default": "./cjs/index.cjs" } } }}This is the standard dual-publish shape. Bundlers, Node ESM, Node CJS, and TypeScript all resolve correctly.
Note: when preserveModules: true (the default) and both ESM + CJS are emitted, the require condition’s types deliberately points back at the ./esm/*.d.mts declarations rather than CJS-side .d.cts files. CJS dts emit is skipped in this mode to dodge a rolldown bug; TypeScript still resolves types correctly because the declarations are reachable through the exports map.
With preserveModules: false
Everything bundled into a single file per format:
builds/└── tiny-pkg/ └── 1.0.0/ ├── package.json ├── README.md ├── esm/ │ ├── index.js │ ├── index.js.map │ └── index.d.ts └── cjs/ ├── index.js ├── index.js.map └── index.d.ts{ "main": "./cjs/index.js", "module": "./esm/index.js", "types": "./esm/index.d.ts"}Use only for trivial single-file packages. Loses stack-trace fidelity.
ESM-only packages
When mainType: "esm" or formats: ["esm"]:
- The generated
package.jsonincludes"type": "module" - The
requirecondition is omitted from theexportsmap - No
cjs/directory is produced mainpoints to the ESM file
{ "type": "module", "main": "./esm/index.mjs", "module": "./esm/index.mjs", "types": "./esm/index.d.mts", "exports": { ".": { "import": { "types": "./esm/index.d.mts", "default": "./esm/index.mjs" } } }}Use for tools that only need to run in modern environments (Vite plugins, Node-only CLIs targeting Node 18+).
What ends up in the published package.json
pkgist generates a clean package.json for the build — it does NOT copy yours verbatim. Specifically:
- Kept:
name,description,keywords,author,license,repository,homepage,bugs,dependencies,peerDependencies,sideEffects,bin,engines - Replaced / set:
name,version,main,module,types,exports, andtype(set to"module"for ESM-only builds) - Dropped: everything else (
devDependencies,scripts,private,workspaces,optionalDependencies,peerDependenciesMeta,files,publishConfig, etc.)
This is intentional — the published package should not carry your build-time tooling or scripts.
bin normalization
The bin field is preserved but normalized: leading ./ is stripped (npm rejects bin values that start with ./). So {"my-cli": "./dist/cli.js"} in source becomes {"my-cli": "dist/cli.js"} in the build.
Source snapshots (optional)
If settings.sourcesDir is set, pkgist archives a full copy of the source (minus .git, node_modules, dist, .turbo, .cache) into <sourcesDir>/<name>/ before every build. Useful for:
- Reconstructing what was published at any point in time
- Diffing published versions without checking out specific tags
- Recovering from accidental source edits between build and publish
Omit sourcesDir to skip this step entirely.
Publish step
npm publish --access <public|restricted> runs from the build directory, not the source. The build dir has the clean package.json + compiled output + cloned files — that’s exactly what ships.
When publish: false is set on a package, the publish step is skipped (build + git still run). Use for internal-only packages.
--no-publish on the CLI skips the publish step for every package, regardless of config.
What’s NOT in the pipeline
- Running tests — pkgist trusts you. Run your own test suite before invoking pkgist.
- Linting / formatting — same. Run as pre-commit hooks or separate CI steps.
- Changelog generation — pkgist doesn’t write a CHANGELOG.md. Pair with conventional-changelog or release-please if you want one.
- Dependency hoisting / workspace install — pkgist doesn’t touch
node_modules. Runyarn install/pnpm installseparately.
This keeps pkgist focused: take source, produce a clean publishable artifact, commit, tag, publish. The rest of the release lifecycle is your call.