@schwabyio/gta 0.12.0 → 0.14.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/README.md +144 -1
- package/dist/FUNCTIONS.md +563 -0
- package/dist/SPEC.md +428 -85
- package/dist/THIRD_PARTY_NOTICES.txt +26 -0
- package/dist/chunks/chunk-KVO46B7X.js +234 -0
- package/dist/chunks/chunk-Q3VRXFS5.js +577 -0
- package/dist/chunks/{chunk-3NSHWS24.js → chunk-ZDHGCHIV.js} +9344 -1952
- package/dist/gta.js +1046 -995
- package/dist/index.d.ts +3 -0
- package/dist/index.js +14 -0
- package/dist/playwright.d.ts +37 -0
- package/dist/playwright.js +178 -0
- package/dist/types.d.ts +243 -0
- package/dist/worker.js +1 -1
- package/package.json +25 -2
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __gtaCreateRequire } from 'node:module';
|
|
3
|
+
const require = __gtaCreateRequire(import.meta.url);
|
|
4
|
+
import {
|
|
5
|
+
COLLECTIONS_DIR,
|
|
6
|
+
DOC_EXTENSION,
|
|
7
|
+
SETTINGS_FILE,
|
|
8
|
+
TagSchema,
|
|
9
|
+
YamlMapError,
|
|
10
|
+
allSkipped,
|
|
11
|
+
discoverProject,
|
|
12
|
+
environmentNames,
|
|
13
|
+
external_exports,
|
|
14
|
+
flagOverrides,
|
|
15
|
+
iterationName,
|
|
16
|
+
loadProjectCollections,
|
|
17
|
+
loadProjectTls,
|
|
18
|
+
projectEnvironments,
|
|
19
|
+
readProject,
|
|
20
|
+
readYamlMap,
|
|
21
|
+
relativePosix,
|
|
22
|
+
resolveFlags,
|
|
23
|
+
resultName,
|
|
24
|
+
stepsForTags
|
|
25
|
+
} from "./chunk-ZDHGCHIV.js";
|
|
26
|
+
|
|
27
|
+
// src/args.ts
|
|
28
|
+
var UsageError = class extends Error {
|
|
29
|
+
name = "UsageError";
|
|
30
|
+
};
|
|
31
|
+
function parseArgs(argv) {
|
|
32
|
+
const parsed = {
|
|
33
|
+
command: null,
|
|
34
|
+
overrides: {},
|
|
35
|
+
help: false,
|
|
36
|
+
version: false,
|
|
37
|
+
json: false,
|
|
38
|
+
flags: []
|
|
39
|
+
};
|
|
40
|
+
for (let i = 0; i < argv.length; i++) {
|
|
41
|
+
const word = argv[i];
|
|
42
|
+
if (word === "-h" || word === "--help") {
|
|
43
|
+
parsed.help = true;
|
|
44
|
+
} else if (word === "-v" || word === "--version") {
|
|
45
|
+
parsed.version = true;
|
|
46
|
+
} else if (word === "--json") {
|
|
47
|
+
parsed.json = true;
|
|
48
|
+
} else if (word === "--flag" || word.startsWith("--flag=")) {
|
|
49
|
+
const value = word === "--flag" ? argv[++i] : word.slice("--flag=".length);
|
|
50
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
51
|
+
throw new UsageError("--flag needs name=value, such as --flag newCheckout=true");
|
|
52
|
+
}
|
|
53
|
+
parsed.flags.push(value);
|
|
54
|
+
} else if (word.startsWith("--")) {
|
|
55
|
+
const body = word.slice(2);
|
|
56
|
+
const equals = body.indexOf("=");
|
|
57
|
+
if (equals > 0) {
|
|
58
|
+
parsed.overrides[body.slice(0, equals)] = body.slice(equals + 1);
|
|
59
|
+
} else {
|
|
60
|
+
const next = argv[i + 1];
|
|
61
|
+
if (next !== void 0 && !next.startsWith("--")) {
|
|
62
|
+
parsed.overrides[body] = next;
|
|
63
|
+
i++;
|
|
64
|
+
} else {
|
|
65
|
+
parsed.overrides[body] = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} else if (word.startsWith("-") && word.length > 1) {
|
|
69
|
+
throw new UsageError(`Unknown option ${word}. Settings are given as --settingsKey value.`);
|
|
70
|
+
} else if (parsed.command === null) {
|
|
71
|
+
parsed.command = word;
|
|
72
|
+
} else {
|
|
73
|
+
throw new UsageError(
|
|
74
|
+
`Unexpected "${word}". To run several collections, separate them with commas: gta ${parsed.command},${word}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return parsed;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/select.ts
|
|
82
|
+
var idOf = (collection) => collection.name;
|
|
83
|
+
var placeOf = (collection) => collection.relativePath.slice(0, -DOC_EXTENSION.length);
|
|
84
|
+
var normalize = (selector) => {
|
|
85
|
+
const cleaned = selector.normalize("NFC").trim().replace(/\\/g, "/").replace(/^(\.\/)+/, "").replace(/^collections\//, "");
|
|
86
|
+
return {
|
|
87
|
+
name: cleaned.replace(/\/+$/, "").replace(/\.yml$/, ""),
|
|
88
|
+
directoryOnly: cleaned.endsWith("/")
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
function findCollection(collections, selector) {
|
|
92
|
+
const { name, directoryOnly } = normalize(selector);
|
|
93
|
+
if (directoryOnly) return void 0;
|
|
94
|
+
return collections.find((c) => idOf(c) === name) ?? collections.find((c) => placeOf(c).normalize("NFC") === name);
|
|
95
|
+
}
|
|
96
|
+
function selectCollections(collections, request, options) {
|
|
97
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
98
|
+
const inGroup = (group) => group.filter((c) => {
|
|
99
|
+
if (c.doc.exclude === true) excluded.add(c);
|
|
100
|
+
return c.doc.exclude !== true;
|
|
101
|
+
});
|
|
102
|
+
let chosen;
|
|
103
|
+
if (request.kind === "list") {
|
|
104
|
+
const unknown = [];
|
|
105
|
+
chosen = [];
|
|
106
|
+
for (const selector of request.selectors) {
|
|
107
|
+
const { name } = normalize(selector);
|
|
108
|
+
const exact = findCollection(collections, selector);
|
|
109
|
+
const inDirectory = collections.filter((c) => c.directory?.normalize("NFC") === name);
|
|
110
|
+
const found = exact ? [exact] : inDirectory.length > 0 ? inGroup(inDirectory) : null;
|
|
111
|
+
if (found) chosen.push(...found.filter((c) => !chosen.includes(c)));
|
|
112
|
+
else unknown.push(selector);
|
|
113
|
+
}
|
|
114
|
+
if (unknown.length > 0) {
|
|
115
|
+
throw new UsageError(
|
|
116
|
+
`No collection or folder is called ${unknown.map((u) => `"${u}"`).join(", ")}. List them with: gta get`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
chosen = inGroup(collections);
|
|
121
|
+
}
|
|
122
|
+
const targets = [];
|
|
123
|
+
let untagged = 0;
|
|
124
|
+
for (const collection of chosen) {
|
|
125
|
+
const id = idOf(collection);
|
|
126
|
+
if (collection.problems.length > 0) {
|
|
127
|
+
const broken = collection.problems.map((p) => p.message).join("; ");
|
|
128
|
+
targets.push({ id, collection, steps: null, broken });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const picked = stepsForTags(collection.doc, options.tags, options.notTags ?? []);
|
|
132
|
+
if (picked.length === 0 && (options.tags.length > 0 || (options.notTags ?? []).length > 0)) {
|
|
133
|
+
untagged++;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const all = picked.length === collection.doc.steps.length;
|
|
137
|
+
targets.push({ id, collection, steps: all ? null : picked, broken: null });
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
targets,
|
|
141
|
+
// Named on its own, an excluded collection ran: it was not left out.
|
|
142
|
+
excluded: [...excluded].filter((c) => !chosen.includes(c)).map(idOf),
|
|
143
|
+
untagged
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
var stepCountOf = (target) => target.steps?.length ?? target.collection.doc.steps.length;
|
|
147
|
+
|
|
148
|
+
// src/report.ts
|
|
149
|
+
function paint(enabled) {
|
|
150
|
+
const wrap = (open, close) => (text) => enabled ? `\x1B[${open}m${text}\x1B[${close}m` : text;
|
|
151
|
+
return {
|
|
152
|
+
red: wrap(31, 39),
|
|
153
|
+
green: wrap(32, 39),
|
|
154
|
+
cyan: wrap(36, 39),
|
|
155
|
+
yellow: wrap(33, 39),
|
|
156
|
+
dim: wrap(2, 22),
|
|
157
|
+
bold: wrap(1, 22)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function colorWanted(stream, env) {
|
|
161
|
+
if (env.FORCE_COLOR !== void 0 && env.FORCE_COLOR !== "0") return true;
|
|
162
|
+
if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") return false;
|
|
163
|
+
return stream.isTTY === true;
|
|
164
|
+
}
|
|
165
|
+
var tally = (target, outcome, durationMs) => tallyOf(target.id, stepCountOf(target), outcome, durationMs);
|
|
166
|
+
function tallyOf(id, stepCount, outcome, durationMs) {
|
|
167
|
+
if (!outcome.ok) {
|
|
168
|
+
const total = stepCount;
|
|
169
|
+
return {
|
|
170
|
+
id,
|
|
171
|
+
passed: false,
|
|
172
|
+
durationMs,
|
|
173
|
+
steps: { total, passed: 0, failed: 0, errored: 0, skipped: total },
|
|
174
|
+
assertions: { total: 0, passed: 0, failed: 0 },
|
|
175
|
+
skipped: false,
|
|
176
|
+
error: outcome.message,
|
|
177
|
+
problems: []
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const { summary: summary2 } = outcome;
|
|
181
|
+
const assertions = summary2.results.flatMap((r) => r.assertions);
|
|
182
|
+
const failed = assertions.filter((a) => a.status === "fail").length;
|
|
183
|
+
return {
|
|
184
|
+
id,
|
|
185
|
+
passed: summary2.failed === 0 && summary2.errored === 0,
|
|
186
|
+
durationMs,
|
|
187
|
+
steps: {
|
|
188
|
+
total: summary2.total,
|
|
189
|
+
passed: summary2.passed,
|
|
190
|
+
failed: summary2.failed,
|
|
191
|
+
errored: summary2.errored,
|
|
192
|
+
skipped: summary2.skipped
|
|
193
|
+
},
|
|
194
|
+
assertions: { total: assertions.length, passed: assertions.length - failed, failed },
|
|
195
|
+
skipped: allSkipped(summary2),
|
|
196
|
+
error: null,
|
|
197
|
+
// Named for the row they ran with, when a data file drove the collection.
|
|
198
|
+
problems: summary2.results.flatMap(
|
|
199
|
+
(r, i) => r.status === "fail" || r.status === "error" ? [{ ...r, item: { ...r.item, name: iterationName(resultName(r), outcome.steps[i]) } }] : []
|
|
200
|
+
)
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
var seconds = (ms) => `${(ms / 1e3).toFixed(3)}s`;
|
|
204
|
+
var rule = (p, width = 100) => p.dim("\u2500".repeat(width));
|
|
205
|
+
var field = (label, value, width = 14) => `${`${label}:`.padEnd(width)}${value}`;
|
|
206
|
+
var NUMBER_COLUMNS = [
|
|
207
|
+
["Time", 9],
|
|
208
|
+
["Steps", 6],
|
|
209
|
+
["Pass", 5],
|
|
210
|
+
["Fail", 5],
|
|
211
|
+
["Error", 6],
|
|
212
|
+
["Skip", 5],
|
|
213
|
+
["Asserts", 8],
|
|
214
|
+
["Pass", 5],
|
|
215
|
+
["Fail", 5]
|
|
216
|
+
];
|
|
217
|
+
var nameWidthFor = (ids) => Math.min(80, Math.max("Collection".length, ...ids.map((id) => id.length)));
|
|
218
|
+
var tableWidth = (nameWidth) => 4 + 2 + nameWidth + 1 + NUMBER_COLUMNS.reduce((n, [, w]) => n + w + 1, 0) - 1 + 2 + "passed".length;
|
|
219
|
+
function tableHeader(nameWidth, p) {
|
|
220
|
+
const cells = NUMBER_COLUMNS.map(([label, width]) => label.padStart(width));
|
|
221
|
+
return p.bold(`${"#".padStart(4)} ${"Collection".padEnd(nameWidth)} ${cells.join(" ")} Result`);
|
|
222
|
+
}
|
|
223
|
+
var nameCell = (id, width) => id.length > width ? `${id.slice(0, width - 1)}\u2026` : id.padEnd(width);
|
|
224
|
+
function tableRow(n, t, nameWidth, p) {
|
|
225
|
+
const name = nameCell(t.id, nameWidth);
|
|
226
|
+
const values = [
|
|
227
|
+
seconds(t.durationMs),
|
|
228
|
+
t.steps.total,
|
|
229
|
+
t.steps.passed,
|
|
230
|
+
t.steps.failed,
|
|
231
|
+
t.steps.errored,
|
|
232
|
+
t.steps.skipped,
|
|
233
|
+
t.assertions.total,
|
|
234
|
+
t.assertions.passed,
|
|
235
|
+
t.assertions.failed
|
|
236
|
+
];
|
|
237
|
+
const cells = values.map((value, i) => String(value).padStart(NUMBER_COLUMNS[i][1]));
|
|
238
|
+
const result = t.skipped ? p.cyan("skipped") : t.passed ? p.green("passed") : p.red("failed");
|
|
239
|
+
return `${String(n).padStart(4)} ${name} ${cells.join(" ")} ${result}`;
|
|
240
|
+
}
|
|
241
|
+
var clip = (text, max = 300) => text.length > max ? `${text.slice(0, max)}\u2026 (${text.length - max} more characters)` : text;
|
|
242
|
+
function failureDetails(tallies, p) {
|
|
243
|
+
const lines = [];
|
|
244
|
+
for (const t of tallies) {
|
|
245
|
+
if (t.passed) continue;
|
|
246
|
+
lines.push(p.bold(t.id));
|
|
247
|
+
if (t.error) lines.push(` ${p.red("\u2717")} ${t.error}`);
|
|
248
|
+
for (const result of t.problems) {
|
|
249
|
+
const where = result.use ? ` ${p.dim(`(${result.use.set} ${result.use.child + 1}/${result.use.of})`)}` : "";
|
|
250
|
+
const status = result.response ? ` ${p.dim(String(result.response.status))}` : "";
|
|
251
|
+
lines.push(` ${p.red("\u2717")} ${result.item.name}${where}${status}`);
|
|
252
|
+
if (result.error) {
|
|
253
|
+
const { phase, script, line, message } = result.error;
|
|
254
|
+
const at = [script && `${script} script`, line && `line ${line}`].filter(Boolean).join(", ");
|
|
255
|
+
lines.push(` ${p.red(`${phase} error${at ? ` (${at})` : ""}:`)} ${clip(message)}`);
|
|
256
|
+
}
|
|
257
|
+
for (const assertion of result.assertions) {
|
|
258
|
+
if (assertion.status !== "fail") continue;
|
|
259
|
+
const message = assertion.message ? `: ${assertion.message}` : "";
|
|
260
|
+
lines.push(` ${assertion.name}${message}`);
|
|
261
|
+
if (assertion.expected !== void 0) {
|
|
262
|
+
lines.push(` ${p.dim("expected")} ${clip(assertion.expected)}`);
|
|
263
|
+
}
|
|
264
|
+
if (assertion.actual !== void 0) {
|
|
265
|
+
lines.push(` ${p.dim("actual ")} ${clip(assertion.actual)}`);
|
|
266
|
+
}
|
|
267
|
+
if (assertion.unasserted?.length) {
|
|
268
|
+
lines.push(` ${p.dim("not asserted")} ${clip(assertion.unasserted.join(", "))}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
lines.push("");
|
|
273
|
+
}
|
|
274
|
+
return lines;
|
|
275
|
+
}
|
|
276
|
+
function totalsOf(tallies) {
|
|
277
|
+
const sum = (pick) => tallies.reduce((n, t) => n + pick(t), 0);
|
|
278
|
+
return {
|
|
279
|
+
collections: {
|
|
280
|
+
total: tallies.length,
|
|
281
|
+
passed: tallies.filter((t) => t.passed && !t.skipped).length,
|
|
282
|
+
failed: tallies.filter((t) => !t.passed).length,
|
|
283
|
+
skipped: tallies.filter((t) => t.skipped).length
|
|
284
|
+
},
|
|
285
|
+
steps: {
|
|
286
|
+
total: sum((t) => t.steps.total),
|
|
287
|
+
passed: sum((t) => t.steps.passed),
|
|
288
|
+
failed: sum((t) => t.steps.failed),
|
|
289
|
+
errored: sum((t) => t.steps.errored),
|
|
290
|
+
skipped: sum((t) => t.steps.skipped)
|
|
291
|
+
},
|
|
292
|
+
assertions: {
|
|
293
|
+
total: sum((t) => t.assertions.total),
|
|
294
|
+
passed: sum((t) => t.assertions.passed),
|
|
295
|
+
failed: sum((t) => t.assertions.failed)
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function summary(totals, durationMs, p) {
|
|
300
|
+
const { collections: c, steps: s, assertions: a } = totals;
|
|
301
|
+
const passed = c.failed === 0;
|
|
302
|
+
return [
|
|
303
|
+
field(
|
|
304
|
+
"Collections",
|
|
305
|
+
`${c.total} total, ${c.passed} passed, ${c.failed} failed, ${c.skipped} skipped`
|
|
306
|
+
),
|
|
307
|
+
field(
|
|
308
|
+
"Steps",
|
|
309
|
+
`${s.total} total, ${s.passed} passed, ${s.failed} failed, ${s.errored} errored, ${s.skipped} skipped`
|
|
310
|
+
),
|
|
311
|
+
field("Assertions", `${a.total} total, ${a.passed} passed, ${a.failed} failed`),
|
|
312
|
+
field("Run time", seconds(durationMs)),
|
|
313
|
+
field("Result", passed ? p.green(p.bold("PASSED")) : p.red(p.bold("FAILED")))
|
|
314
|
+
];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/settings.ts
|
|
318
|
+
import path from "node:path";
|
|
319
|
+
var SETTING_KEYS = {
|
|
320
|
+
environmentType: {
|
|
321
|
+
kind: "string",
|
|
322
|
+
doc: "The environment to run against, by name: environments/<name>.yml (the global project\u2019s too). Unset: no environment."
|
|
323
|
+
},
|
|
324
|
+
limitConcurrency: {
|
|
325
|
+
kind: "integer",
|
|
326
|
+
doc: "How many collections run at once. Steps within a collection always run in order."
|
|
327
|
+
},
|
|
328
|
+
timeoutCollection: {
|
|
329
|
+
kind: "integer",
|
|
330
|
+
doc: "Milliseconds a collection may take before it is stopped and reported failed."
|
|
331
|
+
},
|
|
332
|
+
bail: {
|
|
333
|
+
kind: "boolean",
|
|
334
|
+
doc: "Stop a collection at its first failing step; the rest are reported skipped."
|
|
335
|
+
},
|
|
336
|
+
tags: {
|
|
337
|
+
kind: "list",
|
|
338
|
+
doc: "Run only what has one of these tags (SPEC.md \xA72.4). Empty: everything."
|
|
339
|
+
},
|
|
340
|
+
notTags: {
|
|
341
|
+
kind: "list",
|
|
342
|
+
doc: "Leave out what has one of these tags: a collection tagged so, or a step where steps carry tags."
|
|
343
|
+
},
|
|
344
|
+
generateJUnitResults: {
|
|
345
|
+
kind: "boolean",
|
|
346
|
+
doc: "Write a JUnit XML report of the run to <testResultsBasePath>/junit/junit.xml, for CI."
|
|
347
|
+
},
|
|
348
|
+
generateJsonResults: {
|
|
349
|
+
kind: "boolean",
|
|
350
|
+
doc: "Write the run as JSON to <testResultsBasePath>/json/results.json, for tools and agents."
|
|
351
|
+
},
|
|
352
|
+
generateHtmlResults: {
|
|
353
|
+
kind: "boolean",
|
|
354
|
+
doc: "Write an HTML report of the run: <testResultsBasePath>/html/summary.html and a page per collection."
|
|
355
|
+
},
|
|
356
|
+
autoOpenTestResultHtml: {
|
|
357
|
+
kind: "boolean",
|
|
358
|
+
doc: "Write the HTML report and open its summary in the browser when the run ends."
|
|
359
|
+
},
|
|
360
|
+
testResultsBasePath: {
|
|
361
|
+
kind: "string",
|
|
362
|
+
doc: "Where reports are written: relative to the project folder, or absolute."
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
var KEYS = Object.keys(SETTING_KEYS);
|
|
366
|
+
var positiveInteger = external_exports.number().int().min(1);
|
|
367
|
+
var SettingsSchema = external_exports.strictObject({
|
|
368
|
+
environmentType: external_exports.string().trim().min(1).nullable().default(null),
|
|
369
|
+
limitConcurrency: positiveInteger.default(1),
|
|
370
|
+
timeoutCollection: positiveInteger.default(36e5),
|
|
371
|
+
bail: external_exports.boolean().default(false),
|
|
372
|
+
tags: external_exports.array(TagSchema).default([]),
|
|
373
|
+
notTags: external_exports.array(TagSchema).default([]),
|
|
374
|
+
generateJUnitResults: external_exports.boolean().default(false),
|
|
375
|
+
generateJsonResults: external_exports.boolean().default(false),
|
|
376
|
+
generateHtmlResults: external_exports.boolean().default(false),
|
|
377
|
+
autoOpenTestResultHtml: external_exports.boolean().default(false),
|
|
378
|
+
testResultsBasePath: external_exports.string().trim().min(1).default("test-results")
|
|
379
|
+
});
|
|
380
|
+
var DEFAULT_SETTINGS = SettingsSchema.parse({});
|
|
381
|
+
var SettingsError = class extends Error {
|
|
382
|
+
name = "SettingsError";
|
|
383
|
+
};
|
|
384
|
+
var envNameOf = (key) => `GTA_${key.replace(/[A-Z]/g, (c) => `_${c}`).toUpperCase()}`;
|
|
385
|
+
var DEFAULT_SOURCE = "default";
|
|
386
|
+
async function loadSettings(options) {
|
|
387
|
+
const { root, global = null, overrides = {}, env = process.env } = options;
|
|
388
|
+
const overrideLabel = options.overrideLabel ?? ((key) => `--${key}`);
|
|
389
|
+
const own = await readSettingsFile(path.join(root, SETTINGS_FILE), SETTINGS_FILE);
|
|
390
|
+
if (own === null && !options.optionalFile) {
|
|
391
|
+
throw new SettingsError(
|
|
392
|
+
`There is no ${SETTINGS_FILE} in ${root}.
|
|
393
|
+
gta runs from a project folder: the one holding collections/ and ${SETTINGS_FILE}.
|
|
394
|
+
A minimal ${SETTINGS_FILE}:
|
|
395
|
+
|
|
396
|
+
environmentType: demo
|
|
397
|
+
limitConcurrency: 4
|
|
398
|
+
`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
const sharedLabel = global ? path.posix.join(global.uses, SETTINGS_FILE) : "";
|
|
402
|
+
const shared = global ? await readSettingsFile(path.join(global.root, SETTINGS_FILE), sharedLabel) : null;
|
|
403
|
+
const origin = /* @__PURE__ */ new Map();
|
|
404
|
+
const merged = {};
|
|
405
|
+
const files = [
|
|
406
|
+
[sharedLabel, shared],
|
|
407
|
+
[SETTINGS_FILE, own]
|
|
408
|
+
];
|
|
409
|
+
for (const [label, fromFile] of files) {
|
|
410
|
+
for (const [key, value] of Object.entries(fromFile ?? {})) {
|
|
411
|
+
checkKey(key, label);
|
|
412
|
+
merged[key] = value;
|
|
413
|
+
origin.set(key, label);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
for (const key of KEYS) {
|
|
417
|
+
const value = env[envNameOf(key)];
|
|
418
|
+
if (value === void 0 || value === "") continue;
|
|
419
|
+
merged[key] = coerce(key, value, envNameOf(key));
|
|
420
|
+
origin.set(key, envNameOf(key));
|
|
421
|
+
}
|
|
422
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
423
|
+
checkKey(key, overrideLabel(key));
|
|
424
|
+
merged[key] = coerce(key, value, overrideLabel(key));
|
|
425
|
+
origin.set(key, overrideLabel(key));
|
|
426
|
+
}
|
|
427
|
+
const result = SettingsSchema.safeParse(merged);
|
|
428
|
+
if (!result.success) {
|
|
429
|
+
const lines = result.error.issues.map((issue) => {
|
|
430
|
+
const key = String(issue.path[0] ?? "");
|
|
431
|
+
return ` ${key} (from ${origin.get(key) ?? SETTINGS_FILE}): ${issue.message}`;
|
|
432
|
+
});
|
|
433
|
+
throw new SettingsError(`Settings are not valid:
|
|
434
|
+
${lines.join("\n")}`);
|
|
435
|
+
}
|
|
436
|
+
const sources = Object.fromEntries(
|
|
437
|
+
KEYS.map((key) => [key, origin.get(key) ?? DEFAULT_SOURCE])
|
|
438
|
+
);
|
|
439
|
+
return { settings: result.data, sources };
|
|
440
|
+
}
|
|
441
|
+
async function readSettingsFile(file, label) {
|
|
442
|
+
try {
|
|
443
|
+
return await readYamlMap(file, label, "limitConcurrency: 4");
|
|
444
|
+
} catch (cause) {
|
|
445
|
+
if (cause instanceof YamlMapError) throw new SettingsError(cause.message);
|
|
446
|
+
throw cause;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function checkKey(key, where) {
|
|
450
|
+
if (!Object.hasOwn(SETTING_KEYS, key)) {
|
|
451
|
+
throw new SettingsError(
|
|
452
|
+
`Unknown setting "${key}" (from ${where}). Settings are: ${KEYS.join(", ")}`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function coerce(key, value, where) {
|
|
457
|
+
const { kind } = SETTING_KEYS[key];
|
|
458
|
+
if (value === true) {
|
|
459
|
+
if (kind === "boolean") return true;
|
|
460
|
+
throw new SettingsError(`${where} needs a value`);
|
|
461
|
+
}
|
|
462
|
+
switch (kind) {
|
|
463
|
+
case "integer":
|
|
464
|
+
return /^\s*\d+\s*$/.test(value) ? Number(value) : value;
|
|
465
|
+
case "boolean":
|
|
466
|
+
return value === "true" ? true : value === "false" ? false : value;
|
|
467
|
+
case "list":
|
|
468
|
+
return value.split(",").map((item) => item.trim()).filter((item) => item !== "");
|
|
469
|
+
case "string":
|
|
470
|
+
return value;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/project.ts
|
|
475
|
+
import fs from "node:fs/promises";
|
|
476
|
+
import path2 from "node:path";
|
|
477
|
+
var ProjectError = class extends Error {
|
|
478
|
+
name = "ProjectError";
|
|
479
|
+
};
|
|
480
|
+
async function loadProject(options) {
|
|
481
|
+
const { root, env } = options;
|
|
482
|
+
if (!await isDirectory(path2.join(root, COLLECTIONS_DIR))) {
|
|
483
|
+
throw new ProjectError(
|
|
484
|
+
`There is no ${COLLECTIONS_DIR}/ in ${root}.
|
|
485
|
+
gta runs from a project folder: the one holding ${COLLECTIONS_DIR}/ and ${SETTINGS_FILE}.`
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
const info = await readProject(root);
|
|
489
|
+
const { settings, sources: settingSources } = await loadSettings({
|
|
490
|
+
root,
|
|
491
|
+
global: info.global,
|
|
492
|
+
env,
|
|
493
|
+
...options.overrides ? { overrides: options.overrides } : {},
|
|
494
|
+
...options.overrideLabel ? { overrideLabel: options.overrideLabel } : {},
|
|
495
|
+
...options.optionalSettings ? { optionalFile: true } : {}
|
|
496
|
+
});
|
|
497
|
+
const layout = await discoverProject(root);
|
|
498
|
+
const collections = await loadProjectCollections(root, layout.files);
|
|
499
|
+
const environments = environmentNames(await projectEnvironments(root, info.global));
|
|
500
|
+
if (settings.environmentType && !environments.includes(settings.environmentType)) {
|
|
501
|
+
throw new ProjectError(
|
|
502
|
+
`environmentType (from ${settingSources.environmentType}): there is no environment called "${settings.environmentType}". ` + (environments.length > 0 ? `This project has: ${environments.join(", ")}` : `This project has no environments/.`)
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
let flags;
|
|
506
|
+
try {
|
|
507
|
+
flags = await resolveFlags({
|
|
508
|
+
root,
|
|
509
|
+
environmentName: settings.environmentType,
|
|
510
|
+
overrides: { ...flagOverrides(options.flagArgs ?? [], env), ...options.flagValues },
|
|
511
|
+
env: { ...process.env, ...env }
|
|
512
|
+
});
|
|
513
|
+
} catch (cause) {
|
|
514
|
+
throw new ProjectError(cause.message);
|
|
515
|
+
}
|
|
516
|
+
const tls = await loadProjectTls(root, info);
|
|
517
|
+
if (tls.problems.length > 0) {
|
|
518
|
+
throw new ProjectError(
|
|
519
|
+
`A certificate in tls.ca cannot be used:
|
|
520
|
+
` + tls.problems.map((problem) => ` ${problem.path}: ${problem.message}`).join("\n")
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
root,
|
|
525
|
+
name: info.doc?.name ?? path2.basename(root),
|
|
526
|
+
global: info.global ? { root: info.global.root, uses: info.global.uses } : null,
|
|
527
|
+
settings,
|
|
528
|
+
settingSources,
|
|
529
|
+
flags,
|
|
530
|
+
collections,
|
|
531
|
+
environments,
|
|
532
|
+
caFiles: tls.files.map(
|
|
533
|
+
(file) => `${relativePosix(root, file.file)} (${file.certificates.map((c) => c.subject).join(", ")})`
|
|
534
|
+
),
|
|
535
|
+
warnings: [...info.problems, ...layout.problems].map(
|
|
536
|
+
(problem) => problem.path ? `${problem.path}: ${problem.message}` : problem.message
|
|
537
|
+
)
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
async function isDirectory(target) {
|
|
541
|
+
try {
|
|
542
|
+
return (await fs.stat(target)).isDirectory();
|
|
543
|
+
} catch {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export {
|
|
549
|
+
UsageError,
|
|
550
|
+
parseArgs,
|
|
551
|
+
idOf,
|
|
552
|
+
findCollection,
|
|
553
|
+
selectCollections,
|
|
554
|
+
stepCountOf,
|
|
555
|
+
paint,
|
|
556
|
+
colorWanted,
|
|
557
|
+
tally,
|
|
558
|
+
tallyOf,
|
|
559
|
+
rule,
|
|
560
|
+
field,
|
|
561
|
+
nameWidthFor,
|
|
562
|
+
tableWidth,
|
|
563
|
+
tableHeader,
|
|
564
|
+
nameCell,
|
|
565
|
+
tableRow,
|
|
566
|
+
failureDetails,
|
|
567
|
+
totalsOf,
|
|
568
|
+
summary,
|
|
569
|
+
SETTING_KEYS,
|
|
570
|
+
DEFAULT_SETTINGS,
|
|
571
|
+
SettingsError,
|
|
572
|
+
envNameOf,
|
|
573
|
+
DEFAULT_SOURCE,
|
|
574
|
+
ProjectError,
|
|
575
|
+
loadProject,
|
|
576
|
+
isDirectory
|
|
577
|
+
};
|