@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.
@@ -6096,11 +6096,14 @@ const createNodeDockerJobDefinition = async (context) => {
6096
6096
  (0, packageManagerInstall_1.getDockerAppCopyAndBuildScript)(context),
6097
6097
  context.packageManagerInfo,
6098
6098
  ]);
6099
- // the pnpm store (pulled via the cache above) is copied into the
6100
- // image so the prod install works offline — like the yarn zip cache,
6101
- // which travels inside the copied `.yarn` config folder
6102
- const copyPnpmStore = packageManagerInfo.type === "pnpm" &&
6103
- packageManagerInfo.componentIsInWorkspace;
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 (store not copied yet,
6130
- // so this only touches the small manifest tree). Custom
6131
- // base images without a node user run as root and don't
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 node caches: the package-manager
6171
- * (yarn zip / pnpm store) cache declares itself redundant when the
6172
- * node_modules cache scored an exact-lockfile hit (github skips the
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, or
6202
- * pnpm's content-addressable store (which catladder points at a
6203
- * project-local `.pnpm-store`, see the install script)
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
- const isPnpm = packageManagerInfo.type === "pnpm";
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: isPnpm ? pnpmStoreKey : "yarn",
6216
+ key: "yarn",
6221
6217
  policy,
6222
- paths: [isPnpm ? ".pnpm-store" : ".yarn"],
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: [isPnpm ? "pnpm-lock.yaml" : "yarn.lock"],
6226
- // the package manager's download cache (~hundreds of MB) is only
6227
- // read when packages have to be (re)installed. When the
6228
- // node_modules cache hit for the same lockfile, install verifies
6229
- // its state file and never touches the store — so github skips
6230
- // downloading this cache entirely
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
- const { workspaces } = packageManagerInfo;
6240
- const isPnpm = packageManagerInfo.type === "pnpm";
6241
- const lockFileName = isPnpm ? "pnpm-lock.yaml" : "yarn.lock";
6242
- // yarn berry tracks install state in .yarn/install-state.gz (pnpm's
6243
- // equivalent lives inside node_modules and is covered by the paths)
6244
- const hasInstallState = packageManagerInfo.type === "yarn" && !packageManagerInfo.isClassic;
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 store is global by default; point it at a project-local
6473
- * directory so CI can cache it (the pnpm equivalent of the `.yarn`
6474
- * zip cache). Passed as the --store-dir flag: pnpm 11 stopped reading
6475
- * the npm_config_store_dir env var (settings only come from
6476
- * pnpm-workspace.yaml / global config / CLI flags), the flag works on
6477
- * v10 and v11
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 --store-dir ${getPnpmStoreDirExpression(isInWorkspace(context, packageManagerInfo))}`,
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(context, packageManagerInfo))
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 store was copied into the image (like the yarn zip cache), so
6542
- // the prod install links from it instead of hitting the registry
6543
- const storeDir = packageManagerInfo.componentIsInWorkspace
6544
- ? "/app/.pnpm-store"
6545
- : "/app/$APP_DIR/.pnpm-store";
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
- // the pnpm store (pulled via the cache above) is copied into the
12032
- // image so the prod install works offline — like the yarn zip cache,
12033
- // which travels inside the copied `.yarn` config folder
12034
- const copyPnpmStore = packageManagerInfo.type === "pnpm" &&
12035
- packageManagerInfo.componentIsInWorkspace;
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 (store not copied yet,
12062
- // so this only touches the small manifest tree). Custom
12063
- // base images without a node user run as root and don't
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 node caches: the package-manager
12103
- * (yarn zip / pnpm store) cache declares itself redundant when the
12104
- * node_modules cache scored an exact-lockfile hit (github skips the
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, or
12134
- * pnpm's content-addressable store (which catladder points at a
12135
- * project-local `.pnpm-store`, see the install script)
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
- const isPnpm = packageManagerInfo.type === "pnpm";
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: isPnpm ? pnpmStoreKey : "yarn",
12148
+ key: "yarn",
12153
12149
  policy,
12154
- paths: [isPnpm ? ".pnpm-store" : ".yarn"],
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: [isPnpm ? "pnpm-lock.yaml" : "yarn.lock"],
12158
- // the package manager's download cache (~hundreds of MB) is only
12159
- // read when packages have to be (re)installed. When the
12160
- // node_modules cache hit for the same lockfile, install verifies
12161
- // its state file and never touches the store — so github skips
12162
- // downloading this cache entirely
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
- const { workspaces } = packageManagerInfo;
12172
- const isPnpm = packageManagerInfo.type === "pnpm";
12173
- const lockFileName = isPnpm ? "pnpm-lock.yaml" : "yarn.lock";
12174
- // yarn berry tracks install state in .yarn/install-state.gz (pnpm's
12175
- // equivalent lives inside node_modules and is covered by the paths)
12176
- const hasInstallState = packageManagerInfo.type === "yarn" && !packageManagerInfo.isClassic;
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 store is global by default; point it at a project-local
12405
- * directory so CI can cache it (the pnpm equivalent of the `.yarn`
12406
- * zip cache). Passed as the --store-dir flag: pnpm 11 stopped reading
12407
- * the npm_config_store_dir env var (settings only come from
12408
- * pnpm-workspace.yaml / global config / CLI flags), the flag works on
12409
- * v10 and v11
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 --store-dir ${getPnpmStoreDirExpression(isInWorkspace(context, packageManagerInfo))}`,
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(context, packageManagerInfo))
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 store was copied into the image (like the yarn zip cache), so
12474
- // the prod install links from it instead of hitting the registry
12475
- const storeDir = packageManagerInfo.componentIsInWorkspace
12476
- ? "/app/.pnpm-store"
12477
- : "/app/$APP_DIR/.pnpm-store";
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 --store-dir …` |
44
- | download cache | `.yarn` zip cache | project-local `.pnpm-store`, cache slot scoped by pnpm major |
45
- | node_modules cache | mutable slot per build dir | **lockfile-keyed** slot (a pnpm install over a foreign tree leaves orphaned `.pnpm` dirs) |
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
- `.pnpm-store/` in, `.yarn/*` entries out (keep `.yarn/patches` only if
151
- you left the patches there).
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`, `.pnpm-store`, and the
235
- `node_modules` trees.
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).
@@ -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. That is why catladder keys the pnpm node_modules
86
- cache by the lockfile instead of reusing a mutable slot — and why you
87
- should never reuse a yarn branch's cache for a pnpm branch.
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; node builds cache
62
- `node_modules` and the package manager's download cache (the `.yarn`
63
- zip cache, or for pnpm projects a project-local `.pnpm-store`). GitLab
64
- uses mutable caches with
65
- fallback keys (review branches fall back to the dev cache); GitHub uses
66
- immutable lockfile-keyed caches where only the build job writes.
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
- // the pnpm store (pulled via the cache above) is copied into the
48
- // image so the prod install works offline — like the yarn zip cache,
49
- // which travels inside the copied `.yarn` config folder
50
- const copyPnpmStore = packageManagerInfo.type === "pnpm" &&
51
- packageManagerInfo.componentIsInWorkspace;
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 (store not copied yet,
78
- // so this only touches the small manifest tree). Custom
79
- // base images without a node user run as root and don't
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}`)