@lenne.tech/cli 1.47.0 → 1.48.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/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/update.js +42 -6
- package/build/extensions/api-mode.js +32 -8
- package/build/extensions/git.js +23 -25
- package/build/extensions/server.js +24 -17
- package/build/lib/dev-api-launch.js +58 -6
- package/build/lib/dev-process.js +66 -30
- package/build/lib/dev-state.js +13 -2
- package/build/lib/dev-test-session.js +85 -20
- package/build/lib/git-env.js +46 -0
- package/build/lib/heal-oxlintrc.js +20 -3
- 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/package.json +1 -1
|
@@ -55,8 +55,17 @@ const NewCommand = {
|
|
|
55
55
|
else {
|
|
56
56
|
info('No local changes to discard.');
|
|
57
57
|
}
|
|
58
|
-
// Show commits that would be lost
|
|
59
|
-
|
|
58
|
+
// Show commits that would be lost (none listed when the upstream ref is unknown locally).
|
|
59
|
+
// `2>/dev/null || echo ""` used to swallow the failure, but cmd.exe on Windows has no
|
|
60
|
+
// /dev/null: the redirect failed first and `echo ""` then reported success, so the preview
|
|
61
|
+
// silently claimed there was nothing ahead. try/catch behaves the same on every platform.
|
|
62
|
+
let localCommits = '';
|
|
63
|
+
try {
|
|
64
|
+
localCommits = yield run(`git log origin/${branch}..HEAD --oneline`);
|
|
65
|
+
}
|
|
66
|
+
catch (_d) {
|
|
67
|
+
// ignore - no upstream ref, so nothing ahead to report
|
|
68
|
+
}
|
|
60
69
|
if (localCommits === null || localCommits === void 0 ? void 0 : localCommits.trim()) {
|
|
61
70
|
info('');
|
|
62
71
|
info('Local commits that would be lost:');
|
|
@@ -54,7 +54,17 @@ const NewCommand = {
|
|
|
54
54
|
warning('DRY-RUN MODE - No changes will be made');
|
|
55
55
|
info('');
|
|
56
56
|
// Show commits that would be rebased
|
|
57
|
-
|
|
57
|
+
// try/catch, not `2>/dev/null || echo ""`: on Windows that `echo` succeeds
|
|
58
|
+
// and the git log never runs, so a dry-run would report zero commits to
|
|
59
|
+
// rebase for a branch that has some — a preview that quietly understates
|
|
60
|
+
// what the real command will do.
|
|
61
|
+
let commitsToRebase = '';
|
|
62
|
+
try {
|
|
63
|
+
commitsToRebase = yield run(`git log ${baseBranchPreview}..HEAD --oneline`);
|
|
64
|
+
}
|
|
65
|
+
catch (_k) {
|
|
66
|
+
// Unknown base ref — nothing to preview, which the check below handles.
|
|
67
|
+
}
|
|
58
68
|
if (commitsToRebase === null || commitsToRebase === void 0 ? void 0 : commitsToRebase.trim()) {
|
|
59
69
|
const commitCount = commitsToRebase.trim().split('\n').length;
|
|
60
70
|
info(`Would rebase ${commitCount} commit(s) from branch "${branch}" onto "${baseBranchPreview}":`);
|
|
@@ -10,6 +10,24 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
const path_1 = require("path");
|
|
13
|
+
const git_env_1 = require("../../lib/git-env");
|
|
14
|
+
/**
|
|
15
|
+
* Environment that keeps the `git ls-remote` below non-interactive.
|
|
16
|
+
*
|
|
17
|
+
* This used to be a POSIX prefix on the command string
|
|
18
|
+
* (`GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-…}" git …`).
|
|
19
|
+
* `system.run` shells out through `cmd.exe` on Windows, which reads
|
|
20
|
+
* `VAR=value cmd` as a command name, not as an assignment. Paired with the
|
|
21
|
+
* trailing `|| true` the call then "succeeded" with empty output, so every
|
|
22
|
+
* `lt git reset` on Windows aborted with "No remote branch … found!" — the
|
|
23
|
+
* ls-remote had never run. Handing the variables to the child as its
|
|
24
|
+
* environment works on every platform.
|
|
25
|
+
*
|
|
26
|
+
* The `||` keeps the semantics of the shell's `:-` default: a caller who
|
|
27
|
+
* configured ssh deliberately (a user with a custom agent, or a test harness
|
|
28
|
+
* pinning the behaviour) still wins. See `git.ts#gitInstalled` for why the
|
|
29
|
+
* assignment must never be unconditional.
|
|
30
|
+
*/
|
|
13
31
|
/**
|
|
14
32
|
* Reset current branch
|
|
15
33
|
*/
|
|
@@ -46,7 +64,13 @@ const NewCommand = {
|
|
|
46
64
|
return;
|
|
47
65
|
}
|
|
48
66
|
// Check remote (use short SSH timeout so ls-remote doesn't hang in offline environments)
|
|
49
|
-
|
|
67
|
+
let remoteBranch = '';
|
|
68
|
+
try {
|
|
69
|
+
remoteBranch = yield system.run(`git ls-remote --heads origin ${branch}`, { env: (0, git_env_1.nonInteractiveGitEnv)() });
|
|
70
|
+
}
|
|
71
|
+
catch (_d) {
|
|
72
|
+
// ignore - unreachable remote is reported as "no remote branch" below
|
|
73
|
+
}
|
|
50
74
|
if (!remoteBranch) {
|
|
51
75
|
error(`No remote branch ${branch} found!`);
|
|
52
76
|
return;
|
|
@@ -68,8 +92,14 @@ const NewCommand = {
|
|
|
68
92
|
else {
|
|
69
93
|
info('No local changes to discard.');
|
|
70
94
|
}
|
|
71
|
-
// Show commits that would be lost
|
|
72
|
-
|
|
95
|
+
// Show commits that would be lost (none listed when the upstream ref is unknown locally)
|
|
96
|
+
let localCommits = '';
|
|
97
|
+
try {
|
|
98
|
+
localCommits = yield system.run(`git log origin/${branch}..HEAD --oneline`);
|
|
99
|
+
}
|
|
100
|
+
catch (_e) {
|
|
101
|
+
// ignore - no upstream ref, so nothing ahead to report
|
|
102
|
+
}
|
|
73
103
|
if (localCommits === null || localCommits === void 0 ? void 0 : localCommits.trim()) {
|
|
74
104
|
info('');
|
|
75
105
|
info('Local commits that would be lost:');
|
|
@@ -109,7 +139,9 @@ const NewCommand = {
|
|
|
109
139
|
const projectDir = (0, path_1.dirname)(pkgPath);
|
|
110
140
|
const detectedPm = toolbox.pm.detect(projectDir);
|
|
111
141
|
const installSpin = spin(`Install packages using ${detectedPm}`);
|
|
112
|
-
|
|
142
|
+
// `cwd` instead of `cd <dir> &&`: cmd.exe's `cd` does not switch drives,
|
|
143
|
+
// and it needs no quoting for paths with spaces.
|
|
144
|
+
yield system.run(toolbox.pm.install(detectedPm), { cwd: projectDir });
|
|
113
145
|
installSpin.succeed();
|
|
114
146
|
}
|
|
115
147
|
// Success info
|
|
@@ -10,6 +10,22 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
const path_1 = require("path");
|
|
13
|
+
const git_env_1 = require("../../lib/git-env");
|
|
14
|
+
/**
|
|
15
|
+
* Environment that keeps the `git` calls below non-interactive.
|
|
16
|
+
*
|
|
17
|
+
* This used to be a POSIX prefix on the command string
|
|
18
|
+
* (`GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-…}" git …`).
|
|
19
|
+
* `system.run` shells out through `cmd.exe` on Windows, which does not read
|
|
20
|
+
* `VAR=value cmd` as an assignment but as a command name — so every `lt git
|
|
21
|
+
* update` failed there with "'GIT_TERMINAL_PROMPT' is not recognized". Handing
|
|
22
|
+
* the variables to the child as its environment works on every platform.
|
|
23
|
+
*
|
|
24
|
+
* The `||` keeps the semantics of the shell's `:-` default: a caller who
|
|
25
|
+
* configured ssh deliberately (a user with a custom agent, or a test harness
|
|
26
|
+
* pinning the behaviour) still wins. See `git.ts#gitInstalled` for why the
|
|
27
|
+
* assignment must never be unconditional.
|
|
28
|
+
*/
|
|
13
29
|
/**
|
|
14
30
|
* Update branch
|
|
15
31
|
*/
|
|
@@ -47,10 +63,22 @@ const NewCommand = {
|
|
|
47
63
|
info('');
|
|
48
64
|
info(`Current branch: ${branch}`);
|
|
49
65
|
info('');
|
|
50
|
-
// Fetch to see incoming changes (use short SSH timeout so it doesn't hang offline)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
// Fetch to see incoming changes (use short SSH timeout so it doesn't hang offline).
|
|
67
|
+
// Best effort: offline or without credentials the preview falls back to the local refs.
|
|
68
|
+
try {
|
|
69
|
+
yield run('git fetch', { env: (0, git_env_1.nonInteractiveGitEnv)() });
|
|
70
|
+
}
|
|
71
|
+
catch (_d) {
|
|
72
|
+
// ignore - a failed fetch only makes the preview less current
|
|
73
|
+
}
|
|
74
|
+
// Check for incoming commits (none yet when the upstream ref is unknown locally)
|
|
75
|
+
let incomingCommits = '';
|
|
76
|
+
try {
|
|
77
|
+
incomingCommits = yield run(`git log ${branch}..origin/${branch} --oneline`);
|
|
78
|
+
}
|
|
79
|
+
catch (_e) {
|
|
80
|
+
// ignore - no upstream ref, so nothing incoming to report
|
|
81
|
+
}
|
|
54
82
|
const commits = (incomingCommits === null || incomingCommits === void 0 ? void 0 : incomingCommits.trim().split('\n').filter((c) => c)) || [];
|
|
55
83
|
if (commits.length > 0) {
|
|
56
84
|
info(`Incoming commits (${commits.length}):`);
|
|
@@ -78,7 +106,13 @@ const NewCommand = {
|
|
|
78
106
|
const timer = startTimer();
|
|
79
107
|
// Update
|
|
80
108
|
const updateSpin = spin(`Update branch ${branch}`);
|
|
81
|
-
|
|
109
|
+
try {
|
|
110
|
+
yield run('git fetch', { env: (0, git_env_1.nonInteractiveGitEnv)() });
|
|
111
|
+
}
|
|
112
|
+
catch (_f) {
|
|
113
|
+
// ignore - `git pull --rebase` below fetches again and reports the real failure
|
|
114
|
+
}
|
|
115
|
+
yield run('git pull --rebase', { env: (0, git_env_1.nonInteractiveGitEnv)() });
|
|
82
116
|
updateSpin.succeed();
|
|
83
117
|
// Install packages (unless skipped) with correctly detected package manager (supports monorepo lockfiles)
|
|
84
118
|
if (!skipInstall) {
|
|
@@ -87,7 +121,9 @@ const NewCommand = {
|
|
|
87
121
|
const projectDir = (0, path_1.dirname)(pkgPath);
|
|
88
122
|
const detectedPm = toolbox.pm.detect(projectDir);
|
|
89
123
|
const installSpin = spin(`Install packages using ${detectedPm}`);
|
|
90
|
-
|
|
124
|
+
// `cwd` instead of `cd <dir> &&`: cmd.exe's `cd` does not switch drives,
|
|
125
|
+
// and it needs no quoting for paths with spaces.
|
|
126
|
+
yield run(toolbox.pm.install(detectedPm), { cwd: projectDir });
|
|
91
127
|
installSpin.succeed();
|
|
92
128
|
}
|
|
93
129
|
}
|
|
@@ -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.ApiMode = void 0;
|
|
13
|
+
const glob_1 = require("glob");
|
|
13
14
|
const path_1 = require("path");
|
|
14
15
|
/**
|
|
15
16
|
* API Mode processing for nest-server-starter template
|
|
@@ -114,10 +115,7 @@ class ApiMode {
|
|
|
114
115
|
// 1. Delete files matching filePatterns
|
|
115
116
|
if (modeConfig.filePatterns) {
|
|
116
117
|
for (const pattern of modeConfig.filePatterns) {
|
|
117
|
-
const
|
|
118
|
-
matching: pattern,
|
|
119
|
-
});
|
|
120
|
-
for (const file of matches) {
|
|
118
|
+
for (const file of this.globFiles(projectPath, pattern)) {
|
|
121
119
|
this.filesystem.remove(file);
|
|
122
120
|
}
|
|
123
121
|
}
|
|
@@ -169,6 +167,29 @@ class ApiMode {
|
|
|
169
167
|
this.cleanOrphanImports(projectPath);
|
|
170
168
|
});
|
|
171
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Resolve a POSIX-style glob pattern against `baseDir` and return the
|
|
172
|
+
* matching files as absolute, OS-native paths.
|
|
173
|
+
*
|
|
174
|
+
* Deliberately NOT `filesystem.find()`: gluegun/fs-jetpack builds its
|
|
175
|
+
* matcher by string-concatenating the *resolved* base path in front of
|
|
176
|
+
* the pattern (fs-jetpack/lib/utils/matcher.js). On Windows that base
|
|
177
|
+
* path is backslash-separated, and minimatch reads a backslash inside a
|
|
178
|
+
* pattern as an escape character — so a base of `C:\project\src`
|
|
179
|
+
* collapses to the literal `C:projectsrc` and nothing ever matches.
|
|
180
|
+
* Effect: on Windows no file was deleted and no region was stripped,
|
|
181
|
+
* silently. `glob` keeps pattern and base directory apart and
|
|
182
|
+
* normalises separators itself, so it behaves the same on every
|
|
183
|
+
* platform.
|
|
184
|
+
*/
|
|
185
|
+
globFiles(baseDir, pattern) {
|
|
186
|
+
return (0, glob_1.globSync)(pattern, {
|
|
187
|
+
absolute: true,
|
|
188
|
+
cwd: baseDir,
|
|
189
|
+
dot: true,
|
|
190
|
+
nodir: true,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
172
193
|
/**
|
|
173
194
|
* Strip region markers from all .ts files
|
|
174
195
|
*
|
|
@@ -181,7 +202,7 @@ class ApiMode {
|
|
|
181
202
|
if (!this.filesystem.exists(dir)) {
|
|
182
203
|
continue;
|
|
183
204
|
}
|
|
184
|
-
const files = this.
|
|
205
|
+
const files = this.globFiles(dir, '**/*.ts');
|
|
185
206
|
for (const file of files) {
|
|
186
207
|
const content = this.filesystem.read(file);
|
|
187
208
|
if (!content) {
|
|
@@ -196,7 +217,10 @@ class ApiMode {
|
|
|
196
217
|
// graphql` block with an explicit `graphQl: false,` so GraphQL
|
|
197
218
|
// is cleanly disabled.
|
|
198
219
|
let processed;
|
|
199
|
-
|
|
220
|
+
// `basename` instead of `endsWith('/config.env.ts')`: on Windows the
|
|
221
|
+
// path separator is a backslash, so the POSIX-only suffix check never
|
|
222
|
+
// matched and config.env.ts was processed like any other file.
|
|
223
|
+
if (removeMarker === 'graphql' && (0, path_1.basename)(file) === 'config.env.ts') {
|
|
200
224
|
processed = this.replaceGraphqlRegionsWithDisabled(content, keepMarker);
|
|
201
225
|
}
|
|
202
226
|
else {
|
|
@@ -299,7 +323,7 @@ class ApiMode {
|
|
|
299
323
|
if (!this.filesystem.exists(dir)) {
|
|
300
324
|
continue;
|
|
301
325
|
}
|
|
302
|
-
const files = this.
|
|
326
|
+
const files = this.globFiles(dir, '**/*.ts');
|
|
303
327
|
for (const file of files) {
|
|
304
328
|
const content = this.filesystem.read(file);
|
|
305
329
|
if (!content) {
|
|
@@ -444,7 +468,7 @@ class ApiMode {
|
|
|
444
468
|
if (!this.filesystem.exists(dir)) {
|
|
445
469
|
continue;
|
|
446
470
|
}
|
|
447
|
-
const files = this.
|
|
471
|
+
const files = this.globFiles(dir, '**/*.ts');
|
|
448
472
|
for (const file of files) {
|
|
449
473
|
const content = this.filesystem.read(file);
|
|
450
474
|
if (!content) {
|
package/build/extensions/git.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.Git = void 0;
|
|
13
|
+
const git_env_1 = require("../lib/git-env");
|
|
13
14
|
/**
|
|
14
15
|
* Git functions
|
|
15
16
|
*/
|
|
@@ -119,8 +120,17 @@ class Git {
|
|
|
119
120
|
const result = [];
|
|
120
121
|
// Toolbox features
|
|
121
122
|
const { system } = this.toolbox;
|
|
122
|
-
//
|
|
123
|
-
|
|
123
|
+
// Refresh first, but never let a failed fetch cost us the branch list: the
|
|
124
|
+
// two commands used to be chained with `;`, which cmd.exe does not honour as
|
|
125
|
+
// a separator at all, so on Windows this ran neither reliably. They are two
|
|
126
|
+
// calls now — the fetch is best-effort, the listing is the actual result.
|
|
127
|
+
try {
|
|
128
|
+
yield system.run('git fetch', { env: (0, git_env_1.nonInteractiveGitEnv)() });
|
|
129
|
+
}
|
|
130
|
+
catch (_a) {
|
|
131
|
+
// Offline or no remote — `git show-branch` still lists what is local.
|
|
132
|
+
}
|
|
133
|
+
const branches = yield system.run('git show-branch --list');
|
|
124
134
|
branches.split('\n').forEach((item) => {
|
|
125
135
|
const matches = item.match(/\[(.*?)]/);
|
|
126
136
|
if (matches) {
|
|
@@ -197,27 +207,6 @@ class Git {
|
|
|
197
207
|
/**
|
|
198
208
|
* Check if git is installed (cached for performance)
|
|
199
209
|
*/
|
|
200
|
-
/**
|
|
201
|
-
* Why the git calls in this repo spell `GIT_SSH_COMMAND="\${GIT_SSH_COMMAND:-…}"` (shell default)
|
|
202
|
-
* rather than assigning it outright.
|
|
203
|
-
*
|
|
204
|
-
* These commands do a best-effort `git fetch` to see whether the branch is
|
|
205
|
-
* behind. They set `BatchMode=yes` so ssh fails instead of PROMPTING — but an
|
|
206
|
-
* agent that stalls is not a prompt. On a 1Password-backed machine the agent is
|
|
207
|
-
* reachable and every signature needs an interactive approval; unattended it
|
|
208
|
-
* waits and then reports `communication with agent failed`. Measured: **61 s per
|
|
209
|
-
* fetch**, so `lt git update --dry-run` took 62 s and `lt git create --dry-run`
|
|
210
|
-
* 123 s (two fetches). `ConnectTimeout` does not bound it — that covers the TCP
|
|
211
|
-
* connect, not the agent.
|
|
212
|
-
*
|
|
213
|
-
* Waiting for a human to approve a key is legitimate for an interactive command,
|
|
214
|
-
* so the default is unchanged. What was wrong is that the assignment was
|
|
215
|
-
* UNCONDITIONAL: it overrode a caller who had deliberately configured ssh,
|
|
216
|
-
* including a test harness trying to make the behaviour deterministic. The
|
|
217
|
-
* `:-` default respects an existing value and keeps the old behaviour when there
|
|
218
|
-
* is none. `IdentityAgent=none` in the caller's env then drops the same fetch to
|
|
219
|
-
* ~1 s with a clean `Permission denied (publickey)`.
|
|
220
|
-
*/
|
|
221
210
|
gitInstalled() {
|
|
222
211
|
return __awaiter(this, void 0, void 0, function* () {
|
|
223
212
|
// Return cached result if available
|
|
@@ -272,8 +261,17 @@ class Git {
|
|
|
272
261
|
if (opts.spin) {
|
|
273
262
|
searchSpin = spin(opts.spinText);
|
|
274
263
|
}
|
|
275
|
-
// Update infos
|
|
276
|
-
|
|
264
|
+
// Update infos. Best-effort, but expressed as try/catch rather than
|
|
265
|
+
// `2>/dev/null || true`: on Windows `true` resolves to Git-for-Windows'
|
|
266
|
+
// `true.exe`, so the pipeline succeeded with empty output and the fetch
|
|
267
|
+
// never ran — a silent no-op that read as a successful refresh.
|
|
268
|
+
let fetch = '';
|
|
269
|
+
try {
|
|
270
|
+
fetch = yield system.run('git fetch', { env: (0, git_env_1.nonInteractiveGitEnv)() });
|
|
271
|
+
}
|
|
272
|
+
catch (_b) {
|
|
273
|
+
// Offline or no remote — the branch search below still works on local refs.
|
|
274
|
+
}
|
|
277
275
|
if (fetch.length && !fetch.startsWith('remote')) {
|
|
278
276
|
info(`Could not update infos ${fetch.length}`);
|
|
279
277
|
}
|
|
@@ -44,6 +44,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
45
|
exports.Server = void 0;
|
|
46
46
|
const crypto = __importStar(require("crypto"));
|
|
47
|
+
const glob_1 = require("glob");
|
|
47
48
|
const path_1 = require("path");
|
|
48
49
|
const ts = __importStar(require("typescript"));
|
|
49
50
|
const adopt_upstream_build_allowlist_1 = require("../lib/adopt-upstream-build-allowlist");
|
|
@@ -1232,13 +1233,15 @@ class Server {
|
|
|
1232
1233
|
// only as type annotations — any runtime `new Request(...)` calls refer
|
|
1233
1234
|
// to the global Fetch API Request, not the express one.
|
|
1234
1235
|
const expressImportRegex = /^import\s+\{([^}]*)\}\s+from\s+['"]express['"]\s*;?\s*$/gm;
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1236
|
+
// `glob`, not `filesystem.find`: jetpack builds its matcher by concatenating the
|
|
1237
|
+
// RESOLVED base path in front of the pattern, and minimatch reads a backslash
|
|
1238
|
+
// inside a PATTERN as an escape. On Windows `C:\…\core` + `**/*.ts` therefore
|
|
1239
|
+
// collapses to a literal that matches nothing, so this loop silently patched
|
|
1240
|
+
// no files at all. (minimatch 3 normalised the pattern's separators; the repo
|
|
1241
|
+
// now resolves the hoisted minimatch 10, which does not — see package.json.)
|
|
1242
|
+
// `glob` keeps base and pattern apart and returns absolute OS-native paths.
|
|
1243
|
+
const vendoredTsFiles = (0, glob_1.globSync)('**/*.ts', { absolute: true, cwd: coreDir, dot: true, nodir: true });
|
|
1244
|
+
for (const absPath of vendoredTsFiles) {
|
|
1242
1245
|
if (!filesystem.exists(absPath))
|
|
1243
1246
|
continue;
|
|
1244
1247
|
const content = filesystem.read(absPath) || '';
|
|
@@ -2516,20 +2519,24 @@ class Server {
|
|
|
2516
2519
|
* @param pattern Optional regex for more flexible matching
|
|
2517
2520
|
*/
|
|
2518
2521
|
findStaleImports(dest, needle, pattern) {
|
|
2522
|
+
// Patterns are relative to `dest` and passed to `glob` rather than
|
|
2523
|
+
// `filesystem.find`: jetpack concatenates the resolved base path in front of
|
|
2524
|
+
// the pattern, and minimatch treats a backslash inside a PATTERN as an escape,
|
|
2525
|
+
// so on Windows every slash-bearing pattern below matched nothing. This
|
|
2526
|
+
// function would then report "no stale imports" for a project that has them —
|
|
2527
|
+
// a false clean bill of health on the one check meant to catch a broken
|
|
2528
|
+
// conversion. See the same fix at the vendored-express patch above.
|
|
2519
2529
|
const globs = [
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2530
|
+
'src/server/**/*.ts',
|
|
2531
|
+
'src/main.ts',
|
|
2532
|
+
'src/config.env.ts',
|
|
2533
|
+
'tests/**/*.ts',
|
|
2534
|
+
'migrations/**/*.ts',
|
|
2535
|
+
'scripts/**/*.ts',
|
|
2526
2536
|
];
|
|
2527
2537
|
const stale = [];
|
|
2528
2538
|
for (const glob of globs) {
|
|
2529
|
-
const files =
|
|
2530
|
-
matching: glob.replace(`${dest}/`, ''),
|
|
2531
|
-
recursive: true,
|
|
2532
|
-
}) || [];
|
|
2539
|
+
const files = (0, glob_1.globSync)(glob, { absolute: true, cwd: dest, dot: true, nodir: true });
|
|
2533
2540
|
for (const file of files) {
|
|
2534
2541
|
const raw = this.filesystem.read(file) || '';
|
|
2535
2542
|
// Strip comments before matching. The keyword-anchored pattern is not enough on its own:
|
|
@@ -11,6 +11,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.findCompiledEntry = findCompiledEntry;
|
|
13
13
|
exports.isApiCompiledRequested = isApiCompiledRequested;
|
|
14
|
+
exports.resolveApiRuntime = resolveApiRuntime;
|
|
14
15
|
exports.startCompiledApi = startCompiledApi;
|
|
15
16
|
/**
|
|
16
17
|
* Launch strategy for the API process under `lt dev up`.
|
|
@@ -47,6 +48,50 @@ function isApiCompiledRequested(options = {}) {
|
|
|
47
48
|
const affirmative = (value) => value === true || ['1', 'true', 'yes'].includes(String(value).toLowerCase());
|
|
48
49
|
return affirmative(options.apiCompiled) || affirmative(options['api-compiled']);
|
|
49
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Which runtime to start the API's COMPILED bundle with.
|
|
53
|
+
*
|
|
54
|
+
* A bundle is only portable across runtimes by accident. `nest-base` builds with
|
|
55
|
+
* `Bun.build({ target: 'bun' })`, whose output calls `__require` — a Bun-only
|
|
56
|
+
* shim. Started under node it dies in milliseconds with "__require is not a
|
|
57
|
+
* function" (DEV-3208), and because the caller used to only WARN when the API
|
|
58
|
+
* never answered, the whole Playwright suite then ran against a dead API: every
|
|
59
|
+
* data-dependent spec passed trivially or skipped, so a run that checked nothing
|
|
60
|
+
* looked exactly like one that checked everything.
|
|
61
|
+
*
|
|
62
|
+
* Detected from the project rather than configured, because the project already
|
|
63
|
+
* states it three times over. Any one signal is enough:
|
|
64
|
+
* - a `bun.lock` / `bun.lockb` IN THIS DIRECTORY (not a nested package's),
|
|
65
|
+
* - `engines.bun` in its package.json,
|
|
66
|
+
* - a `build` / `start` script that invokes `bun` as the COMMAND.
|
|
67
|
+
*
|
|
68
|
+
* The script check matches `bun` as a whole word at a command position, never as
|
|
69
|
+
* a substring: `rollup --bundle` and `node -r bunyan …` are Node projects, and a
|
|
70
|
+
* loose `includes('bun')` would switch them to a runtime they never installed.
|
|
71
|
+
*
|
|
72
|
+
* Defaults to `node` whenever nothing says otherwise, including an unreadable or
|
|
73
|
+
* missing package.json — the historical behaviour, and the safe one: guessing
|
|
74
|
+
* `bun` for a project that does not have it installed turns a working start into
|
|
75
|
+
* an exit-127.
|
|
76
|
+
*
|
|
77
|
+
* This deliberately does NOT live in `dev-package-manager`: that module resolves
|
|
78
|
+
* which PACKAGE MANAGER drives a directory (`pnpm run build`), a separate axis
|
|
79
|
+
* from which runtime executes an already-built bundle. A Bun project whose
|
|
80
|
+
* scripts pnpm happens to run still needs `bun dist/main.js`.
|
|
81
|
+
*/
|
|
82
|
+
function resolveApiRuntime(apiDir) {
|
|
83
|
+
var _a, _b, _c;
|
|
84
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(apiDir, 'bun.lock')) || (0, node_fs_1.existsSync)((0, node_path_1.join)(apiDir, 'bun.lockb')))
|
|
85
|
+
return 'bun';
|
|
86
|
+
const pkg = readPackageJson(apiDir);
|
|
87
|
+
if (!pkg)
|
|
88
|
+
return 'node';
|
|
89
|
+
if ((_a = pkg.engines) === null || _a === void 0 ? void 0 : _a.bun)
|
|
90
|
+
return 'bun';
|
|
91
|
+
// `bun` as the command itself, or after a shell separator (`&&`, `;`, `|`).
|
|
92
|
+
const invokesBun = /(?:^|[&|;]\s*)bun\b/;
|
|
93
|
+
return [(_b = pkg.scripts) === null || _b === void 0 ? void 0 : _b.build, (_c = pkg.scripts) === null || _c === void 0 ? void 0 : _c.start].some((script) => script && invokesBun.test(script)) ? 'bun' : 'node';
|
|
94
|
+
}
|
|
50
95
|
/**
|
|
51
96
|
* Build the API and start it compiled (`node dist/src/main.js`). Applies pending
|
|
52
97
|
* migrations first for parity with the ts-node path it replaces (`<pm> run start`
|
|
@@ -70,7 +115,11 @@ function startCompiledApi(options) {
|
|
|
70
115
|
return undefined;
|
|
71
116
|
}
|
|
72
117
|
}
|
|
73
|
-
return (0, dev_process_1.spawnDetached)(
|
|
118
|
+
return (0, dev_process_1.spawnDetached)(resolveApiRuntime(apiDir), [entry], {
|
|
119
|
+
cwd: apiDir,
|
|
120
|
+
env: Object.assign(Object.assign({}, env), { NODE_ENV: 'local' }),
|
|
121
|
+
logFile,
|
|
122
|
+
});
|
|
74
123
|
}
|
|
75
124
|
log.warn(`compiled API unavailable (build exit ${String(build)}) — falling back to \`${pm.bin} start\` (ts-node).`);
|
|
76
125
|
return (0, dev_process_1.spawnDetached)(pm.bin, pm.runScript('start'), { cwd: apiDir, env, logFile });
|
|
@@ -78,12 +127,15 @@ function startCompiledApi(options) {
|
|
|
78
127
|
}
|
|
79
128
|
/** True when `package.json` in `apiDir` defines a script named `name`. */
|
|
80
129
|
function hasScript(apiDir, name) {
|
|
81
|
-
var _a;
|
|
130
|
+
var _a, _b;
|
|
131
|
+
return typeof ((_b = (_a = readPackageJson(apiDir)) === null || _a === void 0 ? void 0 : _a.scripts) === null || _b === void 0 ? void 0 : _b[name]) === 'string';
|
|
132
|
+
}
|
|
133
|
+
/** The parsed `package.json` in `apiDir`, or `undefined` when it is missing or unparsable. */
|
|
134
|
+
function readPackageJson(apiDir) {
|
|
82
135
|
try {
|
|
83
|
-
|
|
84
|
-
return typeof ((_a = pkg.scripts) === null || _a === void 0 ? void 0 : _a[name]) === 'string';
|
|
136
|
+
return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(apiDir, 'package.json'), 'utf8'));
|
|
85
137
|
}
|
|
86
|
-
catch (
|
|
87
|
-
return
|
|
138
|
+
catch (_a) {
|
|
139
|
+
return undefined;
|
|
88
140
|
}
|
|
89
141
|
}
|
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,12 +16,14 @@ exports.bringUpTestSession = bringUpTestSession;
|
|
|
16
16
|
exports.buildShardPlaywrightInvocation = buildShardPlaywrightInvocation;
|
|
17
17
|
exports.buildTestAppEnv = buildTestAppEnv;
|
|
18
18
|
exports.hasTestSession = hasTestSession;
|
|
19
|
+
exports.isStackServing = isStackServing;
|
|
19
20
|
exports.resolveTestSession = resolveTestSession;
|
|
20
21
|
exports.runShardedTestSession = runShardedTestSession;
|
|
21
22
|
exports.shardReportDir = shardReportDir;
|
|
22
23
|
exports.tearDownAllTestSessions = tearDownAllTestSessions;
|
|
23
24
|
exports.tearDownTestSession = tearDownTestSession;
|
|
24
25
|
exports.testAppEntryCandidates = testAppEntryCandidates;
|
|
26
|
+
exports.unreachableStackError = unreachableStackError;
|
|
25
27
|
/**
|
|
26
28
|
* Ephemeral, isolated test session for `lt dev test`.
|
|
27
29
|
*
|
|
@@ -35,12 +37,16 @@ exports.testAppEntryCandidates = testAppEntryCandidates;
|
|
|
35
37
|
* - dev session : svl.localhost / api.svl.localhost → db `<…>-local`
|
|
36
38
|
* - test session : svl-test.localhost / api.svl-test.… → db `<…>-test`
|
|
37
39
|
*
|
|
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
|
-
*
|
|
40
|
+
* Both halves run BUILT for speed + prod-fidelity: the API COMPILED (from `dist`,
|
|
41
|
+
* because ts-node intermittently dies mid-run) and the App as the production Nitro
|
|
42
|
+
* output (`nuxt build` → `node .output/server/index.mjs`, no Vite cold-compile).
|
|
43
|
+
* Each falls back to its dev runner (`pnpm start` / `pnpm dev`) when no build
|
|
44
|
+
* output is found. The API's runtime follows the project (`resolveApiRuntime`):
|
|
45
|
+
* a Bun-built bundle is started with `bun`, everything else with `node`.
|
|
46
|
+
*
|
|
47
|
+
* bringUp waits for BOTH halves to answer and THROWS when one never does, so the
|
|
48
|
+
* suite can never run against a dead stack (DEV-3208) — see `isStackServing` for
|
|
49
|
+
* what counts as answering.
|
|
44
50
|
*
|
|
45
51
|
* Lifecycle: `bringUpTestSession` → run Playwright → `tearDownTestSession`.
|
|
46
52
|
* Teardown is idempotent and residue-free (processes, Caddy block, env bridge,
|
|
@@ -308,7 +314,10 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
308
314
|
const apiEnv = Object.assign(Object.assign(Object.assign({}, exports.TEST_INITIAL_ADMIN_ENV), devEnv.api.env), { NODE_ENV: 'local' });
|
|
309
315
|
let apiSpawn;
|
|
310
316
|
if (build === 0 && entry) {
|
|
311
|
-
|
|
317
|
+
// Runtime follows the PROJECT, never a hardcoded default: a `nest-base` API
|
|
318
|
+
// bundles with `Bun.build({ target: 'bun' })` and dies under node with
|
|
319
|
+
// "__require is not a function" (DEV-3208).
|
|
320
|
+
apiSpawn = (0, dev_process_1.spawnDetached)((0, dev_api_launch_1.resolveApiRuntime)(layout.apiDir), [entry], {
|
|
312
321
|
cwd: layout.apiDir,
|
|
313
322
|
env: apiEnv,
|
|
314
323
|
logFile: (0, path_1.join)(layout.root, '.lt-dev', names.apiLog),
|
|
@@ -387,26 +396,43 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
387
396
|
(0, dev_state_1.saveSession)(layout.root, { pids, startedAt: new Date().toISOString() }, names.sessionFile);
|
|
388
397
|
// ENV bridge for external tooling (kept separate from the dev `.env`).
|
|
389
398
|
(0, dev_env_bridge_1.writeEnvBridge)(layout.root, devEnv, dbName, names.bridgeFile);
|
|
390
|
-
//
|
|
399
|
+
// THIS stack's log paths. Resolved BEFORE the readiness waits below, because
|
|
400
|
+
// the abort messages point at them: the log is the only place a crash-on-boot
|
|
401
|
+
// is recorded, and an abort that withholds it just relocates the guesswork.
|
|
402
|
+
// `apiLogPath` is also handed to the caller so the auth E2E specs (via
|
|
403
|
+
// NEST_SERVER_LOG) read the exact isolated log — correct per shard.
|
|
404
|
+
const apiLogPath = layout.apiDir ? (0, path_1.join)(layout.root, '.lt-dev', names.apiLog) : undefined;
|
|
405
|
+
const appLogPath = layout.appDir ? (0, path_1.join)(layout.root, '.lt-dev', names.appLog) : undefined;
|
|
406
|
+
// Both waits below ABORT rather than warn (DEV-3208). A warning let Playwright
|
|
407
|
+
// run the whole suite against a dead stack: every data-dependent spec passed
|
|
408
|
+
// trivially or skipped, so a run that verified NOTHING was indistinguishable
|
|
409
|
+
// from one that verified everything — worse than a red run, because nobody
|
|
410
|
+
// goes looking. `isStackServing` decides what counts as up, and the liveness
|
|
411
|
+
// guard turns a crash-on-boot into an abort in the same 300ms it took to
|
|
412
|
+
// happen, instead of after the full timeout.
|
|
413
|
+
//
|
|
414
|
+
// `gone` is deliberately true for an ABSENT pid too: reaching here means the
|
|
415
|
+
// spawn was attempted, so no pid means it failed outright and there is nothing
|
|
416
|
+
// left to wait for. Being wrong here costs nothing — the timeout still bounds
|
|
417
|
+
// the wait, so this only ever makes the abort arrive sooner.
|
|
418
|
+
const gone = (pid) => pid === undefined || !(0, dev_state_1.isPidAlive)(pid);
|
|
419
|
+
// Wait for the test App to answer.
|
|
391
420
|
if (appUrl) {
|
|
392
421
|
log.info(log.dim(`Waiting for ${appUrl} …`));
|
|
393
|
-
yield (0, dev_process_1.waitForHttp)(appUrl, 90000);
|
|
422
|
+
const appReady = yield (0, dev_process_1.waitForHttp)(appUrl, 90000, isStackServing, () => gone(pids.app));
|
|
423
|
+
if (!appReady)
|
|
424
|
+
throw unreachableStackError('App', appUrl, appLogPath);
|
|
394
425
|
}
|
|
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".
|
|
426
|
+
// Wait for the test API to actually SERVE before handing off to Playwright.
|
|
427
|
+
// Previously bringUp only waited for the App, so a compiled API still
|
|
428
|
+
// connecting to its database made the first specs skip via the suite's
|
|
429
|
+
// `ensureApiReachableOrSkip` guard (the API-readiness race).
|
|
401
430
|
if (apiUrl) {
|
|
402
431
|
log.info(log.dim(`Waiting for ${apiUrl}/meta …`));
|
|
403
|
-
const apiReady = yield (0, dev_process_1.waitForHttp)(`${apiUrl}/meta`, 120000, (
|
|
432
|
+
const apiReady = yield (0, dev_process_1.waitForHttp)(`${apiUrl}/meta`, 120000, isStackServing, () => gone(pids.api));
|
|
404
433
|
if (!apiReady)
|
|
405
|
-
|
|
434
|
+
throw unreachableStackError('API', `${apiUrl}/meta`, apiLogPath);
|
|
406
435
|
}
|
|
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
436
|
return { apiLogPath, apiUrl, appEnv, appUrl, dbName, pids, testIdentity };
|
|
411
437
|
});
|
|
412
438
|
}
|
|
@@ -467,6 +493,28 @@ function buildTestAppEnv(appEnv) {
|
|
|
467
493
|
function hasTestSession(root) {
|
|
468
494
|
return (0, dev_state_1.loadSession)(root, dev_state_1.TEST_SESSION_FILE) !== null;
|
|
469
495
|
}
|
|
496
|
+
/**
|
|
497
|
+
* Whether an HTTP status proves the component ITSELF answered.
|
|
498
|
+
*
|
|
499
|
+
* Readiness here means liveness, not health: the question is "did my process get
|
|
500
|
+
* far enough to serve HTTP", not "did it like this particular request". So a 404
|
|
501
|
+
* and a 500 both count as ready — they came FROM the upstream — while Caddy's
|
|
502
|
+
* gateway errors do not, because Caddy returns those for the entire window in
|
|
503
|
+
* which its upstream is not yet listening.
|
|
504
|
+
*
|
|
505
|
+
* The 404 case is the load-bearing one. The API probe targets `/meta`, an
|
|
506
|
+
* endpoint `@lenne.tech/nest-server` provides and `nest-base` does not: a healthy
|
|
507
|
+
* nest-base API answers 404 there (verified against a live one). The previous
|
|
508
|
+
* strict-2xx predicate was harmless while a failed wait only logged a warning,
|
|
509
|
+
* but this module now ABORTS on it — under strict 2xx that would block
|
|
510
|
+
* `lt dev test` for every nest-base project on a perfectly healthy API, which is
|
|
511
|
+
* worse than the false-green it replaces. Probing a list of per-framework paths
|
|
512
|
+
* would work too, but it needs updating for every new template; "something other
|
|
513
|
+
* than the proxy answered" needs no such list.
|
|
514
|
+
*/
|
|
515
|
+
function isStackServing(status) {
|
|
516
|
+
return status > 0 && ![502, 503, 504].includes(status);
|
|
517
|
+
}
|
|
470
518
|
/** Build the dedicated test identity + test DB name for a project. */
|
|
471
519
|
function resolveTestSession(layout, baseIdentity, shardIndex, devDbName) {
|
|
472
520
|
const names = testStackNames(shardIndex);
|
|
@@ -661,6 +709,23 @@ function tearDownTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
661
709
|
function testAppEntryCandidates() {
|
|
662
710
|
return [`${exports.TEST_NITRO_OUTPUT_DIR}/server/index.mjs`, '.output/server/index.mjs'];
|
|
663
711
|
}
|
|
712
|
+
/**
|
|
713
|
+
* The abort thrown when a test-stack component never becomes reachable.
|
|
714
|
+
*
|
|
715
|
+
* Phrased as an abort, not a diagnosis, because the cause lives in the component's
|
|
716
|
+
* own log and nowhere else — the process is detached, so its stderr never reaches
|
|
717
|
+
* this terminal. Naming the log file is therefore the whole value of the message:
|
|
718
|
+
* an abort that only says "did not come up" moves the guesswork one step later
|
|
719
|
+
* instead of ending it.
|
|
720
|
+
*
|
|
721
|
+
* `lt dev test` catches this, reports it, tears the stack down and exits non-zero,
|
|
722
|
+
* so Playwright never starts (DEV-3208).
|
|
723
|
+
*/
|
|
724
|
+
function unreachableStackError(component, url, logFile) {
|
|
725
|
+
const where = logFile ? ` Why it never came up: ${logFile}` : '';
|
|
726
|
+
return new Error(`Test ${component} never became reachable at ${url} — aborting before Playwright runs, ` +
|
|
727
|
+
`because a suite run against a dead ${component} passes trivially and proves nothing.${where}`);
|
|
728
|
+
}
|
|
664
729
|
/**
|
|
665
730
|
* Resolve the per-stack file/identity names. For a sharded run (`shardIndex`
|
|
666
731
|
* given) everything gets a `.<i>` / `-<i>` suffix so N stacks coexist without
|
|
@@ -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
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.stripAugmentationBlocks = stripAugmentationBlocks;
|
|
4
4
|
exports.stripVendorSchemaAugmentation = stripVendorSchemaAugmentation;
|
|
5
|
+
const glob_1 = require("glob");
|
|
5
6
|
const fs_utils_1 = require("./fs-utils");
|
|
6
7
|
/**
|
|
7
8
|
* Drop `declare module '<nuxt|@nuxt>/schema' { … }` blocks that only augment
|
|
@@ -118,7 +119,6 @@ function stripAugmentationBlocks(source, onSkip) {
|
|
|
118
119
|
* is still there and only the transform knows it.
|
|
119
120
|
*/
|
|
120
121
|
function stripVendorSchemaAugmentation(options) {
|
|
121
|
-
var _a;
|
|
122
122
|
const { coreDir, filesystem } = options;
|
|
123
123
|
if (!filesystem.isDirectory(coreDir))
|
|
124
124
|
return { touched: [], warnings: [] };
|
|
@@ -128,7 +128,18 @@ function stripVendorSchemaAugmentation(options) {
|
|
|
128
128
|
return { touched: [], warnings: [] };
|
|
129
129
|
const touched = [];
|
|
130
130
|
const warnings = [];
|
|
131
|
-
|
|
131
|
+
// `glob`, not `filesystem.find`: gluegun's find is fs-jetpack, which builds its
|
|
132
|
+
// matcher by concatenating the resolved absolute base path in front of the glob
|
|
133
|
+
// (`fs-jetpack/lib/utils/matcher.js` → `convertPatternToAbsolutePath`). On Windows
|
|
134
|
+
// that base is backslash-separated and minimatch reads a backslash inside a
|
|
135
|
+
// PATTERN as an escape, so `D:\…\core/**/*.ts` collapses to `D:coresrc/**/*.ts`
|
|
136
|
+
// and matches nothing — minimatch normalises separators on the file side only.
|
|
137
|
+
// The strip then reported success while the TS2310 augmentation stayed in every
|
|
138
|
+
// Windows-converted project. `glob` keeps pattern and base directory apart and
|
|
139
|
+
// normalises separators itself. Same fix as `api-mode.ts`'s `globFiles`. glob does
|
|
140
|
+
// not follow symlinked directories, unlike jetpack — which is the direction the
|
|
141
|
+
// guard above already wants: nothing inside a linked checkout gets rewritten.
|
|
142
|
+
for (const file of (0, glob_1.globSync)('**/*.ts', { absolute: true, cwd: coreDir, dot: true, nodir: true })) {
|
|
132
143
|
const content = filesystem.read(file);
|
|
133
144
|
if (!content || !content.includes('PublicRuntimeConfig'))
|
|
134
145
|
continue;
|
|
@@ -33,6 +33,7 @@ exports.healVendorClaudeMd = healVendorClaudeMd;
|
|
|
33
33
|
exports.insertVendorBlockIfMissing = insertVendorBlockIfMissing;
|
|
34
34
|
exports.removeVendorBlock = removeVendorBlock;
|
|
35
35
|
exports.upsertVendorBlock = upsertVendorBlock;
|
|
36
|
+
const path_1 = require("path");
|
|
36
37
|
exports.BACKEND_VENDOR_MARKER = '<!-- lt-vendor-marker -->';
|
|
37
38
|
exports.FRONTEND_VENDOR_MARKER = '<!-- lt-vendor-marker-frontend -->';
|
|
38
39
|
exports.ROOT_VENDOR_MARKER = '<!-- lt-vendor-marker-root -->';
|
|
@@ -182,15 +183,20 @@ function healVendorClaudeMd(fs, state) {
|
|
|
182
183
|
changed.push(path);
|
|
183
184
|
}
|
|
184
185
|
};
|
|
186
|
+
// `join`, not string concatenation with `/`: the returned paths are reported
|
|
187
|
+
// to the user and compared by callers, so they have to be the same spelling
|
|
188
|
+
// the rest of the CLI produces. Concatenating `/` onto a Windows directory
|
|
189
|
+
// yields `C:\ws\projects\api/CLAUDE.md` — it opens fine, but it matches
|
|
190
|
+
// nothing and reads like a bug in the output.
|
|
185
191
|
if (state.apiDir) {
|
|
186
|
-
apply(
|
|
192
|
+
apply((0, path_1.join)(state.apiDir, 'CLAUDE.md'), exports.BACKEND_VENDOR_MARKER, state.backendVendor ? buildBackendVendorBlock() : null);
|
|
187
193
|
}
|
|
188
194
|
if (state.appDir) {
|
|
189
|
-
apply(
|
|
195
|
+
apply((0, path_1.join)(state.appDir, 'CLAUDE.md'), exports.FRONTEND_VENDOR_MARKER, state.frontendVendor ? buildFrontendVendorBlock() : null);
|
|
190
196
|
}
|
|
191
197
|
if (state.workspaceRoot) {
|
|
192
198
|
const anyVendor = state.backendVendor || state.frontendVendor;
|
|
193
|
-
apply(
|
|
199
|
+
apply((0, path_1.join)(state.workspaceRoot, 'CLAUDE.md'), exports.ROOT_VENDOR_MARKER, anyVendor ? buildRootVendorBlock({ backend: state.backendVendor, frontend: state.frontendVendor }) : null);
|
|
194
200
|
}
|
|
195
201
|
return changed;
|
|
196
202
|
}
|
|
@@ -242,6 +248,3 @@ function blockRegex(marker) {
|
|
|
242
248
|
function escapeRegExp(value) {
|
|
243
249
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
244
250
|
}
|
|
245
|
-
function joinPath(dir, file) {
|
|
246
|
-
return dir.endsWith('/') ? `${dir}${file}` : `${dir}/${file}`;
|
|
247
|
-
}
|
|
@@ -81,6 +81,7 @@ exports.runStandaloneWorkspaceGate = runStandaloneWorkspaceGate;
|
|
|
81
81
|
exports.shouldProceedAsStandalone = shouldProceedAsStandalone;
|
|
82
82
|
exports.writeApiConfig = writeApiConfig;
|
|
83
83
|
const yaml = __importStar(require("js-yaml"));
|
|
84
|
+
const path_1 = require("path");
|
|
84
85
|
const ensure_root_dockerignore_1 = require("./ensure-root-dockerignore");
|
|
85
86
|
const hoist_workspace_pnpm_config_1 = require("./hoist-workspace-pnpm-config");
|
|
86
87
|
const remove_nested_lockfiles_1 = require("./remove-nested-lockfiles");
|
|
@@ -104,11 +105,20 @@ function detectSubProjectContext(startDir, filesystem) {
|
|
|
104
105
|
const appDir = filesystem.path(root, 'projects', 'app');
|
|
105
106
|
// Resolve absolute prefixes for a clean startsWith compare. We use
|
|
106
107
|
// the gluegun-resolved paths because all callers go through it.
|
|
108
|
+
//
|
|
109
|
+
// The prefix MUST carry the OS separator, not a literal `/`:
|
|
110
|
+
// `filesystem.path` resolves through Node's `path`, so on Windows both
|
|
111
|
+
// sides come back with backslashes and an `${apiDir}/` prefix can never
|
|
112
|
+
// match. Every cwd below `projects\api\` then read as "not a
|
|
113
|
+
// sub-project", and the standalone gate in `runStandaloneWorkspaceGate`
|
|
114
|
+
// waved the user through instead of pointing them at the workspace root
|
|
115
|
+
// — the exact case (cloning a sibling tree INSIDE an existing
|
|
116
|
+
// sub-project) the check exists to prevent.
|
|
107
117
|
const startAbs = filesystem.path(startDir);
|
|
108
|
-
if (startAbs === apiDir || startAbs.startsWith(
|
|
118
|
+
if (startAbs === apiDir || startAbs.startsWith(apiDir + path_1.sep)) {
|
|
109
119
|
return { kind: 'api', subProjectDir: apiDir, workspaceRoot: root };
|
|
110
120
|
}
|
|
111
|
-
if (startAbs === appDir || startAbs.startsWith(
|
|
121
|
+
if (startAbs === appDir || startAbs.startsWith(appDir + path_1.sep)) {
|
|
112
122
|
return { kind: 'app', subProjectDir: appDir, workspaceRoot: root };
|
|
113
123
|
}
|
|
114
124
|
return null;
|