🪝 pnpm-hoist-layer
use .pnpmfile.cjs
to hoist deps/devDeps to project like
Nuxt Layer
- ✅ 9.12, 9.13, 9.14, 9.15
- ✅ 10.12, 10.13, 10.14
Purpose
pnpm public-hoist-pattern only affects to the top-project (virtual store), not the sub-projects, therefore,
- relative path issues may occur
- copy same deps/devDeps from here to there
- config too many hoist-pattern
how to auto add the dep's deps/devDeps to my project? i.e. give your deps/devDeps to me when i deps on you.
e.g. mobile->common
, common->@nuxt
, after hoist layer,
├── common
│ ├── node_modules
│ │ ├── @nuxt -> ../.pnpm/...
└── mobile
├── node_modules
│ ├── @fessional
│ │ └── razor-common -> ../.pnpm/...
+ │ ├── @nuxt -> ../.pnpm/... // ✅ hoist as layer
How it Works
When using catalog
in pnpm-workspace.yaml
, it is not easy to manage
the dependencies via pnpm add
, the recommended practice is to manually
edit the catalog
and the package.json
, and then run pnpm i
to
install the update, at this point, the following happens to pnpm-hoist-layer.
- make a temporary directory(tmpDir), and write the
.pnpmfile.cjs
hook. - start the sub-process,
pnpm -r i --resolution-only --lockfile-dir=tmpDir
- sub-process quickly resolves packages related to hoistLayer
- top-process parse stdout of sub-process as hoistLayer metadata
- top-process merges hoistLayer metadata via hooks
the hoistLayer metadata is 📝 hoist-layer.json
in the console,
[
{
"name": "hoist1",
"dependencies": {
"date-fns": "catalog:h1",
"lodash-es": "catalog:h1"
},
"devDependencies": {}
},
{
"name": "hoist2",
"dependencies": {
"date-fns": "catalog:h2",
"hoist1": "workspace:*",
"lodash-es": "catalog:h1"
},
"devDependencies": {},
"hoistLayer": [
"hoist1"
]
}
]
Usage
add hoistLayer
to the package.json
*dependencies
- for package resolutionhoistLayer
- to define the layer package, if the item is- string - include it and all its dependencies
- array - include it(
[0]
) but exclude the dependencies([1...]
)
- ⚠️ In complex layered repos, exclude rules may introduce bug and bad isolation.
"devDependencies": {
+ "@fessional/razor": "0.1.0",
+ "@fessional/razor-common": "file:../common",
},
+ "hoistLayer": [
+ [ "@fessional/razor", "semver" ],
+ "@fessional/razor-common",
+ ]
write .pnpmfile.cjs
as pnpm hook\
💾 Opt-1: project install and require
pnpm add -D pnpm-hoist-layer
cat > .pnpmfile.cjs << 'EOF'
const pnpmfile = {};
try {
const hoist = require('pnpm-hoist-layer');
pnpmfile.hooks = hoist.hooks;
console.info('✅ pnpm-hoist-layer is', hoist.version);
} catch {
console.warn('⚠️ pnpm-hoist-layer not found, reinstall to enable layer hoisting.');
}
module.exports = pnpmfile;
EOF
📦 Opt-2: write the content to .pnpmfile.cjs
curl -o .pnpmfile.cjs https://raw.githubusercontent.com/trydofor\
/pnpm-hoist-layer/main/index.js
Known Issues
The deps tree are resolved from top to bottom, and hoist from bottom to top, it's a reverse process.
- ✅ shared-workspace-lockfile=false, may 🐞 peers
- ✅ monorepo + shared-workspace-lockfile=false, but 🐞 default=true
- ✅ pnpm cli at top-dir, but 🐞 sub-dir (
packages/*
) - ✅
--resolution-only
resolvedevDependencies
, but ❗pnpm i
NOT. - ❗ do NOT use
link:
, it do NOT hook - ❗ do NOT deps indirectly , 2+ level deps NOT resolved
- ❗ this is a bad practice
Node and Pnpm
manages nodejs
version by asdf
and pnpm
by corepack
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.15.0
## config zsh
cat >> ~/.zshrc << 'EOF'
export ASDF_NODEJS_AUTO_ENABLE_COREPACK=true
export ASDF_NODEJS_LEGACY_FILE_DYNAMIC_STRATEGY=latest_installed
source "$HOME/.asdf/asdf.sh"
EOF
## support .nvmrc or .node-version
cat >> ~/.asdfrc << 'EOF'
legacy_version_file=yes
EOF
## install nodejs plugin
asdf plugin add nodejs
## install nodejs and corepack enable
asdf install nodejs
## by package.json and corepack
pnpm -v
## Corepack is about to download
## init workspace top-project first
pnpm -w i --ignore-pnpmfile
## init workspace sub-project
pnpm -r i
## to debug with env DEBUG != null
DEBUG=1 pnpm i
## ignore if error
pnpm i --ignore-pnpmfile --ignore-scripts
Test and Diff
Glossary
├── multi pkg, one git (often called "monorepo")
│ ├── with workspace (termed as "mono")
│ └── without workspace (termed as "poly")
└── one pkg, one git (termed as "solo")
node -v #v20.16.0
pnpm -v #9.12.1
pnpm test
# ✅ Success mono1, npmrc={}
# ✅ Success mono1, npmrc={"shared-workspace-lockfile":false}
# ✅ Success mono2, npmrc={}
# ✅ Success mono2, npmrc={"shared-workspace-lockfile":false}
# ✅ Success poly1, npmrc={}
# ✅ Success poly2, npmrc={}
# ✅ Success hoist, npmrc={}
- hoist - hoist auto/manual testing
- mono1 - multi-pkg + workspace, sub
hoistLayer
- mono2 - multi-pkg + workspace, top
hoistLayer
- poly1 - multi-pkg, sub
hoistLayer
- poly2 - multi-pkg, top
hoistLayer
- solo - single pkg as deps for test
Mono before and after
diff mono
from pnpm -r i --ignore-pnpmfile
to pnpm -r i
like this,
## pnpm -r list
Legend: production dependency, optional only, dev only
mono-test-0@1.0.0 pnpm-hoist-layer/test/mono/packages/pkg0
+ dependencies:
+ solo-prd-dep link:../../solo/prd
= devDependencies:
= mono-test-1 link:../pkg1
+ mono-test-2 link:../pkg2
+ solo-dev-dep link:../../solo/dev
mono-test-1@1.0.0 pnpm-hoist-layer/test/mono/packages/pkg1
+ dependencies:
+ solo-prd-dep link:../../solo/prd
= devDependencies:
= mono-test-2 link:../pkg2
+ solo-dev-dep link:../../solo/dev
mono-test-2@1.0.0 pnpm-hoist-layer/test/mono/packages/pkg2
= dependencies:
= solo-prd-dep link:../../solo/prd
= devDependencies:
= solo-dev-dep link:../../solo/dev
## tree -L 4
✅ mono
= ├── node_modules
= ├── package.json
= ├── packages
= │ ├── pkg0
= │ │ ├── node_modules
= │ │ │ ├── mono-test-1 -> ../../../node_modules/.pnpm/
+ │ │ │ └── mono-test-2 -> ../../pkg2
+ │ │ │ ├── solo-dev-dep -> ../../../solo/dev
+ │ │ │ ├── solo-prd-dep -> ../../../solo/prd
= │ │ └── package.json
= │ ├── pkg1
= │ │ ├── node_modules
= │ │ │ └── mono-test-2 -> ../../pkg2
+ │ │ │ ├── solo-dev-dep -> ../../../solo/dev
+ │ │ │ ├── solo-prd-dep -> ../../../solo/prd
= │ │ └── package.json
= │ └── pkg2
= │ ├── node_modules
= │ │ └── solo-dev-dep -> ../../../solo/dev
= │ │ ├── solo-prd-dep -> ../../../solo/prd
= │ └── package.json
= ├── pnpm-lock.yaml
= └── pnpm-workspace.yaml
Poly before and after
diff poly
from pnpm -r i --ignore-pnpmfile
to pnpm -r i
like this,
## pnpm -r list
Legend: production dependency, optional only, dev only
poly-test-0@1.0.0 pnpm-hoist-layer/test/poly/packages/pkg0
+ dependencies:
+ solo-prd-dep link:../../solo/prd
= devDependencies:
= poly-test-1 file:../pkg1
+ poly-test-2 file:../pkg2
+ solo-dev-dep link:../../solo/dev
poly-test-1@1.0.0 pnpm-hoist-layer/test/poly/packages/pkg1
+ dependencies:
+ solo-prd-dep link:../../solo/prd
= devDependencies:
= poly-test-2 file:../pkg2
+ solo-dev-dep link:../../solo/dev
poly-test-2@1.0.0 pnpm-hoist-layer/test/poly/packages/pkg2
= dependencies:
= solo-prd-dep link:../../solo/prd
= devDependencies:
= solo-dev-dep link:../../solo/dev
## tree -L 4
✅ poly
= ├── package.json
= ├── packages
= │ ├── pkg0
= │ │ ├── node_modules
= │ │ │ ├── poly-test-1 -> .pnpm/
+ │ │ │ └── poly-test-2 -> .pnpm/
+ │ │ │ ├── solo-dev-dep -> ../../../solo/dev
+ │ │ │ ├── solo-prd-dep -> ../../../solo/prd
= │ │ ├── package.json
= │ │ └── pnpm-lock.yaml
= │ ├── pkg1
= │ │ ├── node_modules
= │ │ │ └── poly-test-2 -> .pnpm/
+ │ │ │ ├── solo-dev-dep -> ../../../solo/dev
+ │ │ │ ├── solo-prd-dep -> ../../../solo/prd
= │ │ ├── package.json
= │ │ └── pnpm-lock.yaml
= │ └── pkg2
= │ ├── node_modules
= │ │ ├── solo-dev-dep -> ../../../solo/dev
= │ │ └── solo-prd-dep -> ../../../solo/prd
= │ ├── package.json
= │ └── pnpm-lock.yaml
= └── pnpm-lock.yaml