@lenne.tech/cli 1.50.3 → 1.50.5
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/build/commands/dev/doctor.js +48 -42
- package/build/commands/fullstack/update.js +6 -1
- package/build/extensions/api-mode.js +6 -2
- package/build/extensions/server.js +6 -16
- package/build/lib/caddy.js +18 -1
- package/build/lib/dev-patches.js +11 -5
- package/build/lib/express-type-imports.js +54 -0
- package/build/lib/hoist-workspace-pnpm-config.js +133 -5
- package/package.json +8 -4
|
@@ -179,52 +179,58 @@ const DoctorCommand = {
|
|
|
179
179
|
// "matches the canonical CLI version" for a project whose `check` was
|
|
180
180
|
// completely broken — and doctor is the tool people reach for exactly
|
|
181
181
|
// then.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
182
|
+
//
|
|
183
|
+
// Workspace roots only: a standalone starter project carries its own
|
|
184
|
+
// `scripts/check.mjs`, which is not a drifted copy of the root wrapper,
|
|
185
|
+
// and `lt fullstack update` deliberately leaves it alone.
|
|
186
|
+
if (layout.workspace) {
|
|
187
|
+
try {
|
|
188
|
+
const { readFileSync: read } = yield Promise.resolve().then(() => __importStar(require('fs')));
|
|
189
|
+
const { join: j } = yield Promise.resolve().then(() => __importStar(require('path')));
|
|
190
|
+
const { keptWrapperReason, resolveCopySet } = yield Promise.resolve().then(() => __importStar(require('../../lib/heal-check-wrapper')));
|
|
191
|
+
const bundledCheck = j(__dirname, '..', '..', 'templates', 'check', 'check.mjs');
|
|
192
|
+
if (filesystem.exists(bundledCheck)) {
|
|
193
|
+
const missing = [];
|
|
194
|
+
const drifted = [];
|
|
195
|
+
for (const { rel, source } of resolveCopySet(bundledCheck)) {
|
|
196
|
+
const target = j(layout.root, rel);
|
|
197
|
+
if (!filesystem.exists(target)) {
|
|
198
|
+
missing.push(rel);
|
|
199
|
+
}
|
|
200
|
+
else if (read(target, 'utf8') !== read(source, 'utf8')) {
|
|
201
|
+
drifted.push(rel);
|
|
202
|
+
}
|
|
194
203
|
}
|
|
195
|
-
|
|
196
|
-
|
|
204
|
+
// A missing module only breaks `check` when the project's wrapper is the
|
|
205
|
+
// canonical one that imports it (or the wrapper itself is missing). An
|
|
206
|
+
// older wrapper does not import the newer modules (e.g. `lib/*.mjs`) and
|
|
207
|
+
// still runs — calling that "cannot start" would be a false alarm.
|
|
208
|
+
const wrapperDrifted = drifted.includes('scripts/check.mjs');
|
|
209
|
+
// A project wrapper that heal refuses to replace (newer release, or the same
|
|
210
|
+
// release with other content) is not drift to fix: pointing at
|
|
211
|
+
// `lt fullstack update` there would recommend a no-op or a downgrade.
|
|
212
|
+
const kept = keptWrapperReason(layout.root, bundledCheck);
|
|
213
|
+
if (kept) {
|
|
214
|
+
line('INFO', colors.cyan, `check wrapper: ${kept}`);
|
|
215
|
+
}
|
|
216
|
+
else if (missing.length > 0 && !wrapperDrifted) {
|
|
217
|
+
line('ERROR', colors.red, `check wrapper incomplete — missing ${missing.join(', ')}`);
|
|
218
|
+
line('ERROR', colors.red, ' `pnpm run check` cannot start; run `lt fullstack update` to install it');
|
|
219
|
+
}
|
|
220
|
+
else if (drifted.length > 0) {
|
|
221
|
+
const outdated = [...drifted, ...missing.map((rel) => `${rel} (missing)`)];
|
|
222
|
+
const verb = outdated.length === 1 ? 'differs' : 'differ';
|
|
223
|
+
line('WARN', colors.yellow, `${outdated.join(', ')} ${verb} from the canonical CLI version`);
|
|
224
|
+
line('WARN', colors.yellow, ' run `lt fullstack update` to sync it (skips uncommitted local edits)');
|
|
225
|
+
}
|
|
226
|
+
else if (filesystem.exists(j(layout.root, 'scripts', 'check.mjs'))) {
|
|
227
|
+
line('OK', colors.green, 'check wrapper matches the canonical CLI version');
|
|
197
228
|
}
|
|
198
|
-
}
|
|
199
|
-
// A missing module only breaks `check` when the project's wrapper is the
|
|
200
|
-
// canonical one that imports it (or the wrapper itself is missing). An
|
|
201
|
-
// older wrapper does not import the newer modules (e.g. `lib/*.mjs`) and
|
|
202
|
-
// still runs — calling that "cannot start" would be a false alarm.
|
|
203
|
-
const wrapperDrifted = drifted.includes('scripts/check.mjs');
|
|
204
|
-
// A project wrapper that heal refuses to replace (newer release, or the same
|
|
205
|
-
// release with other content) is not drift to fix: pointing at
|
|
206
|
-
// `lt fullstack update` there would recommend a no-op or a downgrade.
|
|
207
|
-
const kept = keptWrapperReason(layout.root, bundledCheck);
|
|
208
|
-
if (kept) {
|
|
209
|
-
line('INFO', colors.cyan, `check wrapper: ${kept}`);
|
|
210
|
-
}
|
|
211
|
-
else if (missing.length > 0 && !wrapperDrifted) {
|
|
212
|
-
line('ERROR', colors.red, `check wrapper incomplete — missing ${missing.join(', ')}`);
|
|
213
|
-
line('ERROR', colors.red, ' `pnpm run check` cannot start; run `lt fullstack update` to install it');
|
|
214
|
-
}
|
|
215
|
-
else if (drifted.length > 0) {
|
|
216
|
-
const outdated = [...drifted, ...missing.map((rel) => `${rel} (missing)`)];
|
|
217
|
-
const verb = outdated.length === 1 ? 'differs' : 'differ';
|
|
218
|
-
line('WARN', colors.yellow, `${outdated.join(', ')} ${verb} from the canonical CLI version`);
|
|
219
|
-
line('WARN', colors.yellow, ' run `lt fullstack update` to sync it (skips uncommitted local edits)');
|
|
220
|
-
}
|
|
221
|
-
else if (filesystem.exists(j(layout.root, 'scripts', 'check.mjs'))) {
|
|
222
|
-
line('OK', colors.green, 'check wrapper matches the canonical CLI version');
|
|
223
229
|
}
|
|
224
230
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
231
|
+
catch (_a) {
|
|
232
|
+
/* best-effort diagnostics */
|
|
233
|
+
}
|
|
228
234
|
}
|
|
229
235
|
// 7.6 oxlint config filename: oxlint only auto-discovers `.oxlintrc.json`.
|
|
230
236
|
// Apps from the template before the rename carry `oxlint.json`, whose
|
|
@@ -195,8 +195,13 @@ const NewCommand = {
|
|
|
195
195
|
// pre-existing projects predate it. This idempotently installs the bundled
|
|
196
196
|
// wrapper (and rewrites the root `check`/`check:raw`) so a migrated project
|
|
197
197
|
// gets the quiet, report-driven check too. No-op once already wired.
|
|
198
|
+
//
|
|
199
|
+
// Only at a workspace root: the bundled file is lt-monorepo's ROOT wrapper.
|
|
200
|
+
// `projects/api` and standalone starter projects ship their own, unmarked
|
|
201
|
+
// `scripts/check.mjs`, which heal reads as legacy — run from there, this
|
|
202
|
+
// replaced the API's check and the starter's tests failed on its content.
|
|
198
203
|
const checkAsset = (0, path_1.join)(__dirname, '..', '..', 'templates', 'check', 'check.mjs');
|
|
199
|
-
const changedCheck = (0, heal_check_wrapper_1.healCheckWrapper)(cwd, checkAsset);
|
|
204
|
+
const changedCheck = isWorkspace ? (0, heal_check_wrapper_1.healCheckWrapper)(cwd, checkAsset) : [];
|
|
200
205
|
if (changedCheck.length > 0) {
|
|
201
206
|
info('');
|
|
202
207
|
// A skip entry is NOT a success — it means the wrapper stayed on its old
|
|
@@ -649,8 +649,12 @@ class ApiMode {
|
|
|
649
649
|
* For non-aliased imports, returns the identifier as-is.
|
|
650
650
|
*/
|
|
651
651
|
resolveImportAlias(identifier) {
|
|
652
|
-
|
|
653
|
-
|
|
652
|
+
// An inline `type` modifier is not part of the name: searching the code for the
|
|
653
|
+
// literal "type LookupAddress" found nothing, so a used specifier got pruned
|
|
654
|
+
// (nest-server's outbound-url.helper.ts → TS2304 in every vendor-mode project).
|
|
655
|
+
const name = identifier.replace(/^type\s+/, '');
|
|
656
|
+
const asMatch = name.match(/^\S+\s+as\s+(\S+)$/);
|
|
657
|
+
return asMatch ? asMatch[1] : name;
|
|
654
658
|
}
|
|
655
659
|
/**
|
|
656
660
|
* Collapse multiple consecutive blank lines into at most one
|
|
@@ -50,6 +50,7 @@ const ts = __importStar(require("typescript"));
|
|
|
50
50
|
const adopt_upstream_build_allowlist_1 = require("../lib/adopt-upstream-build-allowlist");
|
|
51
51
|
const check_freshness_hooks_1 = require("../lib/check-freshness-hooks");
|
|
52
52
|
const cross_env_1 = require("../lib/cross-env");
|
|
53
|
+
const express_type_imports_1 = require("../lib/express-type-imports");
|
|
53
54
|
const markdown_table_1 = require("../lib/markdown-table");
|
|
54
55
|
const migrate_scripts_1 = require("../lib/migrate-scripts");
|
|
55
56
|
const module_specifiers_1 = require("../lib/module-specifiers");
|
|
@@ -1229,12 +1230,10 @@ class Server {
|
|
|
1229
1230
|
// and type imports were erased before runtime. In vendor-mode, vitest/vite
|
|
1230
1231
|
// evaluates the TypeScript source directly and chokes with:
|
|
1231
1232
|
// "The requested module 'express' does not provide an export named 'Response'"
|
|
1232
|
-
// Fix:
|
|
1233
|
-
//
|
|
1234
|
-
//
|
|
1235
|
-
//
|
|
1236
|
-
// to the global Fetch API Request, not the express one.
|
|
1237
|
-
const expressImportRegex = /^import\s+\{([^}]*)\}\s+from\s+['"]express['"]\s*;?\s*$/gm;
|
|
1233
|
+
// Fix: mark the type names in every `import { … } from 'express'` of the
|
|
1234
|
+
// vendored core as type-only. Runtime exports (`json`, `urlencoded`, `Router`
|
|
1235
|
+
// …) stay value imports — see src/lib/express-type-imports.ts for why a
|
|
1236
|
+
// blanket `import type` broke the build (nest-server 11.42.4+).
|
|
1238
1237
|
// `glob`, not `filesystem.find`: jetpack builds its matcher by concatenating the
|
|
1239
1238
|
// RESOLVED base path in front of the pattern, and minimatch reads a backslash
|
|
1240
1239
|
// inside a PATTERN as an escape. On Windows `C:\…\core` + `**/*.ts` therefore
|
|
@@ -1249,16 +1248,7 @@ class Server {
|
|
|
1249
1248
|
const content = filesystem.read(absPath) || '';
|
|
1250
1249
|
if (!content.includes("from 'express'") && !content.includes('from "express"'))
|
|
1251
1250
|
continue;
|
|
1252
|
-
const patched =
|
|
1253
|
-
const cleanNames = names
|
|
1254
|
-
.split(',')
|
|
1255
|
-
.map((n) => n.trim())
|
|
1256
|
-
.filter((n) => n.length > 0 && n !== 'type')
|
|
1257
|
-
// Strip any pre-existing 'type ' prefix on individual names
|
|
1258
|
-
.map((n) => n.replace(/^type\s+/, ''))
|
|
1259
|
-
.join(', ');
|
|
1260
|
-
return `import type { ${cleanNames} } from 'express';`;
|
|
1261
|
-
});
|
|
1251
|
+
const patched = (0, express_type_imports_1.rewriteExpressImportsToTypeOnly)(content);
|
|
1262
1252
|
if (patched !== content) {
|
|
1263
1253
|
filesystem.write(absPath, patched);
|
|
1264
1254
|
}
|
package/build/lib/caddy.js
CHANGED
|
@@ -42,6 +42,8 @@ const path_1 = require("path");
|
|
|
42
42
|
const dev_process_1 = require("./dev-process");
|
|
43
43
|
const platform_1 = require("./platform");
|
|
44
44
|
const CADDYFILE_PATH = process.env.LT_DEV_CADDYFILE || (0, path_1.join)((0, os_1.homedir)(), '.lenneTech', 'Caddyfile');
|
|
45
|
+
/** Upstream connection cap per route — well below the 128 of macOS `somaxconn` (see `renderProjectBlock`). */
|
|
46
|
+
const UPSTREAM_MAX_CONNS = 32;
|
|
45
47
|
const HEADER = '# Managed by `lt dev`. Per-project blocks are bounded by `# >>> lt-dev:<slug> >>>` markers.';
|
|
46
48
|
/** Detect whether `caddy` is on PATH. */
|
|
47
49
|
function caddyAvailable() {
|
|
@@ -115,12 +117,27 @@ function removeProjectBlock(slug) {
|
|
|
115
117
|
* - `localhost` as Caddy upstream resolves to `::1` first on macOS,
|
|
116
118
|
* so it would still pick the IPv6 family and miss the IPv4 bind.
|
|
117
119
|
* Pinning to `127.0.0.1` removes that ambiguity entirely.
|
|
120
|
+
*
|
|
121
|
+
* Every upstream is capped at `UPSTREAM_MAX_CONNS` connections. The browser
|
|
122
|
+
* talks HTTP/2 to Caddy and multiplexes a cold `nuxt dev`'s first page load —
|
|
123
|
+
* around 800 raw modules — over one connection; Caddy used to fan that out
|
|
124
|
+
* into one upstream dial per request. The dev server's accept queue
|
|
125
|
+
* (`somaxconn`, 128 on macOS) overflowed, the excess dials got `connect:
|
|
126
|
+
* connection reset by peer`, Caddy answered 502, and a page missing a module
|
|
127
|
+
* never hydrated: the form looked fine and submitted natively. With the cap,
|
|
128
|
+
* Caddy queues the burst instead (Go's `MaxConnsPerHost` blocks the dial).
|
|
129
|
+
* `~/.lenneTech/caddy.err.log` held about 51 000 such resets across projects
|
|
130
|
+
* when this was found (lt-crm DEV-2899).
|
|
118
131
|
*/
|
|
119
132
|
function renderProjectBlock(slug, routes) {
|
|
120
133
|
const lines = [`# >>> lt-dev:${slug} >>>`];
|
|
121
134
|
for (const route of routes) {
|
|
122
135
|
lines.push(`${route.hostname} {`);
|
|
123
|
-
lines.push(` reverse_proxy 127.0.0.1:${route.upstreamPort}`);
|
|
136
|
+
lines.push(` reverse_proxy 127.0.0.1:${route.upstreamPort} {`);
|
|
137
|
+
lines.push(' transport http {');
|
|
138
|
+
lines.push(` max_conns_per_host ${UPSTREAM_MAX_CONNS}`);
|
|
139
|
+
lines.push(' }');
|
|
140
|
+
lines.push(' }');
|
|
124
141
|
lines.push('}');
|
|
125
142
|
}
|
|
126
143
|
lines.push(`# <<< lt-dev:${slug} <<<`);
|
package/build/lib/dev-patches.js
CHANGED
|
@@ -430,11 +430,14 @@ function patchPlaywrightConfig(file) {
|
|
|
430
430
|
* identically to a live one: the block would silently never load
|
|
431
431
|
* `.lt-dev/.env` again and Playwright would fall back to `localhost:3001`.
|
|
432
432
|
*
|
|
433
|
-
* Quote style
|
|
434
|
-
*
|
|
435
|
-
* project-specific — do not assume).
|
|
436
|
-
*
|
|
437
|
-
*
|
|
433
|
+
* Quote style, intra-line whitespace, the padding inside brackets and
|
|
434
|
+
* trailing commas are normalised away because the consumer's formatter owns
|
|
435
|
+
* this file (which direction it flips is project-specific — do not assume).
|
|
436
|
+
* oxfmt at printWidth 80 wraps the bridge imports and adds a trailing comma;
|
|
437
|
+
* read as a code change, that rewrote playwright.config.ts on every run.
|
|
438
|
+
* Blind spot to keep in mind: a change that differs ONLY in quotes,
|
|
439
|
+
* whitespace or trailing commas is invisible here, which is what
|
|
440
|
+
* `BRIDGE_VERSION` is for.
|
|
438
441
|
*/
|
|
439
442
|
function normaliseBridgeBlock(s) {
|
|
440
443
|
return s
|
|
@@ -443,6 +446,9 @@ function normaliseBridgeBlock(s) {
|
|
|
443
446
|
.filter((l) => l !== '' && !l.startsWith('//'))
|
|
444
447
|
.join(' ')
|
|
445
448
|
.replace(/['"]/g, '"')
|
|
449
|
+
.replace(/,\s*([)\]}])/g, '$1')
|
|
450
|
+
.replace(/([([{])\s+/g, '$1')
|
|
451
|
+
.replace(/\s+([)\]}])/g, '$1')
|
|
446
452
|
.replace(/\s+/g, ' ')
|
|
447
453
|
.trim();
|
|
448
454
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Marks the TYPE names in a vendored file's `express` imports as type-only.
|
|
4
|
+
*
|
|
5
|
+
* Why it exists: express exports `Request`, `Response`, `NextFunction` & co. as types
|
|
6
|
+
* only. The npm-mode framework shipped compiled, so those imports were erased before
|
|
7
|
+
* runtime; vendor mode runs the TypeScript source through vitest/vite, which keeps a
|
|
8
|
+
* plain `import { Request } from 'express'` and fails with
|
|
9
|
+
* "The requested module 'express' does not provide an export named 'Request'".
|
|
10
|
+
*
|
|
11
|
+
* Why it does not simply rewrite the whole import to `import type`: express also has
|
|
12
|
+
* runtime exports, and the vendored core imports some of them as values —
|
|
13
|
+
* `core-body-parser.initializer.ts` (nest-server 11.42.4+) calls `json()` and
|
|
14
|
+
* `urlencoded()`. A blanket `import type { json, urlencoded }` broke the build of every
|
|
15
|
+
* generated vendor-mode project with TS1361. So only names that are NOT runtime exports
|
|
16
|
+
* become type-only; a mixed import keeps its values and gets inline `type` modifiers.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.rewriteExpressImportsToTypeOnly = rewriteExpressImportsToTypeOnly;
|
|
20
|
+
/**
|
|
21
|
+
* The runtime (value) exports of express — `lib/express.js`, v4 and v5. Everything
|
|
22
|
+
* else a TypeScript file can import from 'express' only exists in its type declarations.
|
|
23
|
+
*/
|
|
24
|
+
const EXPRESS_VALUE_EXPORTS = new Set([
|
|
25
|
+
'application',
|
|
26
|
+
'json',
|
|
27
|
+
'query',
|
|
28
|
+
'raw',
|
|
29
|
+
'request',
|
|
30
|
+
'response',
|
|
31
|
+
'Route',
|
|
32
|
+
'Router',
|
|
33
|
+
'static',
|
|
34
|
+
'text',
|
|
35
|
+
'urlencoded',
|
|
36
|
+
]);
|
|
37
|
+
// `[ \t]*`, not `\s*`, after the module name: `\s*$` under the `m` flag also swallowed
|
|
38
|
+
// the line break, and with it the blank line that usually follows the imports.
|
|
39
|
+
const EXPRESS_NAMED_IMPORT = /^import\s+\{([^}]*)\}\s+from\s+['"]express['"][ \t]*;?[ \t]*$/gm;
|
|
40
|
+
/** Rewrites every `import { … } from 'express'` in `content`; returns it unchanged when nothing applies. */
|
|
41
|
+
function rewriteExpressImportsToTypeOnly(content) {
|
|
42
|
+
return content.replace(EXPRESS_NAMED_IMPORT, (_match, names) => {
|
|
43
|
+
const specifiers = names
|
|
44
|
+
.split(',')
|
|
45
|
+
.map((n) => n.trim().replace(/^type\s+/, ''))
|
|
46
|
+
.filter((n) => n.length > 0);
|
|
47
|
+
const isValue = (specifier) => EXPRESS_VALUE_EXPORTS.has(specifier.split(/\s+as\s+/)[0].trim());
|
|
48
|
+
if (!specifiers.some(isValue)) {
|
|
49
|
+
return `import type { ${specifiers.join(', ')} } from 'express';`;
|
|
50
|
+
}
|
|
51
|
+
const marked = specifiers.map((s) => (isValue(s) ? s : `type ${s}`));
|
|
52
|
+
return `import { ${marked.join(', ')} } from 'express';`;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -4,7 +4,9 @@ exports.extractKeyComments = extractKeyComments;
|
|
|
4
4
|
exports.reattachKeyComments = reattachKeyComments;
|
|
5
5
|
exports.hoistPackageManager = hoistPackageManager;
|
|
6
6
|
exports.hoistWorkspacePnpmConfig = hoistWorkspacePnpmConfig;
|
|
7
|
+
const fs_1 = require("fs");
|
|
7
8
|
const js_yaml_1 = require("js-yaml");
|
|
9
|
+
const path_1 = require("path");
|
|
8
10
|
const fs_utils_1 = require("./fs-utils");
|
|
9
11
|
/**
|
|
10
12
|
* pnpm workspace-scoped fields that must live at the workspace root.
|
|
@@ -37,8 +39,16 @@ const fs_utils_1 = require("./fs-utils");
|
|
|
37
39
|
* the same trap one step later: the starter scope-allows known-stale peer ranges
|
|
38
40
|
* there, and without the hoist the root `check:peers` step fails the first
|
|
39
41
|
* `pnpm run check` of every generated project.
|
|
42
|
+
*
|
|
43
|
+
* `patchedDependencies` (`{ "pkg@version": "patches/pkg@version.patch" }`) is the
|
|
44
|
+
* same trap once more, with a file attached. nuxt-base-starter pairs an override
|
|
45
|
+
* (`simple-git: 4.0.2`) with a patch that restores the default export its consumer
|
|
46
|
+
* imports. Hoisting the override without the patch installs the package unpatched,
|
|
47
|
+
* and `nuxt prepare` aborts the first `pnpm install` of every generated project.
|
|
48
|
+
* The value is a path relative to the workspace root, so the file moves with it —
|
|
49
|
+
* see `relocatePatches`.
|
|
40
50
|
*/
|
|
41
|
-
const OBJECT_FIELDS = ['overrides', 'allowBuilds'];
|
|
51
|
+
const OBJECT_FIELDS = ['overrides', 'allowBuilds', 'patchedDependencies'];
|
|
42
52
|
const ARRAY_FIELDS = ['onlyBuiltDependencies', 'ignoredOptionalDependencies', 'minimumReleaseAgeExclude'];
|
|
43
53
|
/** Objects whose inner arrays are unioned and inner maps merged, not replaced. */
|
|
44
54
|
const NESTED_ARRAY_FIELDS = ['auditConfig', 'peerDependencyRules'];
|
|
@@ -333,6 +343,7 @@ function hoistWorkspacePnpmConfig(options) {
|
|
|
333
343
|
const conflicts = [];
|
|
334
344
|
const rootOverrides = [];
|
|
335
345
|
const auditWidenedBy = new Set();
|
|
346
|
+
const relocatedPatches = new Map();
|
|
336
347
|
// Sub-projects that are present and eligible but hand over nothing, because an
|
|
337
348
|
// earlier run already hoisted their settings and deleted the source. They are
|
|
338
349
|
// what turns a root-vs-sub override into a sub-vs-sub disagreement.
|
|
@@ -349,15 +360,31 @@ function hoistWorkspacePnpmConfig(options) {
|
|
|
349
360
|
// across its own two config sources is the same silent-winner bug one scope
|
|
350
361
|
// down, and a shared label hides it.
|
|
351
362
|
const fromPkg = hoistFromSubPackageJson({
|
|
352
|
-
context: {
|
|
363
|
+
context: {
|
|
364
|
+
auditWidenedBy,
|
|
365
|
+
conflicts,
|
|
366
|
+
provenance,
|
|
367
|
+
relocatedPatches,
|
|
368
|
+
rootOverrides,
|
|
369
|
+
sourceLabel: `${subDir}/package.json#pnpm`,
|
|
370
|
+
},
|
|
353
371
|
filesystem,
|
|
372
|
+
projectDir,
|
|
354
373
|
rootWs,
|
|
355
374
|
subPath,
|
|
356
375
|
});
|
|
357
376
|
const fromYaml = hoistFromSubWorkspaceYaml({
|
|
358
377
|
comments,
|
|
359
|
-
context: {
|
|
378
|
+
context: {
|
|
379
|
+
auditWidenedBy,
|
|
380
|
+
conflicts,
|
|
381
|
+
provenance,
|
|
382
|
+
relocatedPatches,
|
|
383
|
+
rootOverrides,
|
|
384
|
+
sourceLabel: `${subDir}/pnpm-workspace.yaml`,
|
|
385
|
+
},
|
|
360
386
|
filesystem,
|
|
387
|
+
projectDir,
|
|
361
388
|
rootWs,
|
|
362
389
|
subPath,
|
|
363
390
|
});
|
|
@@ -547,13 +574,14 @@ function hoistFields(rootWs, source, context) {
|
|
|
547
574
|
}
|
|
548
575
|
/** Source 1: the sub-project's package.json `pnpm` block. */
|
|
549
576
|
function hoistFromSubPackageJson(options) {
|
|
550
|
-
const { filesystem, rootWs, subPath } = options;
|
|
577
|
+
const { filesystem, projectDir, rootWs, subPath } = options;
|
|
551
578
|
const subPkgPath = `${subPath}/package.json`;
|
|
552
579
|
if (!filesystem.exists(subPkgPath))
|
|
553
580
|
return false;
|
|
554
581
|
const subPkg = filesystem.read(subPkgPath, 'json');
|
|
555
582
|
if (!(subPkg === null || subPkg === void 0 ? void 0 : subPkg.pnpm))
|
|
556
583
|
return false;
|
|
584
|
+
relocatePatches({ context: options.context, filesystem, projectDir, source: subPkg.pnpm, subPath });
|
|
557
585
|
if (!hoistFields(rootWs, subPkg.pnpm, options.context))
|
|
558
586
|
return false;
|
|
559
587
|
// If the sub-project's pnpm section is now empty, drop it entirely.
|
|
@@ -566,7 +594,7 @@ function hoistFromSubPackageJson(options) {
|
|
|
566
594
|
/** Source 2: the sub-project's pnpm-workspace.yaml. */
|
|
567
595
|
function hoistFromSubWorkspaceYaml(options) {
|
|
568
596
|
var _a;
|
|
569
|
-
const { comments, filesystem, rootWs, subPath } = options;
|
|
597
|
+
const { comments, filesystem, projectDir, rootWs, subPath } = options;
|
|
570
598
|
const subWsPath = `${subPath}/pnpm-workspace.yaml`;
|
|
571
599
|
if (!filesystem.exists(subWsPath))
|
|
572
600
|
return false;
|
|
@@ -581,6 +609,7 @@ function hoistFromSubWorkspaceYaml(options) {
|
|
|
581
609
|
if (!comments.has(key))
|
|
582
610
|
comments.set(key, block);
|
|
583
611
|
}
|
|
612
|
+
relocatePatches({ context: options.context, filesystem, projectDir, source: ws, subPath });
|
|
584
613
|
if (!hoistFields(rootWs, ws, options.context))
|
|
585
614
|
return false;
|
|
586
615
|
// A settings-only file (no `packages:`) exists solely to carry these
|
|
@@ -658,6 +687,21 @@ function pmVersion(pin) {
|
|
|
658
687
|
const afterAt = pin.slice(pin.lastIndexOf('@') + 1);
|
|
659
688
|
return afterAt.split('+')[0];
|
|
660
689
|
}
|
|
690
|
+
/** Remove `dir` and every parent it empties, stopping below `stopAt`, which is never removed. */
|
|
691
|
+
function pruneEmptyDirs(filesystem, dir, stopAt) {
|
|
692
|
+
var _a;
|
|
693
|
+
const stop = (0, path_1.resolve)(stopAt);
|
|
694
|
+
let current = (0, path_1.resolve)(dir);
|
|
695
|
+
for (;;) {
|
|
696
|
+
const rel = (0, path_1.relative)(stop, current);
|
|
697
|
+
if (!rel || rel.startsWith('..') || (0, path_1.isAbsolute)(rel))
|
|
698
|
+
return;
|
|
699
|
+
if (((_a = filesystem.list(current)) !== null && _a !== void 0 ? _a : ['?']).length > 0)
|
|
700
|
+
return;
|
|
701
|
+
filesystem.remove(current);
|
|
702
|
+
current = (0, path_1.dirname)(current);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
661
705
|
/** Parse a YAML file into a plain object, or null on missing/malformed/non-object. */
|
|
662
706
|
function readYaml(filesystem, path) {
|
|
663
707
|
if (!filesystem.exists(path))
|
|
@@ -746,6 +790,90 @@ function recordRootOverrides(field, rootValue, subValue, context) {
|
|
|
746
790
|
});
|
|
747
791
|
}
|
|
748
792
|
}
|
|
793
|
+
/**
|
|
794
|
+
* Make every `patchedDependencies` path in a sub-project's pnpm config valid at the
|
|
795
|
+
* workspace root, by moving the patch file to `<root>/patches/`.
|
|
796
|
+
*
|
|
797
|
+
* Why the file moves instead of the path just being rewritten to
|
|
798
|
+
* `projects/app/patches/…`: pnpm would find either, but the Dockerfiles copy only the
|
|
799
|
+
* build-context-root `patches/` into their deps stage, and in a monorepo the context IS
|
|
800
|
+
* the root. A path into `projects/app/` installs locally and breaks the image build.
|
|
801
|
+
*
|
|
802
|
+
* Never overwrites, never writes through a symlink, never deletes a patch it could not
|
|
803
|
+
* place. Where the move is impossible the entry points at the file where it is (a local
|
|
804
|
+
* install keeps working) and the reason is reported. A clash with ANOTHER sub-project's
|
|
805
|
+
* entry for the same key is left to `recordObjectFieldProvenance`, which names both
|
|
806
|
+
* sources; reporting it here as well would say the same thing twice.
|
|
807
|
+
*/
|
|
808
|
+
function relocatePatches(options) {
|
|
809
|
+
var _a, _b;
|
|
810
|
+
const { context, filesystem, projectDir, source, subPath } = options;
|
|
811
|
+
const entries = source.patchedDependencies;
|
|
812
|
+
if (!entries || typeof entries !== 'object' || Array.isArray(entries))
|
|
813
|
+
return;
|
|
814
|
+
const rootPatchesDir = (0, path_1.resolve)(projectDir, 'patches');
|
|
815
|
+
const toRootPath = (abs) => (0, path_1.relative)((0, path_1.resolve)(projectDir), abs).split(path_1.sep).join('/');
|
|
816
|
+
const isInside = (dir, path) => {
|
|
817
|
+
const rel = (0, path_1.relative)(dir, path);
|
|
818
|
+
return !!rel && !rel.startsWith('..') && !(0, path_1.isAbsolute)(rel);
|
|
819
|
+
};
|
|
820
|
+
const relocated = {};
|
|
821
|
+
const moved = (_a = context === null || context === void 0 ? void 0 : context.relocatedPatches) !== null && _a !== void 0 ? _a : new Map();
|
|
822
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
823
|
+
relocated[key] = value;
|
|
824
|
+
if (typeof value !== 'string')
|
|
825
|
+
continue;
|
|
826
|
+
const from = (0, path_1.resolve)(subPath, value);
|
|
827
|
+
const fromPath = toRootPath(from);
|
|
828
|
+
relocated[key] = (_b = moved.get(from)) !== null && _b !== void 0 ? _b : fromPath;
|
|
829
|
+
if (moved.has(from))
|
|
830
|
+
continue;
|
|
831
|
+
const leftInPlace = (reason) => {
|
|
832
|
+
context === null || context === void 0 ? void 0 : context.conflicts.push(`patchedDependencies: ${JSON.stringify(key)} — ${reason}, so ${fromPath} was left in place. ` +
|
|
833
|
+
`A local install still finds it, but the Dockerfiles copy only the root patches/ and the image build will not.`);
|
|
834
|
+
};
|
|
835
|
+
if (isInside(rootPatchesDir, from))
|
|
836
|
+
continue;
|
|
837
|
+
if (filesystem.exists(from) !== 'file') {
|
|
838
|
+
context === null || context === void 0 ? void 0 : context.conflicts.push(`patchedDependencies: ${JSON.stringify(key)} points at ${fromPath}, which does not exist. ` +
|
|
839
|
+
`pnpm install fails until the patch file is there.`);
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
if (!isInside((0, path_1.resolve)(subPath), from)) {
|
|
843
|
+
leftInPlace('the patch lives outside its sub-project');
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
if ((0, fs_utils_1.isSymlink)(from)) {
|
|
847
|
+
leftInPlace('the patch file is a symlink');
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
const dirState = filesystem.exists(rootPatchesDir);
|
|
851
|
+
if ((0, fs_utils_1.isSymlink)(rootPatchesDir) || (dirState && dirState !== 'dir')) {
|
|
852
|
+
leftInPlace('patches/ at the workspace root is a symlink or not a directory');
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
const to = (0, path_1.join)(rootPatchesDir, (0, path_1.basename)(from));
|
|
856
|
+
if (filesystem.exists(to)) {
|
|
857
|
+
const identical = filesystem.exists(to) === 'file' && !(0, fs_utils_1.isSymlink)(to) && (0, fs_1.readFileSync)(to).equals((0, fs_1.readFileSync)(from));
|
|
858
|
+
if (identical) {
|
|
859
|
+
filesystem.remove(from);
|
|
860
|
+
pruneEmptyDirs(filesystem, (0, path_1.dirname)(from), subPath);
|
|
861
|
+
relocated[key] = toRootPath(to);
|
|
862
|
+
moved.set(from, toRootPath(to));
|
|
863
|
+
}
|
|
864
|
+
else if (!(context === null || context === void 0 ? void 0 : context.provenance.has(`patchedDependencies.${key}`))) {
|
|
865
|
+
leftInPlace(`${toRootPath(to)} already exists with different content`);
|
|
866
|
+
}
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
filesystem.dir(rootPatchesDir);
|
|
870
|
+
filesystem.move(from, to);
|
|
871
|
+
pruneEmptyDirs(filesystem, (0, path_1.dirname)(from), subPath);
|
|
872
|
+
relocated[key] = toRootPath(to);
|
|
873
|
+
moved.set(from, toRootPath(to));
|
|
874
|
+
}
|
|
875
|
+
source.patchedDependencies = relocated;
|
|
876
|
+
}
|
|
749
877
|
/**
|
|
750
878
|
* One line, not one per advisory: say that hoisting widened the audit
|
|
751
879
|
* suppressions to the whole workspace, and name who brought them.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/cli",
|
|
3
|
-
"version": "1.50.
|
|
3
|
+
"version": "1.50.5",
|
|
4
4
|
"description": "lenne.Tech CLI: lt",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lenne.Tech",
|
|
@@ -58,13 +58,13 @@
|
|
|
58
58
|
"bin"
|
|
59
59
|
],
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@aws-sdk/client-s3": "3.
|
|
61
|
+
"@aws-sdk/client-s3": "3.1148.0",
|
|
62
62
|
"@lenne.tech/cli-plugin-helper": "0.0.14",
|
|
63
63
|
"axios": "1.20.0",
|
|
64
64
|
"bcrypt": "6.0.0",
|
|
65
65
|
"cross-spawn": "7.0.6",
|
|
66
66
|
"defuddle": "0.19.4",
|
|
67
|
-
"ejs": "
|
|
67
|
+
"ejs": "7.0.1",
|
|
68
68
|
"glob": "13.0.6",
|
|
69
69
|
"gluegun": "5.2.2",
|
|
70
70
|
"js-sha256": "1.0.0",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"jsonc-parser": "3.3.1",
|
|
74
74
|
"lodash": "4.18.1",
|
|
75
75
|
"open": "11.0.4",
|
|
76
|
-
"playwright-core": "1.
|
|
76
|
+
"playwright-core": "1.64.0",
|
|
77
77
|
"ts-morph": "28.0.0",
|
|
78
78
|
"ts-node": "10.9.2",
|
|
79
79
|
"turndown": "7.2.4",
|
|
@@ -113,6 +113,7 @@
|
|
|
113
113
|
"//minimatch-note": "Why the minimatch raises exist: when they were added, brace-expansion 1.x and 2.x were END OF LINE for GHSA-mh99-v99m-4gvg (advisory range <=5.0.7 across ALL majors, patched only 5.0.8+), so the only fix was to raise each CONSUMER off minimatch 3.x/5.x/9.x. That premise no longer holds: upstream has since backported every fix (GHSA-mh99 now reads <1.1.17 / >=2.0.0 <2.1.3, and 1.1.21 / 2.1.7 patch all five current advisories). Verified 2026-10-03 with fresh lockfile resolves: without `minimatch@>=4 <10` and `fs-jetpack > minimatch@<10` npm audit is still clean, the tree only gains minimatch 3.1.5 + 5.1.9 and brace-expansion 1.1.21 + 2.1.7 (7 packages). So both are now a DEDUPE measure (one minimatch 10.2.6, one brace-expansion 5.0.12), not a security fix - dropping them is a judgment call, it reopens no advisory. Forcing brace-expansion 5.x globally is still NOT an option: 5.x is ESM/tshy and exports an object ({ expand, ... }) while 1.x/2.x export the function itself, so minimatch 3.x/5.x would die on `expand is not a function`. Each scoped entry was verified against the consumer's actual call site before being added. The eslint / @eslint/eslintrc / @eslint/config-array entries were REMOVED in 1.42.0: eslint 10 requests minimatch ^10.2.5 itself, config-array ^10.2.4, and eslintrc is no longer in the tree at all.",
|
|
114
114
|
"minimatch@>=4 <10": "Bounded raise of every minimatch 4.x-9.x install to 10.2.6, whose brace-expansion ^5.0.8 is patched. As of 2026-10-03 the only consumer it still raises is filelist (gluegun > ejs 3.1.10 > jake > filelist, PRODUCTION-reachable, minimatch ^5.0.1): @typescript-eslint/typescript-estree 8.65 requests ^10.2.2 itself and the jest 30.5 packages no longer depend on minimatch. Verified safe: filelist calls only the static helper `minimatch.match(files, pat, opts)`. Floored at >=4 so the 3.x line is NOT swept in: minimatch 3.x is callable while 9.x/10.x export an object with __esModule but no `default` key, so sweeping it would break any consumer that calls the default export. Dedupe, not security (see //minimatch-note). Drop once filelist requests minimatch >=10.",
|
|
115
115
|
"babel-plugin-istanbul > test-exclude@<8": "Dedupe override, no longer a security fix. Added when babel-plugin-istanbul requested test-exclude ^6.0.0, whose 6.0.0 pins minimatch ^3.0.4 and glob ^7. Since jest 30.5 (babel-plugin-istanbul 8.0.0) the request is ^7.0.1, and test-exclude 7.0.2 already uses minimatch ^10.2.2: a fresh resolve without this entry audits clean (2026-10-03) but adds 23 packages (glob 10.5.0 and its subtree). 8.0.0 uses glob ^13, the major this project already ships, and is still CJS with the same `module.exports = TestExclude` class shape, so babel-plugin-istanbul 8's _interopRequireDefault + `new _testExclude.default(opts)` keeps working. Drop once babel-plugin-istanbul requests test-exclude ^8.",
|
|
116
|
+
"@istanbuljs/load-nyc-config > js-yaml": "Security fix for GHSA-hp3w-g68c-fv3c (sprintf-js, DoS via unbounded precision specifiers, moderate). Advisory range is ALL versions, so there is no patched sprintf-js to raise to. Dev-only path: ts-jest > @jest/transform > babel-plugin-istanbul 8.0.0 > @istanbuljs/load-nyc-config 1.1.0 (js-yaml ^3.13.1) > js-yaml 3.15.2 > argparse 1.0.10 > sprintf-js. Every link is already at its latest release (2026-10-06), and only js-yaml 4.x leaves the chain: it depends on argparse ^2, which has no sprintf-js. `$js-yaml` pins it to this project's own direct js-yaml, so the two never drift. Verified safe: load-nyc-config only calls `require('js-yaml').load(...)` (index.js:80), which 4.x has; 4.x `load` behaves like 3.x `safeLoad`. Drop once load-nyc-config requests js-yaml >=4 or argparse 1.x drops sprintf-js.",
|
|
116
117
|
"fs-jetpack > minimatch@<10": "Same chain, PRODUCTION-reachable (gluegun > fs-jetpack). Safe: lib/utils/matcher.js does `require('minimatch').Minimatch` and uses only `new Minimatch(pattern, { matchBase, nocomment, nocase, dot })`, `.negate` and `.match()` - all verified against 10.x. Not fixable by upgrading fs-jetpack: gluegun@5.2.2 is the latest release and fs-jetpack@5.1.0 still requests minimatch ^5.1.0 (brace-expansion ^2.0.1, also unpatched)."
|
|
117
118
|
},
|
|
118
119
|
"overrides": {
|
|
@@ -126,6 +127,9 @@
|
|
|
126
127
|
"babel-plugin-istanbul": {
|
|
127
128
|
"test-exclude@<8": "8.0.0"
|
|
128
129
|
},
|
|
130
|
+
"@istanbuljs/load-nyc-config": {
|
|
131
|
+
"js-yaml": "$js-yaml"
|
|
132
|
+
},
|
|
129
133
|
"minimatch@>=4 <10": "10.2.6"
|
|
130
134
|
},
|
|
131
135
|
"//jest.workerIdleMemoryLimit": "Recycle a ts-jest worker once its heap passes this. Guards against an intermittent `A jest worker process was terminated by another process: signal=SIGSEGV` that kills ONE suite while every other test passes (seen twice in marketplace*, both under `npm run check`, never reproducible on demand - 0 in 9 targeted runs incl. --maxWorkers=16). It is a worker crash, NOT an assertion failure: the signature is `Test suite failed to run` with the remaining count still green. All versions are current and in-range (node 24 / jest 30 / ts-jest 29 / ts 6), so this is a resilience measure against unbounded worker heap growth, not a proven root-cause fix - if it recurs, capture the suite name and re-open.",
|