@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
|
@@ -46,6 +46,7 @@ exports.Server = void 0;
|
|
|
46
46
|
const crypto = __importStar(require("crypto"));
|
|
47
47
|
const path_1 = require("path");
|
|
48
48
|
const ts = __importStar(require("typescript"));
|
|
49
|
+
const adopt_upstream_build_allowlist_1 = require("../lib/adopt-upstream-build-allowlist");
|
|
49
50
|
const check_freshness_hooks_1 = require("../lib/check-freshness-hooks");
|
|
50
51
|
const markdown_table_1 = require("../lib/markdown-table");
|
|
51
52
|
const strip_comments_1 = require("../lib/strip-comments");
|
|
@@ -1021,6 +1022,22 @@ class Server {
|
|
|
1021
1022
|
// Best-effort — without the lockfile, the closure step falls back to
|
|
1022
1023
|
// the upstream package.json ranges (still better than nothing).
|
|
1023
1024
|
}
|
|
1025
|
+
// Snapshot the upstream pnpm-workspace.yaml. It carries `allowBuilds`, the map
|
|
1026
|
+
// that decides which packages may run install scripts — and vendoring is what
|
|
1027
|
+
// makes the framework's entries the PROJECT's problem: step 5b turns the core's
|
|
1028
|
+
// import closure into direct dependencies, so packages nest-server knew about
|
|
1029
|
+
// (bullmq → msgpackr → msgpackr-extract) become the project's own. pnpm 11 aborts
|
|
1030
|
+
// the install outright on an unlisted one, so a missing entry is an unusable
|
|
1031
|
+
// project rather than a warning. This file is NOT in nest-server's npm tarball,
|
|
1032
|
+
// so the clone is the only moment it can be read at all.
|
|
1033
|
+
let upstreamWorkspaceYaml = '';
|
|
1034
|
+
try {
|
|
1035
|
+
upstreamWorkspaceYaml = filesystem.read(`${tmpClone}/pnpm-workspace.yaml`) || '';
|
|
1036
|
+
}
|
|
1037
|
+
catch (_c) {
|
|
1038
|
+
// Best-effort: an older framework revision may predate the file. The project
|
|
1039
|
+
// then keeps exactly the allowlist its own template shipped.
|
|
1040
|
+
}
|
|
1024
1041
|
// Snapshot the upstream CLAUDE.md for section-merge into projects/api/CLAUDE.md.
|
|
1025
1042
|
// The nest-server CLAUDE.md contains framework-specific instructions that
|
|
1026
1043
|
// Claude Code needs to work correctly with the vendored source (API conventions,
|
|
@@ -1033,7 +1050,7 @@ class Server {
|
|
|
1033
1050
|
upstreamClaudeMd = claudeMdContent;
|
|
1034
1051
|
}
|
|
1035
1052
|
}
|
|
1036
|
-
catch (
|
|
1053
|
+
catch (_d) {
|
|
1037
1054
|
// Non-fatal — if missing, the project CLAUDE.md just won't get upstream sections.
|
|
1038
1055
|
}
|
|
1039
1056
|
// Snapshot the upstream commit SHA for traceability in VENDOR.md.
|
|
@@ -1042,7 +1059,7 @@ class Server {
|
|
|
1042
1059
|
const sha = yield system.run(`git -C ${tmpClone} rev-parse HEAD`);
|
|
1043
1060
|
upstreamCommit = (sha || '').trim();
|
|
1044
1061
|
}
|
|
1045
|
-
catch (
|
|
1062
|
+
catch (_e) {
|
|
1046
1063
|
// Non-fatal — VENDOR.md will just show an empty SHA.
|
|
1047
1064
|
}
|
|
1048
1065
|
try {
|
|
@@ -1325,7 +1342,7 @@ class Server {
|
|
|
1325
1342
|
}
|
|
1326
1343
|
}
|
|
1327
1344
|
}
|
|
1328
|
-
catch (
|
|
1345
|
+
catch (_f) {
|
|
1329
1346
|
// skip unreadable file
|
|
1330
1347
|
}
|
|
1331
1348
|
}
|
|
@@ -1503,6 +1520,14 @@ class Server {
|
|
|
1503
1520
|
upstreamDeps,
|
|
1504
1521
|
upstreamDevDeps,
|
|
1505
1522
|
});
|
|
1523
|
+
// The closure above just made framework-only packages direct dependencies of
|
|
1524
|
+
// this project. Their build-script decisions have to come along, or the first
|
|
1525
|
+
// `pnpm install` stops on ERR_PNPM_IGNORED_BUILDS. Additive only — a decision
|
|
1526
|
+
// the project already made is never overwritten.
|
|
1527
|
+
const adoptedBuilds = (0, adopt_upstream_build_allowlist_1.adoptUpstreamBuildAllowlist)({ dest, filesystem, upstreamWorkspaceYaml });
|
|
1528
|
+
if (adoptedBuilds.length > 0) {
|
|
1529
|
+
this.toolbox.print.info(` vendored core allowlist: adopted ${adoptedBuilds.length} build decision(s) from nest-server → ${adoptedBuilds.join(', ')}`);
|
|
1530
|
+
}
|
|
1506
1531
|
// Add a script to run the local bin/migrate.js. The starter's
|
|
1507
1532
|
// existing migrate:* scripts are already correct for npm mode; we
|
|
1508
1533
|
// need them pointing at the local bin + local ts-compiler.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.adoptUpstreamBuildAllowlist = adoptUpstreamBuildAllowlist;
|
|
4
|
+
const js_yaml_1 = require("js-yaml");
|
|
5
|
+
const fs_utils_1 = require("./fs-utils");
|
|
6
|
+
const hoist_workspace_pnpm_config_1 = require("./hoist-workspace-pnpm-config");
|
|
7
|
+
/**
|
|
8
|
+
* Carry the framework's build-script allowlist into a project that has just
|
|
9
|
+
* vendored it.
|
|
10
|
+
*
|
|
11
|
+
* `allowBuilds` decides which packages may run install scripts. pnpm 11 does not
|
|
12
|
+
* treat an unlisted one as "deny" — it ABORTS the install with
|
|
13
|
+
* `ERR_PNPM_IGNORED_BUILDS` and writes a `set this to true or false` placeholder
|
|
14
|
+
* into the workspace file. So a single missing entry is not a hardening gap; it
|
|
15
|
+
* is a project that cannot be installed at all, on the very first
|
|
16
|
+
* `lt fullstack init`, before anyone has written a line of code.
|
|
17
|
+
*
|
|
18
|
+
* Vendoring is exactly where that gap opens. The conversion resolves the core's
|
|
19
|
+
* import closure into DIRECT dependencies — `import('bullmq')` in
|
|
20
|
+
* core-cron-jobs.service.ts becomes a real `bullmq` dep, which pulls
|
|
21
|
+
* `msgpackr` → `msgpackr-extract`, a package with an install script. nest-server
|
|
22
|
+
* knows about it and lists it. The project does not, and cannot: nest-server's
|
|
23
|
+
* `pnpm-workspace.yaml` is not part of its npm tarball — `files` ships `dist`,
|
|
24
|
+
* `src`, `bin` and the docs, never a repo-root config file — so nothing
|
|
25
|
+
* downstream can read it. Until now the only bridge was a
|
|
26
|
+
* human copying entries into nest-server-starter by hand — and that bridge has
|
|
27
|
+
* already been observed to rot: `@scarf/scarf` sat at `true` in the starter while
|
|
28
|
+
* the framework denied it, for long enough that the drift shipped.
|
|
29
|
+
*
|
|
30
|
+
* Deliberately additive. A key the project has already decided is left exactly as
|
|
31
|
+
* it is, including an explicit `false`: the project is the more specific context,
|
|
32
|
+
* and silently flipping its decision to match the framework would be a worse bug
|
|
33
|
+
* than the one this fixes. Only genuinely absent keys are taken over — together
|
|
34
|
+
* with the comment that explains them, because an entry like
|
|
35
|
+
* `'msgpackr-extract': false` reads as dead weight without it and gets deleted by
|
|
36
|
+
* the next person who runs `pnpm why`.
|
|
37
|
+
*
|
|
38
|
+
* @returns the keys actually adopted, for the caller to report.
|
|
39
|
+
*/
|
|
40
|
+
function adoptUpstreamBuildAllowlist(options) {
|
|
41
|
+
var _a;
|
|
42
|
+
const { dest, filesystem, upstreamWorkspaceYaml } = options;
|
|
43
|
+
if (!upstreamWorkspaceYaml)
|
|
44
|
+
return [];
|
|
45
|
+
const upstream = parseYamlObject(upstreamWorkspaceYaml);
|
|
46
|
+
const upstreamAllow = asStringBoolMap(upstream === null || upstream === void 0 ? void 0 : upstream.allowBuilds);
|
|
47
|
+
if (Object.keys(upstreamAllow).length === 0)
|
|
48
|
+
return [];
|
|
49
|
+
// Never write through a symlinked project: with `--api-link` it points at the
|
|
50
|
+
// user's own nest-server-starter checkout, and this would edit their repo. The
|
|
51
|
+
// same guard already protects `hoistWorkspacePnpmConfig` and
|
|
52
|
+
// `removeNestedLockfiles`; these two libs were the odd ones out.
|
|
53
|
+
if ((0, fs_utils_1.isSymlink)(dest))
|
|
54
|
+
return [];
|
|
55
|
+
const destPath = `${dest}/pnpm-workspace.yaml`;
|
|
56
|
+
// No file to extend means no pnpm settings of the project's own. Writing one
|
|
57
|
+
// here would invent a workspace root the scaffolding did not ask for, so the
|
|
58
|
+
// absence is respected rather than filled in.
|
|
59
|
+
if (!filesystem.exists(destPath))
|
|
60
|
+
return [];
|
|
61
|
+
const destRaw = (_a = filesystem.read(destPath)) !== null && _a !== void 0 ? _a : '';
|
|
62
|
+
const destWs = parseYamlObject(destRaw);
|
|
63
|
+
if (!destWs)
|
|
64
|
+
return [];
|
|
65
|
+
// The project's map is read RAW, not through `asStringBoolMap`. Narrowing it to
|
|
66
|
+
// booleans first was a live deny-bypass with no attacker involved: js-yaml 4
|
|
67
|
+
// uses the YAML 1.2 core schema, so `esbuild: no` and `esbuild: off` parse as
|
|
68
|
+
// STRINGS. A maintainer writing `no` to mean "deny" was read as "no opinion",
|
|
69
|
+
// upstream's `true` was adopted, and the original entry was dropped from the
|
|
70
|
+
// rewritten map as well. Same for `'false'`, for an empty value (null), and for
|
|
71
|
+
// pnpm's own `set this to true or false` placeholder — all five verified.
|
|
72
|
+
//
|
|
73
|
+
// `Object.create(null)` and `hasOwnProperty.call`: with a plain object,
|
|
74
|
+
// `'constructor' in map` is true via the prototype, so eight real package names
|
|
75
|
+
// ('constructor', 'toString', 'valueOf', …) would be treated as already decided
|
|
76
|
+
// and silently never adopted — the ERR_PNPM_IGNORED_BUILDS abort this whole
|
|
77
|
+
// function exists to prevent.
|
|
78
|
+
const merged = Object.assign(Object.create(null), asRawMap(destWs.allowBuilds));
|
|
79
|
+
const adopted = [];
|
|
80
|
+
for (const [pkg, value] of Object.entries(upstreamAllow)) {
|
|
81
|
+
// ANY existing key is a decision, whatever shape YAML gave it.
|
|
82
|
+
if (Object.prototype.hasOwnProperty.call(merged, pkg))
|
|
83
|
+
continue;
|
|
84
|
+
merged[pkg] = value;
|
|
85
|
+
adopted.push(pkg);
|
|
86
|
+
}
|
|
87
|
+
if (adopted.length === 0)
|
|
88
|
+
return [];
|
|
89
|
+
destWs.allowBuilds = Object.fromEntries(Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)));
|
|
90
|
+
// The project's own annotations win where both files comment the same key; the
|
|
91
|
+
// upstream ones fill in only for the keys just adopted, which by definition the
|
|
92
|
+
// project had nothing to say about.
|
|
93
|
+
const comments = (0, hoist_workspace_pnpm_config_1.extractKeyComments)(destRaw);
|
|
94
|
+
for (const [key, block] of (0, hoist_workspace_pnpm_config_1.extractKeyComments)(upstreamWorkspaceYaml)) {
|
|
95
|
+
if (!comments.has(key))
|
|
96
|
+
comments.set(key, block);
|
|
97
|
+
}
|
|
98
|
+
filesystem.write(destPath, (0, hoist_workspace_pnpm_config_1.reattachKeyComments)((0, js_yaml_1.dump)(destWs, { lineWidth: -1, sortKeys: false }), comments));
|
|
99
|
+
return adopted;
|
|
100
|
+
}
|
|
101
|
+
/** The value as a plain key/value map, or an empty one — no narrowing of values. */
|
|
102
|
+
function asRawMap(value) {
|
|
103
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
104
|
+
return {};
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Narrow an unknown value to a `{ pkg: boolean }` map, dropping other shapes.
|
|
109
|
+
*
|
|
110
|
+
* Used for the UPSTREAM side only. There, dropping a non-boolean is right — we
|
|
111
|
+
* adopt only decisions we understand. On the DEST side it is the opposite: see
|
|
112
|
+
* the comment at the merge above.
|
|
113
|
+
*/
|
|
114
|
+
function asStringBoolMap(value) {
|
|
115
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
116
|
+
return {};
|
|
117
|
+
const out = {};
|
|
118
|
+
for (const [key, v] of Object.entries(value)) {
|
|
119
|
+
if (typeof v === 'boolean')
|
|
120
|
+
out[key] = v;
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
/** Parse YAML into a plain object, or null on empty/malformed/non-object input. */
|
|
125
|
+
function parseYamlObject(raw) {
|
|
126
|
+
if (!raw)
|
|
127
|
+
return null;
|
|
128
|
+
let parsed;
|
|
129
|
+
try {
|
|
130
|
+
parsed = (0, js_yaml_1.load)(raw);
|
|
131
|
+
}
|
|
132
|
+
catch (_a) {
|
|
133
|
+
// Malformed upstream YAML must not take the conversion down with it — the
|
|
134
|
+
// vendored project is still usable, it just does not inherit the allowlist.
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
138
|
+
return null;
|
|
139
|
+
return parsed;
|
|
140
|
+
}
|
|
@@ -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
|
/**
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.failRun = failRun;
|
|
4
|
+
/**
|
|
5
|
+
* Mark the current command run as failed.
|
|
6
|
+
*
|
|
7
|
+
* A gluegun command signals failure by printing and returning, and a bare
|
|
8
|
+
* `return` leaves the process at exit code 0. So a scaffold that died halfway
|
|
9
|
+
* reported SUCCESS to every caller that checks `$?` — a CI job, a wrapper
|
|
10
|
+
* script, an agent. That is not theoretical: a `lt fullstack init` whose
|
|
11
|
+
* `pnpm install` aborted on a native build script printed a red spinner and
|
|
12
|
+
* still exited 0, and the half-built workspace was only noticed later, by hand.
|
|
13
|
+
*
|
|
14
|
+
* Call it immediately before every `return` on an error path:
|
|
15
|
+
*
|
|
16
|
+
* failRun(toolbox);
|
|
17
|
+
* return;
|
|
18
|
+
*
|
|
19
|
+
* **`process.exitCode`, not `process.exit()`** — the latter can truncate
|
|
20
|
+
* buffered output, including the spinner's own failure message, which is the one
|
|
21
|
+
* line the operator actually needs.
|
|
22
|
+
*
|
|
23
|
+
* **Guarded by `fromGluegunMenu`**, like the CLI's other exit-code call sites
|
|
24
|
+
* (`dev test`, `dev tunnel`, `tools ocr`, `workspace-integration`): inside the
|
|
25
|
+
* interactive `lt` menu a command is one step of a longer session, and failing
|
|
26
|
+
* the whole session because one step errored is the same over-reach in reverse.
|
|
27
|
+
*
|
|
28
|
+
* Shared rather than redeclared per command: `fullstack init` delegates to
|
|
29
|
+
* `add-api` / `add-app` inside an existing workspace, so all three have to agree
|
|
30
|
+
* on the contract or the exit code depends on which directory the user happened
|
|
31
|
+
* to be standing in — which was exactly the state before this helper existed.
|
|
32
|
+
*/
|
|
33
|
+
function failRun(toolbox) {
|
|
34
|
+
var _a, _b;
|
|
35
|
+
if (!((_b = (_a = toolbox.parameters) === null || _a === void 0 ? void 0 : _a.options) === null || _b === void 0 ? void 0 : _b.fromGluegunMenu)) {
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
}
|
|
38
|
+
}
|