@catladder/cli 0.0.0-next-1d12aca2 → 0.0.0-next-cf4e99eb
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundles/catenv/index.js +61 -80
- package/dist/bundles/cli/index.js +61 -80
- package/dist/bundles/skills/catladder-migrate-package-manager/SKILL.md +8 -8
- package/dist/bundles/skills/catladder-migrate-package-manager/references/yarn-to-pnpm-differences.md +20 -3
- package/dist/bundles/skills/catladder-pipelines/SKILL.md +7 -6
- package/dist/packages/pipeline/src/build/node/buildJob.js +11 -14
- package/dist/packages/pipeline/src/build/node/buildJob.js.map +1 -1
- package/dist/packages/pipeline/src/build/node/cache.d.ts +8 -3
- package/dist/packages/pipeline/src/build/node/cache.js +34 -47
- package/dist/packages/pipeline/src/build/node/cache.js.map +1 -1
- package/dist/packages/pipeline/src/build/node/packageManagerInstall.js +15 -18
- package/dist/packages/pipeline/src/build/node/packageManagerInstall.js.map +1 -1
- package/dist/skills/catladder-migrate-package-manager/SKILL.md +8 -8
- package/dist/skills/catladder-migrate-package-manager/references/yarn-to-pnpm-differences.md +20 -3
- package/dist/skills/catladder-pipelines/SKILL.md +7 -6
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -6096,11 +6096,14 @@ const createNodeDockerJobDefinition = async (context) => {
|
|
|
6096
6096
|
(0, packageManagerInstall_1.getDockerAppCopyAndBuildScript)(context),
|
|
6097
6097
|
context.packageManagerInfo,
|
|
6098
6098
|
]);
|
|
6099
|
-
//
|
|
6100
|
-
//
|
|
6101
|
-
//
|
|
6102
|
-
|
|
6103
|
-
|
|
6099
|
+
// The pnpm store is deliberately NOT copied into the image. It used
|
|
6100
|
+
// to be, so the prod install could link instead of hitting the
|
|
6101
|
+
// registry — but the store lands in an earlier layer than
|
|
6102
|
+
// node_modules, so pnpm cannot hardlink across the overlay boundary
|
|
6103
|
+
// and copies every package instead. Measured on one component: the
|
|
6104
|
+
// in-image install was *slower* that way (28s vs 10s from the
|
|
6105
|
+
// registry), the store added 3GB to the build context and the final
|
|
6106
|
+
// image, and the build took 339s instead of 27s.
|
|
6104
6107
|
// pnpm copies every workspace manifest into the image — as individual
|
|
6105
6108
|
// COPY instructions that many layers exceed docker's depth limit, so
|
|
6106
6109
|
// the job tars them up and a single ADD extracts them
|
|
@@ -6108,8 +6111,6 @@ const createNodeDockerJobDefinition = async (context) => {
|
|
|
6108
6111
|
const WORKSPACE_FILES_TAR = ".catladder-workspace-files.tar";
|
|
6109
6112
|
return {
|
|
6110
6113
|
script: [
|
|
6111
|
-
// the COPY source must exist even when the cache was cold
|
|
6112
|
-
...(copyPnpmStore ? ["mkdir -p .pnpm-store"] : []),
|
|
6113
6114
|
...(isPnpm
|
|
6114
6115
|
? [
|
|
6115
6116
|
`tar -cf ${WORKSPACE_FILES_TAR} ${packageManagerInfo.pathsToCopyInDocker.join(" ")}`,
|
|
@@ -6126,14 +6127,10 @@ const createNodeDockerJobDefinition = async (context) => {
|
|
|
6126
6127
|
`ADD ${WORKSPACE_FILES_TAR} /app/`,
|
|
6127
6128
|
// ADD does not apply --chown to extracted tars, and parent
|
|
6128
6129
|
// dirs of the entries come out root-owned — fix ownership
|
|
6129
|
-
// while the stage still runs as root (
|
|
6130
|
-
//
|
|
6131
|
-
//
|
|
6132
|
-
// need it, hence the fallback
|
|
6130
|
+
// while the stage still runs as root (this only touches the
|
|
6131
|
+
// small manifest tree). Custom base images without a node
|
|
6132
|
+
// user run as root and don't need it, hence the fallback
|
|
6133
6133
|
`RUN chown -R node:node /app || true`,
|
|
6134
|
-
...(copyPnpmStore
|
|
6135
|
-
? [`COPY --chown=node:node .pnpm-store /app/.pnpm-store`]
|
|
6136
|
-
: []),
|
|
6137
6134
|
].join("\n")
|
|
6138
6135
|
: packageManagerInfo.pathsToCopyInDocker
|
|
6139
6136
|
.map((dir) => `COPY --chown=node:node ${dir} /app/${dir}`)
|
|
@@ -6167,10 +6164,9 @@ const lodash_es_1 = __nccwpck_require__(50210);
|
|
|
6167
6164
|
const path_1 = __nccwpck_require__(16928);
|
|
6168
6165
|
const slugify_1 = __importDefault(__nccwpck_require__(84007));
|
|
6169
6166
|
/**
|
|
6170
|
-
* stable reference between the two
|
|
6171
|
-
*
|
|
6172
|
-
*
|
|
6173
|
-
* ~0.6GB download on warm runs)
|
|
6167
|
+
* stable reference between the two yarn caches: the zip cache declares
|
|
6168
|
+
* itself redundant when the node_modules cache scored an exact-lockfile
|
|
6169
|
+
* hit (github skips the ~0.6GB download on warm runs)
|
|
6174
6170
|
*/
|
|
6175
6171
|
const NODE_MODULES_CACHE_ID = "node-modules";
|
|
6176
6172
|
/**
|
|
@@ -6198,36 +6194,36 @@ const getCacheBaseDir = async (context) => {
|
|
|
6198
6194
|
: ".";
|
|
6199
6195
|
};
|
|
6200
6196
|
/**
|
|
6201
|
-
* the package manager's download cache: yarn's `.yarn` zip cache
|
|
6202
|
-
*
|
|
6203
|
-
*
|
|
6197
|
+
* the package manager's download cache: yarn's `.yarn` zip cache.
|
|
6198
|
+
*
|
|
6199
|
+
* pnpm gets nothing. Its store is a content-addressable tree of
|
|
6200
|
+
* hundreds of thousands of small files, and moving it costs more than
|
|
6201
|
+
* the install it saves: measured on a 3800-package monorepo, restoring
|
|
6202
|
+
* the 1 GB store took ~53s on gitlab (which zips file by file) and
|
|
6203
|
+
* ~23s on github, against a from-registry install of ~30-40s. See
|
|
6204
|
+
* getNodeModulesCache for the node_modules half of the same finding.
|
|
6204
6205
|
*/
|
|
6205
6206
|
const getYarnCache = async (context, policy = "pull-push") => {
|
|
6206
6207
|
const packageManagerInfo = await context.packageManagerInfo;
|
|
6207
|
-
|
|
6208
|
+
if (packageManagerInfo.type === "pnpm")
|
|
6209
|
+
return [];
|
|
6208
6210
|
const baseDir = await getCacheBaseDir(context);
|
|
6209
|
-
// pnpm keeps its store in a format-versioned subdir (v10, v11, ...)
|
|
6210
|
-
// and never cleans old ones up — scope the cache slot by major so a
|
|
6211
|
-
// pnpm upgrade starts a fresh slot instead of dragging the dead old
|
|
6212
|
-
// store tree along in every cache transfer (formats change only on
|
|
6213
|
-
// majors)
|
|
6214
|
-
const pnpmStoreKey = `pnpm-v${packageManagerInfo.version.split(".")[0]}`;
|
|
6215
6211
|
return [
|
|
6216
6212
|
{
|
|
6217
6213
|
scope: "buildDir",
|
|
6218
6214
|
pathMode: "relative",
|
|
6219
6215
|
buildDir: baseDir,
|
|
6220
|
-
key:
|
|
6216
|
+
key: "yarn",
|
|
6221
6217
|
policy,
|
|
6222
|
-
paths: [
|
|
6218
|
+
paths: [".yarn"],
|
|
6223
6219
|
// content-key for immutable-cache backends (github); the lockfile
|
|
6224
6220
|
// decides whether the cached content could have changed
|
|
6225
|
-
keyFiles: [
|
|
6226
|
-
// the
|
|
6227
|
-
//
|
|
6228
|
-
//
|
|
6229
|
-
//
|
|
6230
|
-
//
|
|
6221
|
+
keyFiles: ["yarn.lock"],
|
|
6222
|
+
// the yarn zip cache (~hundreds of MB) is only read when packages
|
|
6223
|
+
// have to be (re)installed. When the node_modules cache hit for
|
|
6224
|
+
// the same lockfile, install verifies its state file and never
|
|
6225
|
+
// touches the zips — so github skips downloading this cache
|
|
6226
|
+
// entirely
|
|
6231
6227
|
redundantOnExactHitOf: NODE_MODULES_CACHE_ID,
|
|
6232
6228
|
},
|
|
6233
6229
|
];
|
|
@@ -6236,12 +6232,18 @@ exports.getYarnCache = getYarnCache;
|
|
|
6236
6232
|
const getNodeModulesCache = async (context, policy = "pull-push") => {
|
|
6237
6233
|
const packageManagerInfo = await context.packageManagerInfo;
|
|
6238
6234
|
const baseDir = await getCacheBaseDir(context);
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
//
|
|
6243
|
-
//
|
|
6244
|
-
|
|
6235
|
+
// pnpm gets no node_modules cache either: archiving the tree (root
|
|
6236
|
+
// plus every workspace's symlink farm, >1GB in real monorepos) costs
|
|
6237
|
+
// ~190s per saving job to spare ~6s of install, and a safe pnpm slot
|
|
6238
|
+
// must be keyed by lockfile hash (a pnpm install over a node_modules
|
|
6239
|
+
// from a different lockfile leaves orphaned .pnpm dirs that break
|
|
6240
|
+
// type identity), so it misses on every dependency change anyway.
|
|
6241
|
+
// pnpm projects run entirely uncached — see getYarnCache
|
|
6242
|
+
if (packageManagerInfo.type === "pnpm")
|
|
6243
|
+
return [];
|
|
6244
|
+
const lockFileName = "yarn.lock";
|
|
6245
|
+
// yarn berry tracks install state in .yarn/install-state.gz
|
|
6246
|
+
const hasInstallState = !packageManagerInfo.isClassic;
|
|
6245
6247
|
// We intentionally do not use the contents of the lockfile as a cache key, as install should always guarantee that the files are updated, but it can still use part of the cache if not all packages are up-to-date.
|
|
6246
6248
|
// It would slow down all pipelines whenever one adds a new dependency as it will need to download all node_modules again.
|
|
6247
6249
|
return [
|
|
@@ -6249,19 +6251,7 @@ const getNodeModulesCache = async (context, policy = "pull-push") => {
|
|
|
6249
6251
|
scope: "global",
|
|
6250
6252
|
pathMode: "absolute",
|
|
6251
6253
|
// we use the dirname, not the component name, because in certain cases we have two apps in the same directory and want to share the cache, e.g. when having storybook in the same package.json
|
|
6252
|
-
|
|
6253
|
-
// pnpm gets its own, lockfile-keyed cache: node_modules layouts
|
|
6254
|
-
// are not compatible between package managers (a migrating
|
|
6255
|
-
// project must not inherit the yarn slot), and unlike yarn, a
|
|
6256
|
-
// pnpm install on top of a node_modules from a different lockfile
|
|
6257
|
-
// leaves orphaned .pnpm dirs behind that break type identity —
|
|
6258
|
-
// so the slot changes whenever the lockfile changes
|
|
6259
|
-
key: isPnpm
|
|
6260
|
-
? {
|
|
6261
|
-
prefix: "pnpm-" + (0, slugify_1.default)(baseDir) + "-node-modules",
|
|
6262
|
-
files: [(0, path_1.join)(baseDir, "pnpm-lock.yaml")],
|
|
6263
|
-
}
|
|
6264
|
-
: (0, slugify_1.default)(baseDir) + "-node-modules",
|
|
6254
|
+
key: (0, slugify_1.default)(baseDir) + "-node-modules",
|
|
6265
6255
|
// content-key for immutable-cache backends (github); the lockfile
|
|
6266
6256
|
// decides whether the cached content could have changed (absolute
|
|
6267
6257
|
// pathMode: resolve the lockfile explicitly)
|
|
@@ -6272,12 +6262,6 @@ const getNodeModulesCache = async (context, policy = "pull-push") => {
|
|
|
6272
6262
|
policy,
|
|
6273
6263
|
paths: (0, lodash_es_1.uniq)([
|
|
6274
6264
|
(0, path_1.join)(baseDir, "node_modules"),
|
|
6275
|
-
// pnpm's per-package node_modules are symlink farms worth
|
|
6276
|
-
// keeping warm (yarn hoists into the root, so root-only
|
|
6277
|
-
// matches its historical behavior)
|
|
6278
|
-
...(isPnpm
|
|
6279
|
-
? workspaces.map((w) => (0, path_1.join)(w.location, "node_modules"))
|
|
6280
|
-
: []),
|
|
6281
6265
|
...(hasInstallState ? [(0, path_1.join)(baseDir, ".yarn/install-state.gz")] : []),
|
|
6282
6266
|
]),
|
|
6283
6267
|
},
|
|
@@ -6469,19 +6453,15 @@ const isInWorkspace = (context, packageManagerInfo) => context.type === "workspa
|
|
|
6469
6453
|
*/
|
|
6470
6454
|
const getEnsurePnpmCommand = (version) => `if ! command -v pnpm &> /dev/null; then corepack enable pnpm 2>/dev/null || npm install -g pnpm@${version}; fi`;
|
|
6471
6455
|
/**
|
|
6472
|
-
* pnpm's
|
|
6473
|
-
*
|
|
6474
|
-
*
|
|
6475
|
-
* the
|
|
6476
|
-
*
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
const getPnpmStoreDirExpression = (inWorkspace) => inWorkspace
|
|
6480
|
-
? `"$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.pnpm-store"`
|
|
6481
|
-
: `"$PWD/.pnpm-store"`;
|
|
6482
|
-
const getPnpmInstallCommands = (context, packageManagerInfo) => [
|
|
6456
|
+
* pnpm jobs use pnpm's own (global, per-runner) store location. There
|
|
6457
|
+
* used to be a `--store-dir` pointing at a project-local
|
|
6458
|
+
* `.pnpm-store`, so CI could cache it — that cache is gone (see
|
|
6459
|
+
* getYarnCache), and keeping the store inside the repo only meant it
|
|
6460
|
+
* landed in docker build contexts.
|
|
6461
|
+
*/
|
|
6462
|
+
const getPnpmInstallCommands = (packageManagerInfo) => [
|
|
6483
6463
|
getEnsurePnpmCommand(packageManagerInfo.version),
|
|
6484
|
-
`pnpm install --frozen-lockfile
|
|
6464
|
+
`pnpm install --frozen-lockfile`,
|
|
6485
6465
|
];
|
|
6486
6466
|
exports.DEFAULT_AUDIT_LEVEL = "critical";
|
|
6487
6467
|
/**
|
|
@@ -6525,7 +6505,7 @@ const getPackageManagerInstall = async (context, options) => {
|
|
|
6525
6505
|
? context.build.config.postInstall
|
|
6526
6506
|
: null;
|
|
6527
6507
|
const installSection = packageManagerInfo.type === "pnpm"
|
|
6528
|
-
? (0, gitlab_1.collapseableSection)("pnpminstall", "pnpm install")(getPnpmInstallCommands(
|
|
6508
|
+
? (0, gitlab_1.collapseableSection)("pnpminstall", "pnpm install")(getPnpmInstallCommands(packageManagerInfo))
|
|
6529
6509
|
: (0, gitlab_1.collapseableSection)("yarninstall", "Yarn install")([getYarnInstallCommand(packageManagerInfo)]);
|
|
6530
6510
|
return [
|
|
6531
6511
|
...(0, exports.ensureNodeVersion)(context),
|
|
@@ -6538,11 +6518,12 @@ const getPackageManagerInstall = async (context, options) => {
|
|
|
6538
6518
|
exports.getPackageManagerInstall = getPackageManagerInstall;
|
|
6539
6519
|
const DOCKER_COPY_FILES = `COPY --chown=node:node $APP_DIR .`;
|
|
6540
6520
|
const getPnpmDockerAppCopyAndBuildScript = (packageManagerInfo) => {
|
|
6541
|
-
// the
|
|
6542
|
-
// the
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6521
|
+
// the prod install downloads from the registry: nothing is copied
|
|
6522
|
+
// into the image to link from (see createNodeDockerJobDefinition).
|
|
6523
|
+
// The store it fills is scratch space outside /app, dropped in the
|
|
6524
|
+
// same layer so the packages ship once — node_modules holds
|
|
6525
|
+
// hardlinks, so removing the store keeps the files alive
|
|
6526
|
+
const storeDir = "/tmp/pnpm-store";
|
|
6546
6527
|
// in a workspace, only install the component (and its workspace
|
|
6547
6528
|
// dependencies) — the pnpm equivalent of `yarn workspaces focus`.
|
|
6548
6529
|
// npm package names are shell-safe unquoted (the whole script is
|
|
@@ -6554,7 +6535,7 @@ const getPnpmDockerAppCopyAndBuildScript = (packageManagerInfo) => {
|
|
|
6554
6535
|
return new bash_1.BashExpression(`
|
|
6555
6536
|
${DOCKER_COPY_FILES}
|
|
6556
6537
|
RUN command -v pnpm >/dev/null 2>&1 || npm install -g pnpm@${packageManagerInfo.version}
|
|
6557
|
-
RUN pnpm install --prod --frozen-lockfile --store-dir ${storeDir}${filter}
|
|
6538
|
+
RUN pnpm install --prod --frozen-lockfile --store-dir ${storeDir}${filter} && rm -rf ${storeDir}
|
|
6558
6539
|
`.trim());
|
|
6559
6540
|
};
|
|
6560
6541
|
const getDockerAppCopyAndBuildScript = async (context) => {
|
|
@@ -12028,11 +12028,14 @@ const createNodeDockerJobDefinition = async (context) => {
|
|
|
12028
12028
|
(0, packageManagerInstall_1.getDockerAppCopyAndBuildScript)(context),
|
|
12029
12029
|
context.packageManagerInfo,
|
|
12030
12030
|
]);
|
|
12031
|
-
//
|
|
12032
|
-
//
|
|
12033
|
-
//
|
|
12034
|
-
|
|
12035
|
-
|
|
12031
|
+
// The pnpm store is deliberately NOT copied into the image. It used
|
|
12032
|
+
// to be, so the prod install could link instead of hitting the
|
|
12033
|
+
// registry — but the store lands in an earlier layer than
|
|
12034
|
+
// node_modules, so pnpm cannot hardlink across the overlay boundary
|
|
12035
|
+
// and copies every package instead. Measured on one component: the
|
|
12036
|
+
// in-image install was *slower* that way (28s vs 10s from the
|
|
12037
|
+
// registry), the store added 3GB to the build context and the final
|
|
12038
|
+
// image, and the build took 339s instead of 27s.
|
|
12036
12039
|
// pnpm copies every workspace manifest into the image — as individual
|
|
12037
12040
|
// COPY instructions that many layers exceed docker's depth limit, so
|
|
12038
12041
|
// the job tars them up and a single ADD extracts them
|
|
@@ -12040,8 +12043,6 @@ const createNodeDockerJobDefinition = async (context) => {
|
|
|
12040
12043
|
const WORKSPACE_FILES_TAR = ".catladder-workspace-files.tar";
|
|
12041
12044
|
return {
|
|
12042
12045
|
script: [
|
|
12043
|
-
// the COPY source must exist even when the cache was cold
|
|
12044
|
-
...(copyPnpmStore ? ["mkdir -p .pnpm-store"] : []),
|
|
12045
12046
|
...(isPnpm
|
|
12046
12047
|
? [
|
|
12047
12048
|
`tar -cf ${WORKSPACE_FILES_TAR} ${packageManagerInfo.pathsToCopyInDocker.join(" ")}`,
|
|
@@ -12058,14 +12059,10 @@ const createNodeDockerJobDefinition = async (context) => {
|
|
|
12058
12059
|
`ADD ${WORKSPACE_FILES_TAR} /app/`,
|
|
12059
12060
|
// ADD does not apply --chown to extracted tars, and parent
|
|
12060
12061
|
// dirs of the entries come out root-owned — fix ownership
|
|
12061
|
-
// while the stage still runs as root (
|
|
12062
|
-
//
|
|
12063
|
-
//
|
|
12064
|
-
// need it, hence the fallback
|
|
12062
|
+
// while the stage still runs as root (this only touches the
|
|
12063
|
+
// small manifest tree). Custom base images without a node
|
|
12064
|
+
// user run as root and don't need it, hence the fallback
|
|
12065
12065
|
`RUN chown -R node:node /app || true`,
|
|
12066
|
-
...(copyPnpmStore
|
|
12067
|
-
? [`COPY --chown=node:node .pnpm-store /app/.pnpm-store`]
|
|
12068
|
-
: []),
|
|
12069
12066
|
].join("\n")
|
|
12070
12067
|
: packageManagerInfo.pathsToCopyInDocker
|
|
12071
12068
|
.map((dir) => `COPY --chown=node:node ${dir} /app/${dir}`)
|
|
@@ -12099,10 +12096,9 @@ const lodash_es_1 = __nccwpck_require__(50210);
|
|
|
12099
12096
|
const path_1 = __nccwpck_require__(16928);
|
|
12100
12097
|
const slugify_1 = __importDefault(__nccwpck_require__(84007));
|
|
12101
12098
|
/**
|
|
12102
|
-
* stable reference between the two
|
|
12103
|
-
*
|
|
12104
|
-
*
|
|
12105
|
-
* ~0.6GB download on warm runs)
|
|
12099
|
+
* stable reference between the two yarn caches: the zip cache declares
|
|
12100
|
+
* itself redundant when the node_modules cache scored an exact-lockfile
|
|
12101
|
+
* hit (github skips the ~0.6GB download on warm runs)
|
|
12106
12102
|
*/
|
|
12107
12103
|
const NODE_MODULES_CACHE_ID = "node-modules";
|
|
12108
12104
|
/**
|
|
@@ -12130,36 +12126,36 @@ const getCacheBaseDir = async (context) => {
|
|
|
12130
12126
|
: ".";
|
|
12131
12127
|
};
|
|
12132
12128
|
/**
|
|
12133
|
-
* the package manager's download cache: yarn's `.yarn` zip cache
|
|
12134
|
-
*
|
|
12135
|
-
*
|
|
12129
|
+
* the package manager's download cache: yarn's `.yarn` zip cache.
|
|
12130
|
+
*
|
|
12131
|
+
* pnpm gets nothing. Its store is a content-addressable tree of
|
|
12132
|
+
* hundreds of thousands of small files, and moving it costs more than
|
|
12133
|
+
* the install it saves: measured on a 3800-package monorepo, restoring
|
|
12134
|
+
* the 1 GB store took ~53s on gitlab (which zips file by file) and
|
|
12135
|
+
* ~23s on github, against a from-registry install of ~30-40s. See
|
|
12136
|
+
* getNodeModulesCache for the node_modules half of the same finding.
|
|
12136
12137
|
*/
|
|
12137
12138
|
const getYarnCache = async (context, policy = "pull-push") => {
|
|
12138
12139
|
const packageManagerInfo = await context.packageManagerInfo;
|
|
12139
|
-
|
|
12140
|
+
if (packageManagerInfo.type === "pnpm")
|
|
12141
|
+
return [];
|
|
12140
12142
|
const baseDir = await getCacheBaseDir(context);
|
|
12141
|
-
// pnpm keeps its store in a format-versioned subdir (v10, v11, ...)
|
|
12142
|
-
// and never cleans old ones up — scope the cache slot by major so a
|
|
12143
|
-
// pnpm upgrade starts a fresh slot instead of dragging the dead old
|
|
12144
|
-
// store tree along in every cache transfer (formats change only on
|
|
12145
|
-
// majors)
|
|
12146
|
-
const pnpmStoreKey = `pnpm-v${packageManagerInfo.version.split(".")[0]}`;
|
|
12147
12143
|
return [
|
|
12148
12144
|
{
|
|
12149
12145
|
scope: "buildDir",
|
|
12150
12146
|
pathMode: "relative",
|
|
12151
12147
|
buildDir: baseDir,
|
|
12152
|
-
key:
|
|
12148
|
+
key: "yarn",
|
|
12153
12149
|
policy,
|
|
12154
|
-
paths: [
|
|
12150
|
+
paths: [".yarn"],
|
|
12155
12151
|
// content-key for immutable-cache backends (github); the lockfile
|
|
12156
12152
|
// decides whether the cached content could have changed
|
|
12157
|
-
keyFiles: [
|
|
12158
|
-
// the
|
|
12159
|
-
//
|
|
12160
|
-
//
|
|
12161
|
-
//
|
|
12162
|
-
//
|
|
12153
|
+
keyFiles: ["yarn.lock"],
|
|
12154
|
+
// the yarn zip cache (~hundreds of MB) is only read when packages
|
|
12155
|
+
// have to be (re)installed. When the node_modules cache hit for
|
|
12156
|
+
// the same lockfile, install verifies its state file and never
|
|
12157
|
+
// touches the zips — so github skips downloading this cache
|
|
12158
|
+
// entirely
|
|
12163
12159
|
redundantOnExactHitOf: NODE_MODULES_CACHE_ID,
|
|
12164
12160
|
},
|
|
12165
12161
|
];
|
|
@@ -12168,12 +12164,18 @@ exports.getYarnCache = getYarnCache;
|
|
|
12168
12164
|
const getNodeModulesCache = async (context, policy = "pull-push") => {
|
|
12169
12165
|
const packageManagerInfo = await context.packageManagerInfo;
|
|
12170
12166
|
const baseDir = await getCacheBaseDir(context);
|
|
12171
|
-
|
|
12172
|
-
|
|
12173
|
-
|
|
12174
|
-
//
|
|
12175
|
-
//
|
|
12176
|
-
|
|
12167
|
+
// pnpm gets no node_modules cache either: archiving the tree (root
|
|
12168
|
+
// plus every workspace's symlink farm, >1GB in real monorepos) costs
|
|
12169
|
+
// ~190s per saving job to spare ~6s of install, and a safe pnpm slot
|
|
12170
|
+
// must be keyed by lockfile hash (a pnpm install over a node_modules
|
|
12171
|
+
// from a different lockfile leaves orphaned .pnpm dirs that break
|
|
12172
|
+
// type identity), so it misses on every dependency change anyway.
|
|
12173
|
+
// pnpm projects run entirely uncached — see getYarnCache
|
|
12174
|
+
if (packageManagerInfo.type === "pnpm")
|
|
12175
|
+
return [];
|
|
12176
|
+
const lockFileName = "yarn.lock";
|
|
12177
|
+
// yarn berry tracks install state in .yarn/install-state.gz
|
|
12178
|
+
const hasInstallState = !packageManagerInfo.isClassic;
|
|
12177
12179
|
// We intentionally do not use the contents of the lockfile as a cache key, as install should always guarantee that the files are updated, but it can still use part of the cache if not all packages are up-to-date.
|
|
12178
12180
|
// It would slow down all pipelines whenever one adds a new dependency as it will need to download all node_modules again.
|
|
12179
12181
|
return [
|
|
@@ -12181,19 +12183,7 @@ const getNodeModulesCache = async (context, policy = "pull-push") => {
|
|
|
12181
12183
|
scope: "global",
|
|
12182
12184
|
pathMode: "absolute",
|
|
12183
12185
|
// we use the dirname, not the component name, because in certain cases we have two apps in the same directory and want to share the cache, e.g. when having storybook in the same package.json
|
|
12184
|
-
|
|
12185
|
-
// pnpm gets its own, lockfile-keyed cache: node_modules layouts
|
|
12186
|
-
// are not compatible between package managers (a migrating
|
|
12187
|
-
// project must not inherit the yarn slot), and unlike yarn, a
|
|
12188
|
-
// pnpm install on top of a node_modules from a different lockfile
|
|
12189
|
-
// leaves orphaned .pnpm dirs behind that break type identity —
|
|
12190
|
-
// so the slot changes whenever the lockfile changes
|
|
12191
|
-
key: isPnpm
|
|
12192
|
-
? {
|
|
12193
|
-
prefix: "pnpm-" + (0, slugify_1.default)(baseDir) + "-node-modules",
|
|
12194
|
-
files: [(0, path_1.join)(baseDir, "pnpm-lock.yaml")],
|
|
12195
|
-
}
|
|
12196
|
-
: (0, slugify_1.default)(baseDir) + "-node-modules",
|
|
12186
|
+
key: (0, slugify_1.default)(baseDir) + "-node-modules",
|
|
12197
12187
|
// content-key for immutable-cache backends (github); the lockfile
|
|
12198
12188
|
// decides whether the cached content could have changed (absolute
|
|
12199
12189
|
// pathMode: resolve the lockfile explicitly)
|
|
@@ -12204,12 +12194,6 @@ const getNodeModulesCache = async (context, policy = "pull-push") => {
|
|
|
12204
12194
|
policy,
|
|
12205
12195
|
paths: (0, lodash_es_1.uniq)([
|
|
12206
12196
|
(0, path_1.join)(baseDir, "node_modules"),
|
|
12207
|
-
// pnpm's per-package node_modules are symlink farms worth
|
|
12208
|
-
// keeping warm (yarn hoists into the root, so root-only
|
|
12209
|
-
// matches its historical behavior)
|
|
12210
|
-
...(isPnpm
|
|
12211
|
-
? workspaces.map((w) => (0, path_1.join)(w.location, "node_modules"))
|
|
12212
|
-
: []),
|
|
12213
12197
|
...(hasInstallState ? [(0, path_1.join)(baseDir, ".yarn/install-state.gz")] : []),
|
|
12214
12198
|
]),
|
|
12215
12199
|
},
|
|
@@ -12401,19 +12385,15 @@ const isInWorkspace = (context, packageManagerInfo) => context.type === "workspa
|
|
|
12401
12385
|
*/
|
|
12402
12386
|
const getEnsurePnpmCommand = (version) => `if ! command -v pnpm &> /dev/null; then corepack enable pnpm 2>/dev/null || npm install -g pnpm@${version}; fi`;
|
|
12403
12387
|
/**
|
|
12404
|
-
* pnpm's
|
|
12405
|
-
*
|
|
12406
|
-
*
|
|
12407
|
-
* the
|
|
12408
|
-
*
|
|
12409
|
-
|
|
12410
|
-
|
|
12411
|
-
const getPnpmStoreDirExpression = (inWorkspace) => inWorkspace
|
|
12412
|
-
? `"$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.pnpm-store"`
|
|
12413
|
-
: `"$PWD/.pnpm-store"`;
|
|
12414
|
-
const getPnpmInstallCommands = (context, packageManagerInfo) => [
|
|
12388
|
+
* pnpm jobs use pnpm's own (global, per-runner) store location. There
|
|
12389
|
+
* used to be a `--store-dir` pointing at a project-local
|
|
12390
|
+
* `.pnpm-store`, so CI could cache it — that cache is gone (see
|
|
12391
|
+
* getYarnCache), and keeping the store inside the repo only meant it
|
|
12392
|
+
* landed in docker build contexts.
|
|
12393
|
+
*/
|
|
12394
|
+
const getPnpmInstallCommands = (packageManagerInfo) => [
|
|
12415
12395
|
getEnsurePnpmCommand(packageManagerInfo.version),
|
|
12416
|
-
`pnpm install --frozen-lockfile
|
|
12396
|
+
`pnpm install --frozen-lockfile`,
|
|
12417
12397
|
];
|
|
12418
12398
|
exports.DEFAULT_AUDIT_LEVEL = "critical";
|
|
12419
12399
|
/**
|
|
@@ -12457,7 +12437,7 @@ const getPackageManagerInstall = async (context, options) => {
|
|
|
12457
12437
|
? context.build.config.postInstall
|
|
12458
12438
|
: null;
|
|
12459
12439
|
const installSection = packageManagerInfo.type === "pnpm"
|
|
12460
|
-
? (0, gitlab_1.collapseableSection)("pnpminstall", "pnpm install")(getPnpmInstallCommands(
|
|
12440
|
+
? (0, gitlab_1.collapseableSection)("pnpminstall", "pnpm install")(getPnpmInstallCommands(packageManagerInfo))
|
|
12461
12441
|
: (0, gitlab_1.collapseableSection)("yarninstall", "Yarn install")([getYarnInstallCommand(packageManagerInfo)]);
|
|
12462
12442
|
return [
|
|
12463
12443
|
...(0, exports.ensureNodeVersion)(context),
|
|
@@ -12470,11 +12450,12 @@ const getPackageManagerInstall = async (context, options) => {
|
|
|
12470
12450
|
exports.getPackageManagerInstall = getPackageManagerInstall;
|
|
12471
12451
|
const DOCKER_COPY_FILES = `COPY --chown=node:node $APP_DIR .`;
|
|
12472
12452
|
const getPnpmDockerAppCopyAndBuildScript = (packageManagerInfo) => {
|
|
12473
|
-
// the
|
|
12474
|
-
// the
|
|
12475
|
-
|
|
12476
|
-
|
|
12477
|
-
|
|
12453
|
+
// the prod install downloads from the registry: nothing is copied
|
|
12454
|
+
// into the image to link from (see createNodeDockerJobDefinition).
|
|
12455
|
+
// The store it fills is scratch space outside /app, dropped in the
|
|
12456
|
+
// same layer so the packages ship once — node_modules holds
|
|
12457
|
+
// hardlinks, so removing the store keeps the files alive
|
|
12458
|
+
const storeDir = "/tmp/pnpm-store";
|
|
12478
12459
|
// in a workspace, only install the component (and its workspace
|
|
12479
12460
|
// dependencies) — the pnpm equivalent of `yarn workspaces focus`.
|
|
12480
12461
|
// npm package names are shell-safe unquoted (the whole script is
|
|
@@ -12486,7 +12467,7 @@ const getPnpmDockerAppCopyAndBuildScript = (packageManagerInfo) => {
|
|
|
12486
12467
|
return new bash_1.BashExpression(`
|
|
12487
12468
|
${DOCKER_COPY_FILES}
|
|
12488
12469
|
RUN command -v pnpm >/dev/null 2>&1 || npm install -g pnpm@${packageManagerInfo.version}
|
|
12489
|
-
RUN pnpm install --prod --frozen-lockfile --store-dir ${storeDir}${filter}
|
|
12470
|
+
RUN pnpm install --prod --frozen-lockfile --store-dir ${storeDir}${filter} && rm -rf ${storeDir}
|
|
12490
12471
|
`.trim());
|
|
12491
12472
|
};
|
|
12492
12473
|
const getDockerAppCopyAndBuildScript = async (context) => {
|
|
@@ -40,9 +40,9 @@ change needed:
|
|
|
40
40
|
|
|
41
41
|
| | yarn | pnpm |
|
|
42
42
|
|---|---|---|
|
|
43
|
-
| CI install | `yarn install --immutable` | `pnpm install --frozen-lockfile
|
|
44
|
-
| download cache | `.yarn` zip cache |
|
|
45
|
-
| node_modules cache | mutable slot per build dir | **
|
|
43
|
+
| CI install | `yarn install --immutable` | `pnpm install --frozen-lockfile` |
|
|
44
|
+
| download cache | `.yarn` zip cache | **none** — installing from the registry is cheaper than moving the store |
|
|
45
|
+
| node_modules cache | mutable slot per build dir | **none** (measured: the archive costs more than the install it saves) |
|
|
46
46
|
| docker prod install | `yarn workspaces focus --production` | `pnpm install --prod --frozen-lockfile --filter <pkg>...` |
|
|
47
47
|
| audit job | `yarn npm audit … --all --recursive` | `pnpm audit --prod --audit-level critical` |
|
|
48
48
|
| build/start defaults | `yarn build` / `yarn start` | `pnpm build` / `pnpm start` |
|
|
@@ -146,9 +146,9 @@ skip it and run a bare `pnpm install` — that silently upgrades hundreds
|
|
|
146
146
|
of transitive dependencies at the same time as the package manager
|
|
147
147
|
changes, and you will not know which change broke what.
|
|
148
148
|
|
|
149
|
-
Then set the `packageManager` field, and update `.gitignore`:
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
Then set the `packageManager` field, and update `.gitignore`: `.yarn/*`
|
|
150
|
+
entries out (keep `.yarn/patches` only if you left the patches there).
|
|
151
|
+
Nothing pnpm-specific needs ignoring — the store lives outside the repo.
|
|
152
152
|
|
|
153
153
|
## Step 4 — fix the fallout locally
|
|
154
154
|
|
|
@@ -231,8 +231,8 @@ Mirrored, with one loss to state up front: **there is no importer back**.
|
|
|
231
231
|
every range — the migration necessarily bumps transitive versions.
|
|
232
232
|
Do it deliberately:
|
|
233
233
|
|
|
234
|
-
1. Delete `pnpm-lock.yaml`, `pnpm-workspace.yaml`,
|
|
235
|
-
|
|
234
|
+
1. Delete `pnpm-lock.yaml`, `pnpm-workspace.yaml`, and the `node_modules`
|
|
235
|
+
trees.
|
|
236
236
|
2. Move `packages:` back into package.json `workspaces`, `overrides` →
|
|
237
237
|
`resolutions`, `patchedDependencies` → yarn's patch protocol,
|
|
238
238
|
`allowBuilds` → (yarn runs build scripts by default; nothing needed).
|
package/dist/bundles/skills/catladder-migrate-package-manager/references/yarn-to-pnpm-differences.md
CHANGED
|
@@ -82,9 +82,26 @@ the docker build context.
|
|
|
82
82
|
|
|
83
83
|
**Version drift in `--prod` installs.** `pnpm install --prod` on a
|
|
84
84
|
node_modules tree from a *different* lockfile does not prune orphaned
|
|
85
|
-
`.pnpm` directories
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
`.pnpm` directories — never reuse one branch's node_modules for another
|
|
86
|
+
lockfile, and never a yarn tree for a pnpm branch.
|
|
87
|
+
|
|
88
|
+
**Nothing is cached.** A yarn pipeline caches its zip cache and
|
|
89
|
+
node_modules; a pnpm pipeline caches neither, and does not copy a store
|
|
90
|
+
into docker images either. pnpm installs a 3800-package monorepo from
|
|
91
|
+
the registry in 30-40s, which is less than it costs to move a 1 GB
|
|
92
|
+
store or a 1.3 GB node_modules tree through a CI cache — measured
|
|
93
|
+
side-by-side, caching made jobs about twice as slow on gitlab. Two
|
|
94
|
+
consequences when migrating: CI pulls more from the registry than the
|
|
95
|
+
yarn setup did (worth a registry proxy if that matters to you), and
|
|
96
|
+
`.pnpm-store` should not appear anywhere in the repo or a build
|
|
97
|
+
context.
|
|
98
|
+
|
|
99
|
+
**Docker images must not carry the store.** If a store is copied into
|
|
100
|
+
an image, it lands in a layer below `node_modules`, pnpm cannot hardlink
|
|
101
|
+
across the overlay boundary, and every package is copied instead — the
|
|
102
|
+
install gets *slower* (28s vs 10s from the registry) and the image
|
|
103
|
+
carries the packages twice. Let the `--prod` install download, into a
|
|
104
|
+
store outside the app directory that the same `RUN` layer deletes.
|
|
88
105
|
|
|
89
106
|
**pnpm 11 needs node ≥ 22.13** (pnpm 10: ≥ 18.12), and it does not fail
|
|
90
107
|
politely: it prints a `warn:` line and then crashes on
|
|
@@ -58,12 +58,13 @@ the registry.
|
|
|
58
58
|
|
|
59
59
|
## Caching
|
|
60
60
|
|
|
61
|
-
Cache configuration is generated per build type
|
|
62
|
-
`node_modules` and the
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
Cache configuration is generated per build type. **yarn** node builds
|
|
62
|
+
cache `node_modules` and the `.yarn` zip cache; GitLab uses mutable
|
|
63
|
+
caches with fallback keys (review branches fall back to the dev cache),
|
|
64
|
+
GitHub immutable lockfile-keyed caches where only the build job writes.
|
|
65
|
+
**pnpm** builds cache nothing: measured on a 3800-package monorepo,
|
|
66
|
+
moving the store or node_modules through a CI cache costs more than
|
|
67
|
+
pnpm's from-registry install.
|
|
67
68
|
Caching behavior is changed via the build config in `catladder.ts`,
|
|
68
69
|
not in the YAML.
|
|
69
70
|
|
|
@@ -44,11 +44,14 @@ const createNodeDockerJobDefinition = async (context) => {
|
|
|
44
44
|
(0, packageManagerInstall_1.getDockerAppCopyAndBuildScript)(context),
|
|
45
45
|
context.packageManagerInfo,
|
|
46
46
|
]);
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
// The pnpm store is deliberately NOT copied into the image. It used
|
|
48
|
+
// to be, so the prod install could link instead of hitting the
|
|
49
|
+
// registry — but the store lands in an earlier layer than
|
|
50
|
+
// node_modules, so pnpm cannot hardlink across the overlay boundary
|
|
51
|
+
// and copies every package instead. Measured on one component: the
|
|
52
|
+
// in-image install was *slower* that way (28s vs 10s from the
|
|
53
|
+
// registry), the store added 3GB to the build context and the final
|
|
54
|
+
// image, and the build took 339s instead of 27s.
|
|
52
55
|
// pnpm copies every workspace manifest into the image — as individual
|
|
53
56
|
// COPY instructions that many layers exceed docker's depth limit, so
|
|
54
57
|
// the job tars them up and a single ADD extracts them
|
|
@@ -56,8 +59,6 @@ const createNodeDockerJobDefinition = async (context) => {
|
|
|
56
59
|
const WORKSPACE_FILES_TAR = ".catladder-workspace-files.tar";
|
|
57
60
|
return {
|
|
58
61
|
script: [
|
|
59
|
-
// the COPY source must exist even when the cache was cold
|
|
60
|
-
...(copyPnpmStore ? ["mkdir -p .pnpm-store"] : []),
|
|
61
62
|
...(isPnpm
|
|
62
63
|
? [
|
|
63
64
|
`tar -cf ${WORKSPACE_FILES_TAR} ${packageManagerInfo.pathsToCopyInDocker.join(" ")}`,
|
|
@@ -74,14 +75,10 @@ const createNodeDockerJobDefinition = async (context) => {
|
|
|
74
75
|
`ADD ${WORKSPACE_FILES_TAR} /app/`,
|
|
75
76
|
// ADD does not apply --chown to extracted tars, and parent
|
|
76
77
|
// dirs of the entries come out root-owned — fix ownership
|
|
77
|
-
// while the stage still runs as root (
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
// need it, hence the fallback
|
|
78
|
+
// while the stage still runs as root (this only touches the
|
|
79
|
+
// small manifest tree). Custom base images without a node
|
|
80
|
+
// user run as root and don't need it, hence the fallback
|
|
81
81
|
`RUN chown -R node:node /app || true`,
|
|
82
|
-
...(copyPnpmStore
|
|
83
|
-
? [`COPY --chown=node:node .pnpm-store /app/.pnpm-store`]
|
|
84
|
-
: []),
|
|
85
82
|
].join("\n")
|
|
86
83
|
: packageManagerInfo.pathsToCopyInDocker
|
|
87
84
|
.map((dir) => `COPY --chown=node:node ${dir} /app/${dir}`)
|