@lenne.tech/cli 1.41.2 → 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 +177 -21
- 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,15 +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
|
+
exports.buildShardPlaywrightInvocation = buildShardPlaywrightInvocation;
|
|
17
|
+
exports.buildTestAppEnv = buildTestAppEnv;
|
|
16
18
|
exports.hasTestSession = hasTestSession;
|
|
17
19
|
exports.resolveTestSession = resolveTestSession;
|
|
18
20
|
exports.runShardedTestSession = runShardedTestSession;
|
|
21
|
+
exports.shardReportDir = shardReportDir;
|
|
19
22
|
exports.tearDownAllTestSessions = tearDownAllTestSessions;
|
|
20
23
|
exports.tearDownTestSession = tearDownTestSession;
|
|
24
|
+
exports.testAppEntryCandidates = testAppEntryCandidates;
|
|
21
25
|
/**
|
|
22
26
|
* Ephemeral, isolated test session for `lt dev test`.
|
|
23
27
|
*
|
|
@@ -101,6 +105,47 @@ exports.TEST_INITIAL_ADMIN_ENV = {
|
|
|
101
105
|
NSC__SYSTEM_SETUP__INITIAL_ADMIN__NAME: 'CI Admin',
|
|
102
106
|
NSC__SYSTEM_SETUP__INITIAL_ADMIN__PASSWORD: 'CiThrowawayAdmin123!',
|
|
103
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';
|
|
104
149
|
/**
|
|
105
150
|
* Heuristic for the default local shard count (`--shard auto` / bare `--shard`).
|
|
106
151
|
*
|
|
@@ -234,6 +279,11 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
234
279
|
dbName,
|
|
235
280
|
identity: testIdentity,
|
|
236
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);
|
|
237
287
|
const pids = {};
|
|
238
288
|
// --- API: compiled (`node dist`) for stability; fall back to the project's
|
|
239
289
|
// own dev start script. `skipBuild` (sibling shards) reuses the dist the
|
|
@@ -279,25 +329,41 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
279
329
|
// buildDevEnv sets NUXT_PUBLIC_API_PROXY=false, so the built app talks
|
|
280
330
|
// cross-origin to the test API exactly like prod (the injected session cookie
|
|
281
331
|
// must be a cross-subdomain DOMAIN cookie — see the project's parseCookieHeader).
|
|
282
|
-
// 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. ---
|
|
283
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-*');
|
|
284
350
|
const appPm = (0, dev_package_manager_1.pickPackageManager)(layout.appDir);
|
|
285
351
|
let appBuild = 0;
|
|
286
352
|
if (!skipBuild) {
|
|
287
353
|
log.info(log.dim('Building App (nuxt build, for speed + prod-fidelity) …'));
|
|
288
354
|
appBuild = yield (0, dev_process_1.runChildInherit)(appPm.bin, appPm.runScript('build'), {
|
|
289
355
|
cwd: layout.appDir,
|
|
290
|
-
env:
|
|
356
|
+
env: appEnv,
|
|
291
357
|
});
|
|
292
358
|
}
|
|
293
|
-
const appEntry =
|
|
359
|
+
const appEntry = testAppEntryCandidates()
|
|
294
360
|
.map((rel) => (0, path_1.join)(layout.appDir, rel))
|
|
295
361
|
.find((p) => (0, fs_1.existsSync)(p));
|
|
296
362
|
let appSpawn;
|
|
297
363
|
if (appBuild === 0 && appEntry) {
|
|
298
364
|
appSpawn = (0, dev_process_1.spawnDetached)('node', [appEntry], {
|
|
299
365
|
cwd: layout.appDir,
|
|
300
|
-
env:
|
|
366
|
+
env: appEnv,
|
|
301
367
|
logFile: (0, path_1.join)(layout.root, '.lt-dev', names.appLog),
|
|
302
368
|
});
|
|
303
369
|
}
|
|
@@ -305,7 +371,7 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
305
371
|
log.warn(`built app not available — falling back to \`${appPm.bin} dev\` (slower: cold-compiles routes).`);
|
|
306
372
|
appSpawn = (0, dev_process_1.spawnDetached)(appPm.bin, appPm.runScript('dev'), {
|
|
307
373
|
cwd: layout.appDir,
|
|
308
|
-
env:
|
|
374
|
+
env: appEnv,
|
|
309
375
|
logFile: (0, path_1.join)(layout.root, '.lt-dev', names.appLog),
|
|
310
376
|
});
|
|
311
377
|
}
|
|
@@ -338,9 +404,62 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
338
404
|
// Expose THIS stack's API log path so the caller can point the auth E2E specs
|
|
339
405
|
// (via NEST_SERVER_LOG) at the exact isolated log — correct per shard.
|
|
340
406
|
const apiLogPath = layout.apiDir ? (0, path_1.join)(layout.root, '.lt-dev', names.apiLog) : undefined;
|
|
341
|
-
return { apiLogPath, apiUrl, appEnv
|
|
407
|
+
return { apiLogPath, apiUrl, appEnv, appUrl, dbName, pids, testIdentity };
|
|
342
408
|
});
|
|
343
409
|
}
|
|
410
|
+
/**
|
|
411
|
+
* Build the Playwright CLI argv + per-shard env overrides for ONE shard of a
|
|
412
|
+
* `lt dev test --shard` run.
|
|
413
|
+
*
|
|
414
|
+
* DEV-2676 — reporter handling: this path must NOT pass `--reporter`. A CLI
|
|
415
|
+
* `--reporter` REPLACES the project's whole `reporter` list from
|
|
416
|
+
* playwright.config.ts (Playwright resolves CLI-over-config, it never appends —
|
|
417
|
+
* `Runner._parseConfig` in playwright's `lib/runner/index.js` does
|
|
418
|
+
* `result.reporter = [...configOverrides.reporter]`), silently dropping any
|
|
419
|
+
* release gate a project wires in AS a reporter. SVL's DEV-2098 gate (`./tests/no-skips.reporter.ts`)
|
|
420
|
+
* turns a run RED when a spec was skipped; the old `--reporter=line` here
|
|
421
|
+
* clobbered it, so a skipped test passed with exit 0 under `--shard` — the exact
|
|
422
|
+
* hole this closes. Omitting `--reporter` lets the configured reporters run (the
|
|
423
|
+
* gate included); Playwright auto-prepends a compact `line` (local) / `dot` (CI)
|
|
424
|
+
* reporter when none of them claim stdio, so the captured per-shard log stays
|
|
425
|
+
* readable without us overriding anything.
|
|
426
|
+
*
|
|
427
|
+
* The HTML reporter is the only common config reporter that is shard-hostile:
|
|
428
|
+
* every shard shares one project dir and would write the same
|
|
429
|
+
* `playwright-report/`, racing each other's report files. We hand each shard its
|
|
430
|
+
* own HTML output dir and force `open: never` (defensive — the non-TTY,
|
|
431
|
+
* file-captured child never auto-opens a browser anyway). Both env vars are
|
|
432
|
+
* inert for a project without an HTML reporter, so the fix stays generic.
|
|
433
|
+
*
|
|
434
|
+
* Playwright is invoked via the manager's `exec` (NOT `<pm> run test:e2e -- …`):
|
|
435
|
+
* forwarding option flags through `<pm> run`'s `--` is unreliable — pnpm passed
|
|
436
|
+
* the separator on to Playwright, which then read `--shard` as a file FILTER, so
|
|
437
|
+
* every shard ran the whole suite. `exec` hands args straight to the binary
|
|
438
|
+
* (mirrors CI); the helper inserts `--` for npm so those flags survive.
|
|
439
|
+
*/
|
|
440
|
+
function buildShardPlaywrightInvocation(pm, shardIndex, total, forwarded, htmlReportDir) {
|
|
441
|
+
return {
|
|
442
|
+
args: pm.exec('playwright', ['test', `--shard=${shardIndex}/${total}`, ...forwarded]),
|
|
443
|
+
env: {
|
|
444
|
+
PLAYWRIGHT_HTML_OPEN: 'never',
|
|
445
|
+
PLAYWRIGHT_HTML_OUTPUT_DIR: htmlReportDir,
|
|
446
|
+
},
|
|
447
|
+
};
|
|
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
|
+
}
|
|
344
463
|
/** True when a test session file exists (used by status/down). */
|
|
345
464
|
function hasTestSession(root) {
|
|
346
465
|
return (0, dev_state_1.loadSession)(root, dev_state_1.TEST_SESSION_FILE) !== null;
|
|
@@ -396,21 +515,14 @@ function runShardedTestSession(layout, baseIdentity, log, opts) {
|
|
|
396
515
|
// suite runs under concurrent sharded load, so it can relax navigation /
|
|
397
516
|
// test timeouts (N built SSR servers + N Chromium saturate the CPU and slow
|
|
398
517
|
// every navigation) without loosening them for serial runs.
|
|
399
|
-
|
|
518
|
+
// Reporter + shard args come from the shared helper, which deliberately
|
|
519
|
+
// does NOT inject `--reporter` so the project's own release gate (e.g.
|
|
520
|
+
// SVL's DEV-2098 no-skips reporter) still runs under `--shard` (DEV-2676),
|
|
521
|
+
// and isolates the HTML report per shard.
|
|
522
|
+
const reportDir = shardReportDir(layout.root, index);
|
|
523
|
+
const { args, env: reporterEnv } = buildShardPlaywrightInvocation(opts.pm, index, total, opts.forwarded, reportDir);
|
|
524
|
+
const env = Object.assign(Object.assign(Object.assign(Object.assign({}, ctx.appEnv), { LT_DEV_TEST_SHARDS: String(total), MONGO_URI: `mongodb://127.0.0.1/${ctx.dbName}` }), (ctx.apiLogPath ? { NEST_SERVER_LOG: ctx.apiLogPath } : {})), reporterEnv);
|
|
400
525
|
const logFile = (0, path_1.join)(layout.root, '.lt-dev', `shard.${index}.test.log`);
|
|
401
|
-
// Invoke Playwright DIRECTLY via the manager's `exec` (NOT `<pm> run
|
|
402
|
-
// test:e2e -- …`): forwarding option flags through `<pm> run`'s `--`
|
|
403
|
-
// is unreliable — pnpm passed the separator on to Playwright, which
|
|
404
|
-
// then read `--shard` / `--reporter` as file FILTERS (not options) →
|
|
405
|
-
// every shard ran the whole suite. `<pm> exec` hands args straight
|
|
406
|
-
// to the binary (mirrors CI); the helper inserts `--` for npm so
|
|
407
|
-
// those flags don't get re-parsed as npm's own.
|
|
408
|
-
const args = opts.pm.exec('playwright', [
|
|
409
|
-
'test',
|
|
410
|
-
`--shard=${index}/${total}`,
|
|
411
|
-
'--reporter=line',
|
|
412
|
-
...opts.forwarded,
|
|
413
|
-
]);
|
|
414
526
|
const code = yield (0, dev_process_1.runChildToFile)(opts.pm.bin, args, { cwd: appDir, env, logFile });
|
|
415
527
|
return { code, index, logFile };
|
|
416
528
|
})));
|
|
@@ -426,6 +538,16 @@ function runShardedTestSession(layout, baseIdentity, log, opts) {
|
|
|
426
538
|
return failed === 0 ? 0 : 1;
|
|
427
539
|
});
|
|
428
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* The per-shard HTML report directory. Distinct per shard (the `<index>` in the
|
|
543
|
+
* path) so N shards never race on a shared `playwright-report/` — this is the
|
|
544
|
+
* actual isolation `buildShardPlaywrightInvocation`'s `PLAYWRIGHT_HTML_OUTPUT_DIR`
|
|
545
|
+
* relies on. Extracted as a pure helper so the shard-distinctness guarantee is
|
|
546
|
+
* unit-testable without booting the (deliberately untested) real orchestrator.
|
|
547
|
+
*/
|
|
548
|
+
function shardReportDir(root, shardIndex) {
|
|
549
|
+
return (0, path_1.join)(root, '.lt-dev', `shard.${shardIndex}.playwright-report`);
|
|
550
|
+
}
|
|
429
551
|
/**
|
|
430
552
|
* Tear down the unsharded test stack AND every sharded stack discovered on disk
|
|
431
553
|
* (`state.test.<i>.json` in `.lt-dev/`). Used by `lt dev test down` so a
|
|
@@ -497,11 +619,45 @@ function tearDownTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
497
619
|
delete reg.projects[testIdentity.slug];
|
|
498
620
|
(0, dev_state_1.saveRegistry)(reg);
|
|
499
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
|
+
}
|
|
500
642
|
if (!opts.silent && stopped.length > 0)
|
|
501
643
|
log.info(`Stopped test stack: ${stopped.join(', ')}`);
|
|
502
644
|
return { stopped };
|
|
503
645
|
});
|
|
504
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
|
+
}
|
|
505
661
|
/**
|
|
506
662
|
* Resolve the per-stack file/identity names. For a sharded run (`shardIndex`
|
|
507
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
|
/**
|