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