@vincentt-xr/cli 0.16.0 → 0.18.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/MIGRATION.md +24 -0
- package/README.md +94 -1
- package/dist/cli/index.js +196 -87
- package/dist/cli/usage-text.js +32 -10
- package/dist/domain/api.d.ts +10 -8
- package/dist/domain/api.js +14 -9
- package/dist/domain/command.js +25 -16
- package/dist/domain/copy.d.ts +6 -3
- package/dist/domain/copy.js +10 -6
- package/dist/dx/command.d.ts +24 -4
- package/dist/dx/command.js +80 -25
- package/dist/dx/consent.js +6 -5
- package/dist/dx/journal.d.ts +17 -2
- package/dist/dx/journal.js +19 -3
- package/dist/dx/report.js +4 -3
- package/dist/dx/reportIo.js +6 -5
- package/dist/dx/send.js +6 -5
- package/dist/lib/backend.d.ts +0 -16
- package/dist/lib/backend.js +2 -26
- package/dist/lib/detect.d.ts +87 -0
- package/dist/lib/detect.js +867 -0
- package/dist/lib/diagnostics.js +2 -2
- package/dist/lib/feedback.js +1 -1
- package/dist/lib/feedbackCopy.js +1 -1
- package/dist/lib/initFrom.js +4 -4
- package/dist/lib/lifecycle.d.ts +2 -0
- package/dist/lib/lifecycle.js +9 -3
- package/dist/lib/listProjects.js +2 -1
- package/dist/lib/projects.d.ts +37 -2
- package/dist/lib/projects.js +86 -21
- package/dist/lib/settings.d.ts +113 -0
- package/dist/lib/settings.js +284 -0
- package/dist/lib/settingsCopy.d.ts +6 -0
- package/dist/lib/settingsCopy.js +99 -0
- package/dist/lib/settle.d.ts +61 -0
- package/dist/lib/settle.js +338 -0
- package/dist/lib/template.d.ts +19 -0
- package/dist/lib/template.js +73 -0
- package/dist/lib/terminalText.d.ts +3 -0
- package/dist/lib/terminalText.js +10 -0
- package/dist/lib/version.js +5 -11
- package/dist/lib/whoami.js +10 -3
- package/dist/library/assetsCommand.js +1 -1
- package/dist/library/client.js +8 -3
- package/dist/library/render.js +8 -6
- package/dist/login/authBackend.js +3 -2
- package/dist/login/ensure.d.ts +8 -2
- package/dist/login/ensure.js +21 -10
- package/dist/login/guard.js +5 -4
- package/dist/login/loginCommand.d.ts +2 -2
- package/dist/login/loginCommand.js +29 -5
- package/dist/login/pkce.js +1 -1
- package/dist/login/pkceLogin.js +1 -1
- package/dist/platform/outdated.js +2 -2
- package/dist/platform/upgradeReport.js +16 -12
- package/dist/preview/api.d.ts +24 -11
- package/dist/preview/api.js +42 -15
- package/dist/preview/command.d.ts +29 -0
- package/dist/preview/command.js +61 -1
- package/dist/preview/copy.d.ts +35 -3
- package/dist/preview/copy.js +102 -10
- package/dist/preview/proxy.d.ts +38 -1
- package/dist/preview/proxy.js +151 -2
- package/dist/preview/qrPage.js +1 -1
- package/dist/preview/runner.d.ts +25 -13
- package/dist/preview/runner.js +127 -26
- package/dist/preview/viewers.js +1 -1
- package/dist/publish/api.d.ts +39 -6
- package/dist/publish/api.js +80 -9
- package/dist/publish/command.d.ts +25 -0
- package/dist/publish/command.js +230 -32
- package/dist/publish/copy.d.ts +78 -5
- package/dist/publish/copy.js +271 -37
- package/dist/publish/fit.d.ts +15 -0
- package/dist/publish/fit.js +103 -0
- package/dist/publish/manifest.d.ts +44 -3
- package/dist/publish/manifest.js +172 -3
- package/dist/publish/prompt.d.ts +4 -0
- package/dist/publish/prompt.js +15 -1
- package/dist/publish/upload.d.ts +6 -0
- package/dist/publish/upload.js +11 -1
- package/dist/publish/visibility.d.ts +21 -0
- package/dist/publish/visibility.js +68 -0
- package/dist/relay/annotations.js +9 -4
- package/dist/relay/server.d.ts +21 -0
- package/dist/relay/server.js +35 -1
- package/dist/shared/authError.d.ts +27 -0
- package/dist/shared/authError.js +48 -4
- package/dist/shared/config.d.ts +21 -1
- package/dist/shared/config.js +39 -8
- package/dist/shared/countLimit.d.ts +114 -0
- package/dist/shared/countLimit.js +176 -0
- package/dist/shared/http.d.ts +23 -0
- package/dist/shared/http.js +58 -0
- package/package.json +7 -3
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
// Detection on joining: how does this app run? Text matching over package.json,
|
|
2
|
+
// the lockfile's NAME, and at most one bundler config. Nothing is executed,
|
|
3
|
+
// evaluated, required or installed, because joining is the moment a cloned
|
|
4
|
+
// (possibly hostile) repository is first touched. Every read is capped at 256 KB
|
|
5
|
+
// and every scan below is a single linear pass: no backtracking pattern ever
|
|
6
|
+
// sees repository text.
|
|
7
|
+
//
|
|
8
|
+
// Rules and their order are the contract (how-it-works 2A.2). The first match
|
|
9
|
+
// wins, and a value the CLI cannot read is "not found", never the default.
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
export const DETECT_MAX_BYTES = 256 * 1024;
|
|
12
|
+
const CANDIDATES = ["dev", "serve", "start", "develop"];
|
|
13
|
+
const DEV_DEFAULT_PORT = {
|
|
14
|
+
vite: 5173,
|
|
15
|
+
"webpack-dev-server": 8080,
|
|
16
|
+
parcel: 1234,
|
|
17
|
+
astro: 4321,
|
|
18
|
+
"react-scripts": 3000,
|
|
19
|
+
"http-server": 8080,
|
|
20
|
+
serve: 3000,
|
|
21
|
+
"live-server": 8080,
|
|
22
|
+
};
|
|
23
|
+
const BUILD_DEFAULT_OUTPUT = {
|
|
24
|
+
webpack: "dist",
|
|
25
|
+
vite: "dist",
|
|
26
|
+
parcel: "dist",
|
|
27
|
+
"react-scripts": "build",
|
|
28
|
+
astro: "dist",
|
|
29
|
+
};
|
|
30
|
+
const VITE_CONFIGS = [
|
|
31
|
+
"vite.config.ts",
|
|
32
|
+
"vite.config.mts",
|
|
33
|
+
"vite.config.js",
|
|
34
|
+
"vite.config.mjs",
|
|
35
|
+
"vite.config.cjs",
|
|
36
|
+
"vite.config.cts",
|
|
37
|
+
];
|
|
38
|
+
const WEBPACK_CONFIGS = [
|
|
39
|
+
"webpack.config.js",
|
|
40
|
+
"webpack.config.cjs",
|
|
41
|
+
"webpack.config.mjs",
|
|
42
|
+
"webpack.config.ts",
|
|
43
|
+
"webpack.config.cts",
|
|
44
|
+
"webpack.config.mts",
|
|
45
|
+
];
|
|
46
|
+
/** Split a script body on `&&`, `||`, `;`, `|` and `&`, honouring quotes. Linear. */
|
|
47
|
+
export function splitScript(body) {
|
|
48
|
+
const commands = [];
|
|
49
|
+
let words = [];
|
|
50
|
+
let word = "";
|
|
51
|
+
let inWord = false;
|
|
52
|
+
let quote = null;
|
|
53
|
+
let start = 0;
|
|
54
|
+
const endWord = () => {
|
|
55
|
+
if (inWord)
|
|
56
|
+
words.push(word);
|
|
57
|
+
word = "";
|
|
58
|
+
inWord = false;
|
|
59
|
+
};
|
|
60
|
+
const endCommand = (end) => {
|
|
61
|
+
endWord();
|
|
62
|
+
if (words.length > 0)
|
|
63
|
+
commands.push({ words: unwrap(words), text: body.slice(start, end).trim() });
|
|
64
|
+
words = [];
|
|
65
|
+
};
|
|
66
|
+
for (let i = 0; i < body.length; i++) {
|
|
67
|
+
const c = body[i];
|
|
68
|
+
if (quote) {
|
|
69
|
+
if (c === "\\" && quote === '"' && i + 1 < body.length) {
|
|
70
|
+
word += body[++i];
|
|
71
|
+
}
|
|
72
|
+
else if (c === quote) {
|
|
73
|
+
quote = null;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
word += c;
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (c === "'" || c === '"') {
|
|
81
|
+
quote = c;
|
|
82
|
+
inWord = true;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (c === "&" || c === "|" || c === ";") {
|
|
86
|
+
endCommand(i);
|
|
87
|
+
if ((c === "&" || c === "|") && body[i + 1] === c)
|
|
88
|
+
i++;
|
|
89
|
+
start = i + 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (c === " " || c === "\t" || c === "\n") {
|
|
93
|
+
endWord();
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
word += c;
|
|
97
|
+
inWord = true;
|
|
98
|
+
}
|
|
99
|
+
endCommand(body.length);
|
|
100
|
+
return commands.filter((c) => c.words.length > 0);
|
|
101
|
+
}
|
|
102
|
+
const ENV_ASSIGN = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
103
|
+
/** Drop env assignments and the runners that only locate a binary. */
|
|
104
|
+
function unwrap(words) {
|
|
105
|
+
// An index, never a re-slice per word: a script of 50,000 `A=1` words would
|
|
106
|
+
// otherwise copy the array once per word.
|
|
107
|
+
let i = 0;
|
|
108
|
+
for (let guard = 0; guard < 4; guard++) {
|
|
109
|
+
while (i < words.length && ENV_ASSIGN.test(words[i]))
|
|
110
|
+
i++;
|
|
111
|
+
const head = words[i];
|
|
112
|
+
if (head === "npx" || head === "bunx" || head === "cross-env") {
|
|
113
|
+
i++;
|
|
114
|
+
while (i < words.length && words[i].startsWith("-"))
|
|
115
|
+
i++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if ((head === "pnpm" || head === "npm" || head === "yarn") &&
|
|
119
|
+
(words[i + 1] === "exec" || words[i + 1] === "dlx")) {
|
|
120
|
+
i += 2;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
const w = words.slice(i);
|
|
126
|
+
if (w.length > 0)
|
|
127
|
+
w[0] = path.posix.basename(w[0]);
|
|
128
|
+
return w;
|
|
129
|
+
}
|
|
130
|
+
function subcommand(words) {
|
|
131
|
+
return words.slice(1).find((w) => !w.startsWith("-"));
|
|
132
|
+
}
|
|
133
|
+
function serverMarker(c) {
|
|
134
|
+
const [head] = c.words;
|
|
135
|
+
const sub = subcommand(c.words);
|
|
136
|
+
if ((head === "next" || head === "nuxt" || head === "nuxi") &&
|
|
137
|
+
(sub === "dev" || sub === "start")) {
|
|
138
|
+
return `${head} ${sub}`;
|
|
139
|
+
}
|
|
140
|
+
if (head !== undefined && head.startsWith("remix"))
|
|
141
|
+
return head;
|
|
142
|
+
if (head === "nodemon")
|
|
143
|
+
return "nodemon";
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
function devTool(c) {
|
|
147
|
+
const [head] = c.words;
|
|
148
|
+
const sub = subcommand(c.words);
|
|
149
|
+
switch (head) {
|
|
150
|
+
case "vite":
|
|
151
|
+
if (sub === "build" || sub === "preview" || sub === "optimize")
|
|
152
|
+
return undefined;
|
|
153
|
+
return { tool: "vite", runs: "vite" };
|
|
154
|
+
case "webpack":
|
|
155
|
+
return sub === "serve"
|
|
156
|
+
? { tool: "webpack-dev-server", runs: "webpack serve" }
|
|
157
|
+
: undefined;
|
|
158
|
+
case "webpack-dev-server":
|
|
159
|
+
return { tool: "webpack-dev-server", runs: "webpack-dev-server" };
|
|
160
|
+
case "parcel":
|
|
161
|
+
return sub === "build" ? undefined : { tool: "parcel", runs: "parcel" };
|
|
162
|
+
case "astro":
|
|
163
|
+
return sub === "dev" ? { tool: "astro", runs: "astro dev" } : undefined;
|
|
164
|
+
case "react-scripts":
|
|
165
|
+
return sub === "start"
|
|
166
|
+
? { tool: "react-scripts", runs: "react-scripts start" }
|
|
167
|
+
: undefined;
|
|
168
|
+
case "http-server":
|
|
169
|
+
case "serve":
|
|
170
|
+
case "live-server":
|
|
171
|
+
return { tool: head, runs: head };
|
|
172
|
+
default:
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function buildTool(c) {
|
|
177
|
+
const [head] = c.words;
|
|
178
|
+
const sub = subcommand(c.words);
|
|
179
|
+
if (head === "webpack" || head === "webpack-cli")
|
|
180
|
+
return sub === "serve" ? undefined : "webpack";
|
|
181
|
+
if (head === "vite" && sub === "build")
|
|
182
|
+
return "vite";
|
|
183
|
+
if (head === "parcel" && sub === "build")
|
|
184
|
+
return "parcel";
|
|
185
|
+
if (head === "react-scripts" && sub === "build")
|
|
186
|
+
return "react-scripts";
|
|
187
|
+
if (head === "astro" && sub === "build")
|
|
188
|
+
return "astro";
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
/** The value of `--name v`, `--name=v`, or a short alias `-x v`. */
|
|
192
|
+
function optionValue(words, long, short) {
|
|
193
|
+
for (let i = 1; i < words.length; i++) {
|
|
194
|
+
const w = words[i];
|
|
195
|
+
if (w === long || (short !== undefined && w === short))
|
|
196
|
+
return words[i + 1];
|
|
197
|
+
if (w.startsWith(`${long}=`))
|
|
198
|
+
return w.slice(long.length + 1);
|
|
199
|
+
}
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
function shortBody(body) {
|
|
203
|
+
const flat = body.replace(/\s+/g, " ").trim();
|
|
204
|
+
return flat.length > 48 ? `${flat.slice(0, 47)}…` : flat;
|
|
205
|
+
}
|
|
206
|
+
const REGEX_PRECEDERS = new Set([
|
|
207
|
+
"(",
|
|
208
|
+
",",
|
|
209
|
+
"=",
|
|
210
|
+
":",
|
|
211
|
+
"[",
|
|
212
|
+
"!",
|
|
213
|
+
"&",
|
|
214
|
+
"|",
|
|
215
|
+
"?",
|
|
216
|
+
"{",
|
|
217
|
+
"}",
|
|
218
|
+
";",
|
|
219
|
+
"+",
|
|
220
|
+
"-",
|
|
221
|
+
"*",
|
|
222
|
+
"%",
|
|
223
|
+
"<",
|
|
224
|
+
">",
|
|
225
|
+
"~",
|
|
226
|
+
"^",
|
|
227
|
+
]);
|
|
228
|
+
/** A JS/TS tokenizer just good enough to find object keys. One pass, no backtracking. */
|
|
229
|
+
export function tokenize(src) {
|
|
230
|
+
const out = [];
|
|
231
|
+
let i = 0;
|
|
232
|
+
const n = src.length;
|
|
233
|
+
const isIdStart = (c) => /[A-Za-z_$]/.test(c);
|
|
234
|
+
const isId = (c) => /[A-Za-z0-9_$]/.test(c);
|
|
235
|
+
const prevAllowsRegex = () => {
|
|
236
|
+
const prev = out[out.length - 1];
|
|
237
|
+
if (!prev)
|
|
238
|
+
return true;
|
|
239
|
+
if (prev.t === "p")
|
|
240
|
+
return REGEX_PRECEDERS.has(prev.v);
|
|
241
|
+
return prev.t === "ident" && (prev.v === "return" || prev.v === "typeof");
|
|
242
|
+
};
|
|
243
|
+
while (i < n) {
|
|
244
|
+
const c = src[i];
|
|
245
|
+
if (c === " " || c === "\t" || c === "\n" || c === "\r") {
|
|
246
|
+
i++;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (c === "/" && src[i + 1] === "/") {
|
|
250
|
+
const nl = src.indexOf("\n", i);
|
|
251
|
+
i = nl === -1 ? n : nl + 1;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (c === "/" && src[i + 1] === "*") {
|
|
255
|
+
const end = src.indexOf("*/", i + 2);
|
|
256
|
+
i = end === -1 ? n : end + 2;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (c === "'" || c === '"') {
|
|
260
|
+
let j = i + 1;
|
|
261
|
+
let v = "";
|
|
262
|
+
while (j < n && src[j] !== c && src[j] !== "\n") {
|
|
263
|
+
if (src[j] === "\\" && j + 1 < n) {
|
|
264
|
+
v += src[j + 1];
|
|
265
|
+
j += 2;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
v += src[j];
|
|
269
|
+
j++;
|
|
270
|
+
}
|
|
271
|
+
out.push({ t: "str", v });
|
|
272
|
+
i = j + 1;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (c === "`") {
|
|
276
|
+
let j = i + 1;
|
|
277
|
+
let v = "";
|
|
278
|
+
let interpolated = false;
|
|
279
|
+
while (j < n && src[j] !== "`") {
|
|
280
|
+
if (src[j] === "\\" && j + 1 < n) {
|
|
281
|
+
v += src[j + 1];
|
|
282
|
+
j += 2;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (src[j] === "$" && src[j + 1] === "{")
|
|
286
|
+
interpolated = true;
|
|
287
|
+
v += src[j];
|
|
288
|
+
j++;
|
|
289
|
+
}
|
|
290
|
+
out.push({ t: "tpl", v, interpolated });
|
|
291
|
+
i = j + 1;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (c === "/" && prevAllowsRegex()) {
|
|
295
|
+
let j = i + 1;
|
|
296
|
+
let inClass = false;
|
|
297
|
+
while (j < n && src[j] !== "\n") {
|
|
298
|
+
const d = src[j];
|
|
299
|
+
if (d === "\\") {
|
|
300
|
+
j += 2;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (d === "[")
|
|
304
|
+
inClass = true;
|
|
305
|
+
else if (d === "]")
|
|
306
|
+
inClass = false;
|
|
307
|
+
else if (d === "/" && !inClass)
|
|
308
|
+
break;
|
|
309
|
+
j++;
|
|
310
|
+
}
|
|
311
|
+
j++;
|
|
312
|
+
while (j < n && /[a-z]/.test(src[j]))
|
|
313
|
+
j++;
|
|
314
|
+
out.push({ t: "re" });
|
|
315
|
+
i = j;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (isIdStart(c)) {
|
|
319
|
+
let j = i + 1;
|
|
320
|
+
while (j < n && isId(src[j]))
|
|
321
|
+
j++;
|
|
322
|
+
out.push({ t: "ident", v: src.slice(i, j) });
|
|
323
|
+
i = j;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (/[0-9]/.test(c)) {
|
|
327
|
+
let j = i + 1;
|
|
328
|
+
while (j < n && /[0-9A-Za-z_.]/.test(src[j]))
|
|
329
|
+
j++;
|
|
330
|
+
out.push({ t: "num", v: src.slice(i, j) });
|
|
331
|
+
i = j;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (c === "." && src[i + 1] === "." && src[i + 2] === ".") {
|
|
335
|
+
out.push({ t: "p", v: "..." });
|
|
336
|
+
i += 3;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
out.push({ t: "p", v: c });
|
|
340
|
+
i++;
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
const MAX_KEY_DEPTH = 4;
|
|
345
|
+
/**
|
|
346
|
+
* Every object key whose objects nest DIRECTLY from an anonymous root, keyed by
|
|
347
|
+
* its dotted path (`devServer.port`, `build.outDir`). A key inside a function
|
|
348
|
+
* body, an array, a call argument that is not the config itself, or any value
|
|
349
|
+
* expression is not recorded: its value is judged as "an expression", which
|
|
350
|
+
* detection reports as not found.
|
|
351
|
+
*/
|
|
352
|
+
export function scanConfigKeys(src) {
|
|
353
|
+
const toks = tokenize(src);
|
|
354
|
+
const keys = new Map();
|
|
355
|
+
const record = (p, v) => {
|
|
356
|
+
const k = p.join(".");
|
|
357
|
+
const list = keys.get(k);
|
|
358
|
+
if (list)
|
|
359
|
+
list.push(v);
|
|
360
|
+
else
|
|
361
|
+
keys.set(k, [v]);
|
|
362
|
+
};
|
|
363
|
+
const stack = [];
|
|
364
|
+
// Frames opened by a key. Counted rather than searched, so nesting stays linear.
|
|
365
|
+
let named = 0;
|
|
366
|
+
const closeFor = { "}": "{", "]": "[", ")": "(" };
|
|
367
|
+
let i = 0;
|
|
368
|
+
while (i < toks.length) {
|
|
369
|
+
const tok = toks[i];
|
|
370
|
+
const top = stack[stack.length - 1];
|
|
371
|
+
if (tok.t === "p" && (tok.v === "{" || tok.v === "[" || tok.v === "(")) {
|
|
372
|
+
// An anonymous opener keeps the key path only while every frame so far is
|
|
373
|
+
// anonymous: that is the config object itself, however it is exported.
|
|
374
|
+
stack.push({
|
|
375
|
+
open: tok.v,
|
|
376
|
+
key: null,
|
|
377
|
+
spread: false,
|
|
378
|
+
path: tok.v === "{" && named === 0 ? [] : null,
|
|
379
|
+
});
|
|
380
|
+
i++;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (tok.t === "p" && closeFor[tok.v]) {
|
|
384
|
+
const frame = stack.pop();
|
|
385
|
+
if (frame && frame.key !== null) {
|
|
386
|
+
named--;
|
|
387
|
+
if (frame.path)
|
|
388
|
+
record(frame.path, { kind: "object", spread: frame.spread });
|
|
389
|
+
}
|
|
390
|
+
i++;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const inObject = top?.open === "{" && top.path !== null;
|
|
394
|
+
if (inObject && tok.t === "p" && tok.v === "...") {
|
|
395
|
+
top.spread = true;
|
|
396
|
+
i++;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const prev = toks[i - 1];
|
|
400
|
+
const atKeyPosition = inObject &&
|
|
401
|
+
prev !== undefined &&
|
|
402
|
+
prev.t === "p" &&
|
|
403
|
+
(prev.v === "{" || prev.v === ",");
|
|
404
|
+
if (atKeyPosition && (tok.t === "ident" || tok.t === "str")) {
|
|
405
|
+
const next = toks[i + 1];
|
|
406
|
+
const keyPath = [...top.path, tok.v];
|
|
407
|
+
if (next?.t === "p" && next.v === ":") {
|
|
408
|
+
const after = toks[i + 2];
|
|
409
|
+
if (after?.t === "p" && after.v === "{") {
|
|
410
|
+
// Paths deeper than any key detection looks up are not tracked, which
|
|
411
|
+
// also keeps the copy above constant-sized on hostile nesting.
|
|
412
|
+
stack.push({
|
|
413
|
+
open: "{",
|
|
414
|
+
key: tok.v,
|
|
415
|
+
spread: false,
|
|
416
|
+
path: keyPath.length <= MAX_KEY_DEPTH ? keyPath : null,
|
|
417
|
+
});
|
|
418
|
+
named++;
|
|
419
|
+
i += 3;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const end = valueEnd(toks, i + 2);
|
|
423
|
+
record(keyPath, classify(toks.slice(i + 2, end)));
|
|
424
|
+
i = end;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (next?.t === "p" && (next.v === "," || next.v === "}")) {
|
|
428
|
+
// Shorthand `{ port }`: present, from a variable.
|
|
429
|
+
record(keyPath, { kind: "expr" });
|
|
430
|
+
i++;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
i++;
|
|
435
|
+
}
|
|
436
|
+
return keys;
|
|
437
|
+
}
|
|
438
|
+
/** Index just past a value: the next `,` or closer at depth zero. */
|
|
439
|
+
function valueEnd(toks, from) {
|
|
440
|
+
let depth = 0;
|
|
441
|
+
for (let j = from; j < toks.length; j++) {
|
|
442
|
+
const t = toks[j];
|
|
443
|
+
if (t.t !== "p")
|
|
444
|
+
continue;
|
|
445
|
+
if (t.v === "{" || t.v === "[" || t.v === "(")
|
|
446
|
+
depth++;
|
|
447
|
+
else if (t.v === "}" || t.v === "]" || t.v === ")") {
|
|
448
|
+
if (depth === 0)
|
|
449
|
+
return j;
|
|
450
|
+
depth--;
|
|
451
|
+
}
|
|
452
|
+
else if (t.v === "," && depth === 0)
|
|
453
|
+
return j;
|
|
454
|
+
}
|
|
455
|
+
return toks.length;
|
|
456
|
+
}
|
|
457
|
+
function classify(v) {
|
|
458
|
+
if (v.length === 1) {
|
|
459
|
+
const t = v[0];
|
|
460
|
+
if (t.t === "num" && /^[0-9]+$/.test(t.v))
|
|
461
|
+
return { kind: "number", value: Number(t.v) };
|
|
462
|
+
if (t.t === "str")
|
|
463
|
+
return { kind: "string", value: t.v };
|
|
464
|
+
if (t.t === "tpl" && !t.interpolated)
|
|
465
|
+
return { kind: "string", value: t.v };
|
|
466
|
+
return { kind: "expr" };
|
|
467
|
+
}
|
|
468
|
+
// path.resolve(__dirname, 'a', "b") / path.join(...)
|
|
469
|
+
const [a, dot, fn, open] = v;
|
|
470
|
+
const last = v[v.length - 1];
|
|
471
|
+
if (a?.t === "ident" &&
|
|
472
|
+
a.v === "path" &&
|
|
473
|
+
dot?.t === "p" &&
|
|
474
|
+
dot.v === "." &&
|
|
475
|
+
fn?.t === "ident" &&
|
|
476
|
+
(fn.v === "resolve" || fn.v === "join") &&
|
|
477
|
+
open?.t === "p" &&
|
|
478
|
+
open.v === "(" &&
|
|
479
|
+
last?.t === "p" &&
|
|
480
|
+
last.v === ")") {
|
|
481
|
+
const inner = v.slice(4, -1);
|
|
482
|
+
const args = [[]];
|
|
483
|
+
for (const t of inner) {
|
|
484
|
+
if (t.t === "p" && t.v === ",")
|
|
485
|
+
args.push([]);
|
|
486
|
+
else
|
|
487
|
+
args[args.length - 1].push(t);
|
|
488
|
+
}
|
|
489
|
+
if (args.length > 1 && args[args.length - 1].length === 0)
|
|
490
|
+
args.pop();
|
|
491
|
+
const [first, ...rest] = args;
|
|
492
|
+
if (first?.length !== 1 || first[0].t !== "ident" || first[0].v !== "__dirname") {
|
|
493
|
+
return { kind: "expr" };
|
|
494
|
+
}
|
|
495
|
+
const literals = [];
|
|
496
|
+
for (const arg of rest) {
|
|
497
|
+
const t = arg[0];
|
|
498
|
+
if (arg.length !== 1 ||
|
|
499
|
+
!t ||
|
|
500
|
+
!(t.t === "str" || (t.t === "tpl" && !t.interpolated))) {
|
|
501
|
+
return { kind: "expr" };
|
|
502
|
+
}
|
|
503
|
+
literals.push(t.v);
|
|
504
|
+
}
|
|
505
|
+
return { kind: "dirname", fn: fn.v, args: literals };
|
|
506
|
+
}
|
|
507
|
+
return { kind: "expr" };
|
|
508
|
+
}
|
|
509
|
+
// ---- detection -----------------------------------------------------------------
|
|
510
|
+
const ROOT = "/__vincentt_root__";
|
|
511
|
+
/** A config-relative literal joined against the config's own directory, as repo-relative. */
|
|
512
|
+
function insideRepo(joined) {
|
|
513
|
+
const rel = path.posix.relative(ROOT, joined);
|
|
514
|
+
if (rel === "")
|
|
515
|
+
return { ok: false, why: "root" };
|
|
516
|
+
if (rel.startsWith("..") || path.posix.isAbsolute(rel))
|
|
517
|
+
return { ok: false, why: "outside" };
|
|
518
|
+
return { ok: true, rel };
|
|
519
|
+
}
|
|
520
|
+
function resolveDirname(configDir, v) {
|
|
521
|
+
const base = path.posix.join(ROOT, configDir);
|
|
522
|
+
return v.fn === "join"
|
|
523
|
+
? path.posix.join(base, ...v.args)
|
|
524
|
+
: path.posix.resolve(base, ...v.args);
|
|
525
|
+
}
|
|
526
|
+
/** One key's single meaning: absent, a value, or ambiguous. */
|
|
527
|
+
function lookup(cfg, key) {
|
|
528
|
+
const parts = key.split(".");
|
|
529
|
+
for (let k = 1; k < parts.length; k++) {
|
|
530
|
+
const parent = cfg.keys.get(parts.slice(0, k).join("."));
|
|
531
|
+
if (parent && parent.some((p) => p.kind !== "object"))
|
|
532
|
+
return "ambiguous";
|
|
533
|
+
}
|
|
534
|
+
const values = cfg.keys.get(key);
|
|
535
|
+
if (!values || values.length === 0) {
|
|
536
|
+
const parent = cfg.keys.get(parts.slice(0, -1).join("."));
|
|
537
|
+
if (parent && parent.some((p) => p.kind === "object" && p.spread))
|
|
538
|
+
return "ambiguous";
|
|
539
|
+
return "absent";
|
|
540
|
+
}
|
|
541
|
+
const first = JSON.stringify(values[0]);
|
|
542
|
+
if (values.some((v) => JSON.stringify(v) !== first))
|
|
543
|
+
return "ambiguous";
|
|
544
|
+
return values[0];
|
|
545
|
+
}
|
|
546
|
+
function pmOf(lockfile) {
|
|
547
|
+
if (lockfile === "pnpm-lock.yaml")
|
|
548
|
+
return "pnpm";
|
|
549
|
+
if (lockfile === "yarn.lock")
|
|
550
|
+
return "yarn";
|
|
551
|
+
if (lockfile === "bun.lockb" || lockfile === "bun.lock")
|
|
552
|
+
return "bun";
|
|
553
|
+
return "npm";
|
|
554
|
+
}
|
|
555
|
+
function allMissing(read, reason, pm) {
|
|
556
|
+
return {
|
|
557
|
+
read,
|
|
558
|
+
fields: {},
|
|
559
|
+
pm,
|
|
560
|
+
missing: ["dev.command", "dev.port", "build.command", "build.output"].map((field) => ({
|
|
561
|
+
field,
|
|
562
|
+
reason,
|
|
563
|
+
})),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
export async function detect(probes) {
|
|
567
|
+
const read = ["package.json"];
|
|
568
|
+
let lockfile;
|
|
569
|
+
for (const name of [
|
|
570
|
+
"pnpm-lock.yaml",
|
|
571
|
+
"yarn.lock",
|
|
572
|
+
"bun.lockb",
|
|
573
|
+
"bun.lock",
|
|
574
|
+
"package-lock.json",
|
|
575
|
+
]) {
|
|
576
|
+
if (await probes.exists(name)) {
|
|
577
|
+
lockfile = name;
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (lockfile)
|
|
582
|
+
read.push(lockfile);
|
|
583
|
+
const pm = pmOf(lockfile);
|
|
584
|
+
const pkgRead = await probes.readText("package.json");
|
|
585
|
+
if ("missing" in pkgRead || "notFile" in pkgRead) {
|
|
586
|
+
return allMissing(read, "there is no package.json", pm);
|
|
587
|
+
}
|
|
588
|
+
if ("tooLarge" in pkgRead) {
|
|
589
|
+
return allMissing(read, "file too large to read (package.json, over 256 KB)", pm);
|
|
590
|
+
}
|
|
591
|
+
let scripts = {};
|
|
592
|
+
try {
|
|
593
|
+
const pkg = JSON.parse(pkgRead.text);
|
|
594
|
+
const raw = pkg?.scripts;
|
|
595
|
+
if (typeof raw === "object" && raw !== null) {
|
|
596
|
+
scripts = Object.fromEntries(Object.entries(raw).filter((e) => typeof e[1] === "string"));
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
catch {
|
|
600
|
+
return allMissing(read, "package.json is not valid JSON", pm);
|
|
601
|
+
}
|
|
602
|
+
const fields = {};
|
|
603
|
+
const missing = [];
|
|
604
|
+
const run = (script) => `${pm} run ${script}`;
|
|
605
|
+
// The ONE config read, shared by the port and the output rules.
|
|
606
|
+
let config;
|
|
607
|
+
const readConfig = async (script, words, candidates) => {
|
|
608
|
+
const named = optionValue(words, "--config", "-c");
|
|
609
|
+
let rel;
|
|
610
|
+
if (named !== undefined) {
|
|
611
|
+
const norm = path.posix.normalize(named);
|
|
612
|
+
if (path.posix.isAbsolute(norm) || norm.split("/").includes("..")) {
|
|
613
|
+
return {
|
|
614
|
+
refused: `script "${script}" uses a config outside this project, which Vincentt does not read`,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
rel = norm;
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
for (const c of candidates) {
|
|
621
|
+
if (await probes.exists(c)) {
|
|
622
|
+
rel = c;
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (rel === undefined)
|
|
627
|
+
return { absent: true };
|
|
628
|
+
}
|
|
629
|
+
if (config) {
|
|
630
|
+
return config.rel === rel
|
|
631
|
+
? config
|
|
632
|
+
: {
|
|
633
|
+
refused: `script "${script}" uses ${rel}, a second config Vincentt did not read`,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
const r = await probes.readText(rel);
|
|
637
|
+
if ("text" in r)
|
|
638
|
+
read.push(rel);
|
|
639
|
+
if ("missing" in r)
|
|
640
|
+
config = { rel, unreadable: `${rel} is not there` };
|
|
641
|
+
else if ("tooLarge" in r)
|
|
642
|
+
config = { rel, unreadable: `file too large to read (${rel}, over 256 KB)` };
|
|
643
|
+
else if ("notFile" in r)
|
|
644
|
+
config = { rel, unreadable: `${rel} is not a regular file, so it was not read` };
|
|
645
|
+
else
|
|
646
|
+
config = { rel, keys: scanConfigKeys(r.text) };
|
|
647
|
+
return config;
|
|
648
|
+
};
|
|
649
|
+
// ---- dev.command ----
|
|
650
|
+
let chosen;
|
|
651
|
+
let serverRun;
|
|
652
|
+
for (const name of CANDIDATES) {
|
|
653
|
+
const body = scripts[name];
|
|
654
|
+
if (body === undefined)
|
|
655
|
+
continue;
|
|
656
|
+
for (const c of splitScript(body)) {
|
|
657
|
+
const marker = serverMarker(c);
|
|
658
|
+
if (marker) {
|
|
659
|
+
serverRun ??= `script "${name}" runs ${marker}, which needs a server. Vincentt hosts static builds only.`;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (serverRun) {
|
|
664
|
+
missing.push({ field: "dev.command", reason: serverRun });
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
outer: for (const name of CANDIDATES) {
|
|
668
|
+
const body = scripts[name];
|
|
669
|
+
if (body === undefined)
|
|
670
|
+
continue;
|
|
671
|
+
for (const c of splitScript(body)) {
|
|
672
|
+
const tool = devTool(c);
|
|
673
|
+
if (tool) {
|
|
674
|
+
chosen = { script: name, command: c, ...tool };
|
|
675
|
+
break outer;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (chosen) {
|
|
680
|
+
fields["dev.command"] = {
|
|
681
|
+
value: run(chosen.script),
|
|
682
|
+
evidence: `script "${chosen.script}" runs ${chosen.runs}`,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
const first = CANDIDATES.find((n) => scripts[n] !== undefined);
|
|
687
|
+
missing.push({
|
|
688
|
+
field: "dev.command",
|
|
689
|
+
reason: first === undefined
|
|
690
|
+
? "package.json has no dev, serve, start or develop script"
|
|
691
|
+
: `script "${first}" runs ${shortBody(scripts[first])}, which Vincentt can't read.`,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
// ---- dev.port ----
|
|
696
|
+
if (!chosen) {
|
|
697
|
+
missing.push({
|
|
698
|
+
field: "dev.port",
|
|
699
|
+
reason: "no dev command was found to read a port from",
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
const flagged = optionValue(chosen.command.words, "--port") ??
|
|
704
|
+
optionValue(chosen.command.words, "-p");
|
|
705
|
+
if (flagged !== undefined) {
|
|
706
|
+
const n = /^[0-9]+$/.test(flagged) ? Number(flagged) : Number.NaN;
|
|
707
|
+
if (Number.isInteger(n) && n >= 1 && n <= 65535) {
|
|
708
|
+
fields["dev.port"] = {
|
|
709
|
+
value: n,
|
|
710
|
+
evidence: `script "${chosen.script}" passes --port ${n}`,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
missing.push({
|
|
715
|
+
field: "dev.port",
|
|
716
|
+
reason: `script "${chosen.script}" sets --port from a variable`,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
else if (chosen.tool === "vite" || chosen.tool === "webpack-dev-server") {
|
|
721
|
+
const key = chosen.tool === "vite" ? "server.port" : "devServer.port";
|
|
722
|
+
const cfg = await readConfig(chosen.script, chosen.command.words, chosen.tool === "vite" ? VITE_CONFIGS : WEBPACK_CONFIGS);
|
|
723
|
+
const fallback = () => {
|
|
724
|
+
fields["dev.port"] = {
|
|
725
|
+
value: DEV_DEFAULT_PORT[chosen.tool],
|
|
726
|
+
evidence: `default for ${chosen.tool}`,
|
|
727
|
+
};
|
|
728
|
+
};
|
|
729
|
+
if ("absent" in cfg)
|
|
730
|
+
fallback();
|
|
731
|
+
else if ("refused" in cfg)
|
|
732
|
+
missing.push({ field: "dev.port", reason: cfg.refused });
|
|
733
|
+
else if (cfg.unreadable)
|
|
734
|
+
missing.push({ field: "dev.port", reason: cfg.unreadable });
|
|
735
|
+
else {
|
|
736
|
+
const v = lookup(cfg, key);
|
|
737
|
+
if (v === "absent")
|
|
738
|
+
fallback();
|
|
739
|
+
else if (v !== "ambiguous" &&
|
|
740
|
+
v.kind === "number" &&
|
|
741
|
+
v.value >= 1 &&
|
|
742
|
+
v.value <= 65535) {
|
|
743
|
+
fields["dev.port"] = { value: v.value, evidence: `${cfg.rel} ${key}` };
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
missing.push({
|
|
747
|
+
field: "dev.port",
|
|
748
|
+
reason: `${cfg.rel} sets ${key} from a variable`,
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
fields["dev.port"] = {
|
|
755
|
+
value: DEV_DEFAULT_PORT[chosen.tool],
|
|
756
|
+
evidence: `default for ${chosen.tool}`,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// ---- build.command ----
|
|
761
|
+
const buildBody = scripts.build;
|
|
762
|
+
const buildCommands = buildBody === undefined ? [] : splitScript(buildBody);
|
|
763
|
+
if (buildBody === undefined) {
|
|
764
|
+
missing.push({ field: "build.command", reason: "package.json has no build script" });
|
|
765
|
+
}
|
|
766
|
+
else if (buildCommands.some((c) => devTool(c) !== undefined || serverMarker(c) !== undefined)) {
|
|
767
|
+
missing.push({ field: "build.command", reason: `script "build" runs a dev server` });
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
fields["build.command"] = { value: run("build"), evidence: `script "build"` };
|
|
771
|
+
}
|
|
772
|
+
// ---- build.output ----
|
|
773
|
+
if (buildBody === undefined) {
|
|
774
|
+
missing.push({ field: "build.output", reason: "package.json has no build script" });
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
let tool;
|
|
778
|
+
let cmd;
|
|
779
|
+
for (const c of buildCommands) {
|
|
780
|
+
tool = buildTool(c);
|
|
781
|
+
if (tool) {
|
|
782
|
+
cmd = c;
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const put = (rel, evidence) => {
|
|
787
|
+
fields["build.output"] = { value: rel, evidence };
|
|
788
|
+
};
|
|
789
|
+
const miss = (reason) => {
|
|
790
|
+
missing.push({ field: "build.output", reason });
|
|
791
|
+
};
|
|
792
|
+
const judge = (joined, evidence, what) => {
|
|
793
|
+
const inside = insideRepo(joined);
|
|
794
|
+
if (inside.ok)
|
|
795
|
+
put(inside.rel, evidence);
|
|
796
|
+
else
|
|
797
|
+
miss(inside.why === "root"
|
|
798
|
+
? `${what} is this project's own folder`
|
|
799
|
+
: `${what} is outside this project`);
|
|
800
|
+
};
|
|
801
|
+
if (!tool || !cmd) {
|
|
802
|
+
miss(`script "build" runs ${shortBody(buildBody)}, which Vincentt can't read.`);
|
|
803
|
+
}
|
|
804
|
+
else if (tool === "parcel") {
|
|
805
|
+
const dir = optionValue(cmd.words, "--dist-dir");
|
|
806
|
+
if (dir === undefined)
|
|
807
|
+
put("dist", "default for parcel");
|
|
808
|
+
else
|
|
809
|
+
judge(path.posix.join(ROOT, dir), `script "build" passes --dist-dir ${dir}`, `--dist-dir ${dir}`);
|
|
810
|
+
}
|
|
811
|
+
else if (tool === "webpack" || tool === "vite") {
|
|
812
|
+
const cfg = await readConfig("build", cmd.words, tool === "vite" ? VITE_CONFIGS : WEBPACK_CONFIGS);
|
|
813
|
+
const def = () => put(BUILD_DEFAULT_OUTPUT[tool], `default for ${tool}`);
|
|
814
|
+
if ("absent" in cfg)
|
|
815
|
+
def();
|
|
816
|
+
else if ("refused" in cfg)
|
|
817
|
+
miss(cfg.refused);
|
|
818
|
+
else if (cfg.unreadable)
|
|
819
|
+
miss(cfg.unreadable);
|
|
820
|
+
else if (tool === "webpack") {
|
|
821
|
+
const v = lookup(cfg, "output.path");
|
|
822
|
+
const configDir = path.posix.dirname(cfg.rel);
|
|
823
|
+
if (v === "absent")
|
|
824
|
+
def();
|
|
825
|
+
else if (v !== "ambiguous" && v.kind === "dirname") {
|
|
826
|
+
judge(resolveDirname(configDir, v), `${cfg.rel} output.path`, `${cfg.rel} output.path`);
|
|
827
|
+
}
|
|
828
|
+
else
|
|
829
|
+
miss(`${cfg.rel} sets output.path from a variable`);
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
const configDir = path.posix.dirname(cfg.rel);
|
|
833
|
+
let base = path.posix.join(ROOT, configDir);
|
|
834
|
+
const root = lookup(cfg, "root");
|
|
835
|
+
let rootOk = true;
|
|
836
|
+
if (root !== "absent") {
|
|
837
|
+
if (root !== "ambiguous" && root.kind === "string")
|
|
838
|
+
base = path.posix.resolve(base, root.value);
|
|
839
|
+
else if (root !== "ambiguous" && root.kind === "dirname")
|
|
840
|
+
base = resolveDirname(configDir, root);
|
|
841
|
+
else {
|
|
842
|
+
rootOk = false;
|
|
843
|
+
miss(`${cfg.rel} sets root from a variable`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (rootOk) {
|
|
847
|
+
const v = lookup(cfg, "build.outDir");
|
|
848
|
+
if (v === "absent") {
|
|
849
|
+
judge(path.posix.join(base, "dist"), "default for vite", "vite's default output");
|
|
850
|
+
}
|
|
851
|
+
else if (v !== "ambiguous" && v.kind === "string") {
|
|
852
|
+
judge(path.posix.resolve(base, v.value), `${cfg.rel} build.outDir`, `${cfg.rel} build.outDir`);
|
|
853
|
+
}
|
|
854
|
+
else if (v !== "ambiguous" && v.kind === "dirname") {
|
|
855
|
+
judge(resolveDirname(configDir, v), `${cfg.rel} build.outDir`, `${cfg.rel} build.outDir`);
|
|
856
|
+
}
|
|
857
|
+
else
|
|
858
|
+
miss(`${cfg.rel} sets build.outDir from a variable`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
put(BUILD_DEFAULT_OUTPUT[tool], `default for ${tool}`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return { read, fields, missing, pm };
|
|
867
|
+
}
|