@specbook/vite 0.2.0 → 0.4.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/dist/index.js +422 -93
- package/package.json +3 -3
- package/src/addresses.ts +62 -0
- package/src/definitions.ts +26 -0
- package/src/examples.ts +2 -0
- package/src/index.ts +157 -62
- package/src/proxy.ts +28 -11
- package/src/requests.ts +58 -0
package/dist/index.js
CHANGED
|
@@ -1,9 +1,168 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { existsSync as existsSync3, readdirSync } from "fs";
|
|
3
|
-
import { mkdir as mkdir2, readFile as
|
|
3
|
+
import { mkdir as mkdir2, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
|
|
4
4
|
import { createRequire } from "module";
|
|
5
|
-
import { basename, dirname, join as
|
|
6
|
-
|
|
5
|
+
import { basename, dirname, join as join5, relative as relative4, resolve as resolve2, sep as sep4 } from "path";
|
|
6
|
+
|
|
7
|
+
// ../core/src/faker.ts
|
|
8
|
+
var KEY = "__specbookFaker";
|
|
9
|
+
var store = globalThis;
|
|
10
|
+
var faker = new Proxy({}, {
|
|
11
|
+
get(_t, key) {
|
|
12
|
+
const current = store[KEY];
|
|
13
|
+
if (!current) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
'faker is not initialised. Install "@faker-js/faker" in your project; specbook sets it up before each run.'
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return current[key];
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// ../core/src/template.ts
|
|
23
|
+
var NAME = String.raw`[\w.$\[\]-]+`;
|
|
24
|
+
var WHOLE = new RegExp(String.raw`^\s*\{\{\s*(${NAME})\s*\}\}\s*$`);
|
|
25
|
+
var EMBED = new RegExp(String.raw`\{\{\s*(${NAME})\s*\}\}`, "g");
|
|
26
|
+
var DEFERRED_NAMESPACES = ["secrets", "session"];
|
|
27
|
+
var TemplateError = class extends Error {
|
|
28
|
+
constructor(variable) {
|
|
29
|
+
super(`Missing variable "{{${variable}}}"`);
|
|
30
|
+
this.variable = variable;
|
|
31
|
+
this.name = "TemplateError";
|
|
32
|
+
}
|
|
33
|
+
variable;
|
|
34
|
+
};
|
|
35
|
+
function isDeferred(name) {
|
|
36
|
+
const ns = name.split(".")[0];
|
|
37
|
+
return DEFERRED_NAMESPACES.includes(ns) && name.includes(".");
|
|
38
|
+
}
|
|
39
|
+
var isFakerName = (name) => name.startsWith("faker.");
|
|
40
|
+
function fromFaker(path) {
|
|
41
|
+
let parent = faker;
|
|
42
|
+
let cur = faker;
|
|
43
|
+
for (const part of path.split(".").slice(1)) {
|
|
44
|
+
parent = cur;
|
|
45
|
+
cur = cur?.[part];
|
|
46
|
+
if (cur === void 0) return { found: false };
|
|
47
|
+
}
|
|
48
|
+
return { found: true, value: typeof cur === "function" ? cur.call(parent) : cur };
|
|
49
|
+
}
|
|
50
|
+
var steps = (path) => path.replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
51
|
+
function lookup(scope, path) {
|
|
52
|
+
if (isFakerName(path)) return fromFaker(path);
|
|
53
|
+
let cur = scope;
|
|
54
|
+
for (const part of steps(path)) {
|
|
55
|
+
if (cur !== null && typeof cur === "object" && Object.hasOwn(cur, part)) {
|
|
56
|
+
cur = cur[part];
|
|
57
|
+
} else {
|
|
58
|
+
return { found: false };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { found: true, value: cur };
|
|
62
|
+
}
|
|
63
|
+
function renderString(s, scope) {
|
|
64
|
+
const whole = WHOLE.exec(s);
|
|
65
|
+
if (whole) {
|
|
66
|
+
const name = whole[1];
|
|
67
|
+
if (isDeferred(name)) return s;
|
|
68
|
+
const r = lookup(scope, name);
|
|
69
|
+
if (!r.found) throw new TemplateError(name);
|
|
70
|
+
return r.value;
|
|
71
|
+
}
|
|
72
|
+
return s.replace(EMBED, (m, name) => {
|
|
73
|
+
if (isDeferred(name)) return m;
|
|
74
|
+
const r = lookup(scope, name);
|
|
75
|
+
if (!r.found) throw new TemplateError(name);
|
|
76
|
+
const v = r.value;
|
|
77
|
+
return typeof v === "object" && v !== null ? JSON.stringify(v) : String(v);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function collectPlaceholders(value, into = /* @__PURE__ */ new Set()) {
|
|
81
|
+
if (typeof value === "string") {
|
|
82
|
+
for (const m of value.matchAll(EMBED)) into.add(m[1]);
|
|
83
|
+
} else if (Array.isArray(value)) {
|
|
84
|
+
for (const v of value) collectPlaceholders(v, into);
|
|
85
|
+
} else if (value !== null && typeof value === "object") {
|
|
86
|
+
for (const v of Object.values(value)) collectPlaceholders(v, into);
|
|
87
|
+
}
|
|
88
|
+
return into;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ../core/src/environment.ts
|
|
92
|
+
var AddressError = class extends Error {
|
|
93
|
+
constructor(message, variable) {
|
|
94
|
+
super(message);
|
|
95
|
+
this.variable = variable;
|
|
96
|
+
this.name = "AddressError";
|
|
97
|
+
}
|
|
98
|
+
variable;
|
|
99
|
+
};
|
|
100
|
+
var OAUTH_ADDRESS_FIELDS = [
|
|
101
|
+
"authorizationUrl",
|
|
102
|
+
"tokenUrl",
|
|
103
|
+
"openIdConnectUrl",
|
|
104
|
+
"clientId"
|
|
105
|
+
];
|
|
106
|
+
function environmentScope(cfg, env, fileVars = {}, extra = {}) {
|
|
107
|
+
return {
|
|
108
|
+
...cfg.vars,
|
|
109
|
+
...cfg.environments?.[env]?.vars,
|
|
110
|
+
...fileVars,
|
|
111
|
+
...extra
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function renderAddress(template, scope, what) {
|
|
115
|
+
for (const name of collectPlaceholders(template)) {
|
|
116
|
+
if (isDeferred(name) || isFakerName(name))
|
|
117
|
+
throw new AddressError(
|
|
118
|
+
`${what} cannot use {{${name}}}: only environment variables are available`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
let value;
|
|
122
|
+
try {
|
|
123
|
+
value = renderString(template, scope);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
if (e instanceof TemplateError) throw new AddressError(`${what}: set ${e.variable}`, e.variable);
|
|
126
|
+
throw e;
|
|
127
|
+
}
|
|
128
|
+
if (value !== null && typeof value === "object")
|
|
129
|
+
throw new AddressError(`${what} is not a text value`);
|
|
130
|
+
const out = value === void 0 || value === null ? "" : String(value).trim();
|
|
131
|
+
if (/\{\{/.test(out)) throw new AddressError(`${what} still contains a placeholder: ${out}`);
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
function baseUrlFor(cfg, service, env, scope) {
|
|
135
|
+
const base = cfg.services[service]?.baseUrl;
|
|
136
|
+
const template = typeof base === "string" ? base : base?.[env];
|
|
137
|
+
if (!template) return void 0;
|
|
138
|
+
const what = `No base URL for ${service} in ${env}`;
|
|
139
|
+
const url = /\{\{/.test(template) ? renderAddress(template, scope, what) : template.trim();
|
|
140
|
+
return url ? url.replace(/\/+$/, "") : void 0;
|
|
141
|
+
}
|
|
142
|
+
function oauthFor(oauth, scope, profile = "sign-in") {
|
|
143
|
+
const out = { ...oauth };
|
|
144
|
+
for (const field of OAUTH_ADDRESS_FIELDS) {
|
|
145
|
+
const v = out[field];
|
|
146
|
+
if (typeof v !== "string" || !/\{\{/.test(v)) continue;
|
|
147
|
+
const rendered = renderAddress(v, scope, `${profile} ${field}`);
|
|
148
|
+
if (rendered) out[field] = rendered;
|
|
149
|
+
else delete out[field];
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
function environmentNames(cfg, fileEnvs = [], extra = []) {
|
|
154
|
+
const names = /* @__PURE__ */ new Set(["mock"]);
|
|
155
|
+
const services = Object.values(cfg.services ?? {});
|
|
156
|
+
if (services.some((s) => typeof s.baseUrl === "string")) names.add("live");
|
|
157
|
+
for (const s of services) {
|
|
158
|
+
if (s.baseUrl && typeof s.baseUrl === "object")
|
|
159
|
+
for (const k of Object.keys(s.baseUrl)) names.add(k);
|
|
160
|
+
}
|
|
161
|
+
for (const k of Object.keys(cfg.environments ?? {})) names.add(k);
|
|
162
|
+
for (const k of fileEnvs) names.add(k);
|
|
163
|
+
for (const k of extra) names.add(k);
|
|
164
|
+
return [...names];
|
|
165
|
+
}
|
|
7
166
|
|
|
8
167
|
// ../core/src/oauth.ts
|
|
9
168
|
var OAuthError = class extends Error {
|
|
@@ -318,6 +477,65 @@ function parseDotenv(text) {
|
|
|
318
477
|
return out;
|
|
319
478
|
}
|
|
320
479
|
|
|
480
|
+
// src/index.ts
|
|
481
|
+
import fg4 from "fast-glob";
|
|
482
|
+
|
|
483
|
+
// src/addresses.ts
|
|
484
|
+
var NO_ADDRESSES = {
|
|
485
|
+
services: {},
|
|
486
|
+
tokenOrigins: {},
|
|
487
|
+
discoveryOrigins: {}
|
|
488
|
+
};
|
|
489
|
+
async function addressesFor(cfg, env, files, tokenOriginOf) {
|
|
490
|
+
const services = cfg.services ?? {};
|
|
491
|
+
const known = environmentNames({ services, environments: cfg.environments }, files.environments);
|
|
492
|
+
if (!known.includes(env)) return NO_ADDRESSES;
|
|
493
|
+
const scope = environmentScope(cfg, env, await files.vars());
|
|
494
|
+
const out = { services: {}, tokenOrigins: {}, discoveryOrigins: {} };
|
|
495
|
+
for (const [name, svc] of Object.entries(services)) {
|
|
496
|
+
if (env === "mock" && typeof svc.baseUrl === "string") continue;
|
|
497
|
+
try {
|
|
498
|
+
out.services[name] = baseUrlFor({ services }, name, env, scope);
|
|
499
|
+
} catch {
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
for (const [name, profile] of Object.entries(cfg.auth ?? {})) {
|
|
503
|
+
try {
|
|
504
|
+
if (!profile.oauth) continue;
|
|
505
|
+
const oauth = oauthFor(profile.oauth, scope, name);
|
|
506
|
+
if ("tokenUrl" in oauth && oauth.tokenUrl)
|
|
507
|
+
out.tokenOrigins[name] = new URL(oauth.tokenUrl).origin;
|
|
508
|
+
else if ("openIdConnectUrl" in oauth && oauth.openIdConnectUrl) {
|
|
509
|
+
out.discoveryOrigins[name] = new URL(oauth.openIdConnectUrl).origin;
|
|
510
|
+
const token = await tokenOriginOf(oauth.openIdConnectUrl);
|
|
511
|
+
if (token) out.tokenOrigins[name] = token;
|
|
512
|
+
}
|
|
513
|
+
} catch {
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return out;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/definitions.ts
|
|
520
|
+
function definitionRefs(text) {
|
|
521
|
+
const names = [];
|
|
522
|
+
for (const m of text.matchAll(/"\$ref":"(?:specbook:\/\/[^"#]*)?#\/\$defs\/([^"]+)"/g))
|
|
523
|
+
names.push(m[1].replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
524
|
+
return names;
|
|
525
|
+
}
|
|
526
|
+
function pruneDefinitions(defs, roots) {
|
|
527
|
+
const all = defs.$defs ?? {};
|
|
528
|
+
const keep = /* @__PURE__ */ new Set();
|
|
529
|
+
const queue = [...roots ?? []];
|
|
530
|
+
while (queue.length) {
|
|
531
|
+
const name = queue.pop();
|
|
532
|
+
if (keep.has(name) || !(name in all)) continue;
|
|
533
|
+
keep.add(name);
|
|
534
|
+
queue.push(...definitionRefs(JSON.stringify(all[name])));
|
|
535
|
+
}
|
|
536
|
+
return { ...defs, $defs: Object.fromEntries(Object.entries(all).filter(([k]) => keep.has(k))) };
|
|
537
|
+
}
|
|
538
|
+
|
|
321
539
|
// src/discovery.ts
|
|
322
540
|
var FETCH_TIMEOUT_MS = 5e3;
|
|
323
541
|
function createDiscovery(opts = {}) {
|
|
@@ -511,7 +729,7 @@ async function writeEnvKey(file, key, value) {
|
|
|
511
729
|
import { relative as relative2, sep as sep2 } from "path";
|
|
512
730
|
import fg2 from "fast-glob";
|
|
513
731
|
var IGNORE = ["**/node_modules/**", "**/.generated/**", "**/dist/**", "**/.git/**"];
|
|
514
|
-
var
|
|
732
|
+
var NAME2 = /\.example\.json$/;
|
|
515
733
|
async function scanExamples(root, outDir, operations) {
|
|
516
734
|
const examples = {};
|
|
517
735
|
const assets = {};
|
|
@@ -527,8 +745,9 @@ async function scanExamples(root, outDir, operations) {
|
|
|
527
745
|
const gen = `${root}/${outDir}`;
|
|
528
746
|
for (const abs of await fg2("*/examples/*/*.example.json", { cwd: gen, absolute: true })) {
|
|
529
747
|
const [svc, , id, file] = relative2(gen, abs).split(sep2);
|
|
748
|
+
if (!operations.has(`${svc}/${id}`)) continue;
|
|
530
749
|
add(`${svc}/${id}`, {
|
|
531
|
-
name: file.replace(
|
|
750
|
+
name: file.replace(NAME2, ""),
|
|
532
751
|
abs,
|
|
533
752
|
source: "generated",
|
|
534
753
|
dir: `${svc}/${id}@generated`
|
|
@@ -542,7 +761,7 @@ async function scanExamples(root, outDir, operations) {
|
|
|
542
761
|
const key = `${svc}/${id}`;
|
|
543
762
|
if (!svc || !id || !operations.has(key)) continue;
|
|
544
763
|
add(key, {
|
|
545
|
-
name: file.replace(
|
|
764
|
+
name: file.replace(NAME2, ""),
|
|
546
765
|
abs,
|
|
547
766
|
source: "project",
|
|
548
767
|
dir: `${svc}/${id}@project`
|
|
@@ -622,6 +841,15 @@ async function handleProxy2(req, res, deps) {
|
|
|
622
841
|
const url = req.url ?? "/";
|
|
623
842
|
if (!url.startsWith(`${MOUNT}/`)) return false;
|
|
624
843
|
const config = await deps.getConfig();
|
|
844
|
+
const addressesByEnv = /* @__PURE__ */ new Map();
|
|
845
|
+
const addressesFor2 = (env) => {
|
|
846
|
+
let a = addressesByEnv.get(env);
|
|
847
|
+
if (!a) {
|
|
848
|
+
a = config.resolve(env);
|
|
849
|
+
addressesByEnv.set(env, a);
|
|
850
|
+
}
|
|
851
|
+
return a;
|
|
852
|
+
};
|
|
625
853
|
const secretsByEnv = /* @__PURE__ */ new Map();
|
|
626
854
|
const method = req.method ?? "GET";
|
|
627
855
|
const headers = new Headers();
|
|
@@ -636,12 +864,13 @@ async function handleProxy2(req, res, deps) {
|
|
|
636
864
|
const response = await handleProxy(request, {
|
|
637
865
|
mount: MOUNT,
|
|
638
866
|
policy: {
|
|
639
|
-
upstream: (env, service) => {
|
|
867
|
+
upstream: async (env, service) => {
|
|
868
|
+
const a = await addressesFor2(env);
|
|
640
869
|
if (service.startsWith(AUTH_SERVICE_PREFIX))
|
|
641
|
-
return
|
|
870
|
+
return a.tokenOrigins[decode(service.slice(AUTH_SERVICE_PREFIX.length))];
|
|
642
871
|
if (service.startsWith(DISCOVERY_SERVICE_PREFIX))
|
|
643
|
-
return
|
|
644
|
-
return
|
|
872
|
+
return a.discoveryOrigins[decode(service.slice(DISCOVERY_SERVICE_PREFIX.length))];
|
|
873
|
+
return a.services[service];
|
|
645
874
|
},
|
|
646
875
|
// a developer's own machine: their local services are the point
|
|
647
876
|
allowPrivateHosts: true
|
|
@@ -671,9 +900,47 @@ async function handleProxy2(req, res, deps) {
|
|
|
671
900
|
return true;
|
|
672
901
|
}
|
|
673
902
|
|
|
674
|
-
// src/
|
|
903
|
+
// src/requests.ts
|
|
675
904
|
import { readFile as readFile4 } from "fs/promises";
|
|
676
905
|
import { join as join3 } from "path";
|
|
906
|
+
function parseRequestTarget(src, services) {
|
|
907
|
+
const local = /* @__PURE__ */ new Map();
|
|
908
|
+
for (const m of src.matchAll(/import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g)) {
|
|
909
|
+
const service = m[2].split("/").pop();
|
|
910
|
+
if (!services.has(service) || /^import\s*type/.test(m[0])) continue;
|
|
911
|
+
for (const part of m[1].split(",")) {
|
|
912
|
+
const spec = part.trim();
|
|
913
|
+
if (!spec || spec.startsWith("type ")) continue;
|
|
914
|
+
const [imported, as] = spec.split(/\s+as\s+/);
|
|
915
|
+
local.set(as ?? imported, `${service}/${imported}`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
const used = /* @__PURE__ */ new Set();
|
|
919
|
+
for (const m of src.matchAll(/\b([A-Za-z_$][\w$]*)\s*\.\s*request\s*\(/g)) {
|
|
920
|
+
const key = local.get(m[1]);
|
|
921
|
+
if (key) used.add(key);
|
|
922
|
+
}
|
|
923
|
+
return used.size === 1 ? [...used][0] : void 0;
|
|
924
|
+
}
|
|
925
|
+
async function scanRequests(root, files, services) {
|
|
926
|
+
const meta = {};
|
|
927
|
+
const unresolved = [];
|
|
928
|
+
await Promise.all(
|
|
929
|
+
files.map(async (file) => {
|
|
930
|
+
const key = parseRequestTarget(
|
|
931
|
+
await readFile4(join3(root, file), "utf8").catch(() => ""),
|
|
932
|
+
services
|
|
933
|
+
);
|
|
934
|
+
if (key) meta[file] = key;
|
|
935
|
+
else unresolved.push(file);
|
|
936
|
+
})
|
|
937
|
+
);
|
|
938
|
+
return { meta, unresolved: unresolved.sort() };
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// src/sequences.ts
|
|
942
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
943
|
+
import { join as join4 } from "path";
|
|
677
944
|
var KEYS = ["id", "title", "description", "group", "tags"];
|
|
678
945
|
function readString(src, i) {
|
|
679
946
|
const q = src[i];
|
|
@@ -771,7 +1038,7 @@ async function scanSequences(root, files) {
|
|
|
771
1038
|
const unresolved = [];
|
|
772
1039
|
await Promise.all(
|
|
773
1040
|
files.map(async (file) => {
|
|
774
|
-
const parsed = parseSequenceMeta(await
|
|
1041
|
+
const parsed = parseSequenceMeta(await readFile5(join4(root, file), "utf8").catch(() => ""));
|
|
775
1042
|
if (parsed) meta[parsed.id] = { ...parsed, file };
|
|
776
1043
|
else unresolved.push(file);
|
|
777
1044
|
})
|
|
@@ -788,14 +1055,14 @@ var VIRTUAL_FAKER = "virtual:specbook/faker";
|
|
|
788
1055
|
var RESOLVED_FAKER = `\0${VIRTUAL_FAKER}`;
|
|
789
1056
|
var VIRTUAL_FAKER_LOCALE = "virtual:specbook/faker-locale/";
|
|
790
1057
|
var RESOLVED_FAKER_LOCALE = `\0${VIRTUAL_FAKER_LOCALE}`;
|
|
791
|
-
var
|
|
792
|
-
var
|
|
1058
|
+
var VIRTUAL_MANIFEST = "virtual:specbook/manifest/";
|
|
1059
|
+
var RESOLVED_MANIFEST = `\0${VIRTUAL_MANIFEST}`;
|
|
793
1060
|
var VIRTUAL_OPERATIONS = "virtual:specbook/operations";
|
|
794
1061
|
var RESOLVED_OPERATIONS = `\0${VIRTUAL_OPERATIONS}`;
|
|
795
1062
|
var VIRTUAL_DOC = "virtual:specbook/doc/";
|
|
796
1063
|
var RESOLVED_DOC = `\0${VIRTUAL_DOC}`;
|
|
797
1064
|
function findConfig(root) {
|
|
798
|
-
return [
|
|
1065
|
+
return [join5(root, ".specbook/specbook.config.ts"), join5(root, "specbook.config.ts")].find(
|
|
799
1066
|
existsSync3
|
|
800
1067
|
);
|
|
801
1068
|
}
|
|
@@ -816,7 +1083,7 @@ async function readBody2(req) {
|
|
|
816
1083
|
}
|
|
817
1084
|
function fakerLocales(entry) {
|
|
818
1085
|
const names = /* @__PURE__ */ new Set(["en", "base"]);
|
|
819
|
-
const dir =
|
|
1086
|
+
const dir = join5(dirname(entry), "locale");
|
|
820
1087
|
if (existsSync3(dir))
|
|
821
1088
|
for (const f of readdirSync(dir)) {
|
|
822
1089
|
const m = /^([A-Za-z0-9_]+)\.js$/.exec(f);
|
|
@@ -839,13 +1106,13 @@ function slim(manifests) {
|
|
|
839
1106
|
}
|
|
840
1107
|
async function collectTypes(root, outDir) {
|
|
841
1108
|
const libs = {};
|
|
842
|
-
const req = createRequire(
|
|
1109
|
+
const req = createRequire(join5(root, "package.json"));
|
|
843
1110
|
try {
|
|
844
1111
|
const entry = req.resolve("@specbook/core");
|
|
845
1112
|
const srcDir = dirname(entry);
|
|
846
1113
|
const files = await fg4("**/*.ts", { cwd: srcDir, ignore: ["**/*.test.ts"] });
|
|
847
1114
|
for (const f of files) {
|
|
848
|
-
libs[`file:///node_modules/@specbook/core/src/${f}`] = await
|
|
1115
|
+
libs[`file:///node_modules/@specbook/core/src/${f}`] = await readFile6(join5(srcDir, f), "utf8");
|
|
849
1116
|
}
|
|
850
1117
|
libs["file:///node_modules/@specbook/core/package.json"] = JSON.stringify({
|
|
851
1118
|
name: "@specbook/core",
|
|
@@ -853,24 +1120,27 @@ async function collectTypes(root, outDir) {
|
|
|
853
1120
|
});
|
|
854
1121
|
} catch {
|
|
855
1122
|
}
|
|
856
|
-
const genDir =
|
|
1123
|
+
const genDir = join5(root, outDir);
|
|
857
1124
|
for (const f of await fg4("**/*.ts", { cwd: genDir })) {
|
|
858
|
-
libs[`file:///${outDir}/${f}`] = await
|
|
1125
|
+
libs[`file:///${outDir}/${f}`] = await readFile6(join5(genDir, f), "utf8");
|
|
859
1126
|
}
|
|
860
1127
|
const project = await scanProject(root);
|
|
861
1128
|
const sources = {};
|
|
862
1129
|
for (const f of project.sources.filter((s) => s.endsWith(".ts"))) {
|
|
863
|
-
sources[f] = await
|
|
1130
|
+
sources[f] = await readFile6(join5(root, f), "utf8");
|
|
864
1131
|
}
|
|
865
1132
|
return { libs, sources };
|
|
866
1133
|
}
|
|
867
1134
|
function specbook(options) {
|
|
868
1135
|
const root = resolve2(options.projectRoot);
|
|
869
1136
|
const outDir = options.outDir ?? ".generated";
|
|
1137
|
+
const only = options.operations;
|
|
1138
|
+
const keep = (service, op) => !only || only.has(`${service}/${op}`);
|
|
1139
|
+
const neededDefs = /* @__PURE__ */ new Map();
|
|
870
1140
|
let server;
|
|
871
1141
|
let isBuild = false;
|
|
872
1142
|
let docHtml = [];
|
|
873
|
-
let
|
|
1143
|
+
let manifestCode = /* @__PURE__ */ new Map();
|
|
874
1144
|
let operationsCode = "export const operations = {}";
|
|
875
1145
|
let bundlePlan = /* @__PURE__ */ new Map();
|
|
876
1146
|
let definitionFiles = /* @__PURE__ */ new Map();
|
|
@@ -883,13 +1153,25 @@ function specbook(options) {
|
|
|
883
1153
|
async function registryCode() {
|
|
884
1154
|
const files = await scanProject(root);
|
|
885
1155
|
const manifests = {};
|
|
886
|
-
for (const m of await fg4("*/manifest.json", { cwd:
|
|
887
|
-
const parsed = JSON.parse(await
|
|
1156
|
+
for (const m of await fg4("*/manifest.json", { cwd: join5(root, outDir), absolute: true })) {
|
|
1157
|
+
const parsed = JSON.parse(await readFile6(m, "utf8"));
|
|
888
1158
|
manifests[parsed.service] = parsed;
|
|
889
1159
|
}
|
|
890
|
-
|
|
891
|
-
|
|
1160
|
+
if (only) {
|
|
1161
|
+
for (const [svc, m] of Object.entries(manifests)) {
|
|
1162
|
+
const mm = m;
|
|
1163
|
+
mm.operations = mm.operations.filter((o) => keep(svc, o.id));
|
|
1164
|
+
if (mm.operations.length === 0) delete manifests[svc];
|
|
1165
|
+
else mm.tags = [...new Set(mm.operations.flatMap((o) => o.tags ?? []))];
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
const inScope = (svc, op) => svc in manifests && keep(svc, op);
|
|
1169
|
+
const examples = (await fg4("*/examples/*/*.response.json", {
|
|
1170
|
+
cwd: join5(root, outDir),
|
|
892
1171
|
absolute: true
|
|
1172
|
+
})).filter((abs) => {
|
|
1173
|
+
const rel = relative4(join5(root, outDir), abs).split(sep4);
|
|
1174
|
+
return inScope(rel[0], rel[2]);
|
|
893
1175
|
});
|
|
894
1176
|
const imp = (abs) => {
|
|
895
1177
|
if (isBuild) return `() => import(${JSON.stringify(abs)})`;
|
|
@@ -898,50 +1180,45 @@ function specbook(options) {
|
|
|
898
1180
|
return `(v) => import(/* @vite-ignore */ ${JSON.stringify(`/@fs${abs}`)} + '?t=' + v)`;
|
|
899
1181
|
};
|
|
900
1182
|
const raw = (abs) => `() => import(${JSON.stringify(`${abs}?raw`)}).then((m) => m.default)`;
|
|
901
|
-
const entry = (file) => `{ file: ${JSON.stringify(file)}, load: ${imp(
|
|
1183
|
+
const entry = (file) => `{ file: ${JSON.stringify(file)}, load: ${imp(join5(root, file))}${isBuild ? `, source: ${raw(join5(root, file))}` : ""} }`;
|
|
902
1184
|
const cfg = findConfig(root);
|
|
903
1185
|
const requestsDir = requestsDirFor(root);
|
|
904
|
-
existsSync3(
|
|
1186
|
+
existsSync3(join5(root, "apis")) && !existsSync3(join5(root, "requests")) ? "apis" : "requests";
|
|
905
1187
|
let fakerEntry;
|
|
906
1188
|
try {
|
|
907
|
-
fakerEntry = createRequire(
|
|
1189
|
+
fakerEntry = createRequire(join5(root, "package.json")).resolve("@faker-js/faker");
|
|
908
1190
|
} catch {
|
|
909
1191
|
}
|
|
910
1192
|
const operationEntries = Object.values(manifests).flatMap((m) => {
|
|
911
1193
|
const mm = m;
|
|
912
1194
|
return mm.operations.map(
|
|
913
|
-
(o) => ` ${JSON.stringify(`${mm.service}/${o.id}`)}: ${imp(
|
|
1195
|
+
(o) => ` ${JSON.stringify(`${mm.service}/${o.id}`)}: ${imp(join5(root, outDir, mm.service, "operations", `${o.id}.ts`))}`
|
|
914
1196
|
);
|
|
915
1197
|
});
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
}
|
|
920
|
-
const
|
|
921
|
-
|
|
922
|
-
absolute: true
|
|
923
|
-
});
|
|
1198
|
+
const opOf = (abs, suffix) => {
|
|
1199
|
+
const rel = relative4(join5(root, outDir), abs).split(sep4);
|
|
1200
|
+
return [rel[0], rel[2].replace(suffix, "")];
|
|
1201
|
+
};
|
|
1202
|
+
const schemaFiles = (await fg4("*/schemas/*.request.schema.json", { cwd: join5(root, outDir), absolute: true })).filter((abs) => inScope(...opOf(abs, /\.request\.schema\.json$/)));
|
|
1203
|
+
const responseFiles = (await fg4("*/schemas/*.responses.schema.json", { cwd: join5(root, outDir), absolute: true })).filter((abs) => inScope(...opOf(abs, /\.responses\.schema\.json$/)));
|
|
924
1204
|
const responseEntries = responseFiles.map((abs) => {
|
|
925
|
-
const rel = relative4(
|
|
1205
|
+
const rel = relative4(join5(root, outDir), abs).split(sep4);
|
|
926
1206
|
const op = rel[2].replace(/\.responses\.schema\.json$/, "");
|
|
927
1207
|
return ` ${JSON.stringify(`${rel[0]}/${op}`)}: ${imp(abs)}`;
|
|
928
1208
|
});
|
|
929
|
-
const defFiles = await fg4("*/schemas/definitions.schema.json", {
|
|
930
|
-
cwd: join4(root, outDir),
|
|
931
|
-
absolute: true
|
|
932
|
-
});
|
|
1209
|
+
const defFiles = (await fg4("*/schemas/definitions.schema.json", { cwd: join5(root, outDir), absolute: true })).filter((abs) => relative4(join5(root, outDir), abs).split(sep4)[0] in manifests);
|
|
933
1210
|
const schemaEntries = schemaFiles.map((abs) => {
|
|
934
|
-
const rel = relative4(
|
|
1211
|
+
const rel = relative4(join5(root, outDir), abs).split(sep4);
|
|
935
1212
|
const op = rel[2].replace(/\.request\.schema\.json$/, "");
|
|
936
1213
|
return ` ${JSON.stringify(`${rel[0]}/${op}`)}: ${imp(abs)}`;
|
|
937
1214
|
});
|
|
938
|
-
const docFiles = await fg4("*/docs/*.json", { cwd:
|
|
1215
|
+
const docFiles = (await fg4("*/docs/*.json", { cwd: join5(root, outDir), absolute: true })).filter((abs) => inScope(...opOf(abs, /\.json$/)));
|
|
939
1216
|
const operationDocEntries = docFiles.map((abs) => {
|
|
940
|
-
const rel = relative4(
|
|
1217
|
+
const rel = relative4(join5(root, outDir), abs).split(sep4);
|
|
941
1218
|
return ` ${JSON.stringify(`${rel[0]}/${rel[2].replace(/\.json$/, "")}`)}: ${imp(abs)}`;
|
|
942
1219
|
});
|
|
943
1220
|
const defEntries = defFiles.map((abs) => {
|
|
944
|
-
const service = relative4(
|
|
1221
|
+
const service = relative4(join5(root, outDir), abs).split(sep4)[0];
|
|
945
1222
|
return ` ${JSON.stringify(service)}: ${imp(abs)}`;
|
|
946
1223
|
});
|
|
947
1224
|
const known = new Set(
|
|
@@ -951,11 +1228,15 @@ function specbook(options) {
|
|
|
951
1228
|
})
|
|
952
1229
|
);
|
|
953
1230
|
const sequenceScan = await scanSequences(root, files.sequences);
|
|
1231
|
+
const requestScan = await scanRequests(root, files.requests, new Set(Object.keys(manifests)));
|
|
954
1232
|
for (const f of files.sequences) {
|
|
955
|
-
const src = await
|
|
956
|
-
seqHeaders.set(
|
|
1233
|
+
const src = await readFile6(join5(root, f), "utf8").catch(() => "");
|
|
1234
|
+
seqHeaders.set(join5(root, f), JSON.stringify(parseSequenceMeta(src)));
|
|
957
1235
|
}
|
|
958
1236
|
const scanned = await scanExamples(root, outDir, known);
|
|
1237
|
+
const keepExampleKey = (key) => !only || only.has(key);
|
|
1238
|
+
for (const key of Object.keys(scanned.examples))
|
|
1239
|
+
if (!keepExampleKey(key)) delete scanned.examples[key];
|
|
959
1240
|
const url = (abs) => isBuild ? `() => import(${JSON.stringify(`${abs}?url`)}).then((m) => m.default)` : `() => import(/* @vite-ignore */ ${JSON.stringify(`/@fs${abs}?url`)}).then((m) => m.default)`;
|
|
960
1241
|
const requestExampleEntries = Object.entries(scanned.examples).map(
|
|
961
1242
|
([key, list]) => ` ${JSON.stringify(key)}: [${list.map(
|
|
@@ -977,23 +1258,28 @@ function specbook(options) {
|
|
|
977
1258
|
const assetEntries = Object.entries(scanned.assets).map(
|
|
978
1259
|
([key, abs]) => ` ${JSON.stringify(key)}: ${url(abs)}`
|
|
979
1260
|
);
|
|
980
|
-
const pages = await scanDocs(root)
|
|
1261
|
+
const pages = (await scanDocs(root)).filter(
|
|
1262
|
+
(p) => !only || !p.operation || only.has(p.operation)
|
|
1263
|
+
);
|
|
981
1264
|
docHtml = pages.map((p) => p.html);
|
|
982
1265
|
const docEntries = pages.map(
|
|
983
1266
|
(p, i) => `{ file: ${JSON.stringify(p.file)}, operation: ${JSON.stringify(p.operation)}, sequence: ${JSON.stringify(p.sequence)}, sequenceFile: ${JSON.stringify(p.sequenceFile)}, requestFile: ${JSON.stringify(p.requestFile)}, replace: ${JSON.stringify(p.replace)}, title: ${JSON.stringify(p.title)}, load: () => import(${JSON.stringify(`${VIRTUAL_DOC}${i}`)}).then((m) => m.default) }`
|
|
984
1267
|
);
|
|
985
1268
|
const exampleEntries = examples.map((abs) => {
|
|
986
|
-
const rel = relative4(
|
|
1269
|
+
const rel = relative4(join5(root, outDir), abs).split(sep4);
|
|
987
1270
|
const status = rel[3].replace(/\.response\.json$/, "");
|
|
988
1271
|
const key = `${rel[0]}/${rel[2]}.${status}`;
|
|
989
1272
|
return ` ${JSON.stringify(key)}: ${imp(abs)}`;
|
|
990
1273
|
});
|
|
991
1274
|
if (isBuild) {
|
|
992
|
-
|
|
1275
|
+
const slimmed = slim(manifests);
|
|
1276
|
+
manifestCode = new Map(
|
|
1277
|
+
Object.entries(slimmed).map(([svc, m]) => [svc, `export default ${JSON.stringify(m)}`])
|
|
1278
|
+
);
|
|
993
1279
|
operationsCode = `export const operations = {
|
|
994
1280
|
${operationEntries.join(",\n")}
|
|
995
1281
|
}`;
|
|
996
|
-
const gen =
|
|
1282
|
+
const gen = join5(root, outDir);
|
|
997
1283
|
bundlePlan = /* @__PURE__ */ new Map();
|
|
998
1284
|
const slot = (key) => {
|
|
999
1285
|
const have = bundlePlan.get(key);
|
|
@@ -1033,7 +1319,7 @@ ${operationEntries.join(",\n")}
|
|
|
1033
1319
|
const n = counts.get(tag) ?? 0;
|
|
1034
1320
|
counts.set(tag, n + 1);
|
|
1035
1321
|
operationChunks.set(
|
|
1036
|
-
|
|
1322
|
+
join5(gen, m.service, "operations", `${o.id}.ts`),
|
|
1037
1323
|
`ops-${m.service}-${tag}-${Math.floor(n / 40)}`
|
|
1038
1324
|
);
|
|
1039
1325
|
}
|
|
@@ -1047,8 +1333,8 @@ ${operationEntries.join(",\n")}
|
|
|
1047
1333
|
projectLoads.push(` ${JSON.stringify(`${key}/${e.name}`)}: ${imp(e.abs)}`);
|
|
1048
1334
|
}
|
|
1049
1335
|
const services = Object.keys(manifests);
|
|
1050
|
-
return `
|
|
1051
|
-
export const
|
|
1336
|
+
return `export const mode = "static"
|
|
1337
|
+
export const minimal = ${JSON.stringify(!!only)}
|
|
1052
1338
|
export const project = ${JSON.stringify({ root, outDir, requestsDir })}
|
|
1053
1339
|
// where the data files are: dist/data, whether this chunk is in dist/ or dist/assets/
|
|
1054
1340
|
const dataBase = import.meta.url.replace(/(assets\\/)?[^/]*$/, '') + 'data/'
|
|
@@ -1062,8 +1348,15 @@ const bundle = (key) => {
|
|
|
1062
1348
|
}
|
|
1063
1349
|
const part = (pick) =>
|
|
1064
1350
|
new Proxy({}, { get: (_, key) => (typeof key === 'string' && key.includes('/') ? () => bundle(key).then((b) => ({ default: pick(b) })) : undefined) })
|
|
1065
|
-
export
|
|
1351
|
+
export const manifests = {}
|
|
1352
|
+
const manifestLoaders = {${services.map((svc) => `
|
|
1353
|
+
${JSON.stringify(svc)}: () => import(${JSON.stringify(`${VIRTUAL_MANIFEST}${svc}`)})`).join(",")}
|
|
1354
|
+
}
|
|
1355
|
+
export const loadManifests = (names = Object.keys(manifestLoaders)) =>
|
|
1356
|
+
Promise.all(names.map((svc) => manifests[svc] ? undefined : manifestLoaders[svc]?.().then((m) => { manifests[svc] = m.default }))).then(() => undefined)
|
|
1066
1357
|
export const requests = [${files.requests.map(entry).join(",\n")}]
|
|
1358
|
+
export const requestMeta = ${JSON.stringify(requestScan.meta)}
|
|
1359
|
+
export const unresolvedRequests = ${JSON.stringify(requestScan.unresolved)}
|
|
1067
1360
|
export const sequences = [${files.sequences.map(entry).join(",\n")}]
|
|
1068
1361
|
export const sequenceMeta = ${JSON.stringify(sequenceScan.meta)}
|
|
1069
1362
|
export const unresolvedSequences = ${JSON.stringify(sequenceScan.unresolved)}
|
|
@@ -1119,9 +1412,13 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
|
|
|
1119
1412
|
`;
|
|
1120
1413
|
}
|
|
1121
1414
|
return `export const mode = ${JSON.stringify(isBuild ? "static" : "dev")}
|
|
1415
|
+
export const minimal = ${JSON.stringify(!!only)}
|
|
1122
1416
|
export const project = ${JSON.stringify({ root, outDir, requestsDir })}
|
|
1123
1417
|
export const manifests = ${JSON.stringify(slim(manifests))}
|
|
1418
|
+
export const loadManifests = () => Promise.resolve()
|
|
1124
1419
|
export const requests = [${files.requests.map(entry).join(",\n")}]
|
|
1420
|
+
export const requestMeta = ${JSON.stringify(requestScan.meta)}
|
|
1421
|
+
export const unresolvedRequests = ${JSON.stringify(requestScan.unresolved)}
|
|
1125
1422
|
export const sequences = [${files.sequences.map(entry).join(",\n")}]
|
|
1126
1423
|
export const sequenceMeta = ${JSON.stringify(sequenceScan.meta)}
|
|
1127
1424
|
export const unresolvedSequences = ${JSON.stringify(sequenceScan.unresolved)}
|
|
@@ -1158,33 +1455,24 @@ export const loadFaker = ${fakerEntry ? `() => import('virtual:specbook/faker')`
|
|
|
1158
1455
|
export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: { project: "specbook", services: {} } })'}
|
|
1159
1456
|
`;
|
|
1160
1457
|
}
|
|
1161
|
-
async function
|
|
1458
|
+
async function loadProjectConfig() {
|
|
1162
1459
|
const cfgFile = findConfig(root);
|
|
1163
|
-
if (!server || !cfgFile) return
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
const
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
}
|
|
1180
|
-
return {
|
|
1181
|
-
services: Object.fromEntries(
|
|
1182
|
-
Object.entries(cfg.services ?? {}).map(([k, v]) => [k, { baseUrl: v.baseUrl ?? {} }])
|
|
1183
|
-
),
|
|
1184
|
-
secretNames: cfg.secrets ?? {},
|
|
1185
|
-
tokenOrigins,
|
|
1186
|
-
discoveryOrigins
|
|
1187
|
-
};
|
|
1460
|
+
if (!server || !cfgFile) return void 0;
|
|
1461
|
+
return (await server.ssrLoadModule(cfgFile)).default ?? {};
|
|
1462
|
+
}
|
|
1463
|
+
async function getProxyConfig() {
|
|
1464
|
+
const cfg = await loadProjectConfig();
|
|
1465
|
+
if (!cfg) return { secretNames: {}, resolve: async () => NO_ADDRESSES };
|
|
1466
|
+
const resolve3 = async (env) => isEnvName(env) ? addressesFor(
|
|
1467
|
+
cfg,
|
|
1468
|
+
env,
|
|
1469
|
+
{
|
|
1470
|
+
environments: await listEnvironmentFiles(root),
|
|
1471
|
+
vars: async () => (await readEnvironment(root, env)).vars
|
|
1472
|
+
},
|
|
1473
|
+
tokenOriginOf
|
|
1474
|
+
) : NO_ADDRESSES;
|
|
1475
|
+
return { secretNames: cfg.secrets ?? {}, resolve: resolve3 };
|
|
1188
1476
|
}
|
|
1189
1477
|
return {
|
|
1190
1478
|
name: "specbook",
|
|
@@ -1192,7 +1480,7 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
|
|
|
1192
1480
|
isBuild = c.command === "build";
|
|
1193
1481
|
},
|
|
1194
1482
|
async generateBundle() {
|
|
1195
|
-
const read = async (abs) => JSON.parse(await
|
|
1483
|
+
const read = async (abs) => JSON.parse(await readFile6(abs, "utf8"));
|
|
1196
1484
|
for (const [key, p] of bundlePlan) {
|
|
1197
1485
|
const out = {};
|
|
1198
1486
|
if (p.schema) out.schema = await read(p.schema);
|
|
@@ -1213,6 +1501,13 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
|
|
|
1213
1501
|
await Promise.all(statuses.map(async (n) => [n, await read(p.mock[n])]))
|
|
1214
1502
|
);
|
|
1215
1503
|
}
|
|
1504
|
+
if (only) {
|
|
1505
|
+
const svc = key.slice(0, key.indexOf("/"));
|
|
1506
|
+
const set = neededDefs.get(svc) ?? /* @__PURE__ */ new Set();
|
|
1507
|
+
for (const name of definitionRefs(JSON.stringify([out.schema, out.responses])))
|
|
1508
|
+
set.add(name);
|
|
1509
|
+
neededDefs.set(svc, set);
|
|
1510
|
+
}
|
|
1216
1511
|
this.emitFile({
|
|
1217
1512
|
type: "asset",
|
|
1218
1513
|
fileName: `data/${key}.json`,
|
|
@@ -1223,7 +1518,9 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
|
|
|
1223
1518
|
this.emitFile({
|
|
1224
1519
|
type: "asset",
|
|
1225
1520
|
fileName: `data/${svc}/_definitions.json`,
|
|
1226
|
-
source:
|
|
1521
|
+
source: only ? JSON.stringify(
|
|
1522
|
+
pruneDefinitions(JSON.parse(await readFile6(abs, "utf8")), neededDefs.get(svc))
|
|
1523
|
+
) : await readFile6(abs)
|
|
1227
1524
|
});
|
|
1228
1525
|
},
|
|
1229
1526
|
config(_, env) {
|
|
@@ -1235,7 +1532,8 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
|
|
|
1235
1532
|
// manifests are their own chunk (preloaded with the app, so no extra round trip), and
|
|
1236
1533
|
// operation modules are grouped by tag instead of one chunk per operation
|
|
1237
1534
|
manualChunks(id) {
|
|
1238
|
-
if (id
|
|
1535
|
+
if (id.startsWith(RESOLVED_MANIFEST))
|
|
1536
|
+
return `manifest-${id.slice(RESOLVED_MANIFEST.length)}`;
|
|
1239
1537
|
return operationChunks.get(id);
|
|
1240
1538
|
}
|
|
1241
1539
|
}
|
|
@@ -1247,7 +1545,7 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
|
|
|
1247
1545
|
if (id === VIRTUAL) return RESOLVED;
|
|
1248
1546
|
if (id === VIRTUAL_TYPES) return RESOLVED_TYPES;
|
|
1249
1547
|
if (id === VIRTUAL_FAKER) return RESOLVED_FAKER;
|
|
1250
|
-
if (id
|
|
1548
|
+
if (id.startsWith(VIRTUAL_MANIFEST)) return `\0${id}`;
|
|
1251
1549
|
if (id === VIRTUAL_OPERATIONS) return RESOLVED_OPERATIONS;
|
|
1252
1550
|
if (id.startsWith(VIRTUAL_FAKER_LOCALE)) return `\0${id}`;
|
|
1253
1551
|
if (id.startsWith(VIRTUAL_DOC)) return `\0${id}`;
|
|
@@ -1255,10 +1553,11 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
|
|
|
1255
1553
|
},
|
|
1256
1554
|
async load(id) {
|
|
1257
1555
|
if (id === RESOLVED) return registryCode();
|
|
1258
|
-
if (id
|
|
1556
|
+
if (id.startsWith(RESOLVED_MANIFEST))
|
|
1557
|
+
return manifestCode.get(id.slice(RESOLVED_MANIFEST.length)) ?? "export default {}";
|
|
1259
1558
|
if (id === RESOLVED_OPERATIONS) return operationsCode;
|
|
1260
1559
|
if (id === RESOLVED_FAKER || id.startsWith(RESOLVED_FAKER_LOCALE)) {
|
|
1261
|
-
const r = await this.resolve("@faker-js/faker",
|
|
1560
|
+
const r = await this.resolve("@faker-js/faker", join5(root, "package.json"), {
|
|
1262
1561
|
skipSelf: true
|
|
1263
1562
|
});
|
|
1264
1563
|
if (!r) return "export {}";
|
|
@@ -1267,7 +1566,7 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
|
|
|
1267
1566
|
return `export { ${id.slice(RESOLVED_FAKER_LOCALE.length)} as default } from ${pkg}`;
|
|
1268
1567
|
let entry = r.id;
|
|
1269
1568
|
try {
|
|
1270
|
-
entry = createRequire(
|
|
1569
|
+
entry = createRequire(join5(root, "package.json")).resolve("@faker-js/faker");
|
|
1271
1570
|
} catch {
|
|
1272
1571
|
}
|
|
1273
1572
|
const names = fakerLocales(entry);
|
|
@@ -1294,7 +1593,7 @@ ${names.map((n) => ` ${JSON.stringify(n)}: () => import(${JSON.stringify(`${VIR
|
|
|
1294
1593
|
basename(outDir)
|
|
1295
1594
|
]);
|
|
1296
1595
|
for (const entry of readdirSync(root, { withFileTypes: true }))
|
|
1297
|
-
if (entry.isDirectory() && !SKIP.has(entry.name)) s.watcher.add(
|
|
1596
|
+
if (entry.isDirectory() && !SKIP.has(entry.name)) s.watcher.add(join5(root, entry.name));
|
|
1298
1597
|
const relevant = (file) => {
|
|
1299
1598
|
const rel = relative4(root, file);
|
|
1300
1599
|
if (rel.startsWith("..") || /node_modules/.test(rel)) return false;
|
|
@@ -1318,7 +1617,7 @@ ${names.map((n) => ` ${JSON.stringify(n)}: () => import(${JSON.stringify(`${VIR
|
|
|
1318
1617
|
if (file.endsWith(".md")) return refresh(file);
|
|
1319
1618
|
if (!file.endsWith(".seq.ts")) return;
|
|
1320
1619
|
const header = JSON.stringify(
|
|
1321
|
-
parseSequenceMeta(await
|
|
1620
|
+
parseSequenceMeta(await readFile6(file, "utf8").catch(() => ""))
|
|
1322
1621
|
);
|
|
1323
1622
|
if (seqHeaders.get(file) !== header) {
|
|
1324
1623
|
seqHeaders.set(file, header);
|
|
@@ -1343,7 +1642,7 @@ ${names.map((n) => ` ${JSON.stringify(n)}: () => import(${JSON.stringify(`${VIR
|
|
|
1343
1642
|
const abs = safeProjectPath(root, url.searchParams.get("path") ?? "");
|
|
1344
1643
|
if (!abs || !existsSync3(abs)) return json2(res, 404, { error: "not found" });
|
|
1345
1644
|
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
1346
|
-
return void res.end(await
|
|
1645
|
+
return void res.end(await readFile6(abs, "utf8"));
|
|
1347
1646
|
}
|
|
1348
1647
|
if (url.pathname === "/__specbook/types")
|
|
1349
1648
|
return json2(res, 200, await collectTypes(root, outDir));
|
|
@@ -1362,6 +1661,35 @@ ${names.map((n) => ` ${JSON.stringify(n)}: () => import(${JSON.stringify(`${VIR
|
|
|
1362
1661
|
);
|
|
1363
1662
|
return json2(res, 200, { env, vars: values.vars, secrets, warnings });
|
|
1364
1663
|
}
|
|
1664
|
+
if (url.pathname === "/__specbook/environment" && req.method === "POST") {
|
|
1665
|
+
const origin = req.headers.origin;
|
|
1666
|
+
if (origin && new URL(origin).host !== req.headers.host)
|
|
1667
|
+
return json2(res, 403, { error: "cross-origin request refused" });
|
|
1668
|
+
const { name, from } = JSON.parse(await readBody2(req));
|
|
1669
|
+
if (!isEnvName(name) || name === "mock" || !isEnvName(from))
|
|
1670
|
+
return json2(res, 400, { error: "bad environment name" });
|
|
1671
|
+
const conf = await loadProjectConfig();
|
|
1672
|
+
const known = environmentNames(
|
|
1673
|
+
{ services: conf?.services ?? {}, environments: conf?.environments },
|
|
1674
|
+
await listEnvironmentFiles(root)
|
|
1675
|
+
);
|
|
1676
|
+
if (known.includes(name)) return json2(res, 409, { error: `"${name}" already exists` });
|
|
1677
|
+
if (!known.includes(from))
|
|
1678
|
+
return json2(res, 400, { error: `unknown environment "${from}"` });
|
|
1679
|
+
const own = {
|
|
1680
|
+
...conf?.environments?.[from]?.vars,
|
|
1681
|
+
...(await readEnvironment(root, from)).vars
|
|
1682
|
+
};
|
|
1683
|
+
await ensureEnvironmentsDir(root);
|
|
1684
|
+
const file = varsFile(root, name);
|
|
1685
|
+
await writeFile2(file, `# variables for ${name}, copied from ${from}
|
|
1686
|
+
`);
|
|
1687
|
+
for (const [k, v] of Object.entries(own)) {
|
|
1688
|
+
if (v === null || typeof v === "object" || !isVarName(k)) continue;
|
|
1689
|
+
await writeEnvKey(file, k, String(v));
|
|
1690
|
+
}
|
|
1691
|
+
return json2(res, 200, { ok: true, name });
|
|
1692
|
+
}
|
|
1365
1693
|
if ((url.pathname === "/__specbook/secret" || url.pathname === "/__specbook/var") && req.method === "POST") {
|
|
1366
1694
|
const origin = req.headers.origin;
|
|
1367
1695
|
if (origin && new URL(origin).host !== req.headers.host)
|
|
@@ -1428,6 +1756,7 @@ export {
|
|
|
1428
1756
|
listEnvironmentFiles,
|
|
1429
1757
|
readAllVars,
|
|
1430
1758
|
readEnvironment,
|
|
1759
|
+
requestsDirFor,
|
|
1431
1760
|
safeProjectPath,
|
|
1432
1761
|
scanProject,
|
|
1433
1762
|
secretsFileFor,
|