@lenne.tech/cli 1.47.0 → 1.49.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/dev/test.js +5 -3
- package/build/commands/fullstack/init.js +12 -3
- package/build/commands/git/force-pull.js +11 -2
- package/build/commands/git/rebase.js +11 -1
- package/build/commands/git/reset.js +36 -4
- package/build/commands/git/squash.js +4 -1
- package/build/commands/git/update.js +42 -6
- package/build/extensions/api-mode.js +101 -8
- package/build/extensions/frontend-helper.js +11 -12
- package/build/extensions/git.js +23 -25
- package/build/extensions/server.js +66 -49
- package/build/lib/check-template-sync.js +62 -2
- package/build/lib/config-env-graphql.js +106 -0
- package/build/lib/cross-env.js +94 -0
- package/build/lib/dev-api-launch.js +92 -14
- package/build/lib/dev-package-manager.js +13 -9
- package/build/lib/dev-process.js +66 -30
- package/build/lib/dev-state.js +13 -2
- package/build/lib/dev-test-session.js +166 -55
- package/build/lib/fail-run.js +11 -0
- package/build/lib/git-env.js +46 -0
- package/build/lib/heal-oxlintrc.js +20 -3
- package/build/lib/heal-vendor-migrate-store.js +4 -3
- package/build/lib/migrate-scripts.js +69 -0
- package/build/lib/module-specifiers.js +144 -0
- package/build/lib/spinner-lifetime.js +245 -0
- package/build/lib/strip-vendor-schema-augmentation.js +13 -2
- package/build/lib/vendor-claude-md.js +9 -6
- package/build/lib/workspace-integration.js +12 -2
- package/build/templates/check/check.mjs +59 -9
- package/build/templates/check/lt-monorepo-pin.json +4 -4
- package/docs/commands.md +16 -12
- package/package.json +1 -1
- package/build/lib/strip-comments.js +0 -78
|
@@ -74,16 +74,20 @@ function buildCommand(bin, name) {
|
|
|
74
74
|
},
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Manager behind an override like `LT_PM_BIN`.
|
|
79
|
+
*
|
|
80
|
+
* Reads the file NAME, split on both separators and stripped of a Windows launcher
|
|
81
|
+
* extension: there the binary is `C:\\…\\pnpm.cmd`, which neither `endsWith('pnpm')`
|
|
82
|
+
* nor `includes('/pnpm')` matched — the override then resolved to `unknown` and the
|
|
83
|
+
* caller drove the wrong manager.
|
|
84
|
+
*/
|
|
77
85
|
function inferNameFromBin(bin) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return 'yarn';
|
|
84
|
-
}
|
|
85
|
-
if (lower.endsWith('npm') || lower.includes('/npm')) {
|
|
86
|
-
return 'npm';
|
|
86
|
+
var _a;
|
|
87
|
+
const file = (_a = bin.toLowerCase().split(/[\\/]/).pop()) !== null && _a !== void 0 ? _a : '';
|
|
88
|
+
const name = file.replace(/\.(bat|cmd|exe|ps1)$/, '');
|
|
89
|
+
if (name === 'pnpm' || name === 'yarn' || name === 'npm') {
|
|
90
|
+
return name;
|
|
87
91
|
}
|
|
88
92
|
return 'unknown';
|
|
89
93
|
}
|
package/build/lib/dev-process.js
CHANGED
|
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.checkPortInUse = checkPortInUse;
|
|
13
|
+
exports.detachedSpawnCommand = detachedSpawnCommand;
|
|
13
14
|
exports.killProcessGroup = killProcessGroup;
|
|
14
15
|
exports.listenSnapshot = listenSnapshot;
|
|
15
16
|
exports.rotateLogFile = rotateLogFile;
|
|
@@ -59,6 +60,40 @@ function checkPortInUse(port) {
|
|
|
59
60
|
});
|
|
60
61
|
});
|
|
61
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* How a detached child is actually launched on this platform.
|
|
65
|
+
*
|
|
66
|
+
* POSIX raises the soft file-descriptor limit before exec-ing the real command.
|
|
67
|
+
* macOS's default soft RLIMIT_NOFILE is 256 (launchd/system default), inherited
|
|
68
|
+
* by the terminal that runs `lt dev up` and therefore by these detached children
|
|
69
|
+
* — it is NOT a consequence of the lt-dev LaunchAgent (which runs only Caddy).
|
|
70
|
+
* The dev file-watcher (nest/nuxt → chokidar) exhausts a soft-256 limit on a
|
|
71
|
+
* monorepo → intermittent "EMFILE: too many open files, watch" crashes on boot
|
|
72
|
+
* that force a manual `lt dev up`. Hence `sh -c "ulimit …; exec …"`:
|
|
73
|
+
* - `exec` replaces the shell IN PLACE → the recorded PID and the detached
|
|
74
|
+
* process group are still the real process (PID tracking + group-kill in
|
|
75
|
+
* `terminateProcessGroup` keep working).
|
|
76
|
+
* - `"$0" "$@"` pass cmd + args verbatim — no shell-quoting / injection.
|
|
77
|
+
* - the cascade tries a high limit first, falling back on machines with a
|
|
78
|
+
* lower `kern.maxfilesperproc`; `2>/dev/null` keeps it best-effort.
|
|
79
|
+
*
|
|
80
|
+
* Windows gets the command directly. There is no `/bin/sh` to run the wrapper —
|
|
81
|
+
* spawning it fails outright — and no RLIMIT_NOFILE for `ulimit` to raise, so
|
|
82
|
+
* the wrapper has nothing to offer there even in principle.
|
|
83
|
+
*
|
|
84
|
+
* Note for the POSIX path: because `spawn('/bin/sh', …)` almost always succeeds,
|
|
85
|
+
* a bogus `cmd` does not surface as `pid === undefined` — the inner `exec` fails
|
|
86
|
+
* (exit 127) a few ms later. Callers briefly record a live-then-dead PID, which
|
|
87
|
+
* `classifyComponentHealth` reaps as `dead`/`crashed` on the next status/up. We
|
|
88
|
+
* deliberately don't watch for that here: a detached, unref'd child's 127 exit is
|
|
89
|
+
* racy to observe, and callers must be self-correcting against real crashes anyway.
|
|
90
|
+
*/
|
|
91
|
+
function detachedSpawnCommand(cmd, args, platform = process.platform) {
|
|
92
|
+
if (platform === 'win32')
|
|
93
|
+
return { args, command: cmd };
|
|
94
|
+
const raiseFdLimit = 'ulimit -n 65536 2>/dev/null || ulimit -n 10240 2>/dev/null || true';
|
|
95
|
+
return { args: ['-c', `${raiseFdLimit}; exec "$0" "$@"`, cmd, ...args], command: '/bin/sh' };
|
|
96
|
+
}
|
|
62
97
|
/** Send SIGTERM to a detached process group; falls back to single-PID kill. */
|
|
63
98
|
function killProcessGroup(pid) {
|
|
64
99
|
if (!(0, dev_state_1.isValidPid)(pid))
|
|
@@ -215,6 +250,8 @@ function runChildToFile(cmd, args, opts) {
|
|
|
215
250
|
* so closing prevents fd leaks and avoids racing-write artifacts on
|
|
216
251
|
* filesystems where O_APPEND is not atomic.
|
|
217
252
|
*
|
|
253
|
+
* See `detachedSpawnCommand` for how the child is launched per platform.
|
|
254
|
+
*
|
|
218
255
|
* Returns the child PID, or undefined if spawn failed.
|
|
219
256
|
*/
|
|
220
257
|
function spawnDetached(cmd, args, opts) {
|
|
@@ -223,32 +260,22 @@ function spawnDetached(cmd, args, opts) {
|
|
|
223
260
|
const out = (0, fs_1.openSync)(opts.logFile, 'a');
|
|
224
261
|
let child;
|
|
225
262
|
try {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
// by the terminal that runs `lt dev up` and therefore by these detached children
|
|
229
|
-
// — it is NOT a consequence of the lt-dev LaunchAgent (which runs only Caddy).
|
|
230
|
-
// The dev file-watcher (nest/nuxt → chokidar) exhausts a soft-256 limit on a
|
|
231
|
-
// monorepo → intermittent "EMFILE: too many open files, watch" crashes on boot
|
|
232
|
-
// that force a manual `lt dev up`. We wrap the command in `sh -c "ulimit …; exec …"`:
|
|
233
|
-
// - `exec` replaces the shell IN PLACE → the recorded PID and the detached
|
|
234
|
-
// process group are still the real process (PID tracking + group-kill in
|
|
235
|
-
// `terminateProcessGroup` keep working).
|
|
236
|
-
// - `"$0" "$@"` pass cmd + args verbatim — no shell-quoting / injection.
|
|
237
|
-
// - the cascade tries a high limit first, falling back on machines with a
|
|
238
|
-
// lower `kern.maxfilesperproc`; `2>/dev/null` keeps it best-effort.
|
|
239
|
-
// Note: because the outer `spawn('/bin/sh', …)` almost always succeeds, a bogus
|
|
240
|
-
// `cmd` no longer surfaces as `pid === undefined` here — the inner `exec` fails
|
|
241
|
-
// (exit 127) a few ms later. Callers briefly record a live-then-dead PID, which
|
|
242
|
-
// `classifyComponentHealth` reaps as `dead`/`crashed` on the next status/up. We
|
|
243
|
-
// deliberately don't watch for that here: a detached, unref'd child's 127 exit is
|
|
244
|
-
// racy to observe, and callers must be self-correcting against real crashes anyway.
|
|
245
|
-
const raiseFdLimit = 'ulimit -n 65536 2>/dev/null || ulimit -n 10240 2>/dev/null || true';
|
|
246
|
-
child = (0, child_process_1.spawn)('/bin/sh', ['-c', `${raiseFdLimit}; exec "$0" "$@"`, cmd, ...args], {
|
|
263
|
+
const spawned = detachedSpawnCommand(cmd, args);
|
|
264
|
+
child = (0, child_process_1.spawn)(spawned.command, spawned.args, {
|
|
247
265
|
cwd: opts.cwd,
|
|
248
266
|
detached: true,
|
|
249
267
|
env: opts.env,
|
|
250
268
|
stdio: ['ignore', out, out],
|
|
251
269
|
});
|
|
270
|
+
// spawn reports "could not start this at all" (missing executable, bad cwd)
|
|
271
|
+
// through an ASYNCHRONOUS 'error' event — the try/catch around spawn() never
|
|
272
|
+
// sees it. With no listener, Node escalates an unhandled 'error' to a process
|
|
273
|
+
// crash, so one unstartable child would take the whole CLI down with it. That
|
|
274
|
+
// is not hypothetical: on Windows `/bin/sh` does not exist, and this line is
|
|
275
|
+
// what turned that into "Test suite failed to run" instead of one red test.
|
|
276
|
+
// Nothing to do but swallow it — the child is detached and its log file is
|
|
277
|
+
// already closed below; callers are self-correcting against a dead PID.
|
|
278
|
+
child.on('error', () => undefined);
|
|
252
279
|
child.unref();
|
|
253
280
|
if (child.pid === undefined)
|
|
254
281
|
return undefined;
|
|
@@ -330,7 +357,15 @@ function terminateProcessGroup(pid_1) {
|
|
|
330
357
|
*
|
|
331
358
|
* Resolves `true` on the first matching response, `false` on timeout. Never rejects.
|
|
332
359
|
*/
|
|
333
|
-
function waitForHttp(url, timeoutMs, ready = (status) => status >= 100 && status < 600
|
|
360
|
+
function waitForHttp(url, timeoutMs, ready = (status) => status >= 100 && status < 600,
|
|
361
|
+
/**
|
|
362
|
+
* Optional early exit: return `true` once there is nothing left to wait FOR —
|
|
363
|
+
* typically "the process I am waiting on has exited". Without it a server that
|
|
364
|
+
* crashes on boot is indistinguishable from one that is still starting, and the
|
|
365
|
+
* caller burns the entire timeout (120s for the `lt dev test` API) before
|
|
366
|
+
* learning something that was decided in the first 300ms.
|
|
367
|
+
*/
|
|
368
|
+
abort = () => false) {
|
|
334
369
|
const start = Date.now();
|
|
335
370
|
return new Promise((resolve) => {
|
|
336
371
|
const tick = () => {
|
|
@@ -340,20 +375,21 @@ function waitForHttp(url, timeoutMs, ready = (status) => status >= 100 && status
|
|
|
340
375
|
});
|
|
341
376
|
let status = '';
|
|
342
377
|
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (b) => (status += String(b)));
|
|
378
|
+
const retry = () => {
|
|
379
|
+
// Order matters: a LAST probe already ran above, so a server that came up
|
|
380
|
+
// just before dying is still reported ready. Only then does `abort` end it.
|
|
381
|
+
if (Date.now() - start > timeoutMs || abort())
|
|
382
|
+
return resolve(false);
|
|
383
|
+
setTimeout(tick, 500);
|
|
384
|
+
};
|
|
343
385
|
child.on('close', () => {
|
|
344
386
|
const code = Number(status.trim());
|
|
345
387
|
// `000` (curl could not connect) parses to 0 → never "ready".
|
|
346
388
|
if (Number.isFinite(code) && code > 0 && ready(code))
|
|
347
389
|
return resolve(true);
|
|
348
|
-
|
|
349
|
-
return resolve(false);
|
|
350
|
-
setTimeout(tick, 500);
|
|
351
|
-
});
|
|
352
|
-
child.on('error', () => {
|
|
353
|
-
if (Date.now() - start > timeoutMs)
|
|
354
|
-
return resolve(false);
|
|
355
|
-
setTimeout(tick, 500);
|
|
390
|
+
retry();
|
|
356
391
|
});
|
|
392
|
+
child.on('error', retry);
|
|
357
393
|
};
|
|
358
394
|
tick();
|
|
359
395
|
});
|
package/build/lib/dev-state.js
CHANGED
|
@@ -204,10 +204,21 @@ function loadSession(root, sessionFile = SESSION_FILE) {
|
|
|
204
204
|
}
|
|
205
205
|
return null;
|
|
206
206
|
}
|
|
207
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* True if two paths resolve to the same location (normalising symlinks, e.g. /var → /private/var).
|
|
209
|
+
*
|
|
210
|
+
* `.native` rather than plain `realpathSync`, because on Windows the JS
|
|
211
|
+
* implementation follows symlinks but keeps 8.3 short components: a path spelled
|
|
212
|
+
* `C:\Users\RUNNER~1\…` and the same directory spelled `C:\Users\runneradmin\…`
|
|
213
|
+
* stay different strings. Both spellings occur in practice — `os.tmpdir()` hands
|
|
214
|
+
* out the short form while git prints the long one — so a registry entry written
|
|
215
|
+
* under one and a cwd under the other would read as a DIFFERENT checkout. The
|
|
216
|
+
* native variant asks the OS for the final name and collapses both.
|
|
217
|
+
* No behavioural difference on POSIX.
|
|
218
|
+
*/
|
|
208
219
|
function sameRealPath(a, b) {
|
|
209
220
|
try {
|
|
210
|
-
return
|
|
221
|
+
return fs_1.realpathSync.native(a) === fs_1.realpathSync.native(b);
|
|
211
222
|
}
|
|
212
223
|
catch (_a) {
|
|
213
224
|
return a === b;
|
|
@@ -16,31 +16,41 @@ exports.bringUpTestSession = bringUpTestSession;
|
|
|
16
16
|
exports.buildShardPlaywrightInvocation = buildShardPlaywrightInvocation;
|
|
17
17
|
exports.buildTestAppEnv = buildTestAppEnv;
|
|
18
18
|
exports.hasTestSession = hasTestSession;
|
|
19
|
+
exports.isStackServing = isStackServing;
|
|
20
|
+
exports.migrationFailedError = migrationFailedError;
|
|
19
21
|
exports.resolveTestSession = resolveTestSession;
|
|
20
22
|
exports.runShardedTestSession = runShardedTestSession;
|
|
21
23
|
exports.shardReportDir = shardReportDir;
|
|
24
|
+
exports.startTestApi = startTestApi;
|
|
22
25
|
exports.tearDownAllTestSessions = tearDownAllTestSessions;
|
|
23
26
|
exports.tearDownTestSession = tearDownTestSession;
|
|
24
27
|
exports.testAppEntryCandidates = testAppEntryCandidates;
|
|
28
|
+
exports.unreachableStackError = unreachableStackError;
|
|
25
29
|
/**
|
|
26
30
|
* Ephemeral, isolated test session for `lt dev test`.
|
|
27
31
|
*
|
|
28
32
|
* Brings up a SECOND, fully separate stack (own URLs, own internal ports,
|
|
29
33
|
* own Caddy block, own database) that runs PARALLEL to — and never touches —
|
|
30
34
|
* the developer's `lt dev up` session. Used to run the Playwright E2E suite
|
|
31
|
-
* against a
|
|
32
|
-
*
|
|
35
|
+
* against a dedicated database so a developer can keep working in their own
|
|
36
|
+
* environment while tests run, and so a test run never pollutes dev data. That
|
|
37
|
+
* database outlives the run — the CLI never drops it — so the API is migrated
|
|
38
|
+
* before it starts, like every other environment (DEV-3289).
|
|
33
39
|
*
|
|
34
40
|
* Topology (for slug `svl`):
|
|
35
41
|
* - dev session : svl.localhost / api.svl.localhost → db `<…>-local`
|
|
36
42
|
* - test session : svl-test.localhost / api.svl-test.… → db `<…>-test`
|
|
37
43
|
*
|
|
38
|
-
* Both halves run BUILT for speed + prod-fidelity: the API COMPILED (`
|
|
39
|
-
* ts-node intermittently dies mid-run) and the App as the production Nitro
|
|
40
|
-
* (`nuxt build` → `node .output/server/index.mjs`, no Vite cold-compile).
|
|
41
|
-
* falls back to its dev runner (`pnpm start` / `pnpm dev`) when no build
|
|
42
|
-
* found.
|
|
43
|
-
*
|
|
44
|
+
* Both halves run BUILT for speed + prod-fidelity: the API COMPILED (from `dist`,
|
|
45
|
+
* because ts-node intermittently dies mid-run) and the App as the production Nitro
|
|
46
|
+
* output (`nuxt build` → `node .output/server/index.mjs`, no Vite cold-compile).
|
|
47
|
+
* Each falls back to its dev runner (`pnpm start` / `pnpm dev`) when no build
|
|
48
|
+
* output is found. The API's runtime follows the project (`resolveApiRuntime`):
|
|
49
|
+
* a Bun-built bundle is started with `bun`, everything else with `node`.
|
|
50
|
+
*
|
|
51
|
+
* bringUp waits for BOTH halves to answer and THROWS when one never does, so the
|
|
52
|
+
* suite can never run against a dead stack (DEV-3208) — see `isStackServing` for
|
|
53
|
+
* what counts as answering.
|
|
44
54
|
*
|
|
45
55
|
* Lifecycle: `bringUpTestSession` → run Playwright → `tearDownTestSession`.
|
|
46
56
|
* Teardown is idempotent and residue-free (processes, Caddy block, env bridge,
|
|
@@ -93,12 +103,13 @@ const TEST_PORT_BASE = 4500;
|
|
|
93
103
|
* `systemSetup.initialAdmin.*` and seeds the admin ONCE on an empty DB (it never
|
|
94
104
|
* overwrites an existing admin), so a set-up system exists before the suite runs.
|
|
95
105
|
* A fresh template project's standard auth E2E specs assume such a system — they
|
|
96
|
-
* do no self-setup — so without this they fail LOCALLY on
|
|
106
|
+
* do no self-setup — so without this they fail LOCALLY on a test DB that has no
|
|
107
|
+
* users yet (its first run, or after a global-setup reset it).
|
|
97
108
|
* These are the exact values the lt-monorepo template CI uses
|
|
98
109
|
* (`.gitlab-ci.yml` / `.github/workflows/test.yml`), keeping CI ↔ local parity.
|
|
99
110
|
*
|
|
100
|
-
* Injected ONLY into the `lt dev test` API process (its DB is
|
|
101
|
-
*
|
|
111
|
+
* Injected ONLY into the `lt dev test` API process (its DB is the dedicated
|
|
112
|
+
* `<…>-test` one), NEVER into `lt dev up` — no surprise admin in the dev DB.
|
|
102
113
|
*/
|
|
103
114
|
exports.TEST_INITIAL_ADMIN_ENV = {
|
|
104
115
|
NSC__SYSTEM_SETUP__INITIAL_ADMIN__EMAIL: 'ci-admin@test.com',
|
|
@@ -270,7 +281,11 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
270
281
|
log.info(` app: ${appUrl} → 127.0.0.1:${appPort}`);
|
|
271
282
|
if (apiUrl)
|
|
272
283
|
log.info(` api: ${apiUrl} → 127.0.0.1:${apiPort}`);
|
|
273
|
-
|
|
284
|
+
// No claim about resetting here: the CLI never drops this DB, and whether a
|
|
285
|
+
// project's global-setup does is the project's business. The claim used to be
|
|
286
|
+
// printed unconditionally and sent a diagnosis looking for a reset that never
|
|
287
|
+
// happened instead of at the un-migrated DB (DEV-3289).
|
|
288
|
+
log.info(` db: mongodb://127.0.0.1/${dbName}`);
|
|
274
289
|
log.info('');
|
|
275
290
|
const devEnv = (0, dev_env_1.buildDevEnv)({
|
|
276
291
|
apiInternalPort: apiPort !== null && apiPort !== void 0 ? apiPort : 0,
|
|
@@ -288,40 +303,23 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
288
303
|
// hands it to the Playwright child as well.
|
|
289
304
|
const appEnv = buildTestAppEnv(devEnv.app.env);
|
|
290
305
|
const pids = {};
|
|
291
|
-
// --- API: compiled (`node dist`) for stability;
|
|
292
|
-
// own
|
|
293
|
-
// first shard produced. Per-component PM detection mirrors `lt dev up`:
|
|
294
|
-
// a monorepo with an npm api and a pnpm app must drive each correctly. ---
|
|
306
|
+
// --- API: compiled (`node dist`) for stability, migrated first; falls back to
|
|
307
|
+
// the project's own start script (see `startTestApi`). ---
|
|
295
308
|
if (layout.apiDir && apiPort) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
log.info(log.dim('Building API (compiled, for stable long runs) …'));
|
|
300
|
-
build = yield (0, dev_process_1.runChildInherit)(apiPm.bin, apiPm.runScript('build'), { cwd: layout.apiDir, env: process.env });
|
|
301
|
-
}
|
|
302
|
-
const entry = (0, dev_api_launch_1.findCompiledEntry)(layout.apiDir);
|
|
303
|
-
// Seed a throwaway initial admin into the fresh, isolated test DB so the
|
|
304
|
-
// standard auth E2E specs run against a set-up system — locally exactly like
|
|
305
|
-
// the lt-monorepo template CI. Defaults first so an explicitly inherited
|
|
309
|
+
// Seed a throwaway initial admin into the isolated test DB so the standard
|
|
310
|
+
// auth E2E specs run against a set-up system — locally exactly like the
|
|
311
|
+
// lt-monorepo template CI. Defaults first so an explicitly inherited
|
|
306
312
|
// `NSC__…INITIAL_ADMIN__…` still wins (deliberate override respected). Only
|
|
307
313
|
// reached from `lt dev test` — `lt dev up` never calls bringUpTestSession.
|
|
308
314
|
const apiEnv = Object.assign(Object.assign(Object.assign({}, exports.TEST_INITIAL_ADMIN_ENV), devEnv.api.env), { NODE_ENV: 'local' });
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
else {
|
|
318
|
-
log.warn(`compiled API not available — falling back to \`${apiPm.bin} start\` (ts-node).`);
|
|
319
|
-
apiSpawn = (0, dev_process_1.spawnDetached)(apiPm.bin, apiPm.runScript('start'), {
|
|
320
|
-
cwd: layout.apiDir,
|
|
321
|
-
env: apiEnv,
|
|
322
|
-
logFile: (0, path_1.join)(layout.root, '.lt-dev', names.apiLog),
|
|
323
|
-
});
|
|
324
|
-
}
|
|
315
|
+
const apiSpawn = yield startTestApi({
|
|
316
|
+
apiDir: layout.apiDir,
|
|
317
|
+
apiEnv,
|
|
318
|
+
dbName,
|
|
319
|
+
log,
|
|
320
|
+
logFile: (0, path_1.join)(layout.root, '.lt-dev', names.apiLog),
|
|
321
|
+
skipBuild,
|
|
322
|
+
});
|
|
325
323
|
if (apiSpawn)
|
|
326
324
|
pids.api = apiSpawn.pid;
|
|
327
325
|
}
|
|
@@ -387,26 +385,43 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
387
385
|
(0, dev_state_1.saveSession)(layout.root, { pids, startedAt: new Date().toISOString() }, names.sessionFile);
|
|
388
386
|
// ENV bridge for external tooling (kept separate from the dev `.env`).
|
|
389
387
|
(0, dev_env_bridge_1.writeEnvBridge)(layout.root, devEnv, dbName, names.bridgeFile);
|
|
390
|
-
//
|
|
388
|
+
// THIS stack's log paths. Resolved BEFORE the readiness waits below, because
|
|
389
|
+
// the abort messages point at them: the log is the only place a crash-on-boot
|
|
390
|
+
// is recorded, and an abort that withholds it just relocates the guesswork.
|
|
391
|
+
// `apiLogPath` is also handed to the caller so the auth E2E specs (via
|
|
392
|
+
// NEST_SERVER_LOG) read the exact isolated log — correct per shard.
|
|
393
|
+
const apiLogPath = layout.apiDir ? (0, path_1.join)(layout.root, '.lt-dev', names.apiLog) : undefined;
|
|
394
|
+
const appLogPath = layout.appDir ? (0, path_1.join)(layout.root, '.lt-dev', names.appLog) : undefined;
|
|
395
|
+
// Both waits below ABORT rather than warn (DEV-3208). A warning let Playwright
|
|
396
|
+
// run the whole suite against a dead stack: every data-dependent spec passed
|
|
397
|
+
// trivially or skipped, so a run that verified NOTHING was indistinguishable
|
|
398
|
+
// from one that verified everything — worse than a red run, because nobody
|
|
399
|
+
// goes looking. `isStackServing` decides what counts as up, and the liveness
|
|
400
|
+
// guard turns a crash-on-boot into an abort in the same 300ms it took to
|
|
401
|
+
// happen, instead of after the full timeout.
|
|
402
|
+
//
|
|
403
|
+
// `gone` is deliberately true for an ABSENT pid too: reaching here means the
|
|
404
|
+
// spawn was attempted, so no pid means it failed outright and there is nothing
|
|
405
|
+
// left to wait for. Being wrong here costs nothing — the timeout still bounds
|
|
406
|
+
// the wait, so this only ever makes the abort arrive sooner.
|
|
407
|
+
const gone = (pid) => pid === undefined || !(0, dev_state_1.isPidAlive)(pid);
|
|
408
|
+
// Wait for the test App to answer.
|
|
391
409
|
if (appUrl) {
|
|
392
410
|
log.info(log.dim(`Waiting for ${appUrl} …`));
|
|
393
|
-
yield (0, dev_process_1.waitForHttp)(appUrl, 90000);
|
|
411
|
+
const appReady = yield (0, dev_process_1.waitForHttp)(appUrl, 90000, isStackServing, () => gone(pids.app));
|
|
412
|
+
if (!appReady)
|
|
413
|
+
throw unreachableStackError('App', appUrl, appLogPath);
|
|
394
414
|
}
|
|
395
|
-
// Wait for the test API to actually SERVE
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
// `ensureApiReachableOrSkip` guard (the API-readiness race).
|
|
399
|
-
// required: Caddy answers 502 while its upstream is still booting, which the
|
|
400
|
-
// default (lenient) predicate would accept as "up".
|
|
415
|
+
// Wait for the test API to actually SERVE before handing off to Playwright.
|
|
416
|
+
// Previously bringUp only waited for the App, so a compiled API still
|
|
417
|
+
// connecting to its database made the first specs skip via the suite's
|
|
418
|
+
// `ensureApiReachableOrSkip` guard (the API-readiness race).
|
|
401
419
|
if (apiUrl) {
|
|
402
420
|
log.info(log.dim(`Waiting for ${apiUrl}/meta …`));
|
|
403
|
-
const apiReady = yield (0, dev_process_1.waitForHttp)(`${apiUrl}/meta`, 120000, (
|
|
421
|
+
const apiReady = yield (0, dev_process_1.waitForHttp)(`${apiUrl}/meta`, 120000, isStackServing, () => gone(pids.api));
|
|
404
422
|
if (!apiReady)
|
|
405
|
-
|
|
423
|
+
throw unreachableStackError('API', `${apiUrl}/meta`, apiLogPath);
|
|
406
424
|
}
|
|
407
|
-
// Expose THIS stack's API log path so the caller can point the auth E2E specs
|
|
408
|
-
// (via NEST_SERVER_LOG) at the exact isolated log — correct per shard.
|
|
409
|
-
const apiLogPath = layout.apiDir ? (0, path_1.join)(layout.root, '.lt-dev', names.apiLog) : undefined;
|
|
410
425
|
return { apiLogPath, apiUrl, appEnv, appUrl, dbName, pids, testIdentity };
|
|
411
426
|
});
|
|
412
427
|
}
|
|
@@ -467,6 +482,41 @@ function buildTestAppEnv(appEnv) {
|
|
|
467
482
|
function hasTestSession(root) {
|
|
468
483
|
return (0, dev_state_1.loadSession)(root, dev_state_1.TEST_SESSION_FILE) !== null;
|
|
469
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Whether an HTTP status proves the component ITSELF answered.
|
|
487
|
+
*
|
|
488
|
+
* Readiness here means liveness, not health: the question is "did my process get
|
|
489
|
+
* far enough to serve HTTP", not "did it like this particular request". So a 404
|
|
490
|
+
* and a 500 both count as ready — they came FROM the upstream — while Caddy's
|
|
491
|
+
* gateway errors do not, because Caddy returns those for the entire window in
|
|
492
|
+
* which its upstream is not yet listening.
|
|
493
|
+
*
|
|
494
|
+
* The 404 case is the load-bearing one. The API probe targets `/meta`, an
|
|
495
|
+
* endpoint `@lenne.tech/nest-server` provides and `nest-base` does not: a healthy
|
|
496
|
+
* nest-base API answers 404 there (verified against a live one). The previous
|
|
497
|
+
* strict-2xx predicate was harmless while a failed wait only logged a warning,
|
|
498
|
+
* but this module now ABORTS on it — under strict 2xx that would block
|
|
499
|
+
* `lt dev test` for every nest-base project on a perfectly healthy API, which is
|
|
500
|
+
* worse than the false-green it replaces. Probing a list of per-framework paths
|
|
501
|
+
* would work too, but it needs updating for every new template; "something other
|
|
502
|
+
* than the proxy answered" needs no such list.
|
|
503
|
+
*/
|
|
504
|
+
function isStackServing(status) {
|
|
505
|
+
return status > 0 && ![502, 503, 504].includes(status);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* The abort thrown when the test DB cannot be migrated.
|
|
509
|
+
*
|
|
510
|
+
* An abort rather than a warning for the same reason as `unreachableStackError`:
|
|
511
|
+
* a suite run on an un-migrated DB tests a data state no environment has, and
|
|
512
|
+
* whatever it reports is about the test setup, not about the feature. Unlike the
|
|
513
|
+
* API's boot, `migrate:up` runs in the foreground, so its output is already on
|
|
514
|
+
* screen right above this message.
|
|
515
|
+
*/
|
|
516
|
+
function migrationFailedError(dbName, exitCode) {
|
|
517
|
+
return new Error(`migrate:up failed (exit ${String(exitCode)}) against the test DB "${dbName}" — test API not started, ` +
|
|
518
|
+
'because a suite on an un-migrated DB tests a state no environment has. The migration output is above.');
|
|
519
|
+
}
|
|
470
520
|
/** Build the dedicated test identity + test DB name for a project. */
|
|
471
521
|
function resolveTestSession(layout, baseIdentity, shardIndex, devDbName) {
|
|
472
522
|
const names = testStackNames(shardIndex);
|
|
@@ -551,6 +601,50 @@ function runShardedTestSession(layout, baseIdentity, log, opts) {
|
|
|
551
601
|
function shardReportDir(root, shardIndex) {
|
|
552
602
|
return (0, path_1.join)(root, '.lt-dev', `shard.${shardIndex}.playwright-report`);
|
|
553
603
|
}
|
|
604
|
+
/**
|
|
605
|
+
* Start the test stack's API: build → apply pending migrations → start the
|
|
606
|
+
* compiled bundle, or fall back to the project's own `start` script when no
|
|
607
|
+
* bundle is available.
|
|
608
|
+
*
|
|
609
|
+
* The compiled start bypasses that `start` script and therefore the
|
|
610
|
+
* `migrate:up` it chains in front of the server. The test DB outlives every
|
|
611
|
+
* run — the CLI never drops it — so without the explicit step here it drifted
|
|
612
|
+
* behind every environment that does migrate (DEV-3289). The migration runs
|
|
613
|
+
* with `apiEnv`, the API's own env, so both resolve the same database, and
|
|
614
|
+
* before the spawn, because the API syncs its indexes against whatever it finds
|
|
615
|
+
* on boot. A failed migration throws `migrationFailedError`.
|
|
616
|
+
*
|
|
617
|
+
* `skipBuild` (sibling shards) reuses the dist the first shard produced but
|
|
618
|
+
* still migrates: every shard has a database of its own. The fallback does not
|
|
619
|
+
* migrate itself — the `start` script it runs owns that, exactly as under
|
|
620
|
+
* `lt dev up`. Per-directory package manager, like `lt dev up`: a monorepo with
|
|
621
|
+
* an npm API and a pnpm App must drive each with its own.
|
|
622
|
+
*/
|
|
623
|
+
function startTestApi(opts) {
|
|
624
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
625
|
+
const { apiDir, apiEnv, dbName, log, logFile, skipBuild } = opts;
|
|
626
|
+
const apiPm = (0, dev_package_manager_1.pickPackageManager)(apiDir);
|
|
627
|
+
let build = 0;
|
|
628
|
+
if (!skipBuild) {
|
|
629
|
+
log.info(log.dim('Building API (compiled, for stable long runs) …'));
|
|
630
|
+
build = yield (0, dev_process_1.runChildInherit)(apiPm.bin, apiPm.runScript('build'), { cwd: apiDir, env: process.env });
|
|
631
|
+
}
|
|
632
|
+
const entry = (0, dev_api_launch_1.findCompiledEntry)(apiDir);
|
|
633
|
+
if (build === 0 && entry) {
|
|
634
|
+
const migration = yield (0, dev_api_launch_1.applyPendingMigrations)({ apiDir, env: apiEnv, pm: apiPm });
|
|
635
|
+
if (migration.status === 'failed')
|
|
636
|
+
throw migrationFailedError(dbName, migration.exitCode);
|
|
637
|
+
if (migration.status === 'skipped')
|
|
638
|
+
log.info(log.dim(`no migrate:up script — ${dbName} is not migrated`));
|
|
639
|
+
// Runtime follows the PROJECT, never a hardcoded default: a `nest-base` API
|
|
640
|
+
// bundles with `Bun.build({ target: 'bun' })` and dies under node with
|
|
641
|
+
// "__require is not a function" (DEV-3208).
|
|
642
|
+
return (0, dev_process_1.spawnDetached)((0, dev_api_launch_1.resolveApiRuntime)(apiDir), [entry], { cwd: apiDir, env: apiEnv, logFile });
|
|
643
|
+
}
|
|
644
|
+
log.warn(`compiled API not available — falling back to \`${apiPm.bin} start\` (ts-node).`);
|
|
645
|
+
return (0, dev_process_1.spawnDetached)(apiPm.bin, apiPm.runScript('start'), { cwd: apiDir, env: apiEnv, logFile });
|
|
646
|
+
});
|
|
647
|
+
}
|
|
554
648
|
/**
|
|
555
649
|
* Tear down the unsharded test stack AND every sharded stack discovered on disk
|
|
556
650
|
* (`state.test.<i>.json` in `.lt-dev/`). Used by `lt dev test down` so a
|
|
@@ -661,6 +755,23 @@ function tearDownTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
661
755
|
function testAppEntryCandidates() {
|
|
662
756
|
return [`${exports.TEST_NITRO_OUTPUT_DIR}/server/index.mjs`, '.output/server/index.mjs'];
|
|
663
757
|
}
|
|
758
|
+
/**
|
|
759
|
+
* The abort thrown when a test-stack component never becomes reachable.
|
|
760
|
+
*
|
|
761
|
+
* Phrased as an abort, not a diagnosis, because the cause lives in the component's
|
|
762
|
+
* own log and nowhere else — the process is detached, so its stderr never reaches
|
|
763
|
+
* this terminal. Naming the log file is therefore the whole value of the message:
|
|
764
|
+
* an abort that only says "did not come up" moves the guesswork one step later
|
|
765
|
+
* instead of ending it.
|
|
766
|
+
*
|
|
767
|
+
* `lt dev test` catches this, reports it, tears the stack down and exits non-zero,
|
|
768
|
+
* so Playwright never starts (DEV-3208).
|
|
769
|
+
*/
|
|
770
|
+
function unreachableStackError(component, url, logFile) {
|
|
771
|
+
const where = logFile ? ` Why it never came up: ${logFile}` : '';
|
|
772
|
+
return new Error(`Test ${component} never became reachable at ${url} — aborting before Playwright runs, ` +
|
|
773
|
+
`because a suite run against a dead ${component} passes trivially and proves nothing.${where}`);
|
|
774
|
+
}
|
|
664
775
|
/**
|
|
665
776
|
* Resolve the per-stack file/identity names. For a sharded run (`shardIndex`
|
|
666
777
|
* given) everything gets a `.<i>` / `-<i>` suffix so N stacks coexist without
|
package/build/lib/fail-run.js
CHANGED
|
@@ -20,6 +20,17 @@ exports.failRun = failRun;
|
|
|
20
20
|
* buffered output, including the spinner's own failure message, which is the one
|
|
21
21
|
* line the operator actually needs.
|
|
22
22
|
*
|
|
23
|
+
* That choice carries a precondition worth stating, because it was violated:
|
|
24
|
+
* setting `process.exitCode` only ends the process once **the event loop drains**,
|
|
25
|
+
* and `bin/lt` has no terminal `process.exit()` after `run()`. A running ora
|
|
26
|
+
* spinner holds the loop open on two counts — its `setInterval` for the animation
|
|
27
|
+
* and the `stdin.resume()` its stdin discarder performs. So a command that called
|
|
28
|
+
* `failRun` and returned while a spinner was still spinning did not exit at all:
|
|
29
|
+
* no message, no prompt back, and the terminal left in raw mode. The remedy is to
|
|
30
|
+
* stop the spinner, NOT to switch this helper to `process.exit()` — that would
|
|
31
|
+
* trade a hang for the truncated-output bug this helper exists to avoid.
|
|
32
|
+
* `__tests__/spinner-lifetime.test.ts` enforces the spinner half.
|
|
33
|
+
*
|
|
23
34
|
* **Guarded by `fromGluegunMenu`**, like the CLI's other exit-code call sites
|
|
24
35
|
* (`dev test`, `dev tunnel`, `tools ocr`, `workspace-integration`): inside the
|
|
25
36
|
* interactive `lt` menu a command is one step of a longer session, and failing
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.nonInteractiveGitEnv = nonInteractiveGitEnv;
|
|
4
|
+
/**
|
|
5
|
+
* The environment git subprocesses run with so they never block on a prompt.
|
|
6
|
+
*
|
|
7
|
+
* Two variables, one rule each:
|
|
8
|
+
*
|
|
9
|
+
* `GIT_TERMINAL_PROMPT=0` stops git asking for credentials on a dead terminal.
|
|
10
|
+
* A prompt in a detached or CI child is not a question anyone can answer — it is
|
|
11
|
+
* a hang that looks like a slow network.
|
|
12
|
+
*
|
|
13
|
+
* `GIT_SSH_COMMAND` only supplies a DEFAULT, and the distinction is expensive.
|
|
14
|
+
* `BatchMode=yes` makes ssh fail rather than PROMPT — but an agent that stalls is
|
|
15
|
+
* not a prompt. On a 1Password-backed machine the agent is reachable and every
|
|
16
|
+
* signature needs interactive approval; unattended it waits, then reports
|
|
17
|
+
* `communication with agent failed`. Measured at 61s per fetch, which made
|
|
18
|
+
* `lt git update --dry-run` take 62s and `lt git create --dry-run` 123s (two
|
|
19
|
+
* fetches). `ConnectTimeout` does not bound that — it covers the TCP connect,
|
|
20
|
+
* not the agent.
|
|
21
|
+
*
|
|
22
|
+
* Waiting for a human to approve a key is legitimate for an interactive command,
|
|
23
|
+
* so the default itself is unchanged. What was wrong was assigning it
|
|
24
|
+
* UNCONDITIONALLY: that overrode anyone who had configured ssh deliberately,
|
|
25
|
+
* including a test harness trying to make the behaviour deterministic. A
|
|
26
|
+
* caller's own value therefore always wins (`IdentityAgent=none` in their env
|
|
27
|
+
* drops the same fetch to ~1s with a clean `Permission denied (publickey)`); the
|
|
28
|
+
* fallback only keeps an unconfigured machine from hanging.
|
|
29
|
+
*
|
|
30
|
+
* This exists as an environment object rather than a shell prefix because
|
|
31
|
+
* `VAR=value git …` is not assignment syntax to cmd.exe — it reads the whole
|
|
32
|
+
* thing as a command name and fails with "'GIT_TERMINAL_PROMPT' is not
|
|
33
|
+
* recognized". Handing the child an env works on every platform, and it is the
|
|
34
|
+
* only spelling that does. Keep it here rather than inline at each call site:
|
|
35
|
+
* this is the single place the default is defined, which is what lets a test
|
|
36
|
+
* assert that nobody assigns GIT_SSH_COMMAND unconditionally anywhere else.
|
|
37
|
+
*
|
|
38
|
+
* One caveat if you ever read a variable back off the returned object: spreading
|
|
39
|
+
* `process.env` produces a PLAIN object, which on Windows loses the
|
|
40
|
+
* case-insensitive lookup the real `process.env` proxy provides. The value is
|
|
41
|
+
* intact under its original spelling (`Path`, not `PATH`), and handing the whole
|
|
42
|
+
* object to `spawn` works — but `result.PATH` is undefined there.
|
|
43
|
+
*/
|
|
44
|
+
function nonInteractiveGitEnv(base = process.env) {
|
|
45
|
+
return Object.assign(Object.assign({}, base), { GIT_SSH_COMMAND: base.GIT_SSH_COMMAND || 'ssh -o ConnectTimeout=5 -o BatchMode=yes', GIT_TERMINAL_PROMPT: '0' });
|
|
46
|
+
}
|
|
@@ -38,8 +38,11 @@ const LINT_STAGED_FILES = [
|
|
|
38
38
|
*/
|
|
39
39
|
function findDangerousFixFlagUsage(appDir, workspaceRoot) {
|
|
40
40
|
const base = workspaceRoot !== null && workspaceRoot !== void 0 ? workspaceRoot : appDir;
|
|
41
|
-
const files = [
|
|
42
|
-
|
|
41
|
+
const files = [
|
|
42
|
+
...(workspaceRoot ? [(0, path_1.join)(workspaceRoot, 'scripts', 'check.mjs')] : []),
|
|
43
|
+
...fixFlagFiles(appDir, workspaceRoot),
|
|
44
|
+
];
|
|
45
|
+
return [...new Set(files)].filter((file) => usesDangerousFixFlag(file)).map((file) => repoRelative(base, file));
|
|
43
46
|
}
|
|
44
47
|
/**
|
|
45
48
|
* Remove `--fix-suggestions` and `--fix-dangerously` from the project's lint commands
|
|
@@ -57,7 +60,7 @@ function healDangerousOxlintFixFlags(appDir, workspaceRoot) {
|
|
|
57
60
|
if (!usesDangerousFixFlag(file)) {
|
|
58
61
|
continue;
|
|
59
62
|
}
|
|
60
|
-
const rel = (
|
|
63
|
+
const rel = repoRelative(base, file);
|
|
61
64
|
const dir = (0, path_1.join)(file, '..');
|
|
62
65
|
const name = (0, path_1.relative)(dir, file);
|
|
63
66
|
if (isSymlink(file)) {
|
|
@@ -203,6 +206,20 @@ function isTracked(dir, relPath) {
|
|
|
203
206
|
return false;
|
|
204
207
|
}
|
|
205
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* `file` relative to `base`, always with forward slashes.
|
|
211
|
+
*
|
|
212
|
+
* These strings are reported to the user next to git facts ("not tracked by
|
|
213
|
+
* git", "uncommitted changes") and name files inside the repository, which git
|
|
214
|
+
* itself always spells with `/`. `relative()` returns the OS-native form, so on
|
|
215
|
+
* Windows the report would say `projects\app\package.json` while `git status`
|
|
216
|
+
* says `projects/app/package.json` for the same file. Falls back to the
|
|
217
|
+
* absolute path when there is no relative form (`base` IS the file).
|
|
218
|
+
*/
|
|
219
|
+
function repoRelative(base, file) {
|
|
220
|
+
const rel = (0, path_1.relative)(base, file);
|
|
221
|
+
return rel ? rel.split(path_1.sep).join('/') : file;
|
|
222
|
+
}
|
|
206
223
|
function usesDangerousFixFlag(file) {
|
|
207
224
|
try {
|
|
208
225
|
// Follows a symlink on purpose: a linked config still feeds the lint run, so it
|
|
@@ -90,9 +90,10 @@ const ts = __importStar(require("typescript"));
|
|
|
90
90
|
* Detection runs on the TypeScript AST, not on regex-stripped text. A regex
|
|
91
91
|
* "lexer" has no string/template/regex-literal state, so a `/*` or `//` inside a
|
|
92
92
|
* literal earlier in the file silently erased the guard and triggered the very
|
|
93
|
-
* overwrite this function must avoid.
|
|
94
|
-
*
|
|
95
|
-
*
|
|
93
|
+
* overwrite this function must avoid. Scanning is not enough either: a standalone
|
|
94
|
+
* `ts.createScanner` loop desynchronises on the first interpolating template
|
|
95
|
+
* literal (that is what retired `lib/strip-comments.ts`). The AST solves comments
|
|
96
|
+
* AND nesting in one step, and recognises a backtick require for free.
|
|
96
97
|
*
|
|
97
98
|
* ## Recoverability
|
|
98
99
|
*
|