@specbook/cli 0.3.0 → 0.5.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/agent-guide.md +170 -0
- package/dist/bin.js +512 -266
- package/dist/standalone.js +9 -5
- package/package.json +6 -5
package/dist/bin.js
CHANGED
|
@@ -243,35 +243,6 @@ function exampleBoolean(key) {
|
|
|
243
243
|
key.toLowerCase()
|
|
244
244
|
);
|
|
245
245
|
}
|
|
246
|
-
var FAKE_BY_FORMAT = {
|
|
247
|
-
email: "internet.email",
|
|
248
|
-
uuid: "string.uuid",
|
|
249
|
-
uri: "internet.url",
|
|
250
|
-
url: "internet.url",
|
|
251
|
-
hostname: "internet.domainName",
|
|
252
|
-
ipv4: "internet.ipv4"
|
|
253
|
-
};
|
|
254
|
-
var FAKE_BY_NAME = [
|
|
255
|
-
[/^(first_?name|given_?name)$/, "person.firstName"],
|
|
256
|
-
[/^(last_?name|family_?name|surname)$/, "person.lastName"],
|
|
257
|
-
[/^(full_?name|display_?name)$/, "person.fullName"],
|
|
258
|
-
[/^name$/, "word.noun"],
|
|
259
|
-
[/^(user_?name|login|handle)$/, "internet.username"],
|
|
260
|
-
[/^e?-?mail(_?address)?$/, "internet.email"],
|
|
261
|
-
[/^(phone|phone_?number|mobile)$/, "phone.number"],
|
|
262
|
-
[/^(city|town)$/, "location.city"],
|
|
263
|
-
[/^country$/, "location.country"],
|
|
264
|
-
[/^(postal_?code|zip|zip_?code)$/, "location.zipCode"],
|
|
265
|
-
[/^(line1|address|street|address_?line_?1)$/, "location.streetAddress"],
|
|
266
|
-
[/^(company|organization|org)$/, "company.name"],
|
|
267
|
-
[/^(title|subject)$/, "lorem.sentence"],
|
|
268
|
-
[/^(description|summary|bio|about|body|comment|note|notes|message)$/, "lorem.sentence"],
|
|
269
|
-
[/^(url|website|homepage)$/, "internet.url"]
|
|
270
|
-
];
|
|
271
|
-
function fakerFor(key, format) {
|
|
272
|
-
const path = format && FAKE_BY_FORMAT[format] || FAKE_BY_NAME.find(([re2]) => re2.test(key.toLowerCase()))?.[1];
|
|
273
|
-
return path ? `{{faker.${path}}}` : void 0;
|
|
274
|
-
}
|
|
275
246
|
var MAX_DEPTH = 6;
|
|
276
247
|
var PLACEHOLDER_VALUE = /^(string|str|<string>|\{string\}|string\d+|null)$/i;
|
|
277
248
|
function sanitizeExample(value, key = "", format, status) {
|
|
@@ -308,18 +279,14 @@ function fits(schema, value) {
|
|
|
308
279
|
}
|
|
309
280
|
}
|
|
310
281
|
var HARD_DEPTH = 14;
|
|
311
|
-
function synthesize(schema, resolve16, depth = 0, seen = [], key = "", status,
|
|
282
|
+
function synthesize(schema, resolve16, depth = 0, seen = [], key = "", status, sparse = false) {
|
|
312
283
|
if (!schema) return null;
|
|
313
|
-
const again = (s, k = key, d = depth, sn = seen) => synthesize(s, resolve16, d, sn, k, status,
|
|
284
|
+
const again = (s, k = key, d = depth, sn = seen) => synthesize(s, resolve16, d, sn, k, status, sparse);
|
|
314
285
|
if (schema.$ref) {
|
|
315
286
|
const target = resolve16(schema.$ref);
|
|
316
287
|
if (seen.includes(schema.$ref)) return again(target, key, Math.max(depth, MAX_DEPTH), []);
|
|
317
288
|
return again(target, key, depth, [...seen, schema.$ref]);
|
|
318
289
|
}
|
|
319
|
-
if (fake && schema.type === "string" && !schema.enum) {
|
|
320
|
-
const generated = fakerFor(key, schema.format);
|
|
321
|
-
if (generated) return generated;
|
|
322
|
-
}
|
|
323
290
|
if (schema.example !== void 0 && fits(schema, schema.example))
|
|
324
291
|
return schema.enum ? schema.example : sanitizeExample(schema.example, key, schema.format, status);
|
|
325
292
|
if (schema.default !== void 0 && fits(schema, schema.default)) return schema.default;
|
|
@@ -725,13 +692,12 @@ ${typeBlocks.join("\n\n") || "export {}"}
|
|
|
725
692
|
const fileFieldPaths = body2?.encoding === "multipart" ? fileFields(body2.schema, resolve16) : [];
|
|
726
693
|
const wholeBodyFile = body2?.encoding === "binary";
|
|
727
694
|
const sample = `./${SAMPLE_FILE}`;
|
|
728
|
-
const paramsFor = (
|
|
695
|
+
const paramsFor = () => {
|
|
729
696
|
const pick = (where) => {
|
|
730
697
|
const out = {};
|
|
731
698
|
for (const p of group(where)) {
|
|
732
699
|
if (p.example !== void 0) out[p.name] = p.example;
|
|
733
|
-
else if (p.required)
|
|
734
|
-
out[p.name] = synthesize(p.schema, resolve16, 0, [], p.name, void 0, fake);
|
|
700
|
+
else if (p.required) out[p.name] = synthesize(p.schema, resolve16, 0, [], p.name);
|
|
735
701
|
}
|
|
736
702
|
return Object.keys(out).length ? out : void 0;
|
|
737
703
|
};
|
|
@@ -752,7 +718,7 @@ ${typeBlocks.join("\n\n") || "export {}"}
|
|
|
752
718
|
title: ex.summary ?? key,
|
|
753
719
|
summary: ex.description,
|
|
754
720
|
source: "spec",
|
|
755
|
-
...paramsFor(
|
|
721
|
+
...paramsFor(),
|
|
756
722
|
body: bodyFor(sanitizeExample(ex.value))
|
|
757
723
|
});
|
|
758
724
|
}
|
|
@@ -761,26 +727,14 @@ ${typeBlocks.join("\n\n") || "export {}"}
|
|
|
761
727
|
name: take("example"),
|
|
762
728
|
title: "Example",
|
|
763
729
|
source: "spec",
|
|
764
|
-
...paramsFor(
|
|
730
|
+
...paramsFor(),
|
|
765
731
|
body: bodyFor(sanitizeExample(body2.media.example))
|
|
766
732
|
});
|
|
767
733
|
const plain = {
|
|
768
|
-
...paramsFor(
|
|
769
|
-
body: body2?.schema ? bodyFor(synthesize(body2.schema, resolve16, 0, [], "", void 0,
|
|
770
|
-
};
|
|
771
|
-
const dynamic = {
|
|
772
|
-
...paramsFor(true),
|
|
773
|
-
body: body2?.schema ? bodyFor(synthesize(body2.schema, resolve16, 0, [], "", void 0, true, true)) : void 0
|
|
734
|
+
...paramsFor(),
|
|
735
|
+
body: body2?.schema ? bodyFor(synthesize(body2.schema, resolve16, 0, [], "", void 0, true)) : void 0
|
|
774
736
|
};
|
|
775
737
|
docs.push({ name: take("default"), title: "Default", source: "generated", ...plain });
|
|
776
|
-
if (JSON.stringify(dynamic) !== JSON.stringify(plain))
|
|
777
|
-
docs.push({
|
|
778
|
-
name: take("dynamic"),
|
|
779
|
-
title: "Dynamic",
|
|
780
|
-
summary: "Generated values, fresh on every play.",
|
|
781
|
-
source: "generated",
|
|
782
|
-
...dynamic
|
|
783
|
-
});
|
|
784
738
|
for (const d of docs)
|
|
785
739
|
exampleFiles.push({
|
|
786
740
|
path: `examples/${id}/${d.name}.example.json`,
|
|
@@ -2108,7 +2062,7 @@ function diffAssertion(a3, context = 3) {
|
|
|
2108
2062
|
return [];
|
|
2109
2063
|
}
|
|
2110
2064
|
|
|
2111
|
-
// ../core/src/faker.ts
|
|
2065
|
+
// ../core/src/faker-runtime.ts
|
|
2112
2066
|
var KEY = "__specbookFaker";
|
|
2113
2067
|
var store = globalThis;
|
|
2114
2068
|
var faker = new Proxy({}, {
|
|
@@ -2129,6 +2083,12 @@ function fakerLocaleNames(config = {}, locale) {
|
|
|
2129
2083
|
const name = locale ?? config.default ?? "en";
|
|
2130
2084
|
return [.../* @__PURE__ */ new Set([...config.locales?.[name] ?? [name, "en"], "base"])];
|
|
2131
2085
|
}
|
|
2086
|
+
function fakerLocaleExports(config = {}) {
|
|
2087
|
+
const names = /* @__PURE__ */ new Set(["en", ...fakerLocaleNames(config)]);
|
|
2088
|
+
for (const name of Object.keys(config.locales ?? {}))
|
|
2089
|
+
for (const n of fakerLocaleNames(config, name)) names.add(n);
|
|
2090
|
+
return [...names].sort();
|
|
2091
|
+
}
|
|
2132
2092
|
function installFaker(mod, config = {}, opts = {}) {
|
|
2133
2093
|
const name = opts.locale ?? config.default ?? "en";
|
|
2134
2094
|
const locales = fakerLocaleNames(config, name).map((n) => {
|
|
@@ -2143,8 +2103,10 @@ function installFaker(mod, config = {}, opts = {}) {
|
|
|
2143
2103
|
}
|
|
2144
2104
|
|
|
2145
2105
|
// ../core/src/template.ts
|
|
2146
|
-
var
|
|
2147
|
-
var
|
|
2106
|
+
var NAME = String.raw`[\w.$\[\]-]+`;
|
|
2107
|
+
var WHOLE = new RegExp(String.raw`^\s*\{\{\s*(${NAME})\s*\}\}\s*$`);
|
|
2108
|
+
var EMBED = new RegExp(String.raw`\{\{\s*(${NAME})\s*\}\}`, "g");
|
|
2109
|
+
var rootName = (name) => name.split(/[.[]/, 1)[0];
|
|
2148
2110
|
var DEFERRED_NAMESPACES = ["secrets", "session"];
|
|
2149
2111
|
var TemplateError = class extends Error {
|
|
2150
2112
|
constructor(variable) {
|
|
@@ -2159,21 +2121,32 @@ function isDeferred(name) {
|
|
|
2159
2121
|
return DEFERRED_NAMESPACES.includes(ns) && name.includes(".");
|
|
2160
2122
|
}
|
|
2161
2123
|
var isFakerName = (name) => name.startsWith("faker.");
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
cur = cur?.[part];
|
|
2168
|
-
if (cur === void 0) return { found: false };
|
|
2124
|
+
var FakerPlaceholderError = class extends TemplateError {
|
|
2125
|
+
constructor(variable, message) {
|
|
2126
|
+
super(variable);
|
|
2127
|
+
this.message = message;
|
|
2128
|
+
this.name = "FakerPlaceholderError";
|
|
2169
2129
|
}
|
|
2170
|
-
|
|
2130
|
+
message;
|
|
2131
|
+
};
|
|
2132
|
+
function fromFaker(path) {
|
|
2133
|
+
const [, group, method, ...rest] = path.split(".");
|
|
2134
|
+
const unknown = () => new FakerPlaceholderError(
|
|
2135
|
+
path,
|
|
2136
|
+
`Unknown faker generator "{{${path}}}": use faker.<module>.<method>`
|
|
2137
|
+
);
|
|
2138
|
+
if (!group || !method || rest.length > 0) throw unknown();
|
|
2139
|
+
const mod = faker[group];
|
|
2140
|
+
const fn = typeof mod === "object" && mod !== null ? mod[method] : void 0;
|
|
2141
|
+
if (typeof fn !== "function" || method === "constructor") throw unknown();
|
|
2142
|
+
return { found: true, value: fn.call(mod) };
|
|
2171
2143
|
}
|
|
2144
|
+
var steps = (path) => path.replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
2172
2145
|
function lookup(scope, path) {
|
|
2173
2146
|
if (isFakerName(path)) return fromFaker(path);
|
|
2174
2147
|
let cur = scope;
|
|
2175
|
-
for (const part of path
|
|
2176
|
-
if (cur !== null && typeof cur === "object" && part
|
|
2148
|
+
for (const part of steps(path)) {
|
|
2149
|
+
if (cur !== null && typeof cur === "object" && Object.hasOwn(cur, part)) {
|
|
2177
2150
|
cur = cur[part];
|
|
2178
2151
|
} else {
|
|
2179
2152
|
return { found: false };
|
|
@@ -2181,7 +2154,19 @@ function lookup(scope, path) {
|
|
|
2181
2154
|
}
|
|
2182
2155
|
return { found: true, value: cur };
|
|
2183
2156
|
}
|
|
2157
|
+
function rejectFakerCall(s) {
|
|
2158
|
+
if (!s.includes("faker")) return;
|
|
2159
|
+
const left = /\{\{\s*(faker\.[\s\S]*?)(?:\}\}|$)/.exec(s.replace(EMBED, ""));
|
|
2160
|
+
if (left) {
|
|
2161
|
+
const name = left[1].trim();
|
|
2162
|
+
throw new FakerPlaceholderError(
|
|
2163
|
+
name,
|
|
2164
|
+
`"{{${name}}}" is not a faker placeholder: placeholders take no arguments. Use faker in a request file for those.`
|
|
2165
|
+
);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2184
2168
|
function renderString(s, scope) {
|
|
2169
|
+
rejectFakerCall(s);
|
|
2185
2170
|
const whole = WHOLE.exec(s);
|
|
2186
2171
|
if (whole) {
|
|
2187
2172
|
const name = whole[1];
|
|
@@ -2862,7 +2847,7 @@ function createColors(enabled) {
|
|
|
2862
2847
|
// ../core/src/reporters/list.ts
|
|
2863
2848
|
var MARKS = { pass: "\u2713", fail: "\u2717", error: "!", skipped: "\u2013" };
|
|
2864
2849
|
var formatDuration = (ms) => ms < 1e3 ? `${Math.round(ms)}ms` : ms < 6e4 ? `${(ms / 1e3).toFixed(2)}s` : `${Math.floor(ms / 6e4)}m ${Math.round(ms % 6e4 / 1e3)}s`;
|
|
2865
|
-
var leaves = (
|
|
2850
|
+
var leaves = (steps3) => steps3.flatMap((s) => s.children?.length ? leaves(s.children) : [s]);
|
|
2866
2851
|
function paint(status, c, text) {
|
|
2867
2852
|
return status === "pass" ? c.green(text) : status === "skipped" ? c.dim(text) : status === "error" ? c.yellow(text) : c.red(text);
|
|
2868
2853
|
}
|
|
@@ -2941,21 +2926,21 @@ function formatSummary(results, o = {}) {
|
|
|
2941
2926
|
const c = o.colors ?? createColors(false);
|
|
2942
2927
|
const t = reportTotals(results);
|
|
2943
2928
|
const part = (n, word, paintFn) => n ? paintFn(`${n} ${word}`) : "";
|
|
2944
|
-
const
|
|
2929
|
+
const join20 = (...parts) => parts.filter(Boolean).join(c.dim(" | ")) || c.dim("none");
|
|
2945
2930
|
const lines2 = [
|
|
2946
2931
|
"",
|
|
2947
|
-
`${c.bold(" Flows ")} ${
|
|
2932
|
+
`${c.bold(" Flows ")} ${join20(
|
|
2948
2933
|
part(t.flows.failed, "failed", c.red),
|
|
2949
2934
|
part(t.flows.errored, "errored", c.yellow),
|
|
2950
2935
|
part(t.flows.flaky, "flaky", c.yellow),
|
|
2951
2936
|
part(t.flows.passed, "passed", c.green)
|
|
2952
2937
|
)} ${c.dim(`(${results.length})`)}`,
|
|
2953
|
-
`${c.bold(" Steps ")} ${
|
|
2938
|
+
`${c.bold(" Steps ")} ${join20(
|
|
2954
2939
|
part(t.steps.failed, "failed", c.red),
|
|
2955
2940
|
part(t.steps.skipped, "skipped", c.dim),
|
|
2956
2941
|
part(t.steps.passed, "passed", c.green)
|
|
2957
2942
|
)}`,
|
|
2958
|
-
`${c.bold(" Assertions ")} ${
|
|
2943
|
+
`${c.bold(" Assertions ")} ${join20(
|
|
2959
2944
|
part(t.assertions.failed, "failed", c.red),
|
|
2960
2945
|
part(t.assertions.passed, "passed", c.green)
|
|
2961
2946
|
)}`
|
|
@@ -3030,7 +3015,7 @@ var esc = (s) => String(s).replace(
|
|
|
3030
3015
|
/[<>&"']|[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g,
|
|
3031
3016
|
(c) => ({ "<": "<", ">": ">", "&": "&", '"': """, "'": "'" })[c] ?? ""
|
|
3032
3017
|
);
|
|
3033
|
-
var leafSteps = (
|
|
3018
|
+
var leafSteps = (steps3) => steps3.flatMap((s) => s.children?.length ? leafSteps(s.children) : [s]);
|
|
3034
3019
|
function toJUnit(results, o = {}) {
|
|
3035
3020
|
const suites = results.map((r) => {
|
|
3036
3021
|
const flat = leafSteps(r.steps);
|
|
@@ -3225,6 +3210,31 @@ function buildUrl(base, req) {
|
|
|
3225
3210
|
return `${base.replace(/\/+$/, "")}${req.url}${q2 ? `?${q2}` : ""}`;
|
|
3226
3211
|
}
|
|
3227
3212
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
3213
|
+
var WHY = {
|
|
3214
|
+
ECONNREFUSED: "connection refused",
|
|
3215
|
+
ENOTFOUND: "host not found",
|
|
3216
|
+
EAI_AGAIN: "host not found",
|
|
3217
|
+
ECONNRESET: "connection reset",
|
|
3218
|
+
ETIMEDOUT: "connection timed out",
|
|
3219
|
+
UND_ERR_CONNECT_TIMEOUT: "connection timed out",
|
|
3220
|
+
EHOSTUNREACH: "host unreachable",
|
|
3221
|
+
ENETUNREACH: "network unreachable"
|
|
3222
|
+
};
|
|
3223
|
+
function unreachable2(e, url) {
|
|
3224
|
+
const cause = e?.cause;
|
|
3225
|
+
if (!(e instanceof TypeError) || !cause) return e;
|
|
3226
|
+
const code = cause.code ?? cause.errors?.find((x2) => x2.code)?.code;
|
|
3227
|
+
const tls = code && /^CERT_|^ERR_TLS|SELF_SIGNED|UNABLE_TO_VERIFY/.test(code) ? "certificate problem" : "";
|
|
3228
|
+
const own = typeof cause.message === "string" && cause.message.length <= 120 ? cause.message : "";
|
|
3229
|
+
const why = code && WHY[code] || tls || own;
|
|
3230
|
+
if (!why) return e;
|
|
3231
|
+
let host = url;
|
|
3232
|
+
try {
|
|
3233
|
+
host = new URL(url).host;
|
|
3234
|
+
} catch {
|
|
3235
|
+
}
|
|
3236
|
+
return new Error(`Could not reach ${host}: ${why}.`, { cause: e });
|
|
3237
|
+
}
|
|
3228
3238
|
function createFetchTransport(opts) {
|
|
3229
3239
|
return async (req, signal) => {
|
|
3230
3240
|
if (signal?.aborted) throw new Error("Stopped before the API answered.");
|
|
@@ -3243,8 +3253,9 @@ function createFetchTransport(opts) {
|
|
|
3243
3253
|
ctl.abort();
|
|
3244
3254
|
}, req.timeoutMs) : void 0;
|
|
3245
3255
|
signal?.addEventListener("abort", () => ctl.abort());
|
|
3256
|
+
const url = buildUrl(opts.baseUrl(req.service), req);
|
|
3246
3257
|
try {
|
|
3247
|
-
const res = await f2(
|
|
3258
|
+
const res = await f2(url, {
|
|
3248
3259
|
method: req.method,
|
|
3249
3260
|
headers,
|
|
3250
3261
|
body: body2,
|
|
@@ -3268,7 +3279,7 @@ function createFetchTransport(opts) {
|
|
|
3268
3279
|
} catch (e) {
|
|
3269
3280
|
if (timedOut) throw new Error(`Timed out after ${req.timeoutMs}ms: no answer from the API.`);
|
|
3270
3281
|
if (signal?.aborted) throw new Error("Stopped before the API answered.");
|
|
3271
|
-
throw e;
|
|
3282
|
+
throw unreachable2(e, url);
|
|
3272
3283
|
} finally {
|
|
3273
3284
|
if (timer) clearTimeout(timer);
|
|
3274
3285
|
}
|
|
@@ -3318,7 +3329,7 @@ function usesOf(names, layers, from) {
|
|
|
3318
3329
|
const out = [];
|
|
3319
3330
|
for (const name of names) {
|
|
3320
3331
|
if (isDeferred(name) || isFakerName(name)) continue;
|
|
3321
|
-
const key = name
|
|
3332
|
+
const key = rootName(name);
|
|
3322
3333
|
if (seen.has(key)) continue;
|
|
3323
3334
|
seen.add(key);
|
|
3324
3335
|
for (let i = layers.length - 1; i >= 0; i--) {
|
|
@@ -3587,7 +3598,7 @@ async function execute(rt2, def, name, flow, kind = "request", noAuth = false) {
|
|
|
3587
3598
|
emit(rt2, { type: "step:end", name, status: result.status });
|
|
3588
3599
|
return { result, response, authResults };
|
|
3589
3600
|
}
|
|
3590
|
-
function summarize(
|
|
3601
|
+
function summarize(steps3) {
|
|
3591
3602
|
const t = { steps: 0, passed: 0, failed: 0, skipped: 0, assertions: { passed: 0, failed: 0 } };
|
|
3592
3603
|
const visit = (s) => {
|
|
3593
3604
|
if (s.children?.length) {
|
|
@@ -3600,21 +3611,21 @@ function summarize(steps) {
|
|
|
3600
3611
|
else t.failed++;
|
|
3601
3612
|
for (const a3 of s.assertions) a3.pass ? t.assertions.passed++ : t.assertions.failed++;
|
|
3602
3613
|
};
|
|
3603
|
-
|
|
3614
|
+
steps3.forEach(visit);
|
|
3604
3615
|
return t;
|
|
3605
3616
|
}
|
|
3606
|
-
function overall(
|
|
3617
|
+
function overall(steps3) {
|
|
3607
3618
|
const all = [];
|
|
3608
3619
|
const walk2 = (s) => {
|
|
3609
3620
|
all.push(s);
|
|
3610
3621
|
s.children?.forEach(walk2);
|
|
3611
3622
|
};
|
|
3612
|
-
|
|
3623
|
+
steps3.forEach(walk2);
|
|
3613
3624
|
if (all.some((s) => s.status === "error")) return "error";
|
|
3614
3625
|
if (all.some((s) => s.status === "fail")) return "fail";
|
|
3615
3626
|
return "pass";
|
|
3616
3627
|
}
|
|
3617
|
-
function finish(rt2, kind, meta,
|
|
3628
|
+
function finish(rt2, kind, meta, steps3, startedAt) {
|
|
3618
3629
|
const result = {
|
|
3619
3630
|
schemaVersion: 1,
|
|
3620
3631
|
runId: rid(),
|
|
@@ -3626,11 +3637,11 @@ function finish(rt2, kind, meta, steps, startedAt) {
|
|
|
3626
3637
|
seed: rt2.o.seed,
|
|
3627
3638
|
usedDrafts: rt2.o.usedDrafts ?? false,
|
|
3628
3639
|
tags: meta.tags ?? [],
|
|
3629
|
-
status: overall(
|
|
3640
|
+
status: overall(steps3),
|
|
3630
3641
|
startedAt: new Date(startedAt).toISOString(),
|
|
3631
3642
|
durationMs: (rt2.o.now?.() ?? now0()) - startedAt,
|
|
3632
|
-
totals: summarize(
|
|
3633
|
-
steps
|
|
3643
|
+
totals: summarize(steps3),
|
|
3644
|
+
steps: steps3
|
|
3634
3645
|
};
|
|
3635
3646
|
const masked = maskDeep(result, rt2.mask);
|
|
3636
3647
|
emit(rt2, { type: "done", result: masked });
|
|
@@ -3718,7 +3729,7 @@ async function runSequence(seq, o) {
|
|
|
3718
3729
|
kind: "sequence",
|
|
3719
3730
|
tags: seq.meta.tags
|
|
3720
3731
|
});
|
|
3721
|
-
const
|
|
3732
|
+
const steps3 = [];
|
|
3722
3733
|
const captured = {};
|
|
3723
3734
|
const from = {};
|
|
3724
3735
|
const remember = (values, step) => {
|
|
@@ -3729,7 +3740,7 @@ async function runSequence(seq, o) {
|
|
|
3729
3740
|
try {
|
|
3730
3741
|
inputs = resolveControls(seq.meta.inputs ?? {}, o.inputs);
|
|
3731
3742
|
} catch (e) {
|
|
3732
|
-
|
|
3743
|
+
steps3.push({
|
|
3733
3744
|
name: "inputs",
|
|
3734
3745
|
kind: "input",
|
|
3735
3746
|
status: "error",
|
|
@@ -3738,7 +3749,7 @@ async function runSequence(seq, o) {
|
|
|
3738
3749
|
error: e.message,
|
|
3739
3750
|
durationMs: 0
|
|
3740
3751
|
});
|
|
3741
|
-
return finish(rt2, "sequence", seq.meta,
|
|
3752
|
+
return finish(rt2, "sequence", seq.meta, steps3, t0);
|
|
3742
3753
|
}
|
|
3743
3754
|
const vars = o.vars ?? {};
|
|
3744
3755
|
const ctx = () => ({
|
|
@@ -3748,7 +3759,7 @@ async function runSequence(seq, o) {
|
|
|
3748
3759
|
env: o.env
|
|
3749
3760
|
});
|
|
3750
3761
|
let failed = false;
|
|
3751
|
-
const skip = (name, kind) =>
|
|
3762
|
+
const skip = (name, kind) => steps3.push({ name, kind, status: "skipped", assertions: [], captured: {}, durationMs: 0 });
|
|
3752
3763
|
for (const node of seq.nodes) {
|
|
3753
3764
|
if (failed && !seq.meta.continueOnFailure) {
|
|
3754
3765
|
skip(node.name, node.kind === "step" ? "request" : node.kind);
|
|
@@ -3767,7 +3778,7 @@ async function runSequence(seq, o) {
|
|
|
3767
3778
|
captured,
|
|
3768
3779
|
from
|
|
3769
3780
|
});
|
|
3770
|
-
|
|
3781
|
+
steps3.push(...authResults, result);
|
|
3771
3782
|
remember(result.captured, node.name);
|
|
3772
3783
|
if (result.status !== "pass") failed = true;
|
|
3773
3784
|
} else if (node.kind === "each") {
|
|
@@ -3803,7 +3814,7 @@ async function runSequence(seq, o) {
|
|
|
3803
3814
|
parent.captured = collected;
|
|
3804
3815
|
parent.durationMs = (o.now?.() ?? now0()) - p0;
|
|
3805
3816
|
remember(collected, node.name);
|
|
3806
|
-
|
|
3817
|
+
steps3.push(parent);
|
|
3807
3818
|
if (parent.status !== "pass") failed = true;
|
|
3808
3819
|
} else if (node.kind === "input") {
|
|
3809
3820
|
const controls = node.controls(ctx());
|
|
@@ -3812,7 +3823,7 @@ async function runSequence(seq, o) {
|
|
|
3812
3823
|
const given = o.onInput ? await o.onInput({ name: node.name, controls: described }) : o.inputs ?? {};
|
|
3813
3824
|
const values = resolveControls(controls, given);
|
|
3814
3825
|
inputs = { ...inputs, ...values };
|
|
3815
|
-
|
|
3826
|
+
steps3.push({
|
|
3816
3827
|
name: node.name,
|
|
3817
3828
|
kind: "input",
|
|
3818
3829
|
status: "pass",
|
|
@@ -3842,12 +3853,12 @@ async function runSequence(seq, o) {
|
|
|
3842
3853
|
s.assertions = a3.results;
|
|
3843
3854
|
if (s.status === "pass" && a3.results.some((r) => !r.pass)) s.status = "fail";
|
|
3844
3855
|
emit(rt2, { type: "step:end", name: node.name, status: s.status });
|
|
3845
|
-
|
|
3856
|
+
steps3.push(s);
|
|
3846
3857
|
if (s.status !== "pass") failed = true;
|
|
3847
3858
|
}
|
|
3848
3859
|
} catch (e) {
|
|
3849
3860
|
const isControl = e instanceof ControlError;
|
|
3850
|
-
|
|
3861
|
+
steps3.push({
|
|
3851
3862
|
name: node.name,
|
|
3852
3863
|
kind: node.kind === "step" ? "request" : node.kind,
|
|
3853
3864
|
status: "error",
|
|
@@ -3859,7 +3870,7 @@ async function runSequence(seq, o) {
|
|
|
3859
3870
|
failed = true;
|
|
3860
3871
|
}
|
|
3861
3872
|
}
|
|
3862
|
-
return finish(rt2, "sequence", seq.meta,
|
|
3873
|
+
return finish(rt2, "sequence", seq.meta, steps3, t0);
|
|
3863
3874
|
}
|
|
3864
3875
|
|
|
3865
3876
|
// ../core/src/runner/suite.ts
|
|
@@ -3919,7 +3930,7 @@ function headerLine(h2, colors2) {
|
|
|
3919
3930
|
var retryLine = (colors2, title, attempt, allowed) => ` ${colors2.yellow("\u21BB")} retrying ${title} (attempt ${attempt} of ${allowed})
|
|
3920
3931
|
`;
|
|
3921
3932
|
function withoutLocations(results) {
|
|
3922
|
-
const strip = (
|
|
3933
|
+
const strip = (steps3) => steps3.map((s) => ({
|
|
3923
3934
|
...s,
|
|
3924
3935
|
assertions: s.assertions.map((a3) => ({ ...a3, location: void 0 })),
|
|
3925
3936
|
...s.children ? { children: strip(s.children) } : {}
|
|
@@ -5472,8 +5483,19 @@ var faker2 = new Proxy({}, {
|
|
|
5472
5483
|
return current[key];
|
|
5473
5484
|
}
|
|
5474
5485
|
});
|
|
5475
|
-
|
|
5476
|
-
|
|
5486
|
+
function fakerLocaleNames2(config = {}, locale) {
|
|
5487
|
+
const name = locale ?? config.default ?? "en";
|
|
5488
|
+
return [.../* @__PURE__ */ new Set([...config.locales?.[name] ?? [name, "en"], "base"])];
|
|
5489
|
+
}
|
|
5490
|
+
function fakerLocaleExports2(config = {}) {
|
|
5491
|
+
const names = /* @__PURE__ */ new Set(["en", ...fakerLocaleNames2(config)]);
|
|
5492
|
+
for (const name of Object.keys(config.locales ?? {}))
|
|
5493
|
+
for (const n of fakerLocaleNames2(config, name)) names.add(n);
|
|
5494
|
+
return [...names].sort();
|
|
5495
|
+
}
|
|
5496
|
+
var NAME2 = String.raw`[\w.$\[\]-]+`;
|
|
5497
|
+
var WHOLE2 = new RegExp(String.raw`^\s*\{\{\s*(${NAME2})\s*\}\}\s*$`);
|
|
5498
|
+
var EMBED2 = new RegExp(String.raw`\{\{\s*(${NAME2})\s*\}\}`, "g");
|
|
5477
5499
|
var DEFERRED_NAMESPACES2 = ["secrets", "session"];
|
|
5478
5500
|
var TemplateError2 = class extends Error {
|
|
5479
5501
|
constructor(variable) {
|
|
@@ -5488,21 +5510,32 @@ function isDeferred2(name) {
|
|
|
5488
5510
|
return DEFERRED_NAMESPACES2.includes(ns) && name.includes(".");
|
|
5489
5511
|
}
|
|
5490
5512
|
var isFakerName2 = (name) => name.startsWith("faker.");
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
cur = cur?.[part];
|
|
5497
|
-
if (cur === void 0) return { found: false };
|
|
5513
|
+
var FakerPlaceholderError2 = class extends TemplateError2 {
|
|
5514
|
+
constructor(variable, message) {
|
|
5515
|
+
super(variable);
|
|
5516
|
+
this.message = message;
|
|
5517
|
+
this.name = "FakerPlaceholderError";
|
|
5498
5518
|
}
|
|
5499
|
-
|
|
5519
|
+
message;
|
|
5520
|
+
};
|
|
5521
|
+
function fromFaker2(path) {
|
|
5522
|
+
const [, group, method, ...rest] = path.split(".");
|
|
5523
|
+
const unknown = () => new FakerPlaceholderError2(
|
|
5524
|
+
path,
|
|
5525
|
+
`Unknown faker generator "{{${path}}}": use faker.<module>.<method>`
|
|
5526
|
+
);
|
|
5527
|
+
if (!group || !method || rest.length > 0) throw unknown();
|
|
5528
|
+
const mod = faker2[group];
|
|
5529
|
+
const fn = typeof mod === "object" && mod !== null ? mod[method] : void 0;
|
|
5530
|
+
if (typeof fn !== "function" || method === "constructor") throw unknown();
|
|
5531
|
+
return { found: true, value: fn.call(mod) };
|
|
5500
5532
|
}
|
|
5533
|
+
var steps2 = (path) => path.replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
5501
5534
|
function lookup2(scope, path) {
|
|
5502
5535
|
if (isFakerName2(path)) return fromFaker2(path);
|
|
5503
5536
|
let cur = scope;
|
|
5504
|
-
for (const part of path
|
|
5505
|
-
if (cur !== null && typeof cur === "object" && part
|
|
5537
|
+
for (const part of steps2(path)) {
|
|
5538
|
+
if (cur !== null && typeof cur === "object" && Object.hasOwn(cur, part)) {
|
|
5506
5539
|
cur = cur[part];
|
|
5507
5540
|
} else {
|
|
5508
5541
|
return { found: false };
|
|
@@ -5510,7 +5543,19 @@ function lookup2(scope, path) {
|
|
|
5510
5543
|
}
|
|
5511
5544
|
return { found: true, value: cur };
|
|
5512
5545
|
}
|
|
5546
|
+
function rejectFakerCall2(s) {
|
|
5547
|
+
if (!s.includes("faker")) return;
|
|
5548
|
+
const left = /\{\{\s*(faker\.[\s\S]*?)(?:\}\}|$)/.exec(s.replace(EMBED2, ""));
|
|
5549
|
+
if (left) {
|
|
5550
|
+
const name = left[1].trim();
|
|
5551
|
+
throw new FakerPlaceholderError2(
|
|
5552
|
+
name,
|
|
5553
|
+
`"{{${name}}}" is not a faker placeholder: placeholders take no arguments. Use faker in a request file for those.`
|
|
5554
|
+
);
|
|
5555
|
+
}
|
|
5556
|
+
}
|
|
5513
5557
|
function renderString2(s, scope) {
|
|
5558
|
+
rejectFakerCall2(s);
|
|
5514
5559
|
const whole = WHOLE2.exec(s);
|
|
5515
5560
|
if (whole) {
|
|
5516
5561
|
const name = whole[1];
|
|
@@ -6143,7 +6188,7 @@ async function writeEnvKey(file, key, value) {
|
|
|
6143
6188
|
await writeFile3(file, setDotenvKey(current, key, value));
|
|
6144
6189
|
}
|
|
6145
6190
|
var IGNORE = ["**/node_modules/**", "**/.generated/**", "**/dist/**", "**/.git/**"];
|
|
6146
|
-
var
|
|
6191
|
+
var NAME22 = /\.example\.json$/;
|
|
6147
6192
|
async function scanExamples(root, outDir, operations) {
|
|
6148
6193
|
const examples = {};
|
|
6149
6194
|
const assets = {};
|
|
@@ -6161,7 +6206,7 @@ async function scanExamples(root, outDir, operations) {
|
|
|
6161
6206
|
const [svc, , id, file] = relative22(gen, abs).split(sep22);
|
|
6162
6207
|
if (!operations.has(`${svc}/${id}`)) continue;
|
|
6163
6208
|
add(`${svc}/${id}`, {
|
|
6164
|
-
name: file.replace(
|
|
6209
|
+
name: file.replace(NAME22, ""),
|
|
6165
6210
|
abs,
|
|
6166
6211
|
source: "generated",
|
|
6167
6212
|
dir: `${svc}/${id}@generated`
|
|
@@ -6175,7 +6220,7 @@ async function scanExamples(root, outDir, operations) {
|
|
|
6175
6220
|
const key = `${svc}/${id}`;
|
|
6176
6221
|
if (!svc || !id || !operations.has(key)) continue;
|
|
6177
6222
|
add(key, {
|
|
6178
|
-
name: file.replace(
|
|
6223
|
+
name: file.replace(NAME22, ""),
|
|
6179
6224
|
abs,
|
|
6180
6225
|
source: "project",
|
|
6181
6226
|
dir: `${svc}/${id}@project`
|
|
@@ -6190,7 +6235,7 @@ async function scanExamples(root, outDir, operations) {
|
|
|
6190
6235
|
list.sort((a3, b) => order(a3) - order(b) || a3.name.localeCompare(b.name));
|
|
6191
6236
|
return { examples, assets };
|
|
6192
6237
|
}
|
|
6193
|
-
var order = (e) => e.source === "project" ? 0 : e.name === "default" ? 2 :
|
|
6238
|
+
var order = (e) => e.source === "project" ? 0 : e.name === "default" ? 2 : 1;
|
|
6194
6239
|
var IGNORE2 = ["**/node_modules/**", "**/.generated/**", "**/dist/**", "**/.git/**"];
|
|
6195
6240
|
async function scanProject(root) {
|
|
6196
6241
|
const rel2 = (e) => e.map((f2) => relative3(root, f2).split(sep3).join("/")).sort();
|
|
@@ -6477,7 +6522,7 @@ async function readBody2(req) {
|
|
|
6477
6522
|
req.on("error", fail);
|
|
6478
6523
|
});
|
|
6479
6524
|
}
|
|
6480
|
-
function fakerLocales(entry) {
|
|
6525
|
+
function fakerLocales(entry, cfg) {
|
|
6481
6526
|
const names = /* @__PURE__ */ new Set(["en", "base"]);
|
|
6482
6527
|
const dir = join5(dirname2(entry), "locale");
|
|
6483
6528
|
if (existsSync3(dir))
|
|
@@ -6485,8 +6530,11 @@ function fakerLocales(entry) {
|
|
|
6485
6530
|
const m = /^([A-Za-z0-9_]+)\.js$/.exec(f2);
|
|
6486
6531
|
if (m) names.add(m[1]);
|
|
6487
6532
|
}
|
|
6488
|
-
return [...names].sort();
|
|
6533
|
+
if (!cfg) return [...names].sort();
|
|
6534
|
+
const wanted = new Set(fakerLocaleExports2(cfg));
|
|
6535
|
+
return [...names].filter((n) => wanted.has(n)).sort();
|
|
6489
6536
|
}
|
|
6537
|
+
var USES_FAKER = /@specbook\/core\/faker|\{\{\s*faker\./;
|
|
6490
6538
|
function slim(manifests) {
|
|
6491
6539
|
return Object.fromEntries(
|
|
6492
6540
|
Object.entries(manifests).map(([k, m]) => [
|
|
@@ -6585,6 +6633,14 @@ function specbook(options) {
|
|
|
6585
6633
|
fakerEntry = createRequire(join5(root, "package.json")).resolve("@faker-js/faker");
|
|
6586
6634
|
} catch {
|
|
6587
6635
|
}
|
|
6636
|
+
if (!fakerEntry) {
|
|
6637
|
+
for (const f2 of [...files.requests, ...files.sequences]) {
|
|
6638
|
+
if (USES_FAKER.test(await readFile6(join5(root, f2), "utf8").catch(() => "")))
|
|
6639
|
+
throw new Error(
|
|
6640
|
+
`${f2} uses faker, but @faker-js/faker is not installed in this project. Add it to devDependencies (npm install --save-dev @faker-js/faker).`
|
|
6641
|
+
);
|
|
6642
|
+
}
|
|
6643
|
+
}
|
|
6588
6644
|
const operationEntries = Object.values(manifests).flatMap((m) => {
|
|
6589
6645
|
const mm = m;
|
|
6590
6646
|
return mm.operations.map(
|
|
@@ -6956,7 +7012,10 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
|
|
|
6956
7012
|
const r = await this.resolve("@faker-js/faker", join5(root, "package.json"), {
|
|
6957
7013
|
skipSelf: true
|
|
6958
7014
|
});
|
|
6959
|
-
if (!r)
|
|
7015
|
+
if (!r)
|
|
7016
|
+
throw new Error(
|
|
7017
|
+
"@faker-js/faker could not be resolved from this project. Add it to devDependencies."
|
|
7018
|
+
);
|
|
6960
7019
|
const pkg = JSON.stringify(r.id);
|
|
6961
7020
|
if (id.startsWith(RESOLVED_FAKER_LOCALE))
|
|
6962
7021
|
return `export { ${id.slice(RESOLVED_FAKER_LOCALE.length)} as default } from ${pkg}`;
|
|
@@ -6965,7 +7024,7 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
|
|
|
6965
7024
|
entry = createRequire(join5(root, "package.json")).resolve("@faker-js/faker");
|
|
6966
7025
|
} catch {
|
|
6967
7026
|
}
|
|
6968
|
-
const names = fakerLocales(entry);
|
|
7027
|
+
const names = fakerLocales(entry, options.faker);
|
|
6969
7028
|
return `export { Faker, base } from ${pkg}
|
|
6970
7029
|
export const locales = {
|
|
6971
7030
|
${names.map((n) => ` ${JSON.stringify(n)}: () => import(${JSON.stringify(`${VIRTUAL_FAKER_LOCALE}${n}`)}).then((m) => m.default)`).join(",\n")}
|
|
@@ -7453,16 +7512,44 @@ ${entry}`
|
|
|
7453
7512
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
7454
7513
|
import { createHash as createHash2 } from "crypto";
|
|
7455
7514
|
import { existsSync as existsSync7, statSync as statSync2 } from "fs";
|
|
7456
|
-
import { chmod, mkdir as mkdir6, readdir as readdir3, readFile as
|
|
7515
|
+
import { chmod, mkdir as mkdir6, readdir as readdir3, readFile as readFile14, writeFile as writeFile7 } from "fs/promises";
|
|
7457
7516
|
import { createRequire as createRequire4 } from "module";
|
|
7458
|
-
import { basename as basename4, dirname as dirname6, join as
|
|
7517
|
+
import { basename as basename4, dirname as dirname6, join as join13, relative as relative9, resolve as resolve9, sep as sep7 } from "path";
|
|
7459
7518
|
import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "url";
|
|
7460
7519
|
import { rolldown } from "rolldown";
|
|
7461
7520
|
|
|
7462
|
-
// src/
|
|
7521
|
+
// src/faker-use.ts
|
|
7463
7522
|
import { readFile as readFile10 } from "fs/promises";
|
|
7464
|
-
import { join as join9
|
|
7523
|
+
import { join as join9 } from "path";
|
|
7465
7524
|
import fg6 from "fast-glob";
|
|
7525
|
+
var USES_FAKER2 = /@specbook\/core\/faker|\{\{\s*faker\./;
|
|
7526
|
+
var IGNORE3 = [
|
|
7527
|
+
"**/node_modules/**",
|
|
7528
|
+
"**/.generated/**",
|
|
7529
|
+
"**/dist/**",
|
|
7530
|
+
"**/.git/**",
|
|
7531
|
+
"**/openapi/**",
|
|
7532
|
+
"**/*.d.ts",
|
|
7533
|
+
"**/package.json",
|
|
7534
|
+
"**/package-lock.json",
|
|
7535
|
+
"**/tsconfig*.json"
|
|
7536
|
+
];
|
|
7537
|
+
async function projectUsesFaker(root) {
|
|
7538
|
+
const files = await fg6(["**/*.ts", "**/*.json", ".specbook/**/*.env"], {
|
|
7539
|
+
cwd: root,
|
|
7540
|
+
ignore: IGNORE3,
|
|
7541
|
+
dot: true,
|
|
7542
|
+
absolute: false
|
|
7543
|
+
});
|
|
7544
|
+
for (const f2 of files)
|
|
7545
|
+
if (USES_FAKER2.test(await readFile10(join9(root, f2), "utf8").catch(() => ""))) return true;
|
|
7546
|
+
return false;
|
|
7547
|
+
}
|
|
7548
|
+
|
|
7549
|
+
// src/ops.ts
|
|
7550
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
7551
|
+
import { join as join10, relative as relative7, sep as sep5 } from "path";
|
|
7552
|
+
import fg7 from "fast-glob";
|
|
7466
7553
|
import { parseSync } from "vite";
|
|
7467
7554
|
var prefixOf = (project) => (project.config.codegen?.importPrefix ?? "#api").replace(/\/+$/, "");
|
|
7468
7555
|
var IMPORT_RE = /import\s+(type\s+)?(?:([\w$]+)\s*,?\s*)?(?:\*\s+as\s+[\w$]+|\{([^}]*)\})?\s*from\s*['"]([^'"]+)['"]/g;
|
|
@@ -7509,7 +7596,7 @@ function importedOperations(file, text, prefix = "#api") {
|
|
|
7509
7596
|
return [...found.values()];
|
|
7510
7597
|
}
|
|
7511
7598
|
async function projectSources(root) {
|
|
7512
|
-
return
|
|
7599
|
+
return fg7(["**/*.ts", ".specbook/**/*.ts"], {
|
|
7513
7600
|
cwd: root,
|
|
7514
7601
|
absolute: true,
|
|
7515
7602
|
dot: true,
|
|
@@ -7527,7 +7614,7 @@ async function usedOperations(project) {
|
|
|
7527
7614
|
const out = { named: /* @__PURE__ */ new Map(), whole: /* @__PURE__ */ new Set(), where: /* @__PURE__ */ new Map() };
|
|
7528
7615
|
const prefix = prefixOf(project);
|
|
7529
7616
|
for (const file of (await projectSources(project.root)).sort()) {
|
|
7530
|
-
const text = await
|
|
7617
|
+
const text = await readFile11(file, "utf8").catch(() => "");
|
|
7531
7618
|
if (!text.includes(prefix)) continue;
|
|
7532
7619
|
const rel2 = relative7(project.root, file).split(sep5).join("/");
|
|
7533
7620
|
for (const imp of importedOperations(file, text, prefix)) {
|
|
@@ -7562,10 +7649,10 @@ async function usedOperationKeys(project, outDir) {
|
|
|
7562
7649
|
}
|
|
7563
7650
|
return new Set([...keys].filter((k) => known.has(k)));
|
|
7564
7651
|
}
|
|
7565
|
-
var metaFile = (outDir, service) =>
|
|
7652
|
+
var metaFile = (outDir, service) => join10(outDir, service, "opmeta.json");
|
|
7566
7653
|
async function readJson(file) {
|
|
7567
7654
|
try {
|
|
7568
|
-
return JSON.parse(await
|
|
7655
|
+
return JSON.parse(await readFile11(file, "utf8"));
|
|
7569
7656
|
} catch {
|
|
7570
7657
|
return void 0;
|
|
7571
7658
|
}
|
|
@@ -7599,11 +7686,11 @@ async function unknownOperationsMessage(project, outDir) {
|
|
|
7599
7686
|
}
|
|
7600
7687
|
|
|
7601
7688
|
// src/runtime.ts
|
|
7602
|
-
import { readFile as
|
|
7689
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
7603
7690
|
import { createRequire as createRequire3 } from "module";
|
|
7604
|
-
import { join as
|
|
7691
|
+
import { join as join12, relative as relative8, resolve as resolve8, sep as sep6 } from "path";
|
|
7605
7692
|
import { pathToFileURL } from "url";
|
|
7606
|
-
import
|
|
7693
|
+
import fg8 from "fast-glob";
|
|
7607
7694
|
|
|
7608
7695
|
// src/run-tests.ts
|
|
7609
7696
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -7614,8 +7701,8 @@ import { dirname as dirname5, resolve as resolve7 } from "path";
|
|
|
7614
7701
|
|
|
7615
7702
|
// src/project.ts
|
|
7616
7703
|
import { existsSync as existsSync6 } from "fs";
|
|
7617
|
-
import { readFile as
|
|
7618
|
-
import { dirname as dirname4, join as
|
|
7704
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
7705
|
+
import { dirname as dirname4, join as join11, resolve as resolve6 } from "path";
|
|
7619
7706
|
var CANDIDATES = [
|
|
7620
7707
|
".specbook/specbook.config.ts",
|
|
7621
7708
|
"specbook.config.ts",
|
|
@@ -7625,7 +7712,7 @@ function findConfigFile(cwd) {
|
|
|
7625
7712
|
let dir = resolve6(cwd);
|
|
7626
7713
|
for (; ; ) {
|
|
7627
7714
|
for (const c of CANDIDATES) {
|
|
7628
|
-
const p =
|
|
7715
|
+
const p = join11(dir, c);
|
|
7629
7716
|
if (existsSync6(p)) return p;
|
|
7630
7717
|
}
|
|
7631
7718
|
const up = dirname4(dir);
|
|
@@ -7634,7 +7721,7 @@ function findConfigFile(cwd) {
|
|
|
7634
7721
|
}
|
|
7635
7722
|
}
|
|
7636
7723
|
function rootOf(configFile) {
|
|
7637
|
-
return configFile.endsWith(`${
|
|
7724
|
+
return configFile.endsWith(`${join11(".specbook", "specbook.config.ts")}`) ? dirname4(dirname4(configFile)) : dirname4(configFile);
|
|
7638
7725
|
}
|
|
7639
7726
|
async function loadProject(cwd, loader) {
|
|
7640
7727
|
const configFile = findConfigFile(cwd);
|
|
@@ -7645,7 +7732,7 @@ async function loadProject(cwd, loader) {
|
|
|
7645
7732
|
}
|
|
7646
7733
|
async function readDotenv(path) {
|
|
7647
7734
|
if (!existsSync6(path)) return {};
|
|
7648
|
-
return parseDotenv(await
|
|
7735
|
+
return parseDotenv(await readFile12(path, "utf8"));
|
|
7649
7736
|
}
|
|
7650
7737
|
|
|
7651
7738
|
// src/version.ts
|
|
@@ -7757,13 +7844,16 @@ function transportFor(ctx, env, vars) {
|
|
|
7757
7844
|
}
|
|
7758
7845
|
});
|
|
7759
7846
|
}
|
|
7760
|
-
function originsFor(config, env, vars) {
|
|
7847
|
+
function originsFor(config, env, vars, basePath = false) {
|
|
7761
7848
|
if (env === "mock") return {};
|
|
7762
7849
|
const out = {};
|
|
7763
7850
|
for (const service of Object.keys(config.services)) {
|
|
7764
7851
|
try {
|
|
7765
7852
|
const url = baseUrlFor(config, service, env, vars);
|
|
7766
|
-
if (url)
|
|
7853
|
+
if (url) {
|
|
7854
|
+
const u = new URL(url);
|
|
7855
|
+
out[service] = basePath ? `${u.origin}${u.pathname.replace(/\/+$/, "")}` : u.origin;
|
|
7856
|
+
}
|
|
7767
7857
|
} catch {
|
|
7768
7858
|
}
|
|
7769
7859
|
}
|
|
@@ -7844,6 +7934,7 @@ async function execute2(ctx, o, picked, specs, fileVars, secrets) {
|
|
|
7844
7934
|
const vars = environmentScope(config, o.env, { ...fileVars, ...given });
|
|
7845
7935
|
const transport = transportFor(ctx, o.env, vars);
|
|
7846
7936
|
const origins = originsFor(config, o.env, vars);
|
|
7937
|
+
const addresses = originsFor(config, o.env, vars, true);
|
|
7847
7938
|
const faker3 = await ctx.loadFaker();
|
|
7848
7939
|
const git2 = { ...ctx.git ?? gitInfo(ctx.root) };
|
|
7849
7940
|
const ci = detectCi(process.env);
|
|
@@ -7862,7 +7953,7 @@ async function execute2(ctx, o, picked, specs, fileVars, secrets) {
|
|
|
7862
7953
|
colors: colors2,
|
|
7863
7954
|
quiet: o.quiet,
|
|
7864
7955
|
trace: o.trace,
|
|
7865
|
-
origins,
|
|
7956
|
+
origins: addresses,
|
|
7866
7957
|
cwd: ctx.root,
|
|
7867
7958
|
readSource,
|
|
7868
7959
|
rerun: (r) => `${ctx.command}${r.file && ctx.command === "specbook test" ? ` ${r.file}` : ""} --env ${o.env} --grep ${quote3(escapeRegExp(r.title))}${o.safe ? " --safe" : ""}`
|
|
@@ -7971,7 +8062,7 @@ async function execute2(ctx, o, picked, specs, fileVars, secrets) {
|
|
|
7971
8062
|
toJUnit(clean, {
|
|
7972
8063
|
timestamp: meta.startedAt,
|
|
7973
8064
|
hostname: hostname(),
|
|
7974
|
-
origins,
|
|
8065
|
+
origins: addresses,
|
|
7975
8066
|
properties: { env: o.env, sha: git2.sha, branch: git2.branch, specbook: meta.specbook }
|
|
7976
8067
|
})
|
|
7977
8068
|
);
|
|
@@ -8020,7 +8111,7 @@ async function execute2(ctx, o, picked, specs, fileVars, secrets) {
|
|
|
8020
8111
|
|
|
8021
8112
|
// src/runtime.ts
|
|
8022
8113
|
async function discoverTargets(project, loader, patterns) {
|
|
8023
|
-
const files = (await
|
|
8114
|
+
const files = (await fg8(patterns, {
|
|
8024
8115
|
cwd: project.root,
|
|
8025
8116
|
absolute: true,
|
|
8026
8117
|
ignore: ["**/node_modules/**", "**/.generated/**"]
|
|
@@ -8039,7 +8130,7 @@ function mockLoaderFor(project) {
|
|
|
8039
8130
|
const manifest = (service) => {
|
|
8040
8131
|
let m = manifests.get(service);
|
|
8041
8132
|
if (!m) {
|
|
8042
|
-
m =
|
|
8133
|
+
m = readFile13(join12(outDir, service, "manifest.json"), "utf8").then(JSON.parse);
|
|
8043
8134
|
manifests.set(service, m);
|
|
8044
8135
|
}
|
|
8045
8136
|
return m;
|
|
@@ -8053,8 +8144,8 @@ function mockLoaderFor(project) {
|
|
|
8053
8144
|
for (const name of [String(status), "default"]) {
|
|
8054
8145
|
try {
|
|
8055
8146
|
return JSON.parse(
|
|
8056
|
-
await
|
|
8057
|
-
|
|
8147
|
+
await readFile13(
|
|
8148
|
+
join12(outDir, service, "examples", opId2, `${name}.response.json`),
|
|
8058
8149
|
"utf8"
|
|
8059
8150
|
)
|
|
8060
8151
|
);
|
|
@@ -8067,7 +8158,7 @@ function mockLoaderFor(project) {
|
|
|
8067
8158
|
}
|
|
8068
8159
|
async function loadFaker(project) {
|
|
8069
8160
|
try {
|
|
8070
|
-
const entry = createRequire3(
|
|
8161
|
+
const entry = createRequire3(join12(project.root, "package.json")).resolve("@faker-js/faker");
|
|
8071
8162
|
return await import(pathToFileURL(entry).href);
|
|
8072
8163
|
} catch {
|
|
8073
8164
|
return void 0;
|
|
@@ -8095,9 +8186,9 @@ function git(root) {
|
|
|
8095
8186
|
function standalonePath() {
|
|
8096
8187
|
const here = dirname6(fileURLToPath(import.meta.url));
|
|
8097
8188
|
for (const p of [
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8189
|
+
join13(here, "standalone.js"),
|
|
8190
|
+
join13(here, "..", "standalone.js"),
|
|
8191
|
+
join13(here, "..", "standalone.ts")
|
|
8101
8192
|
])
|
|
8102
8193
|
if (existsSync7(p)) return p;
|
|
8103
8194
|
throw new Error("The test runner is missing from this installation of specbook");
|
|
@@ -8105,7 +8196,7 @@ function standalonePath() {
|
|
|
8105
8196
|
async function walk(dir) {
|
|
8106
8197
|
const entries = await readdir3(dir, { withFileTypes: true }).catch(() => []);
|
|
8107
8198
|
return (await Promise.all(
|
|
8108
|
-
entries.map((e) => e.isDirectory() ? walk(
|
|
8199
|
+
entries.map((e) => e.isDirectory() ? walk(join13(dir, e.name)) : [join13(dir, e.name)])
|
|
8109
8200
|
)).flat();
|
|
8110
8201
|
}
|
|
8111
8202
|
var SKIP = /(^|\/)(node_modules|\.generated|\.git|dist|dist-[^/]+)(\/|$)/;
|
|
@@ -8113,15 +8204,15 @@ var CODE = /\.(ts|tsx|mts|cts|js|mjs|cjs|md)$/;
|
|
|
8113
8204
|
async function embed(project, outDir, used) {
|
|
8114
8205
|
const root = project.root;
|
|
8115
8206
|
const out = { examples: {}, files: {}, bytes: 0 };
|
|
8116
|
-
const read = async (file) => JSON.parse(await
|
|
8207
|
+
const read = async (file) => JSON.parse(await readFile14(file, "utf8"));
|
|
8117
8208
|
for (const [service, ops] of Object.entries(used))
|
|
8118
8209
|
for (const id of Object.keys(ops)) {
|
|
8119
|
-
const dir =
|
|
8210
|
+
const dir = join13(outDir, service, "examples", id);
|
|
8120
8211
|
for (const f2 of await readdir3(dir).catch(() => [])) {
|
|
8121
8212
|
const m = /^(.+)\.example\.json$/.exec(f2);
|
|
8122
8213
|
if (!m) continue;
|
|
8123
8214
|
out.examples[`${service}/${id}/${m[1]}`] = {
|
|
8124
|
-
doc: await read(
|
|
8215
|
+
doc: await read(join13(dir, f2)),
|
|
8125
8216
|
source: "generated",
|
|
8126
8217
|
dir: ""
|
|
8127
8218
|
};
|
|
@@ -8146,24 +8237,21 @@ async function embed(project, outDir, used) {
|
|
|
8146
8237
|
for (const abs of await walk(folder)) {
|
|
8147
8238
|
const rel2 = posix(relative9(root, abs));
|
|
8148
8239
|
if (SKIP.test(rel2) || CODE.test(rel2) || rel2.endsWith(".example.json")) continue;
|
|
8149
|
-
const data2 = await
|
|
8240
|
+
const data2 = await readFile14(abs);
|
|
8150
8241
|
out.files[rel2] = data2.toString("base64");
|
|
8151
8242
|
out.bytes += data2.length;
|
|
8152
8243
|
}
|
|
8153
8244
|
return out;
|
|
8154
8245
|
}
|
|
8155
|
-
async function fakerSource(project
|
|
8246
|
+
async function fakerSource(project) {
|
|
8156
8247
|
let entry;
|
|
8157
8248
|
try {
|
|
8158
|
-
entry = createRequire4(
|
|
8249
|
+
entry = createRequire4(join13(project.root, "package.json")).resolve("@faker-js/faker");
|
|
8159
8250
|
} catch {
|
|
8160
8251
|
return void 0;
|
|
8161
8252
|
}
|
|
8162
|
-
if (!project.config.faker && !/faker/.test(sources)) return void 0;
|
|
8163
8253
|
const cfg = project.config.faker ?? {};
|
|
8164
|
-
const wanted = /* @__PURE__ */ new Set(["Faker", ...
|
|
8165
|
-
for (const name of Object.keys(cfg.locales ?? {}))
|
|
8166
|
-
for (const n of fakerLocaleNames(cfg, name)) wanted.add(n);
|
|
8254
|
+
const wanted = /* @__PURE__ */ new Set(["Faker", ...fakerLocaleExports(cfg)]);
|
|
8167
8255
|
const have = new Set(Object.keys(await import(pathToFileURL2(entry).href)));
|
|
8168
8256
|
const names = [...wanted].filter((n) => have.has(n)).sort();
|
|
8169
8257
|
return `export { ${names.join(", ")} } from ${JSON.stringify(entry)}
|
|
@@ -8193,11 +8281,10 @@ async function bundleCommand(project, loader, o) {
|
|
|
8193
8281
|
const files = [...new Set(chosen.map((t) => t.file))].sort();
|
|
8194
8282
|
const ops = await usedMetadata(project, outDir);
|
|
8195
8283
|
const sourceText = (await Promise.all(
|
|
8196
|
-
[project.configFile, ...files.map((f2) =>
|
|
8197
|
-
(f2) =>
|
|
8284
|
+
[project.configFile, ...files.map((f2) => join13(root, f2))].map(
|
|
8285
|
+
(f2) => readFile14(f2, "utf8").catch(() => "")
|
|
8198
8286
|
)
|
|
8199
8287
|
)).join("\n");
|
|
8200
|
-
const faker3 = await fakerSource(project, sourceText);
|
|
8201
8288
|
const embedded = await embed(project, outDir, ops.services);
|
|
8202
8289
|
const environments = {};
|
|
8203
8290
|
for (const env of await listEnvironmentFiles(root))
|
|
@@ -8211,48 +8298,48 @@ async function bundleCommand(project, loader, o) {
|
|
|
8211
8298
|
examples: embedded.examples,
|
|
8212
8299
|
files: embedded.files
|
|
8213
8300
|
};
|
|
8214
|
-
const entry = [
|
|
8215
|
-
`import { main } from ${JSON.stringify(standalonePath())}`,
|
|
8216
|
-
`import config from ${JSON.stringify(project.configFile)}`,
|
|
8217
|
-
...files.map((f2, i) => `import * as m${i} from ${JSON.stringify(join12(root, f2))}`),
|
|
8218
|
-
...faker3 ? [`import * as fakerModule from ${JSON.stringify(FAKER)}`] : [],
|
|
8219
|
-
`const data = { ...${JSON.stringify(data2)}, config,`,
|
|
8220
|
-
` modules: [${files.map((f2, i) => `{ file: ${JSON.stringify(f2)}, mod: m${i} }`).join(", ")}],`,
|
|
8221
|
-
` faker: ${faker3 ? "fakerModule" : "undefined"} }`,
|
|
8222
|
-
"main(data).then((code) => process.exit(code), (e) => {",
|
|
8223
|
-
" console.error(e instanceof Error ? e.message : e)",
|
|
8224
|
-
" process.exit(2)",
|
|
8225
|
-
"})"
|
|
8226
|
-
].join("\n");
|
|
8227
8301
|
const ours = createRequire4(import.meta.url);
|
|
8228
|
-
const plugin = {
|
|
8229
|
-
name: "specbook-bundle",
|
|
8230
|
-
resolveId(id) {
|
|
8231
|
-
if (id === ENTRY) return ENTRY;
|
|
8232
|
-
if (id === FAKER) return FAKER;
|
|
8233
|
-
if (id.startsWith(`${prefix}/`) && !id.slice(prefix.length + 1).includes("/"))
|
|
8234
|
-
return `\0specbook:api:${id.slice(prefix.length + 1)}`;
|
|
8235
|
-
if (id === "@specbook/core" || id.startsWith("@specbook/core/")) return ours.resolve(id);
|
|
8236
|
-
return void 0;
|
|
8237
|
-
},
|
|
8238
|
-
load(id) {
|
|
8239
|
-
if (id === ENTRY) return entry;
|
|
8240
|
-
if (id === FAKER) return faker3;
|
|
8241
|
-
if (!id.startsWith("\0specbook:api:")) return void 0;
|
|
8242
|
-
const service = id.slice("\0specbook:api:".length);
|
|
8243
|
-
const metas = ops.services[service];
|
|
8244
|
-
if (!metas) throw new Error(`Service "${service}" has no operations your requests use`);
|
|
8245
|
-
return [
|
|
8246
|
-
"import { defineOperation } from '@specbook/core'",
|
|
8247
|
-
...Object.entries(metas).map(
|
|
8248
|
-
([opId2, meta]) => `export const ${opId2} = /* @__PURE__ */ defineOperation(${JSON.stringify(meta)})`
|
|
8249
|
-
),
|
|
8250
|
-
""
|
|
8251
|
-
].join("\n");
|
|
8252
|
-
}
|
|
8253
|
-
};
|
|
8254
8302
|
const warnings = [];
|
|
8255
|
-
|
|
8303
|
+
const compile = async (faker3) => {
|
|
8304
|
+
const entry = [
|
|
8305
|
+
`import { main } from ${JSON.stringify(standalonePath())}`,
|
|
8306
|
+
`import config from ${JSON.stringify(project.configFile)}`,
|
|
8307
|
+
...files.map((f2, i) => `import * as m${i} from ${JSON.stringify(join13(root, f2))}`),
|
|
8308
|
+
...faker3 ? [`import * as fakerModule from ${JSON.stringify(FAKER)}`] : [],
|
|
8309
|
+
`const data = { ...${JSON.stringify(data2)}, config,`,
|
|
8310
|
+
` modules: [${files.map((f2, i) => `{ file: ${JSON.stringify(f2)}, mod: m${i} }`).join(", ")}],`,
|
|
8311
|
+
` faker: ${faker3 ? "fakerModule" : "undefined"} }`,
|
|
8312
|
+
"main(data).then((code) => process.exit(code), (e) => {",
|
|
8313
|
+
" console.error(e instanceof Error ? e.message : e)",
|
|
8314
|
+
" process.exit(2)",
|
|
8315
|
+
"})"
|
|
8316
|
+
].join("\n");
|
|
8317
|
+
const plugin = {
|
|
8318
|
+
name: "specbook-bundle",
|
|
8319
|
+
resolveId(id) {
|
|
8320
|
+
if (id === ENTRY) return ENTRY;
|
|
8321
|
+
if (id === FAKER) return FAKER;
|
|
8322
|
+
if (id.startsWith(`${prefix}/`) && !id.slice(prefix.length + 1).includes("/"))
|
|
8323
|
+
return `\0specbook:api:${id.slice(prefix.length + 1)}`;
|
|
8324
|
+
if (id === "@specbook/core" || id.startsWith("@specbook/core/")) return ours.resolve(id);
|
|
8325
|
+
return void 0;
|
|
8326
|
+
},
|
|
8327
|
+
load(id) {
|
|
8328
|
+
if (id === ENTRY) return entry;
|
|
8329
|
+
if (id === FAKER) return faker3;
|
|
8330
|
+
if (!id.startsWith("\0specbook:api:")) return void 0;
|
|
8331
|
+
const service = id.slice("\0specbook:api:".length);
|
|
8332
|
+
const metas = ops.services[service];
|
|
8333
|
+
if (!metas) throw new Error(`Service "${service}" has no operations your requests use`);
|
|
8334
|
+
return [
|
|
8335
|
+
"import { defineOperation } from '@specbook/core'",
|
|
8336
|
+
...Object.entries(metas).map(
|
|
8337
|
+
([opId2, meta]) => `export const ${opId2} = /* @__PURE__ */ defineOperation(${JSON.stringify(meta)})`
|
|
8338
|
+
),
|
|
8339
|
+
""
|
|
8340
|
+
].join("\n");
|
|
8341
|
+
}
|
|
8342
|
+
};
|
|
8256
8343
|
const build2 = await rolldown({
|
|
8257
8344
|
input: ENTRY,
|
|
8258
8345
|
platform: "node",
|
|
@@ -8265,7 +8352,7 @@ async function bundleCommand(project, loader, o) {
|
|
|
8265
8352
|
}
|
|
8266
8353
|
});
|
|
8267
8354
|
await mkdir6(dirname6(out), { recursive: true });
|
|
8268
|
-
await build2.write({
|
|
8355
|
+
const written = await build2.write({
|
|
8269
8356
|
file: out,
|
|
8270
8357
|
format: "esm",
|
|
8271
8358
|
minify: true,
|
|
@@ -8273,18 +8360,43 @@ async function bundleCommand(project, loader, o) {
|
|
|
8273
8360
|
banner: "#!/usr/bin/env node"
|
|
8274
8361
|
});
|
|
8275
8362
|
await build2.close();
|
|
8363
|
+
return written.output.some(
|
|
8364
|
+
(c) => c.type === "chunk" && Object.keys(c.modules).some((m) => m.endsWith("/core/src/faker.ts"))
|
|
8365
|
+
);
|
|
8366
|
+
};
|
|
8367
|
+
const noFaker = () => console.error(
|
|
8368
|
+
"These tests use faker, but @faker-js/faker is not installed in this project. Add it to devDependencies (npm install --save-dev @faker-js/faker)."
|
|
8369
|
+
);
|
|
8370
|
+
try {
|
|
8371
|
+
const text = `${sourceText}
|
|
8372
|
+
${JSON.stringify(embedded.examples)}
|
|
8373
|
+
${JSON.stringify(environments)}`;
|
|
8374
|
+
let faker3 = USES_FAKER2.test(text) ? await fakerSource(project) : void 0;
|
|
8375
|
+
if (USES_FAKER2.test(text) && !faker3) {
|
|
8376
|
+
noFaker();
|
|
8377
|
+
return 2;
|
|
8378
|
+
}
|
|
8379
|
+
const importsFaker = await compile(faker3);
|
|
8380
|
+
if (importsFaker && !faker3) {
|
|
8381
|
+
faker3 = await fakerSource(project);
|
|
8382
|
+
if (!faker3) {
|
|
8383
|
+
noFaker();
|
|
8384
|
+
return 2;
|
|
8385
|
+
}
|
|
8386
|
+
await compile(faker3);
|
|
8387
|
+
}
|
|
8276
8388
|
} catch (e) {
|
|
8277
8389
|
console.error(
|
|
8278
8390
|
`Could not compile the tests into one file:
|
|
8279
8391
|
${e instanceof Error ? e.message : e}`
|
|
8280
8392
|
);
|
|
8281
8393
|
console.error(
|
|
8282
|
-
"A package that cannot be compiled in (a native addon, a computed require) can stay an import: --external <package>."
|
|
8394
|
+
"A package that cannot be compiled in (a native addon, a computed require) can stay an import: --external <package>. A browser cannot load it, so replace it with fetch or plain code if the request must also run in the portal."
|
|
8283
8395
|
);
|
|
8284
8396
|
return 2;
|
|
8285
8397
|
}
|
|
8286
8398
|
await chmod(out, 493);
|
|
8287
|
-
const bytes = await
|
|
8399
|
+
const bytes = await readFile14(out);
|
|
8288
8400
|
const hash = sha2562(bytes);
|
|
8289
8401
|
await writeFile7(`${out}.sha256`, `${hash} ${basename4(out)}
|
|
8290
8402
|
`);
|
|
@@ -8304,9 +8416,9 @@ ${e instanceof Error ? e.message : e}`
|
|
|
8304
8416
|
|
|
8305
8417
|
// src/commands/check.ts
|
|
8306
8418
|
import { existsSync as existsSync8 } from "fs";
|
|
8307
|
-
import { readFile as
|
|
8308
|
-
import { join as
|
|
8309
|
-
import
|
|
8419
|
+
import { readFile as readFile15 } from "fs/promises";
|
|
8420
|
+
import { join as join14, resolve as resolve10 } from "path";
|
|
8421
|
+
import fg9 from "fast-glob";
|
|
8310
8422
|
async function checkCommand(project, loader) {
|
|
8311
8423
|
const errors = [];
|
|
8312
8424
|
const warnings = [];
|
|
@@ -8325,8 +8437,8 @@ async function checkCommand(project, loader) {
|
|
|
8325
8437
|
if (untracked.includes(name)) continue;
|
|
8326
8438
|
let drift = 0;
|
|
8327
8439
|
for (const f2 of files2) {
|
|
8328
|
-
const p =
|
|
8329
|
-
if (!existsSync8(p) || await
|
|
8440
|
+
const p = join14(outDir, name, f2.path);
|
|
8441
|
+
if (!existsSync8(p) || await readFile15(p, "utf8") !== f2.content) drift++;
|
|
8330
8442
|
}
|
|
8331
8443
|
if (drift)
|
|
8332
8444
|
errors.push(
|
|
@@ -8405,7 +8517,7 @@ async function checkCommand(project, loader) {
|
|
|
8405
8517
|
...fileVarNames
|
|
8406
8518
|
]);
|
|
8407
8519
|
const declared = new Set(Object.keys(cfg.secrets ?? {}));
|
|
8408
|
-
const files = await
|
|
8520
|
+
const files = await fg9("**/*.req.ts", {
|
|
8409
8521
|
cwd: project.root,
|
|
8410
8522
|
absolute: true,
|
|
8411
8523
|
ignore: ["**/node_modules/**", "**/.generated/**"]
|
|
@@ -8432,9 +8544,9 @@ async function checkCommand(project, loader) {
|
|
|
8432
8544
|
...def.dataPatches
|
|
8433
8545
|
]);
|
|
8434
8546
|
const init2 = new Set(Object.keys(def.input.init ?? {}));
|
|
8435
|
-
const roots = new Set([...used2].map(
|
|
8547
|
+
const roots = new Set([...used2].map(rootName));
|
|
8436
8548
|
for (const name of used2) {
|
|
8437
|
-
const root = name
|
|
8549
|
+
const root = rootName(name);
|
|
8438
8550
|
if (name.startsWith("secrets.")) {
|
|
8439
8551
|
if (!declared.has(name.slice(8)))
|
|
8440
8552
|
warnings.push(`[${rel2}] secret "${name.slice(8)}" is not declared in config.secrets`);
|
|
@@ -8468,15 +8580,125 @@ async function generateCommand(project, loader, only) {
|
|
|
8468
8580
|
}
|
|
8469
8581
|
|
|
8470
8582
|
// src/commands/init.ts
|
|
8471
|
-
import { existsSync as
|
|
8472
|
-
import { mkdir as
|
|
8473
|
-
import { join as
|
|
8474
|
-
|
|
8475
|
-
|
|
8583
|
+
import { existsSync as existsSync10 } from "fs";
|
|
8584
|
+
import { mkdir as mkdir8, readFile as readFile17, writeFile as writeFile9 } from "fs/promises";
|
|
8585
|
+
import { join as join16 } from "path";
|
|
8586
|
+
|
|
8587
|
+
// src/commands/agents.ts
|
|
8588
|
+
import { existsSync as existsSync9, realpathSync } from "fs";
|
|
8589
|
+
import { mkdir as mkdir7, readFile as readFile16, writeFile as writeFile8 } from "fs/promises";
|
|
8590
|
+
import { join as join15 } from "path";
|
|
8591
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8592
|
+
var FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
8593
|
+
var START = "<!-- specbook:start";
|
|
8594
|
+
var END = "<!-- specbook:end -->";
|
|
8595
|
+
function agentBlock(eol = "\n") {
|
|
8596
|
+
return [
|
|
8597
|
+
`${START} (written by specbook init; \`specbook init --agents\` refreshes it) -->`,
|
|
8598
|
+
"## Specbook",
|
|
8599
|
+
"",
|
|
8600
|
+
"This project uses Specbook: requests (`requests/**/*.req.ts`) and flows (`sequences/**/*.seq.ts`) built from OpenAPI specs run as",
|
|
8601
|
+
"the docs, as tests and as monitors. Before adding or changing one, read `.specbook/agent-guide.md`. In short:",
|
|
8602
|
+
"",
|
|
8603
|
+
"- Import operations from `#api/<service>`. Never edit `.generated/`.",
|
|
8604
|
+
"- Label every `assert`, and use `capture` to pass values between steps.",
|
|
8605
|
+
"- Write isomorphic code: requests and flows also run in the browser, so no Node built-ins, `Buffer` or `process.env`.",
|
|
8606
|
+
"- Run `npx specbook check` and `npx specbook test` before you finish.",
|
|
8607
|
+
END
|
|
8608
|
+
].join(eol);
|
|
8609
|
+
}
|
|
8610
|
+
function withBlock(text) {
|
|
8611
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
8612
|
+
const block = agentBlock(eol);
|
|
8613
|
+
const start = text.indexOf(START);
|
|
8614
|
+
if (start >= 0) {
|
|
8615
|
+
const end = text.indexOf(END, start);
|
|
8616
|
+
if (end < 0) return { text, status: "unterminated" };
|
|
8617
|
+
const next = text.slice(0, start) + block + text.slice(end + END.length);
|
|
8618
|
+
return { text: next, status: next === text ? "unchanged" : "updated" };
|
|
8619
|
+
}
|
|
8620
|
+
if (!text.trim()) return { text: `${block}${eol}`, status: "added" };
|
|
8621
|
+
const lead = text.endsWith("\n") ? "" : eol;
|
|
8622
|
+
return { text: `${text}${lead}${eol}${block}${eol}`, status: "added" };
|
|
8623
|
+
}
|
|
8624
|
+
var importsAgents = (text) => /^@AGENTS\.md\b/m.test(text);
|
|
8625
|
+
async function readGuide() {
|
|
8626
|
+
for (const rel2 of ["../agent-guide.md", "../../agent-guide.md"])
|
|
8627
|
+
try {
|
|
8628
|
+
return await readFile16(fileURLToPath2(new URL(rel2, import.meta.url)), "utf8");
|
|
8629
|
+
} catch {
|
|
8630
|
+
}
|
|
8631
|
+
throw new Error("agent-guide.md is missing from the @specbook/cli package");
|
|
8632
|
+
}
|
|
8633
|
+
async function agentsStep(cwd, agents) {
|
|
8634
|
+
if (agents === false) return;
|
|
8635
|
+
const dir = join15(cwd, ".specbook");
|
|
8476
8636
|
await mkdir7(dir, { recursive: true });
|
|
8477
|
-
const
|
|
8478
|
-
|
|
8637
|
+
const guide = await readGuide();
|
|
8638
|
+
await writeFile8(
|
|
8639
|
+
join15(dir, "agent-guide.md"),
|
|
8640
|
+
`<!-- Written by specbook init v${cliVersion()}. Re-run \`specbook init\` to refresh it, and put project rules in AGENTS.md. -->
|
|
8641
|
+
|
|
8642
|
+
${guide}`
|
|
8643
|
+
);
|
|
8644
|
+
console.log("\u2713 wrote .specbook/agent-guide.md");
|
|
8645
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8646
|
+
const found = [];
|
|
8647
|
+
for (const f2 of FILES) {
|
|
8648
|
+
if (!existsSync9(join15(cwd, f2))) continue;
|
|
8649
|
+
const real = realpathSync(join15(cwd, f2));
|
|
8650
|
+
if (seen.has(real)) continue;
|
|
8651
|
+
seen.add(real);
|
|
8652
|
+
found.push(f2);
|
|
8653
|
+
}
|
|
8654
|
+
if (found.length === 0) {
|
|
8479
8655
|
await writeFile8(
|
|
8656
|
+
join15(cwd, "AGENTS.md"),
|
|
8657
|
+
`# AGENTS.md
|
|
8658
|
+
|
|
8659
|
+
Instructions for AI coding agents working in this repository.
|
|
8660
|
+
|
|
8661
|
+
${agentBlock()}
|
|
8662
|
+
`
|
|
8663
|
+
);
|
|
8664
|
+
console.log("\u2713 created AGENTS.md (read by Claude Code, Codex and other coding agents)");
|
|
8665
|
+
return;
|
|
8666
|
+
}
|
|
8667
|
+
if (!agents) {
|
|
8668
|
+
console.log(
|
|
8669
|
+
`${found.join(" and ")} already ${found.length > 1 ? "exist" : "exists"} and was not changed. To add Specbook's section to ${found.length > 1 ? "them" : "it"}: specbook init --agents`
|
|
8670
|
+
);
|
|
8671
|
+
return;
|
|
8672
|
+
}
|
|
8673
|
+
for (const file of found) {
|
|
8674
|
+
const path = join15(cwd, file);
|
|
8675
|
+
const text = await readFile16(path, "utf8");
|
|
8676
|
+
if (file === "CLAUDE.md" && found.includes("AGENTS.md") && importsAgents(text)) {
|
|
8677
|
+
console.log("CLAUDE.md imports AGENTS.md: its section covers Claude Code too");
|
|
8678
|
+
continue;
|
|
8679
|
+
}
|
|
8680
|
+
const r = withBlock(text);
|
|
8681
|
+
if (r.status === "unterminated") {
|
|
8682
|
+
console.log(`! ${file} has a "${START}" marker with no end marker; left unchanged`);
|
|
8683
|
+
continue;
|
|
8684
|
+
}
|
|
8685
|
+
if (r.status === "unchanged") {
|
|
8686
|
+
console.log(`${file} already has Specbook's section`);
|
|
8687
|
+
continue;
|
|
8688
|
+
}
|
|
8689
|
+
await writeFile8(path, r.text);
|
|
8690
|
+
console.log(`\u2713 ${r.status === "added" ? "added" : "updated"} Specbook's section in ${file}`);
|
|
8691
|
+
}
|
|
8692
|
+
}
|
|
8693
|
+
|
|
8694
|
+
// src/commands/init.ts
|
|
8695
|
+
var FAKER_VERSION = "9.3.0";
|
|
8696
|
+
async function initCommand(cwd, opts = {}) {
|
|
8697
|
+
const dir = join16(cwd, ".specbook");
|
|
8698
|
+
await mkdir8(dir, { recursive: true });
|
|
8699
|
+
const cfg = join16(dir, "specbook.config.ts");
|
|
8700
|
+
if (!existsSync10(cfg)) {
|
|
8701
|
+
await writeFile9(
|
|
8480
8702
|
cfg,
|
|
8481
8703
|
`import { defineConfig } from '@specbook/core'
|
|
8482
8704
|
|
|
@@ -8495,55 +8717,63 @@ export default defineConfig({
|
|
|
8495
8717
|
console.log("\u2713 created .specbook/specbook.config.ts");
|
|
8496
8718
|
}
|
|
8497
8719
|
const envDir = await ensureEnvironmentsDir(cwd);
|
|
8498
|
-
const example =
|
|
8499
|
-
if (!
|
|
8500
|
-
await
|
|
8720
|
+
const example = join16(envDir, "staging.secrets.env.example");
|
|
8721
|
+
if (!existsSync10(example)) {
|
|
8722
|
+
await writeFile9(
|
|
8501
8723
|
example,
|
|
8502
8724
|
"# Copy to staging.secrets.env (gitignored) and fill in. One KEY=value per line.\n# YOUR_API_KEY=\n"
|
|
8503
8725
|
);
|
|
8504
|
-
await
|
|
8505
|
-
|
|
8726
|
+
await writeFile9(
|
|
8727
|
+
join16(envDir, "staging.env"),
|
|
8506
8728
|
'# Non-secret variables for the "staging" environment (committed).\n# region=eu-west-1\n'
|
|
8507
8729
|
);
|
|
8508
8730
|
console.log("\u2713 created .specbook/environments/ (staging.env, staging.secrets.env.example)");
|
|
8509
8731
|
}
|
|
8510
|
-
const pkgPath =
|
|
8511
|
-
const pkg =
|
|
8732
|
+
const pkgPath = join16(cwd, "package.json");
|
|
8733
|
+
const pkg = existsSync10(pkgPath) ? JSON.parse(await readFile17(pkgPath, "utf8")) : { name: "my-apis", private: true, type: "module" };
|
|
8512
8734
|
pkg.imports = { ...pkg.imports ?? {}, "#api/*": "./.generated/*/index.ts" };
|
|
8513
8735
|
pkg.scripts = { ...pkg.scripts ?? {} };
|
|
8514
8736
|
if (!pkg.scripts.prepare) pkg.scripts.prepare = "specbook generate";
|
|
8515
|
-
|
|
8737
|
+
pkg.devDependencies = { ...pkg.devDependencies ?? {} };
|
|
8738
|
+
const hasFaker = ["dependencies", "devDependencies", "peerDependencies"].some(
|
|
8739
|
+
(k) => pkg[k]?.["@faker-js/faker"]
|
|
8740
|
+
);
|
|
8741
|
+
if (!hasFaker) pkg.devDependencies["@faker-js/faker"] = FAKER_VERSION;
|
|
8742
|
+
await writeFile9(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
8516
8743
|
`);
|
|
8517
|
-
console.log(
|
|
8518
|
-
|
|
8519
|
-
|
|
8744
|
+
console.log(
|
|
8745
|
+
`\u2713 added the "#api/*" subpath import, a "prepare" script${hasFaker ? "" : " and @faker-js/faker"} to package.json`
|
|
8746
|
+
);
|
|
8747
|
+
const gi = join16(cwd, ".gitignore");
|
|
8748
|
+
const current = existsSync10(gi) ? await readFile17(gi, "utf8") : "";
|
|
8520
8749
|
const missing = [".generated", ".specbook/secrets.local.env"].filter(
|
|
8521
8750
|
(entry) => !current.split("\n").some((l) => l.trim() === entry)
|
|
8522
8751
|
);
|
|
8523
8752
|
if (missing.length) {
|
|
8524
|
-
await
|
|
8753
|
+
await writeFile9(
|
|
8525
8754
|
gi,
|
|
8526
8755
|
`${current}${current.endsWith("\n") || !current ? "" : "\n"}${missing.join("\n")}
|
|
8527
8756
|
`
|
|
8528
8757
|
);
|
|
8529
8758
|
console.log(`\u2713 gitignored ${missing.join(", ")}`);
|
|
8530
8759
|
}
|
|
8760
|
+
await agentsStep(cwd, opts.agents);
|
|
8531
8761
|
}
|
|
8532
8762
|
|
|
8533
8763
|
// src/commands/preview.ts
|
|
8534
8764
|
import { spawn } from "child_process";
|
|
8535
|
-
import { existsSync as
|
|
8536
|
-
import { mkdir as
|
|
8765
|
+
import { existsSync as existsSync11, readFileSync as readFileSync2 } from "fs";
|
|
8766
|
+
import { mkdir as mkdir9, mkdtemp as mkdtemp2, rm as rm4, symlink, writeFile as writeFile11 } from "fs/promises";
|
|
8537
8767
|
import { createRequire as createRequire6 } from "module";
|
|
8538
8768
|
import { tmpdir as tmpdir2 } from "os";
|
|
8539
|
-
import { dirname as dirname8, extname, join as
|
|
8769
|
+
import { dirname as dirname8, extname, join as join18, resolve as resolve12 } from "path";
|
|
8540
8770
|
import SwaggerParser4 from "@apidevtools/swagger-parser";
|
|
8541
8771
|
|
|
8542
8772
|
// src/commands/ui.ts
|
|
8543
|
-
import { copyFile, cp, readFile as
|
|
8773
|
+
import { copyFile, cp, readFile as readFile18, stat, writeFile as writeFile10 } from "fs/promises";
|
|
8544
8774
|
import { createRequire as createRequire5 } from "module";
|
|
8545
|
-
import { basename as basename5, dirname as dirname7, join as
|
|
8546
|
-
import { fileURLToPath as
|
|
8775
|
+
import { basename as basename5, dirname as dirname7, join as join17, resolve as resolve11, sep as sep8 } from "path";
|
|
8776
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
8547
8777
|
import { build, createServer } from "vite";
|
|
8548
8778
|
function uiRoot() {
|
|
8549
8779
|
const require2 = createRequire5(import.meta.url);
|
|
@@ -8562,10 +8792,10 @@ function enclosingNodeModules(file) {
|
|
|
8562
8792
|
}
|
|
8563
8793
|
function dependencyFolders(root) {
|
|
8564
8794
|
const found = /* @__PURE__ */ new Set([
|
|
8565
|
-
...enclosingNodeModules(
|
|
8566
|
-
...enclosingNodeModules(
|
|
8795
|
+
...enclosingNodeModules(fileURLToPath3(import.meta.url)),
|
|
8796
|
+
...enclosingNodeModules(join17(root, "package.json"))
|
|
8567
8797
|
]);
|
|
8568
|
-
for (const from of [
|
|
8798
|
+
for (const from of [join17(root, "package.json"), fileURLToPath3(import.meta.url)]) {
|
|
8569
8799
|
const require2 = createRequire5(from);
|
|
8570
8800
|
for (const name of ["vite", "prism-code-editor", "preact", "@preact/signals"]) {
|
|
8571
8801
|
try {
|
|
@@ -8590,7 +8820,12 @@ function uiConfig(project, operations) {
|
|
|
8590
8820
|
rolldownOptions: { transform: { jsx: { runtime: "automatic", importSource: "preact" } } }
|
|
8591
8821
|
},
|
|
8592
8822
|
plugins: [
|
|
8593
|
-
specbook({
|
|
8823
|
+
specbook({
|
|
8824
|
+
projectRoot: project.root,
|
|
8825
|
+
outDir: project.config.codegen?.outDir,
|
|
8826
|
+
operations,
|
|
8827
|
+
faker: project.config.faker
|
|
8828
|
+
})
|
|
8594
8829
|
],
|
|
8595
8830
|
server: {
|
|
8596
8831
|
fs: {
|
|
@@ -8623,7 +8858,7 @@ async function devCommand(project, port = 5173) {
|
|
|
8623
8858
|
}
|
|
8624
8859
|
async function buildCommand(project, outDir, options = {}) {
|
|
8625
8860
|
const out = resolve11(project.root, outDir);
|
|
8626
|
-
const generated =
|
|
8861
|
+
const generated = join17(project.root, project.config.codegen?.outDir ?? ".generated");
|
|
8627
8862
|
const operations = options.minimal ? await usedOperationKeys(project, generated) : void 0;
|
|
8628
8863
|
if (operations && operations.size === 0)
|
|
8629
8864
|
throw new Error("Nothing to publish: no request or flow imports an operation (see --minimal)");
|
|
@@ -8645,25 +8880,25 @@ async function buildCommand(project, outDir, options = {}) {
|
|
|
8645
8880
|
}
|
|
8646
8881
|
}
|
|
8647
8882
|
});
|
|
8648
|
-
await copyMetadata(
|
|
8883
|
+
await copyMetadata(join17(generated, "specbook.json"), join17(out, "specbook.json"), operations);
|
|
8649
8884
|
}
|
|
8650
8885
|
async function copyMetadata(from, to, operations) {
|
|
8651
8886
|
try {
|
|
8652
8887
|
if (!operations) return await copyFile(from, to);
|
|
8653
|
-
const meta = JSON.parse(await
|
|
8888
|
+
const meta = JSON.parse(await readFile18(from, "utf8"));
|
|
8654
8889
|
for (const [svc, s] of Object.entries(meta.services)) {
|
|
8655
8890
|
s.operations = Object.fromEntries(
|
|
8656
8891
|
Object.entries(s.operations).filter(([id]) => operations.has(`${svc}/${id}`))
|
|
8657
8892
|
);
|
|
8658
8893
|
if (Object.keys(s.operations).length === 0) delete meta.services[svc];
|
|
8659
8894
|
}
|
|
8660
|
-
await
|
|
8895
|
+
await writeFile10(to, `${JSON.stringify(meta)}
|
|
8661
8896
|
`);
|
|
8662
8897
|
} catch {
|
|
8663
8898
|
}
|
|
8664
8899
|
}
|
|
8665
8900
|
async function copyViewer(project, outDir, viewerDir = resolve11(uiRoot(), "dist-viewer")) {
|
|
8666
|
-
const has2 = await stat(
|
|
8901
|
+
const has2 = await stat(join17(viewerDir, "index.html")).then(
|
|
8667
8902
|
() => true,
|
|
8668
8903
|
() => false
|
|
8669
8904
|
);
|
|
@@ -8682,10 +8917,10 @@ var slug3 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/
|
|
|
8682
8917
|
async function link(root, name) {
|
|
8683
8918
|
try {
|
|
8684
8919
|
let dir = dirname8(createRequire6(import.meta.url).resolve(name));
|
|
8685
|
-
while (!
|
|
8920
|
+
while (!existsSync11(join18(dir, "package.json")) || JSON.parse(readFileSync2(join18(dir, "package.json"), "utf8")).name !== name)
|
|
8686
8921
|
dir = dirname8(dir);
|
|
8687
|
-
const dest =
|
|
8688
|
-
await
|
|
8922
|
+
const dest = join18(root, "node_modules", ...name.split("/"));
|
|
8923
|
+
await mkdir9(dirname8(dest), { recursive: true });
|
|
8689
8924
|
await symlink(dir, dest, "dir");
|
|
8690
8925
|
} catch {
|
|
8691
8926
|
}
|
|
@@ -8705,7 +8940,7 @@ async function previewCommand(source, o = {}) {
|
|
|
8705
8940
|
const { servers } = openApiServers(api, source);
|
|
8706
8941
|
const where = servers.length === 0 ? "" : servers.length === 1 && servers[0]?.name === "live" ? `, live calls go to ${servers[0].url}` : `
|
|
8707
8942
|
Environments: ${servers.map((s) => `${s.name} \u2192 ${s.url}`).join(", ")}`;
|
|
8708
|
-
const root = await mkdtemp2(
|
|
8943
|
+
const root = await mkdtemp2(join18(tmpdir2(), "specbook-preview-"));
|
|
8709
8944
|
const cleanup = () => rm4(root, { recursive: true, force: true }).catch(() => {
|
|
8710
8945
|
});
|
|
8711
8946
|
process.once("SIGINT", () => cleanup().then(() => process.exit(0)));
|
|
@@ -8715,9 +8950,9 @@ async function previewCommand(source, o = {}) {
|
|
|
8715
8950
|
const res = await fetch(source);
|
|
8716
8951
|
if (!res.ok) throw new Error(`Could not fetch ${source} (${res.status})`);
|
|
8717
8952
|
const ext = /ya?ml/.test(extname(new URL(source).pathname)) ? ".yaml" : ".json";
|
|
8718
|
-
specFile =
|
|
8719
|
-
await
|
|
8720
|
-
await
|
|
8953
|
+
specFile = join18(root, `openapi/api${ext}`);
|
|
8954
|
+
await mkdir9(dirname8(specFile), { recursive: true });
|
|
8955
|
+
await writeFile11(specFile, await res.text());
|
|
8721
8956
|
}
|
|
8722
8957
|
const {
|
|
8723
8958
|
auth,
|
|
@@ -8750,14 +8985,14 @@ async function previewCommand(source, o = {}) {
|
|
|
8750
8985
|
}
|
|
8751
8986
|
}
|
|
8752
8987
|
};
|
|
8753
|
-
const configFile =
|
|
8754
|
-
await
|
|
8755
|
-
await
|
|
8988
|
+
const configFile = join18(root, ".specbook/specbook.config.ts");
|
|
8989
|
+
await mkdir9(dirname8(configFile), { recursive: true });
|
|
8990
|
+
await writeFile11(configFile, `export default ${JSON.stringify(config, null, 2)}
|
|
8756
8991
|
`);
|
|
8757
8992
|
await link(root, "@specbook/core");
|
|
8758
8993
|
await link(root, "@faker-js/faker");
|
|
8759
8994
|
console.log(`Generating "${title}" \u2026`);
|
|
8760
|
-
const out = await generateToDisk({ spec: specFile, service, outDir:
|
|
8995
|
+
const out = await generateToDisk({ spec: specFile, service, outDir: join18(root, ".generated") });
|
|
8761
8996
|
console.log(`\u2713 ${out.operations.length} operations${where}`);
|
|
8762
8997
|
const project = { root, configFile, config };
|
|
8763
8998
|
const server = await devCommand(project, o.port ?? 5173);
|
|
@@ -8826,18 +9061,18 @@ import { resolve as resolve15 } from "path";
|
|
|
8826
9061
|
|
|
8827
9062
|
// src/examples.ts
|
|
8828
9063
|
import { File } from "buffer";
|
|
8829
|
-
import { readFile as
|
|
8830
|
-
import { basename as basename6, dirname as dirname9, join as
|
|
8831
|
-
import
|
|
8832
|
-
var
|
|
9064
|
+
import { readFile as readFile19 } from "fs/promises";
|
|
9065
|
+
import { basename as basename6, dirname as dirname9, join as join19, resolve as resolve14 } from "path";
|
|
9066
|
+
import fg10 from "fast-glob";
|
|
9067
|
+
var IGNORE4 = ["**/node_modules/**", "**/.generated/**", "**/dist/**", "**/.git/**"];
|
|
8833
9068
|
function exampleLoadersFor(project) {
|
|
8834
9069
|
const outDir = resolve14(project.root, project.config.codegen?.outDir ?? ".generated");
|
|
8835
9070
|
const example = async (op, name) => {
|
|
8836
9071
|
const rel2 = `${op.service}/${op.id}/${name}.example.json`;
|
|
8837
|
-
const [custom] = await
|
|
8838
|
-
const file = custom ??
|
|
9072
|
+
const [custom] = await fg10(`**/${rel2}`, { cwd: project.root, absolute: true, ignore: IGNORE4 });
|
|
9073
|
+
const file = custom ?? join19(outDir, op.service, "examples", op.id, `${name}.example.json`);
|
|
8839
9074
|
try {
|
|
8840
|
-
const doc = JSON.parse(await
|
|
9075
|
+
const doc = JSON.parse(await readFile19(file, "utf8"));
|
|
8841
9076
|
return { ...doc, source: custom ? "project" : "generated", dir: dirname9(file) };
|
|
8842
9077
|
} catch {
|
|
8843
9078
|
return void 0;
|
|
@@ -8845,7 +9080,7 @@ function exampleLoadersFor(project) {
|
|
|
8845
9080
|
};
|
|
8846
9081
|
const read = async (path, dir) => {
|
|
8847
9082
|
const abs = resolve14(dir ?? project.root, path);
|
|
8848
|
-
return new File([await
|
|
9083
|
+
return new File([await readFile19(abs)], basename6(abs));
|
|
8849
9084
|
};
|
|
8850
9085
|
return { example, readFile: read };
|
|
8851
9086
|
}
|
|
@@ -8876,7 +9111,8 @@ async function testCommand(project, loader, o) {
|
|
|
8876
9111
|
readEnvironment: (env) => readEnvironment(project.root, env),
|
|
8877
9112
|
examples: exampleLoadersFor(project),
|
|
8878
9113
|
mock: mockLoaderFor(project),
|
|
8879
|
-
|
|
9114
|
+
// only a project that uses faker pays for importing it
|
|
9115
|
+
loadFaker: async () => await projectUsesFaker(project.root) ? loadFaker(project) : void 0,
|
|
8880
9116
|
readSource: (file) => {
|
|
8881
9117
|
try {
|
|
8882
9118
|
return readFileSync3(file, "utf8");
|
|
@@ -8960,7 +9196,7 @@ var TEST_HELP = ` --env <name> environment (default: mock, for
|
|
|
8960
9196
|
--input k=v flow input value (repeatable)
|
|
8961
9197
|
--var k=v variable override (repeatable)
|
|
8962
9198
|
--secrets-file <path> dotenv file with secrets
|
|
8963
|
-
--locale <l> --seed <n>
|
|
9199
|
+
--locale <l> --seed <n> generated values: locale / seed for a repeatable run
|
|
8964
9200
|
--notify <url> POST a summary to a webhook when something fails`;
|
|
8965
9201
|
var parseValue = (v2) => {
|
|
8966
9202
|
try {
|
|
@@ -9012,7 +9248,9 @@ function testOptions(values, patterns) {
|
|
|
9012
9248
|
// src/bin.ts
|
|
9013
9249
|
var HELP = `specbook <command>
|
|
9014
9250
|
|
|
9015
|
-
init scaffold config, #api/* import
|
|
9251
|
+
init scaffold config, #api/* import, the environments folder and an agent guide
|
|
9252
|
+
--agents add a Specbook section to an existing AGENTS.md or CLAUDE.md (never edited otherwise)
|
|
9253
|
+
--no-agents skip the agent guide and AGENTS.md
|
|
9016
9254
|
add <service> --spec <p|url> register an OpenAPI spec in the config and generate it; an address is downloaded to
|
|
9017
9255
|
openapi/ and recorded in openapi/sources.json
|
|
9018
9256
|
add --refresh [service...] download the recorded addresses again, say what changed and regenerate it
|
|
@@ -9028,7 +9266,7 @@ var HELP = `specbook <command>
|
|
|
9028
9266
|
${TEST_HELP}
|
|
9029
9267
|
bundle [--out <file>] compile the tests into one .mjs that runs with just node: for CI or ops, no install
|
|
9030
9268
|
--tag <t> only the requests and flows with this tag (repeatable)
|
|
9031
|
-
--external <pkg> leave a package as an import (
|
|
9269
|
+
--external <pkg> leave a package as an import (installed where the file runs; a browser cannot load it; repeatable)
|
|
9032
9270
|
check validate specs, generated code drift, imported operations, placeholders and environment files
|
|
9033
9271
|
secrets [--env <name>] show where each declared secret resolves from (never values)
|
|
9034
9272
|
--secrets-file <path> include a dotenv file, as test would
|
|
@@ -9046,6 +9284,8 @@ async function main() {
|
|
|
9046
9284
|
out: { type: "string" },
|
|
9047
9285
|
external: { type: "string", multiple: true },
|
|
9048
9286
|
"no-open": { type: "boolean" },
|
|
9287
|
+
agents: { type: "boolean" },
|
|
9288
|
+
"no-agents": { type: "boolean" },
|
|
9049
9289
|
viewer: { type: "boolean" },
|
|
9050
9290
|
minimal: { type: "boolean" },
|
|
9051
9291
|
help: { type: "boolean", short: "h" }
|
|
@@ -9070,7 +9310,13 @@ async function main() {
|
|
|
9070
9310
|
});
|
|
9071
9311
|
}
|
|
9072
9312
|
if (cmd === "init") {
|
|
9073
|
-
|
|
9313
|
+
if (values.agents && values["no-agents"]) {
|
|
9314
|
+
console.error("init: pass --agents or --no-agents, not both\n");
|
|
9315
|
+
return 3;
|
|
9316
|
+
}
|
|
9317
|
+
await initCommand(cwd, {
|
|
9318
|
+
agents: values["no-agents"] ? false : values.agents ? true : void 0
|
|
9319
|
+
});
|
|
9074
9320
|
return 0;
|
|
9075
9321
|
}
|
|
9076
9322
|
if (!["generate", "test", "bundle", "dev", "build", "add", "check", "secrets"].includes(cmd)) {
|