@lenne.tech/cli 1.49.0 → 1.50.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/doctor.js +27 -10
- package/build/commands/dev/down.js +48 -5
- package/build/commands/dev/status.js +32 -8
- package/build/commands/dev/up.js +59 -21
- package/build/extensions/server.js +1 -1
- package/build/lib/caddy.js +17 -10
- package/build/lib/check-template-sync.js +1 -1
- package/build/lib/dev-env-bridge.js +7 -0
- package/build/lib/dev-env.js +44 -4
- package/build/lib/dev-log-tail.js +91 -0
- package/build/lib/dev-patches.js +15 -5
- package/build/lib/dev-process.js +362 -127
- package/build/lib/dev-project.js +14 -1
- package/build/lib/dev-prune.js +15 -5
- package/build/lib/dev-service.js +29 -15
- package/build/lib/dev-state.js +40 -4
- package/build/lib/dev-test-session.js +58 -36
- package/build/lib/dev-ticket.js +13 -1
- package/build/lib/hoist-workspace-pnpm-config.js +17 -6
- package/build/lib/platform.js +53 -1
- package/build/templates/nest-server-starter/README.md.ejs +13 -6
- package/package.json +4 -1
package/build/lib/dev-prune.js
CHANGED
|
@@ -43,6 +43,7 @@ const child_process_1 = require("child_process");
|
|
|
43
43
|
const fs_1 = require("fs");
|
|
44
44
|
const path_1 = require("path");
|
|
45
45
|
const dev_project_1 = require("./dev-project");
|
|
46
|
+
const dev_state_1 = require("./dev-state");
|
|
46
47
|
const dev_ticket_1 = require("./dev-ticket");
|
|
47
48
|
/** Branch prefix `lt ticket start` creates worktrees on. */
|
|
48
49
|
const TICKET_BRANCH_PREFIX = 'feat/';
|
|
@@ -188,9 +189,15 @@ function planOrphanTicketDbSweep(args) {
|
|
|
188
189
|
* never auto-dropped).
|
|
189
190
|
*/
|
|
190
191
|
function planRegistryPrune(registry) {
|
|
191
|
-
return Object.entries(registry.projects)
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
return (Object.entries(registry.projects)
|
|
193
|
+
// A relative stored path is not evidence of anything: `existsSync` resolves
|
|
194
|
+
// it against the current process's cwd, so a perfectly live project reads as
|
|
195
|
+
// gone from any other directory — and pruning it reclaims its slug and its
|
|
196
|
+
// reserved internal ports for the next project that asks. "Cannot verify" is
|
|
197
|
+
// not "absent"; these are left alone until a registry write from inside the
|
|
198
|
+
// project replaces the path with an absolute one.
|
|
199
|
+
.filter(([, entry]) => (0, dev_state_1.isVerifiablePath)(entry.path) && !(0, fs_1.existsSync)(entry.path))
|
|
200
|
+
.map(([key]) => key));
|
|
194
201
|
}
|
|
195
202
|
/**
|
|
196
203
|
* Orphaned smoke-test databases. The `/lt-dev:fullstack:smoke-test` command
|
|
@@ -218,12 +225,15 @@ function planSmokeTestDbSweep(args) {
|
|
|
218
225
|
const entries = Object.entries(registry.projects);
|
|
219
226
|
const smokeRunLive = entries.some(([key, entry]) => (key === exports.SMOKE_TEST_DB_PREFIX || key.startsWith(`${exports.SMOKE_TEST_DB_PREFIX}-`)) &&
|
|
220
227
|
entry.path &&
|
|
221
|
-
(0, fs_1.existsSync)(entry.path));
|
|
228
|
+
(!(0, dev_state_1.isVerifiablePath)(entry.path) || (0, fs_1.existsSync)(entry.path)));
|
|
222
229
|
if (smokeRunLive)
|
|
223
230
|
return [];
|
|
224
231
|
const protectedNames = new Set();
|
|
225
232
|
for (const [, entry] of entries) {
|
|
226
|
-
|
|
233
|
+
// `!isVerifiablePath` → we cannot prove the checkout is gone, so the database
|
|
234
|
+
// keeps its protection. Erring the other way would expose a live project's DB
|
|
235
|
+
// to the orphan sweep on the strength of a path we know we cannot read.
|
|
236
|
+
if (entry.dbName && entry.path && (!(0, dev_state_1.isVerifiablePath)(entry.path) || (0, fs_1.existsSync)(entry.path))) {
|
|
227
237
|
protectedNames.add(entry.dbName);
|
|
228
238
|
protectedNames.add((0, dev_project_1.deriveTestDbName)(entry.dbName));
|
|
229
239
|
}
|
package/build/lib/dev-service.js
CHANGED
|
@@ -51,6 +51,8 @@ const fs_1 = require("fs");
|
|
|
51
51
|
const os_1 = require("os");
|
|
52
52
|
const path_1 = require("path");
|
|
53
53
|
const caddy_1 = require("./caddy");
|
|
54
|
+
const dev_process_1 = require("./dev-process");
|
|
55
|
+
const platform_1 = require("./platform");
|
|
54
56
|
/**
|
|
55
57
|
* Resolve the user's home directory in a test-overridable way.
|
|
56
58
|
*
|
|
@@ -139,12 +141,12 @@ function installService() {
|
|
|
139
141
|
ok: false,
|
|
140
142
|
};
|
|
141
143
|
}
|
|
142
|
-
const caddyBin = opts.caddyBin || (yield resolveCaddyBin());
|
|
144
|
+
const caddyBin = opts.caddyBin || (yield resolveCaddyBin(opts.lookup));
|
|
143
145
|
if (!caddyBin) {
|
|
144
146
|
return {
|
|
145
147
|
bootstrapped: false,
|
|
146
148
|
created: false,
|
|
147
|
-
message:
|
|
149
|
+
message: caddyMissingMessage(),
|
|
148
150
|
ok: false,
|
|
149
151
|
};
|
|
150
152
|
}
|
|
@@ -249,14 +251,19 @@ StandardError=append:${cfg.errFile}
|
|
|
249
251
|
WantedBy=default.target
|
|
250
252
|
`;
|
|
251
253
|
}
|
|
252
|
-
/**
|
|
254
|
+
/**
|
|
255
|
+
* Absolute path of `caddy`, so launchd/systemd get a guaranteed path rather than
|
|
256
|
+
* a PATH lookup in an environment they do not share with the user's shell.
|
|
257
|
+
*
|
|
258
|
+
* Resolved in-process instead of by spawning `which`: there is no `which` on
|
|
259
|
+
* Windows, and `where.exe` prints every match rather than the first. The async
|
|
260
|
+
* signature is kept because callers await it and a future platform may need to
|
|
261
|
+
* ask something that does spawn.
|
|
262
|
+
*/
|
|
253
263
|
function resolveCaddyBin() {
|
|
254
|
-
return __awaiter(this,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return undefined;
|
|
258
|
-
const line = r.stdout.split('\n').find((s) => s.trim().length > 0);
|
|
259
|
-
return line ? line.trim() : undefined;
|
|
264
|
+
return __awaiter(this, arguments, void 0, function* (options = {}) {
|
|
265
|
+
var _a;
|
|
266
|
+
return (_a = (0, platform_1.findExecutable)('caddy', options)) !== null && _a !== void 0 ? _a : undefined;
|
|
260
267
|
});
|
|
261
268
|
}
|
|
262
269
|
/** Inject a custom runner (tests). Pass `null` to reset to the real spawner. */
|
|
@@ -315,6 +322,16 @@ function waitForServiceReady() {
|
|
|
315
322
|
return false;
|
|
316
323
|
});
|
|
317
324
|
}
|
|
325
|
+
/** Install hint for the current platform — `brew` is not advice on Linux or Windows. */
|
|
326
|
+
function caddyMissingMessage() {
|
|
327
|
+
if (process.platform === 'darwin') {
|
|
328
|
+
return 'caddy not found on PATH. Install with `brew install caddy`.';
|
|
329
|
+
}
|
|
330
|
+
if (process.platform === 'win32') {
|
|
331
|
+
return 'caddy not found on PATH. Install with `winget install CaddyServer.Caddy` or `scoop install caddy`.';
|
|
332
|
+
}
|
|
333
|
+
return 'caddy not found on PATH. Install it with your package manager.';
|
|
334
|
+
}
|
|
318
335
|
function defaultShellRunner(cmd, args) {
|
|
319
336
|
return new Promise((resolve) => {
|
|
320
337
|
var _a, _b;
|
|
@@ -396,13 +413,10 @@ function installLinux(paths, changed, existed) {
|
|
|
396
413
|
};
|
|
397
414
|
});
|
|
398
415
|
}
|
|
416
|
+
/** See `caddy.ts#caddyDaemonRunning` for why this is not a `curl` any more. */
|
|
399
417
|
function pingCaddyAdmin() {
|
|
400
|
-
return
|
|
401
|
-
|
|
402
|
-
stdio: ['ignore', 'ignore', 'ignore'],
|
|
403
|
-
});
|
|
404
|
-
child.on('error', () => resolve(false));
|
|
405
|
-
child.on('close', (code) => resolve(code === 0));
|
|
418
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
419
|
+
return (yield (0, dev_process_1.httpStatus)('http://127.0.0.1:2019/config/', 1000)) !== null;
|
|
406
420
|
});
|
|
407
421
|
}
|
|
408
422
|
function sleep(ms) {
|
package/build/lib/dev-state.js
CHANGED
|
@@ -18,6 +18,7 @@ exports.clearSession = clearSession;
|
|
|
18
18
|
exports.detectSlugConflict = detectSlugConflict;
|
|
19
19
|
exports.isPidAlive = isPidAlive;
|
|
20
20
|
exports.isValidPid = isValidPid;
|
|
21
|
+
exports.isVerifiablePath = isVerifiablePath;
|
|
21
22
|
exports.loadRegistry = loadRegistry;
|
|
22
23
|
exports.loadSession = loadSession;
|
|
23
24
|
exports.sameRealPath = sameRealPath;
|
|
@@ -142,13 +143,33 @@ function clearSession(root, sessionFile = SESSION_FILE) {
|
|
|
142
143
|
*/
|
|
143
144
|
function detectSlugConflict(slug, root) {
|
|
144
145
|
const entry = loadRegistry().projects[slug];
|
|
145
|
-
|
|
146
|
+
// A relative stored path cannot be compared to anything: its base is lost, so
|
|
147
|
+
// it names a different directory from every cwd. Entries like that were written
|
|
148
|
+
// by `lt fullstack init` before the path was absolutised, and comparing one
|
|
149
|
+
// produced "slug 't-5' is also registered to another checkout: t-5". Claiming a
|
|
150
|
+
// conflict we cannot establish is worse than staying quiet — the next registry
|
|
151
|
+
// write from inside the project repairs the entry anyway.
|
|
152
|
+
if (!(entry === null || entry === void 0 ? void 0 : entry.path) || !isVerifiablePath(entry.path) || sameRealPath(entry.path, root))
|
|
146
153
|
return null;
|
|
147
154
|
const session = loadSession(entry.path);
|
|
148
155
|
const otherSessionAlive = !!session && [session.pids.api, session.pids.app].some((p) => typeof p === 'number' && isPidAlive(p));
|
|
149
156
|
return { otherPath: entry.path, otherSessionAlive };
|
|
150
157
|
}
|
|
151
|
-
/**
|
|
158
|
+
/**
|
|
159
|
+
* Check whether a process with the given PID is currently alive.
|
|
160
|
+
*
|
|
161
|
+
* Signal 0 delivers nothing; it only asks the kernel whether the target exists
|
|
162
|
+
* and may be signalled. The two failures mean opposite things, and collapsing
|
|
163
|
+
* them was a bug:
|
|
164
|
+
*
|
|
165
|
+
* - `ESRCH` — no such process. Dead.
|
|
166
|
+
* - `EPERM` — **the process EXISTS**, this user may not signal it.
|
|
167
|
+
*
|
|
168
|
+
* Reading `EPERM` as dead made `classifyComponentHealth` report a running
|
|
169
|
+
* component as `dead`, so `lt dev up` killed and restarted a healthy stack.
|
|
170
|
+
* Reproducible on any POSIX machine: `process.kill(1, 0)` throws `EPERM`.
|
|
171
|
+
* More likely on Windows, where an elevated shell is ordinary.
|
|
172
|
+
*/
|
|
152
173
|
function isPidAlive(pid) {
|
|
153
174
|
if (!isValidPid(pid))
|
|
154
175
|
return false;
|
|
@@ -156,14 +177,29 @@ function isPidAlive(pid) {
|
|
|
156
177
|
process.kill(pid, 0);
|
|
157
178
|
return true;
|
|
158
179
|
}
|
|
159
|
-
catch (
|
|
160
|
-
return
|
|
180
|
+
catch (err) {
|
|
181
|
+
return (err === null || err === void 0 ? void 0 : err.code) === 'EPERM';
|
|
161
182
|
}
|
|
162
183
|
}
|
|
163
184
|
/** Validate a PID — positive integer, within plausible range. */
|
|
164
185
|
function isValidPid(pid) {
|
|
165
186
|
return typeof pid === 'number' && Number.isInteger(pid) && pid > 0 && pid < 4194304;
|
|
166
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* True when a stored registry path can be checked against the file system.
|
|
190
|
+
*
|
|
191
|
+
* A relative path in the registry has lost the directory it was relative TO, so
|
|
192
|
+
* `existsSync` / `realpathSync` answer about whatever directory the current
|
|
193
|
+
* process started in. Every such answer is meaningless, and two of them were
|
|
194
|
+
* actively harmful: `planRegistryPrune` reclaimed a live project's slug and
|
|
195
|
+
* reserved ports, and `detectSlugConflict` reported a project as colliding with
|
|
196
|
+
* itself. Legacy entries are therefore treated as "cannot determine" rather than
|
|
197
|
+
* as "gone"; running any `lt dev` command inside the project rewrites the entry
|
|
198
|
+
* with an absolute path and clears the condition.
|
|
199
|
+
*/
|
|
200
|
+
function isVerifiablePath(storedPath) {
|
|
201
|
+
return !!storedPath && (0, path_1.isAbsolute)(storedPath);
|
|
202
|
+
}
|
|
167
203
|
/** Load the central registry; returns an empty one if missing or unreadable. */
|
|
168
204
|
function loadRegistry() {
|
|
169
205
|
if (!(0, fs_1.existsSync)(REGISTRY_PATH))
|
|
@@ -1,31 +1,4 @@
|
|
|
1
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.TEST_NITRO_OUTPUT_DIR = exports.TEST_NUXT_BUILD_DIR = exports.TEST_INITIAL_ADMIN_ENV = void 0;
|
|
13
|
-
exports.ensurePlaywrightBrowsers = ensurePlaywrightBrowsers;
|
|
14
|
-
exports.autoShardCount = autoShardCount;
|
|
15
|
-
exports.bringUpTestSession = bringUpTestSession;
|
|
16
|
-
exports.buildShardPlaywrightInvocation = buildShardPlaywrightInvocation;
|
|
17
|
-
exports.buildTestAppEnv = buildTestAppEnv;
|
|
18
|
-
exports.hasTestSession = hasTestSession;
|
|
19
|
-
exports.isStackServing = isStackServing;
|
|
20
|
-
exports.migrationFailedError = migrationFailedError;
|
|
21
|
-
exports.resolveTestSession = resolveTestSession;
|
|
22
|
-
exports.runShardedTestSession = runShardedTestSession;
|
|
23
|
-
exports.shardReportDir = shardReportDir;
|
|
24
|
-
exports.startTestApi = startTestApi;
|
|
25
|
-
exports.tearDownAllTestSessions = tearDownAllTestSessions;
|
|
26
|
-
exports.tearDownTestSession = tearDownTestSession;
|
|
27
|
-
exports.testAppEntryCandidates = testAppEntryCandidates;
|
|
28
|
-
exports.unreachableStackError = unreachableStackError;
|
|
29
2
|
/**
|
|
30
3
|
* Ephemeral, isolated test session for `lt dev test`.
|
|
31
4
|
*
|
|
@@ -56,7 +29,33 @@ exports.unreachableStackError = unreachableStackError;
|
|
|
56
29
|
* Teardown is idempotent and residue-free (processes, Caddy block, env bridge,
|
|
57
30
|
* session file, registry entry), so a stale session is always safely reclaimed.
|
|
58
31
|
*/
|
|
59
|
-
|
|
32
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
33
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
34
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
35
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
36
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
37
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
38
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.TEST_NITRO_OUTPUT_DIR = exports.TEST_NUXT_BUILD_DIR = exports.TEST_INITIAL_ADMIN_ENV = void 0;
|
|
43
|
+
exports.ensurePlaywrightBrowsers = ensurePlaywrightBrowsers;
|
|
44
|
+
exports.autoShardCount = autoShardCount;
|
|
45
|
+
exports.bringUpTestSession = bringUpTestSession;
|
|
46
|
+
exports.buildShardPlaywrightInvocation = buildShardPlaywrightInvocation;
|
|
47
|
+
exports.buildTestAppEnv = buildTestAppEnv;
|
|
48
|
+
exports.hasTestSession = hasTestSession;
|
|
49
|
+
exports.isStackServing = isStackServing;
|
|
50
|
+
exports.migrationFailedError = migrationFailedError;
|
|
51
|
+
exports.resolveTestSession = resolveTestSession;
|
|
52
|
+
exports.runShardedTestSession = runShardedTestSession;
|
|
53
|
+
exports.shardReportDir = shardReportDir;
|
|
54
|
+
exports.startTestApi = startTestApi;
|
|
55
|
+
exports.tearDownAllTestSessions = tearDownAllTestSessions;
|
|
56
|
+
exports.tearDownTestSession = tearDownTestSession;
|
|
57
|
+
exports.testAppEntryCandidates = testAppEntryCandidates;
|
|
58
|
+
exports.unreachableStackError = unreachableStackError;
|
|
60
59
|
const fs_1 = require("fs");
|
|
61
60
|
const os_1 = require("os");
|
|
62
61
|
const path_1 = require("path");
|
|
@@ -69,6 +68,7 @@ const dev_package_manager_1 = require("./dev-package-manager");
|
|
|
69
68
|
const dev_patches_1 = require("./dev-patches");
|
|
70
69
|
const dev_process_1 = require("./dev-process");
|
|
71
70
|
const dev_project_1 = require("./dev-project");
|
|
71
|
+
const platform_1 = require("./platform");
|
|
72
72
|
const dev_state_1 = require("./dev-state");
|
|
73
73
|
/**
|
|
74
74
|
* Make sure the Playwright browser build the app's OWN playwright version needs
|
|
@@ -83,12 +83,22 @@ const dev_state_1 = require("./dev-state");
|
|
|
83
83
|
* fails (offline), the suite still runs and Playwright's own error explains it.
|
|
84
84
|
*/
|
|
85
85
|
function ensurePlaywrightBrowsers(appDir, pm, logInfo) {
|
|
86
|
+
const note = () => {
|
|
87
|
+
logInfo === null || logInfo === void 0 ? void 0 : logInfo('playwright install chromium failed — continuing (the suite reports missing browsers itself).');
|
|
88
|
+
};
|
|
86
89
|
try {
|
|
87
90
|
const args = pm.exec('playwright', ['install', 'chromium']);
|
|
88
|
-
(
|
|
91
|
+
// `spawnCmdSync` (spawnSync) does NOT throw on a non-zero exit, unlike the
|
|
92
|
+
// `execFileSync` this replaced — so the status is checked explicitly.
|
|
93
|
+
// Without it the failure would be silent and the suite would die later on a
|
|
94
|
+
// missing browser, which reads as a broken spec rather than a missing install.
|
|
95
|
+
const result = (0, platform_1.spawnCmdSync)(pm.bin, args, { cwd: appDir, stdio: 'ignore', timeout: 300000 });
|
|
96
|
+
if (result.error || result.status !== 0) {
|
|
97
|
+
note();
|
|
98
|
+
}
|
|
89
99
|
}
|
|
90
100
|
catch (_a) {
|
|
91
|
-
|
|
101
|
+
note();
|
|
92
102
|
}
|
|
93
103
|
}
|
|
94
104
|
const TEST_API_LOG = 'api.test.log';
|
|
@@ -240,11 +250,14 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
240
250
|
taken.add(apiPort);
|
|
241
251
|
appPort = layout.appDir ? (0, dev_state_1.allocateInternalPort)(TEST_PORT_BASE, taken) : undefined;
|
|
242
252
|
const portsToCheck = [apiPort, appPort].filter((p) => typeof p === 'number');
|
|
243
|
-
const
|
|
253
|
+
const probe = yield (0, dev_process_1.probePorts)(portsToCheck);
|
|
244
254
|
for (const p of portsToCheck) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
255
|
+
if (!probe.bound.has(p))
|
|
256
|
+
continue;
|
|
257
|
+
const owner = probe.owners.get(p);
|
|
258
|
+
throw new Error(owner
|
|
259
|
+
? `test internal port ${p} already in use by ${owner.command} (pid ${owner.pid}).`
|
|
260
|
+
: `test internal port ${p} already in use by an unidentified process.`);
|
|
248
261
|
}
|
|
249
262
|
// Reserve immediately (still under the lock) so a concurrent run sees these
|
|
250
263
|
// ports as taken. PIDs are written to the session file after spawn, below.
|
|
@@ -405,10 +418,19 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
405
418
|
// left to wait for. Being wrong here costs nothing — the timeout still bounds
|
|
406
419
|
// the wait, so this only ever makes the abort arrive sooner.
|
|
407
420
|
const gone = (pid) => pid === undefined || !(0, dev_state_1.isPidAlive)(pid);
|
|
421
|
+
// The probes go to the LOOPBACK address, not the public name: this is Node
|
|
422
|
+
// asking, and on Windows `*.localhost` does not resolve for Node at all — the
|
|
423
|
+
// waits would time out against a stack that is up, and `bringUpTestSession`
|
|
424
|
+
// aborts before Playwright ever starts. Probing the port directly also measures
|
|
425
|
+
// the component rather than the proxy, which is the better question here.
|
|
426
|
+
// The public name stays in the MESSAGES, because that is what the developer
|
|
427
|
+
// opens; the loopback pair is printed next to it above.
|
|
428
|
+
const appProbeUrl = (0, dev_env_1.internalUrl)(appPort) || appUrl;
|
|
429
|
+
const apiProbeUrl = (0, dev_env_1.internalUrl)(apiPort) || apiUrl;
|
|
408
430
|
// Wait for the test App to answer.
|
|
409
431
|
if (appUrl) {
|
|
410
432
|
log.info(log.dim(`Waiting for ${appUrl} …`));
|
|
411
|
-
const appReady = yield (0, dev_process_1.waitForHttp)(
|
|
433
|
+
const appReady = yield (0, dev_process_1.waitForHttp)(appProbeUrl, 90000, isStackServing, () => gone(pids.app));
|
|
412
434
|
if (!appReady)
|
|
413
435
|
throw unreachableStackError('App', appUrl, appLogPath);
|
|
414
436
|
}
|
|
@@ -418,7 +440,7 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
418
440
|
// `ensureApiReachableOrSkip` guard (the API-readiness race).
|
|
419
441
|
if (apiUrl) {
|
|
420
442
|
log.info(log.dim(`Waiting for ${apiUrl}/meta …`));
|
|
421
|
-
const apiReady = yield (0, dev_process_1.waitForHttp)(`${
|
|
443
|
+
const apiReady = yield (0, dev_process_1.waitForHttp)(`${apiProbeUrl}/meta`, 120000, isStackServing, () => gone(pids.api));
|
|
422
444
|
if (!apiReady)
|
|
423
445
|
throw unreachableStackError('API', `${apiUrl}/meta`, apiLogPath);
|
|
424
446
|
}
|
package/build/lib/dev-ticket.js
CHANGED
|
@@ -57,6 +57,7 @@ const dev_package_manager_1 = require("./dev-package-manager");
|
|
|
57
57
|
const dev_patches_1 = require("./dev-patches");
|
|
58
58
|
const dev_project_1 = require("./dev-project");
|
|
59
59
|
const dev_state_1 = require("./dev-state");
|
|
60
|
+
const platform_1 = require("./platform");
|
|
60
61
|
/** Marker file (under `.lt-dev/`) that tags a worktree with its ticket id. */
|
|
61
62
|
const TICKET_MARKER = 'ticket';
|
|
62
63
|
/** Where ticket databases live. Deliberately loopback-only — never a remote/prod host. */
|
|
@@ -281,8 +282,19 @@ function gitRefExists(repoDir, ref) {
|
|
|
281
282
|
* for fresh scaffolds without a lockfile yet.
|
|
282
283
|
*/
|
|
283
284
|
function installWorktreeDeps(dir) {
|
|
285
|
+
var _a;
|
|
284
286
|
const pm = (0, dev_package_manager_1.pickPackageManager)(dir);
|
|
285
|
-
(
|
|
287
|
+
// `spawnCmdSync` (spawnSync) does NOT throw on a non-zero exit, unlike the
|
|
288
|
+
// `execFileSync` this replaced. The contract here is to throw: the only caller
|
|
289
|
+
// (`lt ticket start`) catches and warns, and swallowing the failure would hand
|
|
290
|
+
// the developer a half-installed worktree with no hint that anything went wrong.
|
|
291
|
+
const result = (0, platform_1.spawnCmdSync)(pm.bin, pm.installArgs, { cwd: dir, stdio: 'inherit' });
|
|
292
|
+
if (result.error) {
|
|
293
|
+
throw result.error;
|
|
294
|
+
}
|
|
295
|
+
if (result.status !== 0) {
|
|
296
|
+
throw new Error(`${pm.bin} ${pm.installArgs.join(' ')} exited with ${(_a = result.status) !== null && _a !== void 0 ? _a : 'a signal'}`);
|
|
297
|
+
}
|
|
286
298
|
}
|
|
287
299
|
/**
|
|
288
300
|
* True for ticket ids whose derived database would collide with a project-level
|
|
@@ -32,11 +32,16 @@ const fs_utils_1 = require("./fs-utils");
|
|
|
32
32
|
* starter's assessed-advisory allowlist is destroyed rather than merely ignored,
|
|
33
33
|
* and the generated project's very first pipeline goes red on an advisory that
|
|
34
34
|
* was already justified upstream.
|
|
35
|
+
*
|
|
36
|
+
* `peerDependencyRules` (`{ allowedVersions: {...}, ignoreMissing: [...] }`) is
|
|
37
|
+
* the same trap one step later: the starter scope-allows known-stale peer ranges
|
|
38
|
+
* there, and without the hoist the root `check:peers` step fails the first
|
|
39
|
+
* `pnpm run check` of every generated project.
|
|
35
40
|
*/
|
|
36
41
|
const OBJECT_FIELDS = ['overrides', 'allowBuilds'];
|
|
37
42
|
const ARRAY_FIELDS = ['onlyBuiltDependencies', 'ignoredOptionalDependencies', 'minimumReleaseAgeExclude'];
|
|
38
|
-
/** Objects whose
|
|
39
|
-
const NESTED_ARRAY_FIELDS = ['auditConfig'];
|
|
43
|
+
/** Objects whose inner arrays are unioned and inner maps merged, not replaced. */
|
|
44
|
+
const NESTED_ARRAY_FIELDS = ['auditConfig', 'peerDependencyRules'];
|
|
40
45
|
/** The union of all three, in declaration order. Declared here, with its inputs,
|
|
41
46
|
* because the comment-carrying helpers below default their `fields` parameter to it. */
|
|
42
47
|
const WORKSPACE_SCOPED_PNPM_FIELDS = [...OBJECT_FIELDS, ...ARRAY_FIELDS, ...NESTED_ARRAY_FIELDS];
|
|
@@ -611,11 +616,13 @@ function mergePnpmFieldValue(field, rootValue, subValue) {
|
|
|
611
616
|
const subArr = Array.isArray(subValue) ? subValue : [];
|
|
612
617
|
return Array.from(new Set([...rootArr, ...subArr])).sort((a, b) => a.localeCompare(b));
|
|
613
618
|
}
|
|
614
|
-
// Nested (`auditConfig.ignoreGhsas`
|
|
615
|
-
//
|
|
616
|
-
// key-by-key merge would drop
|
|
619
|
+
// Nested (`auditConfig.ignoreGhsas`, `peerDependencyRules.allowedVersions`, …):
|
|
620
|
+
// union each inner array and merge each inner map instead of letting the
|
|
621
|
+
// sub-project's object replace the root's. A plain key-by-key merge would drop
|
|
622
|
+
// every advisory or peer rule the root had already justified.
|
|
617
623
|
if (isNestedArrayField(field)) {
|
|
618
|
-
const
|
|
624
|
+
const isMap = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
625
|
+
const asObj = (v) => (isMap(v) ? v : {});
|
|
619
626
|
const rootObj = asObj(rootValue);
|
|
620
627
|
const subObj = asObj(subValue);
|
|
621
628
|
const merged = Object.assign({}, rootObj);
|
|
@@ -625,6 +632,10 @@ function mergePnpmFieldValue(field, rootValue, subValue) {
|
|
|
625
632
|
const b = Array.isArray(value) ? value : [];
|
|
626
633
|
merged[key] = Array.from(new Set([...a, ...b])).sort((x, y) => x.localeCompare(y));
|
|
627
634
|
}
|
|
635
|
+
else if (isMap(value) && isMap(merged[key])) {
|
|
636
|
+
const inner = Object.assign(Object.assign({}, merged[key]), value);
|
|
637
|
+
merged[key] = Object.fromEntries(Object.entries(inner).sort(([x], [y]) => x.localeCompare(y)));
|
|
638
|
+
}
|
|
628
639
|
else {
|
|
629
640
|
merged[key] = value;
|
|
630
641
|
}
|
package/build/lib/platform.js
CHANGED
|
@@ -1,10 +1,44 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.findExecutable = findExecutable;
|
|
4
37
|
exports.isWindows = isWindows;
|
|
38
|
+
exports.spawnCmd = spawnCmd;
|
|
5
39
|
exports.spawnCmdSync = spawnCmdSync;
|
|
6
40
|
exports.windowsAppData = windowsAppData;
|
|
7
|
-
const cross_spawn_1 = require("cross-spawn");
|
|
41
|
+
const cross_spawn_1 = __importStar(require("cross-spawn"));
|
|
8
42
|
const fs_1 = require("fs");
|
|
9
43
|
const path_1 = require("path");
|
|
10
44
|
const DEFAULT_PATHEXT = '.COM;.EXE;.BAT;.CMD';
|
|
@@ -48,6 +82,24 @@ function findExecutable(name, options = {}) {
|
|
|
48
82
|
function isWindows(platform = process.platform) {
|
|
49
83
|
return platform === 'win32';
|
|
50
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* `spawn` that also runs `.cmd`/`.bat` shims on Windows.
|
|
87
|
+
*
|
|
88
|
+
* The asynchronous counterpart of `spawnCmdSync`, with the same delegation and
|
|
89
|
+
* the same argument limit — see there. Needed because every long-running child
|
|
90
|
+
* the CLI starts (`pnpm run dev`, `pnpm start`, a Playwright run) is spawned
|
|
91
|
+
* asynchronously, and `pnpm` is `pnpm.cmd` on Windows: Node refuses to exec a
|
|
92
|
+
* `.cmd` directly (CVE-2024-27980, `EINVAL`), so without this nothing starts
|
|
93
|
+
* there at all.
|
|
94
|
+
*
|
|
95
|
+
* `detached` and `stdio` pass through unchanged, so a caller keeps its process
|
|
96
|
+
* group and its log-file descriptors. On Windows cross-spawn interposes
|
|
97
|
+
* `cmd.exe /c`, which means the reported pid is that of `cmd.exe` — the child
|
|
98
|
+
* tree, not a process group, is what a killer has to walk there.
|
|
99
|
+
*/
|
|
100
|
+
function spawnCmd(command, args = [], options = {}) {
|
|
101
|
+
return (0, cross_spawn_1.default)(command, [...args], options);
|
|
102
|
+
}
|
|
51
103
|
/**
|
|
52
104
|
* `spawnSync` that also runs `.cmd`/`.bat` shims on Windows.
|
|
53
105
|
*
|
|
@@ -91,14 +91,21 @@ JavaScript file: src/main.ts
|
|
|
91
91
|
see [Debug.run.xml](.run/Debug.run.xml)
|
|
92
92
|
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
<% if (props.vendor) { -%>
|
|
95
|
+
## Working on the framework core
|
|
96
|
+
This project runs in vendor mode: the `@lenne.tech/nest-server` core is part of the source tree under `src/core/`, not an npm package, so there is nothing to link.
|
|
97
|
+
|
|
98
|
+
- Read `src/core/VENDOR.md` first. It records the upstream baseline and every local change to the core.
|
|
99
|
+
- Put project-specific code outside `src/core/`: extend, inherit from or override the core, never patch it for one project.
|
|
100
|
+
- Pull in upstream changes with `/lt-dev:backend:update-nest-server-core`, and propose generally useful core fixes upstream with `/lt-dev:backend:contribute-nest-server-core`.
|
|
96
101
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
<% } else { -%>
|
|
103
|
+
## Test & debug the NestServer package in this project
|
|
104
|
+
1. Clone [NestServer](https://github.com/lenneTech/nest-server): `git clone https://github.com/lenneTech/nest-server.git`
|
|
105
|
+
2. Link it into this project with `pnpm run link:nest-server` and start the server: `pnpm start`
|
|
106
|
+
3. Switch back to the published package when you are done: `pnpm run unlink:nest-server`
|
|
101
107
|
|
|
108
|
+
<% } -%>
|
|
102
109
|
## Deployment with deploy.party
|
|
103
110
|
|
|
104
111
|
This project is prepared for deployment with deploy.party.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.50.0",
|
|
4
4
|
"description": "lenne.Tech CLI: lt",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lenne.Tech",
|
|
@@ -134,6 +134,9 @@
|
|
|
134
134
|
"rootDir": "__tests__",
|
|
135
135
|
"testTimeout": 60000,
|
|
136
136
|
"workerIdleMemoryLimit": "512MB",
|
|
137
|
+
"setupFilesAfterEnv": [
|
|
138
|
+
"<rootDir>/support/signal-guard.ts"
|
|
139
|
+
],
|
|
137
140
|
"testMatch": [
|
|
138
141
|
"<rootDir>/*.test.ts"
|
|
139
142
|
],
|