@lenne.tech/cli 1.42.0 → 1.44.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.
- package/build/commands/dev/doctor.js +30 -7
- package/build/commands/fullstack/add-api.js +6 -0
- package/build/commands/fullstack/add-app.js +7 -0
- package/build/commands/fullstack/init.js +32 -3
- package/build/commands/fullstack/update.js +24 -1
- package/build/commands/git/reset.js +1 -1
- package/build/commands/git/update.js +2 -2
- package/build/extensions/frontend-helper.js +19 -0
- package/build/extensions/git.js +23 -2
- package/build/extensions/server.js +28 -3
- package/build/lib/adopt-upstream-build-allowlist.js +140 -0
- package/build/lib/fail-run.js +38 -0
- package/build/lib/heal-check-wrapper.js +189 -27
- package/build/lib/hoist-workspace-pnpm-config.js +173 -5
- package/build/lib/strip-vendor-schema-augmentation.js +213 -0
- package/build/lib/vendor-claude-md.js +15 -0
- package/build/templates/check/build-test-gate.mjs +107 -0
- package/build/templates/check/check.mjs +422 -52
- package/docs/VENDOR-MODE-WORKFLOW.md +35 -0
- package/docs/commands.md +9 -0
- package/package.json +8 -4
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stripAugmentationBlocks = stripAugmentationBlocks;
|
|
4
|
+
exports.stripVendorSchemaAugmentation = stripVendorSchemaAugmentation;
|
|
5
|
+
const fs_utils_1 = require("./fs-utils");
|
|
6
|
+
/**
|
|
7
|
+
* Drop `declare module '<nuxt|@nuxt>/schema' { … }` blocks that only augment
|
|
8
|
+
* `PublicRuntimeConfig`, leaving a note in their place.
|
|
9
|
+
*
|
|
10
|
+
* Brace-counted rather than regex-matched to the closing brace: a block may grow
|
|
11
|
+
* more members, and a pattern pinned to today's exact two lines would silently
|
|
12
|
+
* stop matching the moment it does — reintroducing the bug with no test failing,
|
|
13
|
+
* because the symptom only appears in a generated project.
|
|
14
|
+
*
|
|
15
|
+
* A block that augments anything OTHER than `PublicRuntimeConfig` is left alone.
|
|
16
|
+
* Those carry real declarations (module options, hooks) with no cycle, and
|
|
17
|
+
* deleting them would trade a typing bug for a worse one.
|
|
18
|
+
*/
|
|
19
|
+
function stripAugmentationBlocks(source, onSkip) {
|
|
20
|
+
const OPEN = /declare module ['"](?:@nuxt|nuxt)\/schema['"]\s*\{/g;
|
|
21
|
+
let out = source;
|
|
22
|
+
// A forward cursor rather than restarting `exec` from 0 after every rewrite.
|
|
23
|
+
// The restart made the matching branch O(k²) in the block count — measured
|
|
24
|
+
// 40 ms at 400 blocks, 558 ms at 1600. Real input is k=2, so this is shape
|
|
25
|
+
// rather than cost; the cursor is simply the honest way to write it, and it
|
|
26
|
+
// removes the recursion the non-matching branch needed.
|
|
27
|
+
let searchFrom = 0;
|
|
28
|
+
for (;;) {
|
|
29
|
+
OPEN.lastIndex = searchFrom;
|
|
30
|
+
const match = OPEN.exec(out);
|
|
31
|
+
if (!match)
|
|
32
|
+
break;
|
|
33
|
+
const bodyStart = match.index + match[0].length;
|
|
34
|
+
const end = matchingBrace(out, bodyStart);
|
|
35
|
+
if (end === -1) {
|
|
36
|
+
// Unbalanced (or a brace the scanner could not follow). Leaving it is the
|
|
37
|
+
// safe direction — truncating would break the file — but it must NOT be
|
|
38
|
+
// silent: the augmentation stays, so `config.public.*` is `unknown` in the
|
|
39
|
+
// generated project and nothing said so. Report and stop.
|
|
40
|
+
onSkip === null || onSkip === void 0 ? void 0 : onSkip("could not find the end of a `declare module '…/schema'` block; the augmentation was left in place. " +
|
|
41
|
+
'Remove it by hand, or `config.public.*` will type as `unknown` in this project.');
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
const body = out.slice(bodyStart, end);
|
|
45
|
+
if (!/\bPublicRuntimeConfig\b/.test(body) || /\binterface\s+(?!PublicRuntimeConfig\b)/.test(body)) {
|
|
46
|
+
// Not ours, or carries other declarations too — keep it and move past it.
|
|
47
|
+
searchFrom = end + 1;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const note = '// The `nuxt/schema` PublicRuntimeConfig augmentation was removed by `lt` when this\n' +
|
|
51
|
+
'// core was vendored. In node_modules it is harmless; as project source it augments\n' +
|
|
52
|
+
'// the same interface twice (`nuxt/schema` re-exports `@nuxt/schema`) and closes a\n' +
|
|
53
|
+
"// cycle with Nuxt's generated runtime-config types — TS2310, hidden by skipLibCheck,\n" +
|
|
54
|
+
'// which makes every `config.public.*` read `unknown`. The keys are unaffected: Nuxt\n' +
|
|
55
|
+
"// writes them into the generated types from the module's runtime-config defaults.\n" +
|
|
56
|
+
'// Do not restore it here; fix it upstream in @lenne.tech/nuxt-extensions.';
|
|
57
|
+
out = `${out.slice(0, match.index)}${note}${out.slice(end + 1)}`;
|
|
58
|
+
searchFrom = match.index + note.length;
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Remove the `nuxt/schema` runtime-config augmentations from a vendored
|
|
64
|
+
* nuxt-extensions core.
|
|
65
|
+
*
|
|
66
|
+
* ## What breaks without this
|
|
67
|
+
*
|
|
68
|
+
* `runtime/types/module.ts` in nuxt-extensions ends with:
|
|
69
|
+
*
|
|
70
|
+
* declare module 'nuxt/schema' { interface PublicRuntimeConfig extends LtExtensionsPublicRuntimeConfig {} }
|
|
71
|
+
* declare module '@nuxt/schema' { interface PublicRuntimeConfig extends LtExtensionsPublicRuntimeConfig {} }
|
|
72
|
+
*
|
|
73
|
+
* In npm mode that file ships as a `.d.ts` inside `node_modules` and never
|
|
74
|
+
* enters the consumer's TypeScript program. Vendoring copies it to
|
|
75
|
+
* `app/core/runtime/types/module.ts`, which the project's own `include` picks up
|
|
76
|
+
* unconditionally — and `nuxt/schema` re-exports `@nuxt/schema`, so augmenting
|
|
77
|
+
* both names decorates ONE interface twice. Nuxt's generated
|
|
78
|
+
* `.nuxt/types/runtime-config.d.ts` then closes the loop with its own
|
|
79
|
+
* `interface PublicRuntimeConfig extends UserPublicRuntimeConfig` (imported from
|
|
80
|
+
* `nuxt/schema`), and TypeScript reports:
|
|
81
|
+
*
|
|
82
|
+
* .nuxt/types/runtime-config.d.ts: error TS2310:
|
|
83
|
+
* Type 'PublicRuntimeConfig' recursively references itself as a base type.
|
|
84
|
+
*
|
|
85
|
+
* An interface in that state resolves every member to `unknown`. So in every
|
|
86
|
+
* vendor-mode project — the DEFAULT for `lt fullstack init` — `config.public.x`
|
|
87
|
+
* is `unknown` rather than its declared type, and `nuxt typecheck` fails on
|
|
88
|
+
* ordinary, correct code.
|
|
89
|
+
*
|
|
90
|
+
* ## Why it took so long to find
|
|
91
|
+
*
|
|
92
|
+
* Nuxt sets `skipLibCheck: true`, which suppresses TS2310 because it is reported
|
|
93
|
+
* in a `.d.ts`. The cause is therefore invisible and only the consequence shows:
|
|
94
|
+
* a plain `Argument of type 'unknown' is not assignable to parameter of type
|
|
95
|
+
* 'string'` at a call site that is not wrong. That is why the trap was previously
|
|
96
|
+
* written up as "vendor mode does not emit the schema block" — in
|
|
97
|
+
* `nuxt-base-starter/nuxt-base-template/CLAUDE.md` and in the JSDoc of that
|
|
98
|
+
* template's `app/utils/app-origin.ts`, both since corrected. The block IS
|
|
99
|
+
* emitted, and is byte-identical between the two modes (`diff` of the two
|
|
100
|
+
* generated `.nuxt/types/runtime-config.d.ts` is empty). Measured 2026-08-22 by
|
|
101
|
+
* converting the template and re-running the type gate with
|
|
102
|
+
* `--skipLibCheck false`.
|
|
103
|
+
*
|
|
104
|
+
* ## Why removing it costs nothing
|
|
105
|
+
*
|
|
106
|
+
* `ltExtensions` does not reach the consumer through this augmentation. The
|
|
107
|
+
* module sets its runtime-config defaults at build time, so Nuxt writes the whole
|
|
108
|
+
* shape into `SharedPublicRuntimeConfig` in the generated file. Verified after
|
|
109
|
+
* stripping: `config.public.ltExtensions.auth.enabled` is `boolean`,
|
|
110
|
+
* `.basePath` is `string`, `config.public.siteUrl` is `string`, and the type gate
|
|
111
|
+
* is clean.
|
|
112
|
+
*
|
|
113
|
+
* The conversion is the right owner: it is the step that turns package typings
|
|
114
|
+
* into project source, so it owns what that change of status implies.
|
|
115
|
+
*
|
|
116
|
+
* @returns the files that were modified, plus any block it could not process —
|
|
117
|
+
* which the caller MUST surface, because a skipped block means the bug
|
|
118
|
+
* is still there and only the transform knows it.
|
|
119
|
+
*/
|
|
120
|
+
function stripVendorSchemaAugmentation(options) {
|
|
121
|
+
var _a;
|
|
122
|
+
const { coreDir, filesystem } = options;
|
|
123
|
+
if (!filesystem.isDirectory(coreDir))
|
|
124
|
+
return { touched: [], warnings: [] };
|
|
125
|
+
// A linked sub-project points at the user's own checkout; rewriting files there
|
|
126
|
+
// would edit their repository. Same guard the workspace helpers already apply.
|
|
127
|
+
if ((0, fs_utils_1.isSymlink)(coreDir))
|
|
128
|
+
return { touched: [], warnings: [] };
|
|
129
|
+
const touched = [];
|
|
130
|
+
const warnings = [];
|
|
131
|
+
for (const file of (_a = filesystem.find(coreDir, { matching: '**/*.ts' })) !== null && _a !== void 0 ? _a : []) {
|
|
132
|
+
const content = filesystem.read(file);
|
|
133
|
+
if (!content || !content.includes('PublicRuntimeConfig'))
|
|
134
|
+
continue;
|
|
135
|
+
const patched = stripAugmentationBlocks(content, (reason) => {
|
|
136
|
+
warnings.push(`${file}: ${reason}`);
|
|
137
|
+
});
|
|
138
|
+
if (patched === content)
|
|
139
|
+
continue;
|
|
140
|
+
filesystem.write(file, patched);
|
|
141
|
+
touched.push(file);
|
|
142
|
+
}
|
|
143
|
+
return { touched, warnings };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Index of the `}` closing the block whose body starts at `from`, or -1.
|
|
147
|
+
*
|
|
148
|
+
* Skips over string literals, template literals and comments. Counting raw
|
|
149
|
+
* braces looked adequate — the augmentation bodies are two plain
|
|
150
|
+
* `interface … extends … {}` lines — but a fuzz pass found both failure modes,
|
|
151
|
+
* and both are silent:
|
|
152
|
+
*
|
|
153
|
+
* - a `}` inside a string (`{ open: '}' }`) drops depth to 0 early, so the strip
|
|
154
|
+
* cuts mid-block and leaves a stray `}` behind. The vendored file then does not
|
|
155
|
+
* compile, in a project the developer just generated.
|
|
156
|
+
* - a `{` inside a string (`type X = '{'`) never balances, this returns -1, the
|
|
157
|
+
* caller stops, and the augmentation is silently RETAINED — the exact TS2310
|
|
158
|
+
* bug the whole transform exists to remove, with nothing printed.
|
|
159
|
+
*
|
|
160
|
+
* Neither triggers on today's nuxt-extensions. But this file's own contract is
|
|
161
|
+
* that the block may grow members (that is why it counts braces instead of
|
|
162
|
+
* matching a fixed pattern), and the day a member carries a brace in a string is
|
|
163
|
+
* the day it misfires.
|
|
164
|
+
*/
|
|
165
|
+
function matchingBrace(text, from) {
|
|
166
|
+
let depth = 1;
|
|
167
|
+
for (let i = from; i < text.length; i++) {
|
|
168
|
+
const ch = text[i];
|
|
169
|
+
// Line comment — nothing structural until the newline.
|
|
170
|
+
if (ch === '/' && text[i + 1] === '/') {
|
|
171
|
+
const nl = text.indexOf('\n', i);
|
|
172
|
+
if (nl === -1)
|
|
173
|
+
return -1;
|
|
174
|
+
i = nl;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
// Block comment.
|
|
178
|
+
if (ch === '/' && text[i + 1] === '*') {
|
|
179
|
+
const close = text.indexOf('*/', i + 2);
|
|
180
|
+
if (close === -1)
|
|
181
|
+
return -1;
|
|
182
|
+
i = close + 1;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
// String or template literal. Templates may nest `${…}`, which would need a
|
|
186
|
+
// full parser to follow — so a template is treated as opaque, which is the
|
|
187
|
+
// safe direction: at worst a brace inside `${}` is ignored and the caller
|
|
188
|
+
// gets -1 and warns, rather than cutting the file in the wrong place.
|
|
189
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
190
|
+
const quote = ch;
|
|
191
|
+
i++;
|
|
192
|
+
while (i < text.length && text[i] !== quote) {
|
|
193
|
+
if (text[i] === '\\')
|
|
194
|
+
i++;
|
|
195
|
+
// An unterminated single/double-quoted string cannot span a newline.
|
|
196
|
+
else if (text[i] === '\n' && quote !== '`')
|
|
197
|
+
return -1;
|
|
198
|
+
i++;
|
|
199
|
+
}
|
|
200
|
+
if (i >= text.length)
|
|
201
|
+
return -1;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (ch === '{')
|
|
205
|
+
depth++;
|
|
206
|
+
else if (ch === '}') {
|
|
207
|
+
depth--;
|
|
208
|
+
if (depth === 0)
|
|
209
|
+
return i;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return -1;
|
|
213
|
+
}
|
|
@@ -92,6 +92,21 @@ function buildFrontendVendorBlock() {
|
|
|
92
92
|
'- **Contribute back:** run `/lt-dev:frontend:contribute-nuxt-extensions-core`.',
|
|
93
93
|
'- **Freshness check:** `pnpm run check:vendor-freshness` warns when',
|
|
94
94
|
' upstream has a newer release than the baseline.',
|
|
95
|
+
'',
|
|
96
|
+
'**If `config.public.*` types as `unknown`** (projects vendored before lt CLI',
|
|
97
|
+
'1.43.0), check with:',
|
|
98
|
+
'',
|
|
99
|
+
' grep -rn "declare module \'@nuxt/schema\'" app/core/',
|
|
100
|
+
'',
|
|
101
|
+
'A match means the vendored core still augments `PublicRuntimeConfig` under both',
|
|
102
|
+
'`nuxt/schema` and `@nuxt/schema`. Those are one interface (the former re-exports',
|
|
103
|
+
"the latter), and as project source they close a cycle with Nuxt's generated",
|
|
104
|
+
'runtime-config types — TS2310, which `skipLibCheck` hides, so every',
|
|
105
|
+
'`config.public.*` read silently becomes `unknown`. Delete both blocks from',
|
|
106
|
+
'`app/core/runtime/types/module.ts`; nothing is lost, because `ltExtensions`',
|
|
107
|
+
"reaches the app through the module's runtime-config defaults either way.",
|
|
108
|
+
'New conversions strip them automatically — but a core update copies upstream',
|
|
109
|
+
'verbatim, so re-run the grep after every sync.',
|
|
95
110
|
]);
|
|
96
111
|
}
|
|
97
112
|
/**
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-group mutual exclusion between CPU-heavy steps and contention-sensitive
|
|
3
|
+
* test suites (DEV-2524, cause 2).
|
|
4
|
+
*
|
|
5
|
+
* The root check wrapper (scripts/check.mjs) runs each workspace project's step
|
|
6
|
+
* chain concurrently by default. The api group's `test` step is the API e2e
|
|
7
|
+
* suite, whose Better-Auth session validation is sensitive to CPU contention:
|
|
8
|
+
* it tips into intermittent 401/500 the moment another group saturates the
|
|
9
|
+
* machine — most reliably the app group's `nuxt build`. Measured in DEV-2524:
|
|
10
|
+
* the same commit was red under the parallel run and green (2418/2418) under
|
|
11
|
+
* `--sequential`. This gate guarantees a heavy step and a sensitive test step
|
|
12
|
+
* never run at the same time, while:
|
|
13
|
+
* - same-class steps may still overlap (two builds, or two test suites)
|
|
14
|
+
* → the parallel design is preserved elsewhere;
|
|
15
|
+
* - every other step kind (format, lint, server-start, …) never touches the
|
|
16
|
+
* gate at all and stays fully parallel.
|
|
17
|
+
*
|
|
18
|
+
* Which steps belong to which class is `gateClass()` in check.mjs, not this
|
|
19
|
+
* file: the gate is a generic two-class lock and does not know what a "build"
|
|
20
|
+
* is. Note that the classes are named "build" and "test" only because those are
|
|
21
|
+
* the labels the caller passes; the lock treats them as two opaque, mutually
|
|
22
|
+
* exclusive classes.
|
|
23
|
+
*
|
|
24
|
+
* It is a two-class fair lock: whichever class is active admits every waiter of
|
|
25
|
+
* that class; the other class waits until the active class fully drains, then
|
|
26
|
+
* takes over as a batch. Handover is FIFO between the two classes, so neither
|
|
27
|
+
* can starve the other.
|
|
28
|
+
*
|
|
29
|
+
* Starvation-freedom is workload-bounded, not unconditional: an arriving acquire
|
|
30
|
+
* of the ACTIVE class barges ahead of an already-queued opposite-class waiter, so
|
|
31
|
+
* an unbounded stream of same-class arrivals could in theory starve the other
|
|
32
|
+
* class. That cannot happen here — the check wrapper issues FINITELY MANY gated
|
|
33
|
+
* steps per group, so the active class always drains and the queued batch is then
|
|
34
|
+
* admitted. (Do not restate this as "one test then one build per group": the root
|
|
35
|
+
* group has a `test` and no build at all, and a chain's shape is the project's to
|
|
36
|
+
* choose. Finiteness is what the argument needs, and finiteness is what holds.)
|
|
37
|
+
*/
|
|
38
|
+
export function createBuildTestGate() {
|
|
39
|
+
let activeClass = null; // 'test' | 'build' | null
|
|
40
|
+
let activeCount = 0;
|
|
41
|
+
const queue = []; // FIFO of { klass, resolve }
|
|
42
|
+
|
|
43
|
+
function admit(klass, resolve) {
|
|
44
|
+
activeClass = klass;
|
|
45
|
+
activeCount += 1;
|
|
46
|
+
resolve();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function acquire(klass) {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
if (activeClass === null || activeClass === klass) {
|
|
52
|
+
admit(klass, resolve);
|
|
53
|
+
} else {
|
|
54
|
+
queue.push({ klass, resolve });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function release() {
|
|
60
|
+
// Guard against an unbalanced release. Without it the counter goes negative
|
|
61
|
+
// and the lock stops excluding: with two holders and one stray release,
|
|
62
|
+
// `activeCount` reaches 0 while a holder is still running, so the opposite
|
|
63
|
+
// class is admitted alongside it — silently, and precisely when the machine
|
|
64
|
+
// is busiest. No current caller can double-release (runGroup acquires and
|
|
65
|
+
// releases exactly once per gated step), but this is shipped into every
|
|
66
|
+
// generated project and there are two release sites per acquire.
|
|
67
|
+
if (activeCount <= 0) return;
|
|
68
|
+
|
|
69
|
+
activeCount -= 1;
|
|
70
|
+
if (activeCount > 0) return;
|
|
71
|
+
|
|
72
|
+
activeClass = null;
|
|
73
|
+
if (queue.length === 0) return;
|
|
74
|
+
|
|
75
|
+
// Active class fully drained with waiters pending: hand over to the class of
|
|
76
|
+
// the oldest waiter, admitting every queued waiter of that class as a batch.
|
|
77
|
+
//
|
|
78
|
+
// Via the public API the queue only ever holds a SINGLE class at a time (a
|
|
79
|
+
// same-class acquire is admitted immediately and never queues, so only the
|
|
80
|
+
// opposite class waits while one class is active). The `carried` re-queue is
|
|
81
|
+
// therefore always empty in practice — kept as a defensive guard so the
|
|
82
|
+
// handover stays correct if the admission rule ever changes.
|
|
83
|
+
const nextClass = queue[0].klass;
|
|
84
|
+
const carried = [];
|
|
85
|
+
for (const waiter of queue) {
|
|
86
|
+
if (waiter.klass === nextClass) {
|
|
87
|
+
admit(waiter.klass, waiter.resolve);
|
|
88
|
+
} else {
|
|
89
|
+
carried.push(waiter);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
queue.length = 0;
|
|
93
|
+
queue.push(...carried);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
acquire,
|
|
98
|
+
release,
|
|
99
|
+
// Read-only accessors, exposed for assertions / telemetry only.
|
|
100
|
+
get activeClass() {
|
|
101
|
+
return activeClass;
|
|
102
|
+
},
|
|
103
|
+
get activeCount() {
|
|
104
|
+
return activeCount;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|