@lenne.tech/cli 1.32.1 → 1.34.0

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.
@@ -15,40 +15,41 @@ const js_yaml_1 = require("js-yaml");
15
15
  * overrides defined only in projects/api/package.json never reach the
16
16
  * install resolver. Hoisting them to the root fixes both the warning
17
17
  * and the actual dependency-resolution behavior.
18
+ *
19
+ * Object-valued fields (`overrides`, `allowBuilds`) merge key-by-key;
20
+ * array-valued fields union + dedupe + sort. `minimumReleaseAgeExclude`
21
+ * is hoisted too so a sub-project's first-party exemption (e.g.
22
+ * `@lenne.tech/*`) keeps working in the monorepo — otherwise the
23
+ * minimum-release-age gate would block freshly published own packages.
18
24
  */
19
- const WORKSPACE_SCOPED_PNPM_FIELDS = ['overrides', 'onlyBuiltDependencies', 'ignoredOptionalDependencies'];
20
- /**
21
- * pnpm 11 renamed `onlyBuiltDependencies` (string array) to `allowBuilds`
22
- * (a `{ pkg: boolean }` map). A migrated sub-project pnpm-workspace.yaml
23
- * usually carries BOTH for cross-version compatibility, but we must not
24
- * rely on the array twin always being present: we normalise `allowBuilds`
25
- * back into `onlyBuiltDependencies` before hoisting (see
26
- * `normalizeAllowBuilds`) so the build-allowlist survives into the pnpm-10
27
- * monorepo root even when the file only carries the pnpm-11 object form.
28
- */
29
- const PNPM11_BUILD_KEY = 'allowBuilds';
25
+ const OBJECT_FIELDS = ['overrides', 'allowBuilds'];
26
+ const ARRAY_FIELDS = ['onlyBuiltDependencies', 'ignoredOptionalDependencies', 'minimumReleaseAgeExclude'];
27
+ const WORKSPACE_SCOPED_PNPM_FIELDS = [...OBJECT_FIELDS, ...ARRAY_FIELDS];
28
+ const isArrayField = (field) => ARRAY_FIELDS.includes(field);
30
29
  /**
31
- * Hoist workspace-scoped pnpm config from sub-projects into the root
32
- * package.json. After this runs, sub-project package.json files no
33
- * longer have `overrides`, `onlyBuiltDependencies`, or
34
- * `ignoredOptionalDependencies`, and the root package.json contains
35
- * the merged union.
30
+ * Hoist workspace-scoped pnpm config from sub-projects into the monorepo
31
+ * root `pnpm-workspace.yaml`. After this runs, sub-project pnpm config
32
+ * (package.json#pnpm or a settings-only pnpm-workspace.yaml) is gone, and
33
+ * the root pnpm-workspace.yaml carries the merged union next to `packages:`.
34
+ *
35
+ * Why pnpm-workspace.yaml and not package.json#pnpm: the monorepo root
36
+ * pins pnpm 11 (via `packageManager`), and pnpm 11 SILENTLY IGNORES the
37
+ * `pnpm` block in package.json — overrides/build-allowlists/etc. declared
38
+ * there never take effect, regressing `pnpm audit` and the minimum-release
39
+ * -age exemptions. pnpm-workspace.yaml is the pnpm-recommended home and is
40
+ * read by both pnpm 10 and 11.
36
41
  *
37
42
  * Two sources are read per sub-project, because the two starters store
38
43
  * their pnpm config differently:
39
44
  *
40
- * 1. `<sub>/package.json` `pnpm` block — nest-server-starter, and
41
- * nuxt-base-template before its pnpm-11 migration.
42
- * 2. `<sub>/pnpm-workspace.yaml` — nuxt-base-template after the
43
- * migration. pnpm 11 silently ignores the `pnpm` block in
44
- * package.json, so the template moved overrides into
45
- * pnpm-workspace.yaml. Inside a monorepo that nested file would
46
- * (a) not be hoisted by the old package.json-only logic, regressing
47
- * the CVE overrides, and (b) declare a nested workspace root that
48
- * conflicts with the monorepo's own pnpm-workspace.yaml. We hoist
49
- * its fields into the root package.json (the lt-monorepo root pins
50
- * pnpm@10 via `packageManager`, where package.json#pnpm IS honored)
51
- * and remove the now-redundant nested file.
45
+ * 1. `<sub>/package.json` `pnpm` block — nest-server-starter, and any
46
+ * template that has not migrated to the pnpm-11 layout yet.
47
+ * 2. `<sub>/pnpm-workspace.yaml` — nuxt-base-template (pnpm-11 layout).
48
+ * Inside a monorepo that nested file would (a) not be hoisted if we
49
+ * only read package.json, regressing the CVE overrides, and (b)
50
+ * declare a nested workspace root that conflicts with the monorepo's
51
+ * own pnpm-workspace.yaml. We hoist its fields into the root and
52
+ * remove the now-redundant nested file.
52
53
  *
53
54
  * Symlinked sub-projects are skipped entirely: in `--frontend-link` /
54
55
  * `--api-link` mode `projects/app` (or `projects/api`) points at the
@@ -64,14 +65,11 @@ const PNPM11_BUILD_KEY = 'allowBuilds';
64
65
  function hoistWorkspacePnpmConfig(options) {
65
66
  var _a;
66
67
  const { filesystem, projectDir, subProjects } = options;
67
- const rootPkgPath = `${projectDir}/package.json`;
68
- if (!filesystem.exists(rootPkgPath))
69
- return;
70
- const rootPkg = filesystem.read(rootPkgPath, 'json');
71
- if (!rootPkg)
72
- return;
73
- (_a = rootPkg.pnpm) !== null && _a !== void 0 ? _a : (rootPkg.pnpm = {});
74
- const rootPnpm = rootPkg.pnpm;
68
+ const rootWsPath = `${projectDir}/pnpm-workspace.yaml`;
69
+ // The root pnpm-workspace.yaml is the destination. It normally exists (the
70
+ // lt-monorepo clone ships one declaring `packages:`); start from it so
71
+ // `packages:` and any root-owned settings are preserved.
72
+ const rootWs = (_a = readYaml(filesystem, rootWsPath)) !== null && _a !== void 0 ? _a : {};
75
73
  let rootChanged = false;
76
74
  for (const subDir of subProjects) {
77
75
  const subPath = `${projectDir}/${subDir}`;
@@ -81,27 +79,30 @@ function hoistWorkspacePnpmConfig(options) {
81
79
  // checkout in link mode.
82
80
  if (isSymlink(subPath))
83
81
  continue;
84
- if (hoistFromSubPackageJson({ filesystem, rootPnpm, subPath })) {
82
+ if (hoistFromSubPackageJson({ filesystem, rootWs, subPath })) {
85
83
  rootChanged = true;
86
84
  }
87
- if (hoistFromSubWorkspaceYaml({ filesystem, rootPnpm, subPath })) {
85
+ if (hoistFromSubWorkspaceYaml({ filesystem, rootWs, subPath })) {
88
86
  rootChanged = true;
89
87
  }
90
88
  }
91
89
  if (rootChanged) {
92
- filesystem.write(rootPkgPath, `${JSON.stringify(rootPkg, null, 2)}\n`);
90
+ // Keep allowBuilds (pnpm 11) and onlyBuiltDependencies (pnpm 10) in sync so
91
+ // the build-script allowlist survives regardless of which key pnpm reads.
92
+ syncBuildAllowlists(rootWs);
93
+ filesystem.write(rootWsPath, (0, js_yaml_1.dump)(rootWs, { lineWidth: -1, sortKeys: false }));
93
94
  }
94
95
  }
95
96
  /**
96
- * Move the workspace-scoped pnpm fields from `source` into `rootPnpm`,
97
+ * Move the workspace-scoped pnpm fields from `source` into `rootWs`,
97
98
  * deleting each moved field from `source`. Returns true if anything moved.
98
99
  */
99
- function hoistFields(rootPnpm, source) {
100
+ function hoistFields(rootWs, source) {
100
101
  let changed = false;
101
102
  for (const field of WORKSPACE_SCOPED_PNPM_FIELDS) {
102
103
  if (source[field] === undefined)
103
104
  continue;
104
- rootPnpm[field] = mergePnpmFieldValue(field, rootPnpm[field], source[field]);
105
+ rootWs[field] = mergePnpmFieldValue(field, rootWs[field], source[field]);
105
106
  delete source[field];
106
107
  changed = true;
107
108
  }
@@ -109,14 +110,14 @@ function hoistFields(rootPnpm, source) {
109
110
  }
110
111
  /** Source 1: the sub-project's package.json `pnpm` block. */
111
112
  function hoistFromSubPackageJson(options) {
112
- const { filesystem, rootPnpm, subPath } = options;
113
+ const { filesystem, rootWs, subPath } = options;
113
114
  const subPkgPath = `${subPath}/package.json`;
114
115
  if (!filesystem.exists(subPkgPath))
115
116
  return false;
116
117
  const subPkg = filesystem.read(subPkgPath, 'json');
117
118
  if (!(subPkg === null || subPkg === void 0 ? void 0 : subPkg.pnpm))
118
119
  return false;
119
- if (!hoistFields(rootPnpm, subPkg.pnpm))
120
+ if (!hoistFields(rootWs, subPkg.pnpm))
120
121
  return false;
121
122
  // If the sub-project's pnpm section is now empty, drop it entirely.
122
123
  if (Object.keys(subPkg.pnpm).length === 0) {
@@ -125,37 +126,23 @@ function hoistFromSubPackageJson(options) {
125
126
  filesystem.write(subPkgPath, `${JSON.stringify(subPkg, null, 2)}\n`);
126
127
  return true;
127
128
  }
128
- /** Source 2: the sub-project's pnpm-workspace.yaml (pnpm-11 layout). */
129
+ /** Source 2: the sub-project's pnpm-workspace.yaml. */
129
130
  function hoistFromSubWorkspaceYaml(options) {
130
- const { filesystem, rootPnpm, subPath } = options;
131
+ const { filesystem, rootWs, subPath } = options;
131
132
  const subWsPath = `${subPath}/pnpm-workspace.yaml`;
132
133
  if (!filesystem.exists(subWsPath))
133
134
  return false;
134
- const raw = filesystem.read(subWsPath);
135
- if (!raw)
135
+ const ws = readYaml(filesystem, subWsPath);
136
+ if (!ws)
136
137
  return false;
137
- let parsed;
138
- try {
139
- parsed = (0, js_yaml_1.load)(raw);
140
- }
141
- catch (_a) {
142
- // Malformed YAML — leave it untouched rather than risk data loss.
143
- return false;
144
- }
145
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
146
- return false;
147
- const ws = parsed;
148
- // Fold the pnpm-11 `allowBuilds` map into `onlyBuiltDependencies` so it is
149
- // hoisted rather than discarded — even when the array twin is absent.
150
- normalizeAllowBuilds(ws);
151
- if (!hoistFields(rootPnpm, ws))
138
+ if (!hoistFields(rootWs, ws))
152
139
  return false;
153
140
  // A settings-only file (no `packages:`) exists solely to carry these
154
141
  // hoisted keys — once emptied it would only declare a nested workspace
155
142
  // root, so remove it. A file that declares `packages:` is a real (rare)
156
143
  // nested workspace; keep it minus the hoisted keys.
157
144
  if (Array.isArray(ws.packages) && ws.packages.length > 0) {
158
- filesystem.write(subWsPath, (0, js_yaml_1.dump)(ws));
145
+ filesystem.write(subWsPath, (0, js_yaml_1.dump)(ws, { lineWidth: -1, sortKeys: false }));
159
146
  }
160
147
  else {
161
148
  filesystem.remove(subWsPath);
@@ -174,19 +161,16 @@ function isSymlink(path) {
174
161
  /**
175
162
  * Merge two values for a pnpm workspace-scoped field.
176
163
  *
177
- * pnpm expects:
178
- * - `overrides` object ({pkg: version})
179
- * - `onlyBuiltDependencies` → array of strings
180
- * - `ignoredOptionalDependencies` → array of strings
164
+ * Arrays (`onlyBuiltDependencies`, `ignoredOptionalDependencies`,
165
+ * `minimumReleaseAgeExclude`): deduplicated, alphabetically sorted union.
181
166
  *
182
- * Arrays: deduplicated, alphabetically sorted union.
183
- * Objects: sub-project values take precedence over root (sub-projects
184
- * like nest-server-starter own the authoritative CVE override list for
185
- * their transitive deps; the root usually only seeds cross-cutting
186
- * handlebars/minimatch patches).
167
+ * Objects (`overrides`, `allowBuilds`): key-by-key merge where sub-project
168
+ * values take precedence over root (sub-projects like nest-server-starter
169
+ * own the authoritative CVE override list for their transitive deps; the
170
+ * root usually only seeds cross-cutting patches).
187
171
  */
188
172
  function mergePnpmFieldValue(field, rootValue, subValue) {
189
- if (field === 'onlyBuiltDependencies' || field === 'ignoredOptionalDependencies') {
173
+ if (isArrayField(field)) {
190
174
  const rootArr = Array.isArray(rootValue) ? rootValue : [];
191
175
  const subArr = Array.isArray(subValue) ? subValue : [];
192
176
  return Array.from(new Set([...rootArr, ...subArr])).sort((a, b) => a.localeCompare(b));
@@ -198,26 +182,55 @@ function mergePnpmFieldValue(field, rootValue, subValue) {
198
182
  const merged = Object.assign(Object.assign({}, rootObj), subObj);
199
183
  return Object.fromEntries(Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)));
200
184
  }
185
+ /** Parse a YAML file into a plain object, or null on missing/malformed/non-object. */
186
+ function readYaml(filesystem, path) {
187
+ if (!filesystem.exists(path))
188
+ return null;
189
+ const raw = filesystem.read(path);
190
+ if (!raw)
191
+ return null;
192
+ let parsed;
193
+ try {
194
+ parsed = (0, js_yaml_1.load)(raw);
195
+ }
196
+ catch (_a) {
197
+ // Malformed YAML — leave it untouched rather than risk data loss.
198
+ return null;
199
+ }
200
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
201
+ return null;
202
+ return parsed;
203
+ }
201
204
  /**
202
- * Fold a pnpm-11 `allowBuilds: { pkg: boolean }` map into the pnpm-10
203
- * `onlyBuiltDependencies: string[]` form (packages whose value is `true`),
204
- * unioned with any existing array, then remove the `allowBuilds` key so the
205
- * redundant object form does not linger. Mutates `ws` in place.
205
+ * Keep the two build-allowlist forms consistent and complete after hoisting:
206
+ * - `allowBuilds` (pnpm 11) is a `{ pkg: boolean }` map.
207
+ * - `onlyBuiltDependencies` (pnpm 10) is a `string[]` of allowed packages.
206
208
  *
207
- * No-op when `allowBuilds` is absent or not an object map — an unexpected
208
- * shape is left untouched rather than risk silent data loss.
209
+ * Sub-templates carry one or both (nest-server-starter only `allowBuilds`,
210
+ * nuxt-base-template both). After merging we derive the full set of allowed
211
+ * packages from BOTH forms and write back canonical, sorted twins so the
212
+ * allowlist survives whichever key the active pnpm version reads. Explicit
213
+ * `false` entries in `allowBuilds` are preserved (and excluded from the
214
+ * array). No-op when neither key is present. Mutates `ws` in place.
209
215
  */
210
- function normalizeAllowBuilds(ws) {
211
- const raw = ws[PNPM11_BUILD_KEY];
212
- if (!raw || typeof raw !== 'object' || Array.isArray(raw))
216
+ function syncBuildAllowlists(ws) {
217
+ const arr = Array.isArray(ws.onlyBuiltDependencies) ? ws.onlyBuiltDependencies : [];
218
+ const obj = ws.allowBuilds && typeof ws.allowBuilds === 'object' && !Array.isArray(ws.allowBuilds)
219
+ ? ws.allowBuilds
220
+ : {};
221
+ if (arr.length === 0 && Object.keys(obj).length === 0)
213
222
  return;
214
- const allowed = Object.entries(raw)
215
- .filter(([, enabled]) => enabled === true)
216
- .map(([pkg]) => pkg);
217
- if (allowed.length > 0) {
218
- const existing = Array.isArray(ws.onlyBuiltDependencies) ? ws.onlyBuiltDependencies : [];
219
- // Order here is irrelevant — mergePnpmFieldValue sorts the union on hoist.
220
- ws.onlyBuiltDependencies = Array.from(new Set([...allowed, ...existing]));
221
- }
222
- delete ws[PNPM11_BUILD_KEY];
223
+ // Every array entry implies allowBuilds[entry] = true unless already set.
224
+ const map = {};
225
+ for (const [pkg, enabled] of Object.entries(obj))
226
+ map[pkg] = enabled === true;
227
+ for (const pkg of arr)
228
+ if (map[pkg] === undefined)
229
+ map[pkg] = true;
230
+ const allowed = Object.entries(map)
231
+ .filter(([, enabled]) => enabled)
232
+ .map(([pkg]) => pkg)
233
+ .sort((a, b) => a.localeCompare(b));
234
+ ws.allowBuilds = Object.fromEntries(Object.entries(map).sort(([a], [b]) => a.localeCompare(b)));
235
+ ws.onlyBuiltDependencies = allowed;
223
236
  }
@@ -210,9 +210,14 @@ function upsertVendorBlock(content, marker, newBlock) {
210
210
  }
211
211
  return content.replace(blockRegex(marker), newBlock);
212
212
  }
213
- /** Join block lines into the canonical `marker … --- ` shape (ends with `---\n`). */
213
+ /** Join block lines into the canonical `marker … --- ` shape (ends with `---\n\n`). */
214
214
  function block(lines) {
215
- return [...lines, '', '---', ''].join('\n');
215
+ // Blank line AFTER the `---` too, so the prepended block is separated from
216
+ // the following template heading by an empty line — oxfmt requires a blank
217
+ // line between a thematic break and a heading, otherwise `format:check`
218
+ // fails on a freshly vendored CLAUDE.md. blockRegex's `---\s*\n?` absorbs
219
+ // the extra newline, so removeVendorBlock stays round-trip-safe.
220
+ return [...lines, '', '---', '', ''].join('\n');
216
221
  }
217
222
  /** Build the regex that matches an existing block from its marker to the first `---`. */
218
223
  function blockRegex(marker) {