@lenne.tech/cli 1.41.3 → 1.43.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/deployment/create.js +18 -1
- package/build/commands/dev/vscode.js +173 -0
- 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 +17 -0
- 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/dev-test-session.js +119 -7
- package/build/lib/dev-ticket.js +17 -2
- package/build/lib/fail-run.js +38 -0
- package/build/lib/heal-vendor-migrate-store.js +285 -0
- package/build/lib/hoist-workspace-pnpm-config.js +230 -5
- package/build/lib/strip-vendor-schema-augmentation.js +213 -0
- package/build/lib/vendor-claude-md.js +15 -0
- package/build/lib/vscode-settings.js +351 -0
- package/docs/LT-ECOSYSTEM-GUIDE.md +1 -1
- package/docs/VENDOR-MODE-WORKFLOW.md +35 -0
- package/docs/commands.md +140 -1
- package/package.json +36 -17
|
@@ -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,351 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EXCLUDED_FROM_PROFILE = exports.MEMORY_PROFILE = void 0;
|
|
4
|
+
exports.buildMergedValue = buildMergedValue;
|
|
5
|
+
exports.detectVariants = detectVariants;
|
|
6
|
+
exports.diffProfile = diffProfile;
|
|
7
|
+
exports.formatChange = formatChange;
|
|
8
|
+
exports.isEnablingFlagSet = isEnablingFlagSet;
|
|
9
|
+
exports.isPreventingFlagSet = isPreventingFlagSet;
|
|
10
|
+
exports.selectVariants = selectVariants;
|
|
11
|
+
exports.settingsPathFor = settingsPathFor;
|
|
12
|
+
exports.tuneSettingsFile = tuneSettingsFile;
|
|
13
|
+
const fs_1 = require("fs");
|
|
14
|
+
const os_1 = require("os");
|
|
15
|
+
const path_1 = require("path");
|
|
16
|
+
/**
|
|
17
|
+
* The tuning profile.
|
|
18
|
+
*
|
|
19
|
+
* Every value here was verified against the shipped extension manifests
|
|
20
|
+
* rather than copied from a blog post — three commonly recommended keys are
|
|
21
|
+
* deliberately ABSENT, see `EXCLUDED_FROM_PROFILE`.
|
|
22
|
+
*
|
|
23
|
+
* The exclude globs use `.nuxt*` / `.output*`, not `.nuxt` / `.output`. Both the
|
|
24
|
+
* framework and this CLI spawn SIBLING build directories — `.nuxt-check` for the
|
|
25
|
+
* check chain, `.nuxt-test` / `.output-test` for `lt dev test` — and a glob
|
|
26
|
+
* segment matches whole path segments, so `**\/.nuxt/**` does not cover
|
|
27
|
+
* `.nuxt-test`. A `.output-test` tree measures 37-294 MB; leaving it watched and
|
|
28
|
+
* indexed would undo the very saving this profile exists to make.
|
|
29
|
+
*/
|
|
30
|
+
exports.MEMORY_PROFILE = {
|
|
31
|
+
'files.watcherExclude': {
|
|
32
|
+
reason: 'Stops the file watcher from following build output and dependencies (CPU + memory per window).',
|
|
33
|
+
value: {
|
|
34
|
+
'**/.git/objects/**': true,
|
|
35
|
+
'**/.git/subtree-cache/**': true,
|
|
36
|
+
'**/.nuxt*/**': true,
|
|
37
|
+
'**/.output*/**': true,
|
|
38
|
+
'**/dist/**': true,
|
|
39
|
+
'**/node_modules/**': true,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
'search.exclude': {
|
|
43
|
+
reason: 'Keeps full-text search from indexing generated trees.',
|
|
44
|
+
value: {
|
|
45
|
+
'**/.nuxt*/**': true,
|
|
46
|
+
'**/.output*/**': true,
|
|
47
|
+
'**/dist/**': true,
|
|
48
|
+
'**/node_modules/**': true,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
'typescript.disableAutomaticTypeAcquisition': {
|
|
52
|
+
reason: 'Skips the @types download/scan pass — pointless in projects that declare their own types.',
|
|
53
|
+
value: true,
|
|
54
|
+
},
|
|
55
|
+
'typescript.preferences.includePackageJsonAutoImports': {
|
|
56
|
+
reason: 'Auto-import no longer scans every package.json in the workspace — the single biggest monorepo win.',
|
|
57
|
+
value: 'off',
|
|
58
|
+
},
|
|
59
|
+
'typescript.tsserver.maxTsServerMemory': {
|
|
60
|
+
reason: 'Lowers the per-server heap ceiling from the 3072 MB default; multiplies across every open root.',
|
|
61
|
+
value: 2048,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Keys intentionally NOT in the profile, with the measurement or manifest
|
|
66
|
+
* check that ruled them out. Surfaced by `lt dev vscode --explain` so the
|
|
67
|
+
* reasoning survives beyond the session that produced it.
|
|
68
|
+
*/
|
|
69
|
+
exports.EXCLUDED_FROM_PROFILE = [
|
|
70
|
+
{
|
|
71
|
+
key: 'typescript.tsserver.useSyntaxServer: "never"',
|
|
72
|
+
why: 'Would drop the 2nd server per root, but those measured only ~1.7 GB across 16 roots while costing editor responsiveness.',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: 'vue.server.hybridMode',
|
|
76
|
+
why: 'Does not exist in Volar 3.x — hybrid mode is unconditional there. Setting it is a silent no-op.',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: 'typescript.tsserver.maxTsServerMemory: 4096+',
|
|
80
|
+
why: 'v8 pointer compression caps the heap near 4 GB, so values above it are ignored (microsoft/vscode#127105).',
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
/**
|
|
84
|
+
* Merge semantics per value type.
|
|
85
|
+
*
|
|
86
|
+
* A plain value is replaced. An object value (the exclude maps) is merged with
|
|
87
|
+
* the user's existing entries winning, so tuning never silently drops a
|
|
88
|
+
* project-specific exclusion someone added by hand.
|
|
89
|
+
*
|
|
90
|
+
* A non-object `before` under an object-valued key (an array, say) is NOT
|
|
91
|
+
* mergeable and is replaced — VS Code's exclude settings are objects, so such a
|
|
92
|
+
* value is already being ignored by VS Code itself. That is the one case where
|
|
93
|
+
* the "never drops" promise above does not hold, and `--revert` cannot restore
|
|
94
|
+
* it either; the `.bak` is the recovery path.
|
|
95
|
+
*/
|
|
96
|
+
function buildMergedValue(before, desired) {
|
|
97
|
+
if (!isPlainObject(desired)) {
|
|
98
|
+
return desired;
|
|
99
|
+
}
|
|
100
|
+
if (!isPlainObject(before)) {
|
|
101
|
+
return Object.assign({}, desired);
|
|
102
|
+
}
|
|
103
|
+
return Object.assign(Object.assign({}, desired), before);
|
|
104
|
+
}
|
|
105
|
+
/** Every known variant, flagged by whether its settings file exists. */
|
|
106
|
+
function detectVariants(platform = process.platform, home = (0, os_1.homedir)()) {
|
|
107
|
+
const known = [
|
|
108
|
+
{ dir: 'Code', id: 'code', label: 'VS Code' },
|
|
109
|
+
{ dir: 'Code - Insiders', id: 'insiders', label: 'VS Code Insiders' },
|
|
110
|
+
{ dir: 'Cursor', id: 'cursor', label: 'Cursor' },
|
|
111
|
+
{ dir: 'VSCodium', id: 'vscodium', label: 'VSCodium' },
|
|
112
|
+
];
|
|
113
|
+
return known.map((k) => {
|
|
114
|
+
const settingsPath = settingsPathFor(k.dir, platform, home);
|
|
115
|
+
return { id: k.id, installed: (0, fs_1.existsSync)(settingsPath), label: k.label, settingsPath };
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Compare the profile against a parsed settings object.
|
|
120
|
+
*
|
|
121
|
+
* Object-valued keys (the two exclude maps) are treated as SETS of entries in
|
|
122
|
+
* both directions: applying merges (the user's entries win), and reverting
|
|
123
|
+
* subtracts only the entries this profile contributes. Deleting the whole key on
|
|
124
|
+
* revert would take the user's hand-maintained exclusions with it — an undo that
|
|
125
|
+
* destroys data the tool never added is worse than no undo at all.
|
|
126
|
+
*
|
|
127
|
+
* Scalar keys are removed outright on revert, which restores VS Code's own
|
|
128
|
+
* default. An explicit pre-existing scalar (`maxTsServerMemory: 3072`, say) is
|
|
129
|
+
* not restored — recovering that is what the `.bak` is for.
|
|
130
|
+
*/
|
|
131
|
+
function diffProfile(current, remove = false) {
|
|
132
|
+
return Object.entries(exports.MEMORY_PROFILE).map(([key, entry]) => {
|
|
133
|
+
const before = current[key];
|
|
134
|
+
const after = remove ? buildRevertedValue(before, entry.value) : buildMergedValue(before, entry.value);
|
|
135
|
+
let action;
|
|
136
|
+
if (remove) {
|
|
137
|
+
action = before === undefined || sameJson(before, after) ? 'unchanged' : 'removed';
|
|
138
|
+
}
|
|
139
|
+
else if (before === undefined) {
|
|
140
|
+
action = 'added';
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
action = sameJson(before, after) ? 'unchanged' : 'changed';
|
|
144
|
+
}
|
|
145
|
+
return { action, after, before, key };
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* One `key: before → after` line, coloured by what happens to it.
|
|
150
|
+
*
|
|
151
|
+
* Lives here rather than in the command so it can be unit-tested; it takes the
|
|
152
|
+
* colour helpers as an argument and is otherwise pure.
|
|
153
|
+
*/
|
|
154
|
+
function formatChange(change, colors) {
|
|
155
|
+
const short = (v) => {
|
|
156
|
+
if (v === undefined) {
|
|
157
|
+
return '—';
|
|
158
|
+
}
|
|
159
|
+
const s = JSON.stringify(v);
|
|
160
|
+
return s.length > 52 ? `${s.slice(0, 49)}…` : s;
|
|
161
|
+
};
|
|
162
|
+
const tag = {
|
|
163
|
+
added: colors.green('+'),
|
|
164
|
+
changed: colors.yellow('~'),
|
|
165
|
+
removed: colors.red('-'),
|
|
166
|
+
unchanged: colors.dim('='),
|
|
167
|
+
}[change.action];
|
|
168
|
+
if (change.action === 'unchanged') {
|
|
169
|
+
return `${tag} ${colors.dim(change.key)} ${colors.dim(short(change.before))}`;
|
|
170
|
+
}
|
|
171
|
+
return `${tag} ${change.key}: ${colors.dim(short(change.before))} → ${short(change.after)}`;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Whether an ENABLING flag (`--revert`, `--explain`) is set.
|
|
175
|
+
*
|
|
176
|
+
* gluegun declares no booleans to yargs-parser, so `--revert=true` arrives as
|
|
177
|
+
* the STRING `'true'`. For a flag that turns something ON, a parse quirk that
|
|
178
|
+
* reads as "not set" fails CLOSED, which is the safe direction.
|
|
179
|
+
*/
|
|
180
|
+
function isEnablingFlagSet(value) {
|
|
181
|
+
return value === true || value === 'true';
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Whether a PREVENTING flag (`--dry-run`) is set.
|
|
185
|
+
*
|
|
186
|
+
* Deliberately NOT `=== true || === 'true'`. That idiom is exactly backwards for
|
|
187
|
+
* a flag whose job is to STOP a write: `--dry-run=1` parses to the number `1`
|
|
188
|
+
* and `--dry-run=yes` to a string, both of which would read as "not set" and let
|
|
189
|
+
* the write proceed — the failure mode this repo already paid for once with
|
|
190
|
+
* `--keep-db` (see CLAUDE.md). Presence is intent; only an explicit negation
|
|
191
|
+
* proceeds.
|
|
192
|
+
*/
|
|
193
|
+
function isPreventingFlagSet(options, ...names) {
|
|
194
|
+
return names.some((name) => {
|
|
195
|
+
if (!(name in options)) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
const v = options[name];
|
|
199
|
+
return v !== false && v !== 'false' && v !== 0 && v !== '0';
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Split the detected variants into what to act on, given an optional `--variant`.
|
|
204
|
+
*
|
|
205
|
+
* `unknownFilter` is reported separately from "nothing installed": a bare
|
|
206
|
+
* `--variant` parses to boolean `true` and matches no id, and conflating that
|
|
207
|
+
* with "no editor found" told users VS Code was missing while it was installed.
|
|
208
|
+
*/
|
|
209
|
+
function selectVariants(all, filter) {
|
|
210
|
+
const installed = all.filter((v) => v.installed);
|
|
211
|
+
if (filter === undefined || filter === null) {
|
|
212
|
+
return { targets: installed };
|
|
213
|
+
}
|
|
214
|
+
const wanted = String(filter);
|
|
215
|
+
if (!all.some((v) => v.id === wanted)) {
|
|
216
|
+
return { targets: [], unknownFilter: wanted };
|
|
217
|
+
}
|
|
218
|
+
return { targets: installed.filter((v) => v.id === wanted) };
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Absolute path to a variant's user-settings file, per platform.
|
|
222
|
+
*
|
|
223
|
+
* All VS Code forks reuse the upstream layout, only the application support
|
|
224
|
+
* directory name differs. The joiner is selected per platform rather than taken
|
|
225
|
+
* from the ambient `path`, so the Windows branch produces real Windows paths
|
|
226
|
+
* (and stays assertable) when called from a test on macOS or Linux.
|
|
227
|
+
*/
|
|
228
|
+
function settingsPathFor(dirName, platform = process.platform, home = (0, os_1.homedir)()) {
|
|
229
|
+
if (platform === 'win32') {
|
|
230
|
+
const appData = process.env.APPDATA || path_1.win32.join(home, 'AppData', 'Roaming');
|
|
231
|
+
return path_1.win32.join(appData, dirName, 'User', 'settings.json');
|
|
232
|
+
}
|
|
233
|
+
if (platform === 'darwin') {
|
|
234
|
+
return path_1.posix.join(home, 'Library', 'Application Support', dirName, 'User', 'settings.json');
|
|
235
|
+
}
|
|
236
|
+
return path_1.posix.join(home, '.config', dirName, 'User', 'settings.json');
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Apply (or revert) the profile on one settings file.
|
|
240
|
+
*
|
|
241
|
+
* JSONC-safe: the file is edited through `jsonc-parser`, which preserves
|
|
242
|
+
* comments, key order and the user's formatting. A naive
|
|
243
|
+
* `JSON.parse` → `JSON.stringify` round-trip would silently delete every
|
|
244
|
+
* comment in a file people hand-maintain.
|
|
245
|
+
*
|
|
246
|
+
* `jsonc-parser` is required lazily: gluegun eagerly loads every command module
|
|
247
|
+
* on every `lt` invocation, and this is its only consumer, so a top-level import
|
|
248
|
+
* would put the load cost on `lt --version` too. Matches how the repo already
|
|
249
|
+
* treats `open`, `js-yaml`, `playwright-core` and `ts-morph`.
|
|
250
|
+
*
|
|
251
|
+
* Never writes when nothing would change, so re-running is a true no-op. Every
|
|
252
|
+
* failure is returned as `error` rather than thrown — the caller renders it per
|
|
253
|
+
* installation and carries on with the others.
|
|
254
|
+
*/
|
|
255
|
+
function tuneSettingsFile(settingsPath, options = {}) {
|
|
256
|
+
var _a;
|
|
257
|
+
const { dryRun = false, remove = false } = options;
|
|
258
|
+
if (!(0, fs_1.existsSync)(settingsPath)) {
|
|
259
|
+
return { changes: [], error: `settings file not found: ${settingsPath}`, settingsPath, written: false };
|
|
260
|
+
}
|
|
261
|
+
// Never write THROUGH a symlink — the target may be any file the user's
|
|
262
|
+
// account can reach, and we were asked to tune settings, not to overwrite it.
|
|
263
|
+
if (isSymbolicLink(settingsPath)) {
|
|
264
|
+
return { changes: [], error: `refusing to write through a symlink: ${settingsPath}`, settingsPath, written: false };
|
|
265
|
+
}
|
|
266
|
+
const { applyEdits, modify, parse, printParseErrorCode } = require('jsonc-parser');
|
|
267
|
+
let raw;
|
|
268
|
+
try {
|
|
269
|
+
raw = (0, fs_1.readFileSync)(settingsPath, 'utf8');
|
|
270
|
+
}
|
|
271
|
+
catch (e) {
|
|
272
|
+
return { changes: [], error: `cannot read ${settingsPath}: ${errText(e)}`, settingsPath, written: false };
|
|
273
|
+
}
|
|
274
|
+
const errors = [];
|
|
275
|
+
const current = ((_a = parse(raw, errors, { allowTrailingComma: true })) !== null && _a !== void 0 ? _a : {});
|
|
276
|
+
// Refuse to touch a file we cannot read reliably — writing into a
|
|
277
|
+
// malformed settings.json risks destroying the user's configuration.
|
|
278
|
+
if (errors.length > 0) {
|
|
279
|
+
const first = errors[0];
|
|
280
|
+
return {
|
|
281
|
+
changes: [],
|
|
282
|
+
error: `cannot parse ${settingsPath}: ${printParseErrorCode(first.error)} at offset ${first.offset}`,
|
|
283
|
+
settingsPath,
|
|
284
|
+
written: false,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const changes = diffProfile(current, remove);
|
|
288
|
+
const effective = changes.filter((c) => c.action !== 'unchanged');
|
|
289
|
+
if (effective.length === 0 || dryRun) {
|
|
290
|
+
return { changes, settingsPath, written: false };
|
|
291
|
+
}
|
|
292
|
+
let content = raw;
|
|
293
|
+
for (const change of effective) {
|
|
294
|
+
// `after === undefined` deletes the key; an exclude map emptied by the
|
|
295
|
+
// revert subtraction lands here too.
|
|
296
|
+
const edits = modify(content, [change.key], change.after, {
|
|
297
|
+
formattingOptions: { insertSpaces: true, tabSize: 4 },
|
|
298
|
+
});
|
|
299
|
+
content = applyEdits(content, edits);
|
|
300
|
+
}
|
|
301
|
+
// Keep the FIRST backup. It is the only record of what the file looked like
|
|
302
|
+
// before this tool ever touched it — overwriting it on a later run (notably on
|
|
303
|
+
// `--revert`, which would then back up the *tuned* file) destroys exactly the
|
|
304
|
+
// state a user reaching for the backup wants to get back to.
|
|
305
|
+
const backupPath = `${settingsPath}.bak`;
|
|
306
|
+
try {
|
|
307
|
+
if (!(0, fs_1.existsSync)(backupPath)) {
|
|
308
|
+
(0, fs_1.copyFileSync)(settingsPath, backupPath);
|
|
309
|
+
}
|
|
310
|
+
(0, fs_1.writeFileSync)(settingsPath, content, 'utf8');
|
|
311
|
+
}
|
|
312
|
+
catch (e) {
|
|
313
|
+
return { changes, error: `cannot write ${settingsPath}: ${errText(e)}`, settingsPath, written: false };
|
|
314
|
+
}
|
|
315
|
+
return { backupPath, changes, settingsPath, written: true };
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The value a key should hold after `--revert`.
|
|
319
|
+
*
|
|
320
|
+
* For an object-valued profile entry: the user's map minus our own entries, or
|
|
321
|
+
* `undefined` when nothing of theirs remains. For anything else: `undefined`
|
|
322
|
+
* (delete the key).
|
|
323
|
+
*/
|
|
324
|
+
function buildRevertedValue(before, desired) {
|
|
325
|
+
if (!isPlainObject(desired) || !isPlainObject(before)) {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
const kept = Object.fromEntries(Object.entries(before).filter(([k]) => !(k in desired)));
|
|
329
|
+
return Object.keys(kept).length > 0 ? kept : undefined;
|
|
330
|
+
}
|
|
331
|
+
/** Message text of an unknown thrown value. */
|
|
332
|
+
function errText(e) {
|
|
333
|
+
return e instanceof Error ? e.message : String(e);
|
|
334
|
+
}
|
|
335
|
+
/** Narrow to a non-null, non-array object. */
|
|
336
|
+
function isPlainObject(value) {
|
|
337
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
338
|
+
}
|
|
339
|
+
/** True when `path` is a symlink (never follows it). */
|
|
340
|
+
function isSymbolicLink(path) {
|
|
341
|
+
try {
|
|
342
|
+
return (0, fs_1.lstatSync)(path).isSymbolicLink();
|
|
343
|
+
}
|
|
344
|
+
catch (_a) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/** Structural equality via JSON, sufficient for the plain values in the profile. */
|
|
349
|
+
function sameJson(a, b) {
|
|
350
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
351
|
+
}
|
|
@@ -153,7 +153,7 @@ All code generators are **mode-aware**: In vendor mode they use relative paths t
|
|
|
153
153
|
| Command | Purpose |
|
|
154
154
|
|---------|---------|
|
|
155
155
|
| `lt fullstack init` | Create fullstack monorepo (see above) |
|
|
156
|
-
| `lt fullstack update` | Show mode-specific update instructions
|
|
156
|
+
| `lt fullstack update` | Show mode-specific update instructions — **and self-heal generated project scaffolding** (see `docs/commands.md`) |
|
|
157
157
|
| **`lt fullstack convert-mode`** | **Convert backend AND frontend between npm and vendor mode in a single command** |
|
|
158
158
|
|
|
159
159
|
**Fullstack mode conversion in one command:**
|
|
@@ -443,6 +443,41 @@ git commit -m "chore: revert fullstack to npm mode
|
|
|
443
443
|
|
|
444
444
|
## Troubleshooting
|
|
445
445
|
|
|
446
|
+
### Problem: `config.public.*` is `unknown` in a project vendored before 1.43.0
|
|
447
|
+
|
|
448
|
+
**Symptom.** `nuxt typecheck` fails with `Argument of type 'unknown' is not
|
|
449
|
+
assignable to parameter of type 'string'` on code that is correct, at any
|
|
450
|
+
`useRuntimeConfig().public.x` read.
|
|
451
|
+
|
|
452
|
+
**Check whether you are affected** — version-independent:
|
|
453
|
+
|
|
454
|
+
```bash
|
|
455
|
+
grep -rn "declare module '@nuxt/schema'" projects/app/app/core/
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**Cause.** The vendored nuxt-extensions core augments `PublicRuntimeConfig` under
|
|
459
|
+
both `nuxt/schema` and `@nuxt/schema`. The former re-exports the latter, so that
|
|
460
|
+
is one interface decorated twice; as project source it closes a cycle with Nuxt's
|
|
461
|
+
generated runtime-config types. TypeScript reports
|
|
462
|
+
`TS2310: Type 'PublicRuntimeConfig' recursively references itself as a base type`,
|
|
463
|
+
which Nuxt's `skipLibCheck: true` suppresses — so only the confusing symptom is
|
|
464
|
+
visible. See it with:
|
|
465
|
+
|
|
466
|
+
```bash
|
|
467
|
+
cd projects/app && npx vue-tsc --noEmit -p .nuxt-check/tsconfig.json --skipLibCheck false | grep TS2310
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
**Repair.** Delete the two `declare module '…/schema'` blocks from
|
|
471
|
+
`projects/app/app/core/runtime/types/module.ts`. Nothing is lost — `ltExtensions`
|
|
472
|
+
reaches the consumer through the module's runtime-config defaults, which Nuxt
|
|
473
|
+
writes into the generated types either way.
|
|
474
|
+
|
|
475
|
+
New conversions strip the blocks automatically (`stripVendorSchemaAugmentation`).
|
|
476
|
+
**Re-check after every core update:** the updater copies upstream files verbatim
|
|
477
|
+
and would bring them back. The template's own `CLAUDE.md` carries the same repair
|
|
478
|
+
note for the project side.
|
|
479
|
+
|
|
480
|
+
|
|
446
481
|
### Problem: `tsc` fails with `new Error('msg', { cause })` error
|
|
447
482
|
|
|
448
483
|
**Cause:** TypeScript target is too old (ES2020 or lower).
|