@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.
@@ -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
+ }