@lenne.tech/cli 1.41.3 → 1.42.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/update.js +17 -0
- package/build/lib/dev-test-session.js +119 -7
- package/build/lib/dev-ticket.js +17 -2
- package/build/lib/heal-vendor-migrate-store.js +285 -0
- package/build/lib/hoist-workspace-pnpm-config.js +59 -2
- package/build/lib/vscode-settings.js +351 -0
- package/docs/LT-ECOSYSTEM-GUIDE.md +1 -1
- package/docs/commands.md +131 -1
- package/package.json +31 -16
|
@@ -268,7 +268,24 @@ const NewCommand = {
|
|
|
268
268
|
info(' (an explicit node blocks the parent wildcard for names below it)');
|
|
269
269
|
info(' 4. Set the stage env vars in TurboOps (per stage), e.g. for production:');
|
|
270
270
|
info(` NODE_ENV=production, NSC__BASE_URL=https://api.${domain},`);
|
|
271
|
-
|
|
271
|
+
// The DB host is spelled out per stage on purpose. Every other variable in
|
|
272
|
+
// this checklist carries a concrete value; leaving this one as a bare name
|
|
273
|
+
// forces the reader to invent it, and the only reference in sight is the
|
|
274
|
+
// project's own docker-compose.yml, where the service is called `mongo`.
|
|
275
|
+
// `mongodb://mongo:27017/...` is the natural guess — and the wrong one: the
|
|
276
|
+
// short name is a Swarm alias on a network shared by every stack, so it
|
|
277
|
+
// resolves to a FOREIGN project's database (and to a different one on each
|
|
278
|
+
// connection). Symptoms are split-brain writes, sessions that vanish, and
|
|
279
|
+
// data quietly landing in someone else's MongoDB. See DEV-2140.
|
|
280
|
+
info(` NSC__MONGOOSE__URI=mongodb://<user>:<pass>@${project}-production_mongo:27017/${project}?authSource=admin,`);
|
|
281
|
+
info(` (dev stage: mongodb://<user>:<pass>@${project}-dev_mongo:27017/${project}?authSource=admin)`);
|
|
282
|
+
info(' NOTE: always the stack-prefixed host `<project>-<stage>_mongo`, never a bare');
|
|
283
|
+
info(' `mongo` — the short name is shared across stacks and resolves to a FOREIGN');
|
|
284
|
+
info(' database, non-deterministically per connection.');
|
|
285
|
+
info(' The stack-prefix fixes WHICH database you reach, not WHO may reach it:');
|
|
286
|
+
info(' the overlay network is shared, so the DB credentials are the actual');
|
|
287
|
+
info(' boundary. Set them in the mongo service and never deploy it open.');
|
|
288
|
+
info(' NSC__BETTER_AUTH__SECRET, NSC__AI__ENCRYPTION_SECRET,');
|
|
272
289
|
if (isAngular) {
|
|
273
290
|
info(' NSC__EMAIL__SMTP__*, NSC__EMAIL__DEFAULT_SENDER__EMAIL');
|
|
274
291
|
info(' The Angular app needs no URL env vars — they are baked into');
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.help = void 0;
|
|
13
|
+
const vscode_settings_1 = require("../../lib/vscode-settings");
|
|
14
|
+
/**
|
|
15
|
+
* Tune VS Code's USER settings for machines that keep many lt monorepos open.
|
|
16
|
+
*
|
|
17
|
+
* Each open workspace root spawns its own pair of TypeScript servers, and the
|
|
18
|
+
* "semantic" one of each pair is what actually holds the memory. Eight open
|
|
19
|
+
* monorepos (api + app root each) therefore means 16 semantic servers — enough
|
|
20
|
+
* to push a 32 GB machine deep into swap. This command applies the verified
|
|
21
|
+
* profile in `lib/vscode-settings.ts` to every detected installation.
|
|
22
|
+
*
|
|
23
|
+
* Safety properties, all load-bearing:
|
|
24
|
+
* - JSONC-aware, so comments and formatting in a hand-maintained
|
|
25
|
+
* settings.json survive (a JSON.parse round-trip would delete them).
|
|
26
|
+
* - Refuses to write into a settings.json it cannot parse.
|
|
27
|
+
* - Backs up to `settings.json.bak` before the first write.
|
|
28
|
+
* - Merges the object-valued exclude maps, so hand-added entries are kept —
|
|
29
|
+
* and `--revert` SUBTRACTS only those same entries again, so an undo never
|
|
30
|
+
* takes a hand-maintained exclusion with it.
|
|
31
|
+
* - Keeps the FIRST `.bak`, so a later run (including the revert) cannot
|
|
32
|
+
* overwrite the record of the pre-tuning state.
|
|
33
|
+
* - No-op on re-run.
|
|
34
|
+
*
|
|
35
|
+
* `--revert` restores VS Code's default for the scalar keys rather than any
|
|
36
|
+
* explicit value that preceded them; the `.bak` is the recovery path for those.
|
|
37
|
+
*/
|
|
38
|
+
const VsCodeCommand = {
|
|
39
|
+
alias: ['vsc'],
|
|
40
|
+
description: 'Tune VS Code memory settings',
|
|
41
|
+
hidden: false,
|
|
42
|
+
name: 'vscode',
|
|
43
|
+
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
44
|
+
const { parameters, print: { colors, info }, prompt: { confirm }, } = toolbox;
|
|
45
|
+
// `--dry-run` PREVENTS a write, so it reads presence-as-intent: `--dry-run=1`
|
|
46
|
+
// must not fall through and write. `--revert` / `--explain` merely enable
|
|
47
|
+
// something, so the usual `=== true || === 'true'` is safe there.
|
|
48
|
+
const dryRun = (0, vscode_settings_1.isPreventingFlagSet)(parameters.options, 'dry-run', 'dryRun');
|
|
49
|
+
const revert = (0, vscode_settings_1.isEnablingFlagSet)(parameters.options.revert);
|
|
50
|
+
const explain = (0, vscode_settings_1.isEnablingFlagSet)(parameters.options.explain);
|
|
51
|
+
// This command writes OUTSIDE the project, into the user's global editor
|
|
52
|
+
// settings. A `defaults.noConfirm` in a repo-local `lt.config.json` — which
|
|
53
|
+
// is discovered by walking up from cwd, i.e. can come from a cloned repo —
|
|
54
|
+
// must not be able to silence that prompt. Only an explicit CLI flag does.
|
|
55
|
+
const noConfirm = (0, vscode_settings_1.isEnablingFlagSet)(parameters.options.noConfirm);
|
|
56
|
+
info('');
|
|
57
|
+
info(colors.bold(`lt dev vscode${revert ? ' --revert' : ''}${dryRun ? ' (dry run)' : ''}`));
|
|
58
|
+
info(colors.dim('─'.repeat(64)));
|
|
59
|
+
if (explain) {
|
|
60
|
+
info(colors.bold('\nProfile:'));
|
|
61
|
+
for (const [key, entry] of Object.entries(vscode_settings_1.MEMORY_PROFILE)) {
|
|
62
|
+
info(` ${colors.cyan(key)}`);
|
|
63
|
+
info(` ${colors.dim(entry.reason)}`);
|
|
64
|
+
}
|
|
65
|
+
info(colors.bold('\nDeliberately NOT set:'));
|
|
66
|
+
for (const item of vscode_settings_1.EXCLUDED_FROM_PROFILE) {
|
|
67
|
+
info(` ${colors.yellow(item.key)}`);
|
|
68
|
+
info(` ${colors.dim(item.why)}`);
|
|
69
|
+
}
|
|
70
|
+
info('');
|
|
71
|
+
if (!parameters.options.fromGluegunMenu)
|
|
72
|
+
process.exit();
|
|
73
|
+
return 'dev vscode: explained';
|
|
74
|
+
}
|
|
75
|
+
const all = (0, vscode_settings_1.detectVariants)();
|
|
76
|
+
const { targets, unknownFilter } = (0, vscode_settings_1.selectVariants)(all, parameters.options.variant);
|
|
77
|
+
// A bare `--variant` parses to boolean `true` and matches no id. Reporting
|
|
78
|
+
// that as "no installation found" told users their editor was missing while
|
|
79
|
+
// it was installed — two different problems deserve two different messages.
|
|
80
|
+
if (unknownFilter) {
|
|
81
|
+
info(colors.yellow(` Unknown --variant "${unknownFilter}".`));
|
|
82
|
+
info(colors.dim(` Valid values: ${all.map((v) => v.id).join(' | ')}`));
|
|
83
|
+
if (!parameters.options.fromGluegunMenu)
|
|
84
|
+
process.exit(1);
|
|
85
|
+
return 'dev vscode: unknown variant';
|
|
86
|
+
}
|
|
87
|
+
if (targets.length === 0) {
|
|
88
|
+
info(colors.yellow(' No VS Code installation with a user settings.json found.'));
|
|
89
|
+
info(colors.dim(` Looked for: ${all.map((v) => v.label).join(', ')}`));
|
|
90
|
+
if (!parameters.options.fromGluegunMenu)
|
|
91
|
+
process.exit(1);
|
|
92
|
+
return 'dev vscode: no installation found';
|
|
93
|
+
}
|
|
94
|
+
// Preview first — the user sees the exact before/after per key before
|
|
95
|
+
// anything is written.
|
|
96
|
+
let pending = 0;
|
|
97
|
+
for (const target of targets) {
|
|
98
|
+
const preview = (0, vscode_settings_1.tuneSettingsFile)(target.settingsPath, { dryRun: true, remove: revert });
|
|
99
|
+
info(`\n ${colors.bold(target.label)} ${colors.dim(target.settingsPath)}`);
|
|
100
|
+
if (preview.error) {
|
|
101
|
+
info(` ${colors.red('skipped')} — ${preview.error}`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
for (const change of preview.changes) {
|
|
105
|
+
info(` ${(0, vscode_settings_1.formatChange)(change, colors)}`);
|
|
106
|
+
if (change.action !== 'unchanged')
|
|
107
|
+
pending++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (pending === 0) {
|
|
111
|
+
info(colors.green('\n✓ already up to date — nothing to do\n'));
|
|
112
|
+
if (!parameters.options.fromGluegunMenu)
|
|
113
|
+
process.exit();
|
|
114
|
+
return 'dev vscode: no changes needed';
|
|
115
|
+
}
|
|
116
|
+
if (dryRun) {
|
|
117
|
+
info(colors.dim(`\n${pending} change(s) would be applied. Re-run without --dry-run to apply.\n`));
|
|
118
|
+
if (!parameters.options.fromGluegunMenu)
|
|
119
|
+
process.exit();
|
|
120
|
+
return `dev vscode: dry run, ${pending} pending change(s)`;
|
|
121
|
+
}
|
|
122
|
+
if (!noConfirm && !(yield confirm(`Apply ${pending} change(s)?`, true))) {
|
|
123
|
+
info(colors.dim('\nAborted — nothing written.\n'));
|
|
124
|
+
if (!parameters.options.fromGluegunMenu)
|
|
125
|
+
process.exit();
|
|
126
|
+
return 'dev vscode: aborted';
|
|
127
|
+
}
|
|
128
|
+
let applied = 0;
|
|
129
|
+
for (const target of targets) {
|
|
130
|
+
const result = (0, vscode_settings_1.tuneSettingsFile)(target.settingsPath, { remove: revert });
|
|
131
|
+
if (result.error) {
|
|
132
|
+
info(` ${colors.red('✗')} ${target.label}: ${result.error}`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!result.written)
|
|
136
|
+
continue;
|
|
137
|
+
applied += result.changes.filter((c) => c.action !== 'unchanged').length;
|
|
138
|
+
info(` ${colors.green('✓')} ${target.label} updated ${colors.dim(`(backup: ${result.backupPath})`)}`);
|
|
139
|
+
}
|
|
140
|
+
info(colors.dim('\n Restart VS Code (or run "Developer: Reload Window") for the TS servers to pick this up.\n'));
|
|
141
|
+
if (!parameters.options.fromGluegunMenu)
|
|
142
|
+
process.exit();
|
|
143
|
+
return `dev vscode: applied ${applied} change(s)`;
|
|
144
|
+
}),
|
|
145
|
+
};
|
|
146
|
+
exports.help = {
|
|
147
|
+
aliases: ['vsc'],
|
|
148
|
+
configuration: 'none (writes global editor settings — --noConfirm must be passed explicitly)',
|
|
149
|
+
description: "Apply a verified low-memory profile to VS Code's user settings. Targets the per-workspace TypeScript servers, which dominate memory when many monorepos are open at once.",
|
|
150
|
+
examples: ['dev vscode', 'dev vscode --dry-run', 'dev vscode --explain', 'dev vscode --revert'],
|
|
151
|
+
features: [
|
|
152
|
+
'JSONC-aware — preserves comments and formatting; refuses to write an unparseable file or a symlink.',
|
|
153
|
+
'Backs up to settings.json.bak (the first one is kept) and merges object-valued keys, keeping hand-added entries.',
|
|
154
|
+
'Detects VS Code, Insiders, Cursor and VSCodium; idempotent, with --revert subtracting only its own entries.',
|
|
155
|
+
],
|
|
156
|
+
name: 'vscode',
|
|
157
|
+
options: [
|
|
158
|
+
{ description: 'Show what would change without writing', flag: '--dry-run', type: 'boolean' },
|
|
159
|
+
{ description: 'Remove the profile keys again', flag: '--revert', type: 'boolean' },
|
|
160
|
+
{
|
|
161
|
+
description: 'Print the profile with reasons, plus the keys deliberately left out',
|
|
162
|
+
flag: '--explain',
|
|
163
|
+
type: 'boolean',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
description: 'Limit to one installation: code | insiders | cursor | vscodium',
|
|
167
|
+
flag: '--variant',
|
|
168
|
+
type: 'string',
|
|
169
|
+
},
|
|
170
|
+
{ description: 'Skip the confirmation prompt', flag: '--noConfirm', type: 'boolean' },
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
module.exports = Object.assign(VsCodeCommand, { help: exports.help });
|
|
@@ -14,6 +14,7 @@ const dev_patches_1 = require("../../lib/dev-patches");
|
|
|
14
14
|
const framework_detection_1 = require("../../lib/framework-detection");
|
|
15
15
|
const frontend_framework_detection_1 = require("../../lib/frontend-framework-detection");
|
|
16
16
|
const heal_check_wrapper_1 = require("../../lib/heal-check-wrapper");
|
|
17
|
+
const heal_vendor_migrate_store_1 = require("../../lib/heal-vendor-migrate-store");
|
|
17
18
|
const vendor_claude_md_1 = require("../../lib/vendor-claude-md");
|
|
18
19
|
/**
|
|
19
20
|
* Update a fullstack workspace — mode-aware.
|
|
@@ -208,6 +209,22 @@ const NewCommand = {
|
|
|
208
209
|
info('');
|
|
209
210
|
success(' Added `.lt-dev/` to .gitignore');
|
|
210
211
|
}
|
|
212
|
+
// ── Self-heal: repair the vendor-mode migration store ──────────────────
|
|
213
|
+
//
|
|
214
|
+
// `migrations-utils/migrate.js` is written ONCE, at conversion time. Projects
|
|
215
|
+
// converted before the template stopped requiring ts-node unconditionally keep
|
|
216
|
+
// the broken file forever — it is project scaffolding, not `src/core/`, so no
|
|
217
|
+
// update path ever revisits it. Those containers die with
|
|
218
|
+
// `Cannot find module 'ts-node'` before applying a single migration, and stay
|
|
219
|
+
// healthy while doing so, because the entrypoint degrades the failure to a
|
|
220
|
+
// warning on purpose. Idempotent, and deliberately blind to stores that guard
|
|
221
|
+
// the require their own way.
|
|
222
|
+
const migrateStoreAsset = (0, path_1.join)(__dirname, '..', '..', 'templates', 'vendor-scripts', 'migrate-store.js');
|
|
223
|
+
const changedStore = (0, heal_vendor_migrate_store_1.healVendorMigrateStore)(apiDir, migrateStoreAsset);
|
|
224
|
+
if (changedStore.length > 0) {
|
|
225
|
+
info('');
|
|
226
|
+
success(` Repaired the vendor migration store: ${changedStore.join(', ')}`);
|
|
227
|
+
}
|
|
211
228
|
info('');
|
|
212
229
|
info(colors.bold('For a comprehensive update of everything, use:'));
|
|
213
230
|
info('');
|
|
@@ -9,17 +9,19 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.TEST_INITIAL_ADMIN_ENV = void 0;
|
|
12
|
+
exports.TEST_NITRO_OUTPUT_DIR = exports.TEST_NUXT_BUILD_DIR = exports.TEST_INITIAL_ADMIN_ENV = void 0;
|
|
13
13
|
exports.ensurePlaywrightBrowsers = ensurePlaywrightBrowsers;
|
|
14
14
|
exports.autoShardCount = autoShardCount;
|
|
15
15
|
exports.bringUpTestSession = bringUpTestSession;
|
|
16
16
|
exports.buildShardPlaywrightInvocation = buildShardPlaywrightInvocation;
|
|
17
|
+
exports.buildTestAppEnv = buildTestAppEnv;
|
|
17
18
|
exports.hasTestSession = hasTestSession;
|
|
18
19
|
exports.resolveTestSession = resolveTestSession;
|
|
19
20
|
exports.runShardedTestSession = runShardedTestSession;
|
|
20
21
|
exports.shardReportDir = shardReportDir;
|
|
21
22
|
exports.tearDownAllTestSessions = tearDownAllTestSessions;
|
|
22
23
|
exports.tearDownTestSession = tearDownTestSession;
|
|
24
|
+
exports.testAppEntryCandidates = testAppEntryCandidates;
|
|
23
25
|
/**
|
|
24
26
|
* Ephemeral, isolated test session for `lt dev test`.
|
|
25
27
|
*
|
|
@@ -103,6 +105,47 @@ exports.TEST_INITIAL_ADMIN_ENV = {
|
|
|
103
105
|
NSC__SYSTEM_SETUP__INITIAL_ADMIN__NAME: 'CI Admin',
|
|
104
106
|
NSC__SYSTEM_SETUP__INITIAL_ADMIN__PASSWORD: 'CiThrowawayAdmin123!',
|
|
105
107
|
};
|
|
108
|
+
/**
|
|
109
|
+
* The Nuxt build directory the test stack's app process uses — never the one
|
|
110
|
+
* `nuxt dev` / the IDE write (`.nuxt`), never the check chain's (`.nuxt-check`).
|
|
111
|
+
*
|
|
112
|
+
* `@nuxt/cli` takes its lock ON the build directory
|
|
113
|
+
* (`acquireLock(nuxt.options.buildDir)`), so sharing it does not merely
|
|
114
|
+
* interleave writes — it makes the second command ABORT: a `lt dev test` next to
|
|
115
|
+
* a parked `lt dev up` died with "Another Nuxt dev is already running", the test
|
|
116
|
+
* app never started, and every spec then failed on a missing selector. That
|
|
117
|
+
* reads like broken specs while being pure infrastructure, which is what made it
|
|
118
|
+
* expensive to diagnose. A directory of its own frees the lock and the writes in
|
|
119
|
+
* one move.
|
|
120
|
+
*
|
|
121
|
+
* Projects whose `nuxt.config.ts` does not (yet) read `NUXT_BUILD_DIR` simply
|
|
122
|
+
* ignore it, so this needs no per-project case distinction.
|
|
123
|
+
*/
|
|
124
|
+
exports.TEST_NUXT_BUILD_DIR = '.nuxt-test';
|
|
125
|
+
/**
|
|
126
|
+
* The Nitro OUTPUT directory the test stack builds into (DEV-2724).
|
|
127
|
+
*
|
|
128
|
+
* A second axis from `TEST_NUXT_BUILD_DIR`, not a duplicate of it: `buildDir`
|
|
129
|
+
* and Nitro's `output.dir` are unrelated knobs, so isolating the former left
|
|
130
|
+
* `.output/` shared. That matters here more than anywhere else, because this
|
|
131
|
+
* stack does not run `nuxt dev` — it serves the production bundle, rebuilding on
|
|
132
|
+
* every run, and therefore overwrites the tree a local `pnpm run build` (or a
|
|
133
|
+
* server started from it) is using.
|
|
134
|
+
*
|
|
135
|
+
* NEITHER variable is framework-native — verified against `@nuxt/schema` 4.4.8,
|
|
136
|
+
* `nitropack` 2.13.4 and `c12`: none of them reads `NUXT_BUILD_DIR` or
|
|
137
|
+
* `NITRO_OUTPUT_DIR`. Both levers are opened by the project's own
|
|
138
|
+
* `nuxt.config.ts` (`buildDir: process.env.NUXT_BUILD_DIR || '.nuxt'`,
|
|
139
|
+
* `nitro.output.dir: process.env.NITRO_OUTPUT_DIR || '.output'`); nuxt-base-starter
|
|
140
|
+
* ≥ 2.16.0 ships both. Do NOT write "unlike NUXT_BUILD_DIR, …" here: that
|
|
141
|
+
* asymmetric contrast silently promotes one of them to a framework feature and
|
|
142
|
+
* makes readers forward only the other, which reintroduces the collision this
|
|
143
|
+
* whole mechanism exists to prevent.
|
|
144
|
+
*
|
|
145
|
+
* Projects that forward neither simply keep building into `.nuxt` / `.output`,
|
|
146
|
+
* which is why `testAppEntryCandidates()` still looks there.
|
|
147
|
+
*/
|
|
148
|
+
exports.TEST_NITRO_OUTPUT_DIR = '.output-test';
|
|
106
149
|
/**
|
|
107
150
|
* Heuristic for the default local shard count (`--shard auto` / bare `--shard`).
|
|
108
151
|
*
|
|
@@ -236,6 +279,11 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
236
279
|
dbName,
|
|
237
280
|
identity: testIdentity,
|
|
238
281
|
});
|
|
282
|
+
// Every app process below — the build, the built server, and the dev-server
|
|
283
|
+
// fallback — runs with THIS env, so the test stack never touches the build dir
|
|
284
|
+
// `nuxt dev` and the IDE use. Declared out here because the session context
|
|
285
|
+
// hands it to the Playwright child as well.
|
|
286
|
+
const appEnv = buildTestAppEnv(devEnv.app.env);
|
|
239
287
|
const pids = {};
|
|
240
288
|
// --- API: compiled (`node dist`) for stability; fall back to the project's
|
|
241
289
|
// own dev start script. `skipBuild` (sibling shards) reuses the dist the
|
|
@@ -281,25 +329,41 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
281
329
|
// buildDevEnv sets NUXT_PUBLIC_API_PROXY=false, so the built app talks
|
|
282
330
|
// cross-origin to the test API exactly like prod (the injected session cookie
|
|
283
331
|
// must be a cross-subdomain DOMAIN cookie — see the project's parseCookieHeader).
|
|
284
|
-
// Rebuilt every run so the suite never hits stale code (no build-skip / reuse).
|
|
332
|
+
// Rebuilt every run so the suite never hits stale code (no build-skip / reuse).
|
|
333
|
+
// `appEnv` pins NUXT_BUILD_DIR to its own dir, which is what lets this run next
|
|
334
|
+
// to a parked `lt dev up` at all: the Nuxt lock sits on the build dir, so the
|
|
335
|
+
// build below used to abort outright against a running dev server (DEV-2715).
|
|
336
|
+
// The fallback needs it just as much — that path IS a second `nuxt dev`.
|
|
337
|
+
// It also pins NITRO_OUTPUT_DIR (DEV-2724), so the rebuild below stops
|
|
338
|
+
// overwriting the `.output/` a local `pnpm run build` is serving — and the
|
|
339
|
+
// entry lookup moves with it, or the spawn would find nothing and drop to the
|
|
340
|
+
// slow `pnpm dev` fallback. ---
|
|
285
341
|
if (layout.appDir && appPort) {
|
|
342
|
+
// The isolated build trees are new names this CLI invented, so a project
|
|
343
|
+
// whose `.gitignore` predates the starter's `.nuxt-*` / `.output-*` globs
|
|
344
|
+
// leaves them UNTRACKED — a `git add -A` away from committing a Nitro
|
|
345
|
+
// bundle (which inlines runtimeConfig defaults), and enough to make
|
|
346
|
+
// `lt ticket stop` see uncommitted work. Idempotent, like every other
|
|
347
|
+
// `addToGitignore` call.
|
|
348
|
+
(0, dev_patches_1.addToGitignore)(layout.appDir, '.nuxt-*');
|
|
349
|
+
(0, dev_patches_1.addToGitignore)(layout.appDir, '.output-*');
|
|
286
350
|
const appPm = (0, dev_package_manager_1.pickPackageManager)(layout.appDir);
|
|
287
351
|
let appBuild = 0;
|
|
288
352
|
if (!skipBuild) {
|
|
289
353
|
log.info(log.dim('Building App (nuxt build, for speed + prod-fidelity) …'));
|
|
290
354
|
appBuild = yield (0, dev_process_1.runChildInherit)(appPm.bin, appPm.runScript('build'), {
|
|
291
355
|
cwd: layout.appDir,
|
|
292
|
-
env:
|
|
356
|
+
env: appEnv,
|
|
293
357
|
});
|
|
294
358
|
}
|
|
295
|
-
const appEntry =
|
|
359
|
+
const appEntry = testAppEntryCandidates()
|
|
296
360
|
.map((rel) => (0, path_1.join)(layout.appDir, rel))
|
|
297
361
|
.find((p) => (0, fs_1.existsSync)(p));
|
|
298
362
|
let appSpawn;
|
|
299
363
|
if (appBuild === 0 && appEntry) {
|
|
300
364
|
appSpawn = (0, dev_process_1.spawnDetached)('node', [appEntry], {
|
|
301
365
|
cwd: layout.appDir,
|
|
302
|
-
env:
|
|
366
|
+
env: appEnv,
|
|
303
367
|
logFile: (0, path_1.join)(layout.root, '.lt-dev', names.appLog),
|
|
304
368
|
});
|
|
305
369
|
}
|
|
@@ -307,7 +371,7 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
307
371
|
log.warn(`built app not available — falling back to \`${appPm.bin} dev\` (slower: cold-compiles routes).`);
|
|
308
372
|
appSpawn = (0, dev_process_1.spawnDetached)(appPm.bin, appPm.runScript('dev'), {
|
|
309
373
|
cwd: layout.appDir,
|
|
310
|
-
env:
|
|
374
|
+
env: appEnv,
|
|
311
375
|
logFile: (0, path_1.join)(layout.root, '.lt-dev', names.appLog),
|
|
312
376
|
});
|
|
313
377
|
}
|
|
@@ -340,7 +404,7 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
340
404
|
// Expose THIS stack's API log path so the caller can point the auth E2E specs
|
|
341
405
|
// (via NEST_SERVER_LOG) at the exact isolated log — correct per shard.
|
|
342
406
|
const apiLogPath = layout.apiDir ? (0, path_1.join)(layout.root, '.lt-dev', names.apiLog) : undefined;
|
|
343
|
-
return { apiLogPath, apiUrl, appEnv
|
|
407
|
+
return { apiLogPath, apiUrl, appEnv, appUrl, dbName, pids, testIdentity };
|
|
344
408
|
});
|
|
345
409
|
}
|
|
346
410
|
/**
|
|
@@ -382,6 +446,20 @@ function buildShardPlaywrightInvocation(pm, shardIndex, total, forwarded, htmlRe
|
|
|
382
446
|
},
|
|
383
447
|
};
|
|
384
448
|
}
|
|
449
|
+
/**
|
|
450
|
+
* The env the test stack's APP process runs with: the isolated dev env plus its
|
|
451
|
+
* own Nuxt build dir.
|
|
452
|
+
*
|
|
453
|
+
* The pin goes LAST on purpose — unlike `TEST_INITIAL_ADMIN_ENV`, which is
|
|
454
|
+
* spread first so a deliberately inherited credential still wins. Here the
|
|
455
|
+
* isolation IS the contract: `buildDevEnv` seeds the app env from `process.env`,
|
|
456
|
+
* so a shell that exports `NUXT_BUILD_DIR` (left over from debugging a check
|
|
457
|
+
* run, say) would otherwise hand the test stack the dev — or the gate — dir
|
|
458
|
+
* straight back, and the collision returns silently.
|
|
459
|
+
*/
|
|
460
|
+
function buildTestAppEnv(appEnv) {
|
|
461
|
+
return Object.assign(Object.assign({}, appEnv), { NITRO_OUTPUT_DIR: exports.TEST_NITRO_OUTPUT_DIR, NUXT_BUILD_DIR: exports.TEST_NUXT_BUILD_DIR });
|
|
462
|
+
}
|
|
385
463
|
/** True when a test session file exists (used by status/down). */
|
|
386
464
|
function hasTestSession(root) {
|
|
387
465
|
return (0, dev_state_1.loadSession)(root, dev_state_1.TEST_SESSION_FILE) !== null;
|
|
@@ -541,11 +619,45 @@ function tearDownTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
541
619
|
delete reg.projects[testIdentity.slug];
|
|
542
620
|
(0, dev_state_1.saveRegistry)(reg);
|
|
543
621
|
}
|
|
622
|
+
// Reclaim the isolated build trees. They exist only for this stack and are
|
|
623
|
+
// rebuilt from scratch on every run (there is no build-skip / reuse), so
|
|
624
|
+
// keeping them buys nothing and costs 37-294 MB per project — permanently,
|
|
625
|
+
// since nothing else ever removes them. Only the SUFFIXED names are touched:
|
|
626
|
+
// a project that does not forward the env vars builds into the shared
|
|
627
|
+
// `.nuxt` / `.output`, which belong to the developer's own `pnpm run build`.
|
|
628
|
+
if (layout.appDir) {
|
|
629
|
+
for (const dir of [exports.TEST_NUXT_BUILD_DIR, exports.TEST_NITRO_OUTPUT_DIR]) {
|
|
630
|
+
const path = (0, path_1.join)(layout.appDir, dir);
|
|
631
|
+
try {
|
|
632
|
+
if ((0, fs_1.existsSync)(path)) {
|
|
633
|
+
(0, fs_1.rmSync)(path, { force: true, recursive: true });
|
|
634
|
+
stopped.push(`${dir}/`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
catch (_a) {
|
|
638
|
+
// Best effort — a locked build tree must not fail the teardown.
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
544
642
|
if (!opts.silent && stopped.length > 0)
|
|
545
643
|
log.info(`Stopped test stack: ${stopped.join(', ')}`);
|
|
546
644
|
return { stopped };
|
|
547
645
|
});
|
|
548
646
|
}
|
|
647
|
+
/**
|
|
648
|
+
* Where to look for the built server entry, in priority order.
|
|
649
|
+
*
|
|
650
|
+
* The isolated dir comes FIRST and that ordering is load-bearing: `.find()` takes
|
|
651
|
+
* the first hit and a stale `.output/` from an earlier local build is the normal
|
|
652
|
+
* case, so the shared path first would serve that stale bundle while the fresh
|
|
653
|
+
* build sat unused. The shared path remains as a fallback for projects whose
|
|
654
|
+
* `nuxt.config.ts` does not forward `NITRO_OUTPUT_DIR` yet — without it they
|
|
655
|
+
* would find no entry at all and drop to the slow `pnpm dev` fallback, which is
|
|
656
|
+
* a second `nuxt dev` and re-takes the build-dir lock DEV-2715 just freed.
|
|
657
|
+
*/
|
|
658
|
+
function testAppEntryCandidates() {
|
|
659
|
+
return [`${exports.TEST_NITRO_OUTPUT_DIR}/server/index.mjs`, '.output/server/index.mjs'];
|
|
660
|
+
}
|
|
549
661
|
/**
|
|
550
662
|
* Resolve the per-stack file/identity names. For a sharded run (`shardIndex`
|
|
551
663
|
* given) everything gets a `.<i>` / `-<i>` suffix so N stacks coexist without
|
package/build/lib/dev-ticket.js
CHANGED
|
@@ -587,8 +587,23 @@ function runWithProjectDriver(driverPaths, action, env) {
|
|
|
587
587
|
return { outcome: 'failed', stdout: '' };
|
|
588
588
|
}
|
|
589
589
|
}
|
|
590
|
-
/**
|
|
591
|
-
|
|
590
|
+
/**
|
|
591
|
+
* Framework-generated / ephemeral paths a dev/build run dirties (never real work).
|
|
592
|
+
*
|
|
593
|
+
* `.nuxt` and `.output` carry an optional `-<suffix>`: the build directory is no
|
|
594
|
+
* longer a single well-known name. The check chain builds into `.nuxt-check` and
|
|
595
|
+
* `lt dev test` into `.nuxt-test` / `.output-test`, precisely so they do not
|
|
596
|
+
* collide with a parked `nuxt dev` (the Nuxt lock sits on the build dir).
|
|
597
|
+
*
|
|
598
|
+
* Without the suffix these read as REAL developer work — a glob segment matches
|
|
599
|
+
* whole segments, so `.nuxt` never covered `.nuxt-test` — and
|
|
600
|
+
* `worktreeSafetyReport` then classifies a 300 MB build tree as uncommitted
|
|
601
|
+
* work, making `lt ticket stop` REFUSE to remove the worktree over files the
|
|
602
|
+
* developer never wrote. Only projects whose app `.gitignore` predates the
|
|
603
|
+
* starter's `.nuxt-*` globs are affected, but that is exactly the population the
|
|
604
|
+
* rest of this CLI's heal machinery exists to serve.
|
|
605
|
+
*/
|
|
606
|
+
const GENERATED_PATHS = /(^|\/)(\.nuxtrc|\.nuxt(-[\w.]+)?|\.nitro|\.output(-[\w.]+)?|dist|\.turbo|\.cache|\.eslintcache)(\/|$)|\.tsbuildinfo$/;
|
|
592
607
|
/** The three git-tracked configs `lt dev up` self-heals to be env-aware. */
|
|
593
608
|
const LT_DEV_MANAGED_CONFIG = /(?:^|\/)(?:config\.env\.ts|nuxt\.config\.ts|playwright\.config\.ts)$/;
|
|
594
609
|
/**
|