@octalmesh/seagull-core 0.0.2 → 0.1.1
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/CHANGELOG.md +38 -0
- package/README.md +173 -39
- package/dist/index.mjs +218 -152
- package/package.json +6 -6
- package/src/config/loader.test.ts +505 -0
- package/src/config/loader.ts +31 -12
- package/src/config/publishing.test.ts +92 -0
- package/src/config/publishing.ts +9 -5
- package/src/config/resolve-config-file.test.ts +60 -0
- package/src/config/schema.test.ts +466 -0
- package/src/config/schema.ts +16 -10
- package/src/config/spec-format.test.ts +54 -0
- package/src/config/spec-format.ts +48 -0
- package/src/config/template.test.ts +168 -0
- package/src/config/types.ts +3 -3
- package/src/generator/generator.test.ts +59 -0
- package/src/generator/registry.test.ts +84 -0
- package/src/generator/types.ts +9 -14
- package/src/generators/openapi-generator-cli/openapi-generator-cli.generator.test.ts +259 -0
- package/src/generators/openapi-generator-cli/patchers/go-module.patcher.test.ts +119 -0
- package/src/generators/openapi-generator-cli/patchers/maven.patcher.test.ts +141 -0
- package/src/generators/openapi-generator-cli/patchers/npm.patcher.test.ts +132 -0
- package/src/generators/openapi-typescript/openapi-typescript.generator.test.ts +190 -0
- package/src/git/git.test.ts +234 -0
- package/src/git/git.ts +50 -4
- package/src/index.ts +8 -0
- package/src/process/exec.test.ts +103 -0
- package/src/process/exec.ts +1 -2
- package/src/process/resolve-bin.test.ts +43 -0
- package/src/readme/default-templates.test.ts +179 -0
- package/src/readme/default-templates.ts +5 -10
- package/src/readme/readme-renderer.test.ts +121 -0
- package/src/readme/readme-renderer.ts +3 -7
- package/src/redocly/redocly-sync.test.ts +190 -0
- package/src/redocly/redocly-sync.ts +3 -1
- package/src/test-support/fixtures.ts +65 -0
- package/src/version/version.test.ts +93 -0
- package/src/version/version.ts +4 -2
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
7
|
+
|
|
8
|
+
import { loadConfig } from "./loader";
|
|
9
|
+
|
|
10
|
+
function baseConfig(): Record<string, unknown> {
|
|
11
|
+
return {
|
|
12
|
+
configVersion: 1,
|
|
13
|
+
vars: {
|
|
14
|
+
org: "octalmesh",
|
|
15
|
+
platform: "web",
|
|
16
|
+
repository: { owner: "OctalMesh", repo: "ows-contracts" },
|
|
17
|
+
},
|
|
18
|
+
paths: { dist: "dist" },
|
|
19
|
+
docs: {
|
|
20
|
+
server: { host: "localhost", port: 8080 },
|
|
21
|
+
metadata: {
|
|
22
|
+
title: "OWS Docs",
|
|
23
|
+
description: "desc",
|
|
24
|
+
favicon: "fav.ico",
|
|
25
|
+
baseServerUrl: "https://octalmesh.com",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
publishing: {
|
|
29
|
+
branch: "sdk/svc-{service}/{id}",
|
|
30
|
+
tag: "svc-{service}-{id}-v{version}",
|
|
31
|
+
repositoryUrl:
|
|
32
|
+
"https://github.com/{vars.repository.owner}/{vars.repository.repo}",
|
|
33
|
+
npm: { registry: "https://npm.pkg.github.com", access: "public" },
|
|
34
|
+
maven: {
|
|
35
|
+
repositoryId: "github",
|
|
36
|
+
repositoryUrl:
|
|
37
|
+
"https://maven.pkg.github.com/{vars.repository.owner}/{vars.repository.repo}",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
generators: {
|
|
41
|
+
"ts-client": {
|
|
42
|
+
tool: "openapi-generator",
|
|
43
|
+
generator: "typescript-fetch",
|
|
44
|
+
lang: "typescript",
|
|
45
|
+
kind: "client",
|
|
46
|
+
package: "@{vars.org}/{service}-client",
|
|
47
|
+
additionalProperties: { supportsES6: true },
|
|
48
|
+
},
|
|
49
|
+
"ts-server": {
|
|
50
|
+
tool: "openapi-typescript",
|
|
51
|
+
lang: "typescript",
|
|
52
|
+
kind: "server",
|
|
53
|
+
package: "@{vars.org}/{service}-server",
|
|
54
|
+
},
|
|
55
|
+
"java-client": {
|
|
56
|
+
tool: "openapi-generator",
|
|
57
|
+
generator: "java",
|
|
58
|
+
lang: "java",
|
|
59
|
+
kind: "client",
|
|
60
|
+
maven: {
|
|
61
|
+
groupId: "com.{vars.org}.{service}",
|
|
62
|
+
artifactId: "{service}-client",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
contracts: [
|
|
67
|
+
{
|
|
68
|
+
name: "auth",
|
|
69
|
+
title: "Auth Service API",
|
|
70
|
+
entrypoint: "specs/auth/openapi.yaml",
|
|
71
|
+
artifacts: ["ts-client", "ts-server"],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "catalog",
|
|
75
|
+
title: "Catalog Service API",
|
|
76
|
+
entrypoint: "specs/catalog/openapi.yaml",
|
|
77
|
+
artifacts: [
|
|
78
|
+
{
|
|
79
|
+
generator: "ts-client",
|
|
80
|
+
as: "ts-client-legacy",
|
|
81
|
+
overrides: {
|
|
82
|
+
package: "@{vars.org}/{service}-client-legacy",
|
|
83
|
+
additionalProperties: { legacy: true },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe("loadConfig", () => {
|
|
93
|
+
let dir: string;
|
|
94
|
+
|
|
95
|
+
beforeEach(async () => {
|
|
96
|
+
dir = await mkdtemp(path.join(tmpdir(), "seagull-loader-"));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
afterEach(async () => {
|
|
100
|
+
await rm(dir, { recursive: true, force: true });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
async function writeConfig(
|
|
104
|
+
config: Record<string, unknown>,
|
|
105
|
+
filename = "seagull.yaml",
|
|
106
|
+
): Promise<string> {
|
|
107
|
+
const configPath = path.join(dir, filename);
|
|
108
|
+
|
|
109
|
+
await writeFile(configPath, stringifyYaml(config));
|
|
110
|
+
|
|
111
|
+
return configPath;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
it("throws a readable, multi-issue error for an invalid config", async () => {
|
|
115
|
+
const configPath = await writeConfig({ configVersion: 1 });
|
|
116
|
+
|
|
117
|
+
expect(() => loadConfig(configPath)).toThrow(/Invalid seagull\.yaml/);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("loads a minimal valid config and resolves every top-level field", async () => {
|
|
121
|
+
const configPath = await writeConfig(baseConfig());
|
|
122
|
+
const config = loadConfig(configPath);
|
|
123
|
+
|
|
124
|
+
expect(config.configVersion).toBe(1);
|
|
125
|
+
expect(config.rootDir).toBe(dir);
|
|
126
|
+
expect(config.vars).toEqual({
|
|
127
|
+
org: "octalmesh",
|
|
128
|
+
platform: "web",
|
|
129
|
+
repository: { owner: "OctalMesh", repo: "ows-contracts" },
|
|
130
|
+
});
|
|
131
|
+
expect(config.paths.dist).toBe(path.join(dir, "dist"));
|
|
132
|
+
expect(config.paths.specs).toBe(path.join(dir, "dist", "specs"));
|
|
133
|
+
expect(config.paths.docs).toBe(path.join(dir, "dist", "docs"));
|
|
134
|
+
expect(config.paths.sdk).toBe(path.join(dir, "dist", "sdk"));
|
|
135
|
+
expect(config.paths.specFormat).toEqual(["json"]);
|
|
136
|
+
expect(config.contracts).toHaveLength(2);
|
|
137
|
+
expect(config.allArtifacts).toHaveLength(3);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("respects an explicit paths.specFormat override, accepting a single value or a list", async () => {
|
|
141
|
+
const cfg = baseConfig();
|
|
142
|
+
|
|
143
|
+
(cfg.paths as Record<string, unknown>) = {
|
|
144
|
+
dist: "dist",
|
|
145
|
+
specFormat: "yaml",
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const configPath = await writeConfig(cfg);
|
|
149
|
+
const config = loadConfig(configPath);
|
|
150
|
+
|
|
151
|
+
expect(config.paths.specFormat).toEqual(["yaml"]);
|
|
152
|
+
|
|
153
|
+
(cfg.paths as Record<string, unknown>) = {
|
|
154
|
+
dist: "dist",
|
|
155
|
+
specFormat: ["yaml", "json"],
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const configPath2 = await writeConfig(cfg);
|
|
159
|
+
const config2 = loadConfig(configPath2);
|
|
160
|
+
|
|
161
|
+
expect(config2.paths.specFormat).toEqual(["yaml", "json"]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("respects explicit paths.specs/docs/sdk overrides instead of dist-relative defaults", async () => {
|
|
165
|
+
const cfg = baseConfig();
|
|
166
|
+
|
|
167
|
+
(cfg.paths as Record<string, unknown>) = {
|
|
168
|
+
dist: "dist",
|
|
169
|
+
specs: "custom-specs",
|
|
170
|
+
docs: "custom-docs",
|
|
171
|
+
sdk: "custom-sdk",
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const configPath = await writeConfig(cfg);
|
|
175
|
+
const config = loadConfig(configPath);
|
|
176
|
+
|
|
177
|
+
expect(config.paths.specs).toBe(path.join(dir, "custom-specs"));
|
|
178
|
+
expect(config.paths.docs).toBe(path.join(dir, "custom-docs"));
|
|
179
|
+
expect(config.paths.sdk).toBe(path.join(dir, "custom-sdk"));
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("resolves each contract's entrypoint to an absolute path, and a rootDir-relative one", async () => {
|
|
183
|
+
const configPath = await writeConfig(baseConfig());
|
|
184
|
+
const config = loadConfig(configPath);
|
|
185
|
+
|
|
186
|
+
const auth = config.contracts.find((c) => c.name === "auth")!;
|
|
187
|
+
|
|
188
|
+
expect(auth.entrypoint).toBe(path.join(dir, "specs/auth/openapi.yaml"));
|
|
189
|
+
expect(auth.entrypointRelative).toBe(
|
|
190
|
+
path.join("specs", "auth", "openapi.yaml"),
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("interpolates templated fields on every artifact (package, additionalProperties)", async () => {
|
|
195
|
+
const configPath = await writeConfig(baseConfig());
|
|
196
|
+
const config = loadConfig(configPath);
|
|
197
|
+
|
|
198
|
+
const auth = config.contracts.find((c) => c.name === "auth")!;
|
|
199
|
+
const tsClient = auth.artifacts.find((a) => a.id === "ts-client")!;
|
|
200
|
+
|
|
201
|
+
expect(tsClient.package).toBe("@octalmesh/auth-client");
|
|
202
|
+
expect(tsClient.additionalProperties).toEqual({ supportsES6: true });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("computes each artifact's outputDir as <sdkDir>/<contract>/<id>", async () => {
|
|
206
|
+
const configPath = await writeConfig(baseConfig());
|
|
207
|
+
const config = loadConfig(configPath);
|
|
208
|
+
|
|
209
|
+
const auth = config.contracts.find((c) => c.name === "auth")!;
|
|
210
|
+
const tsServer = auth.artifacts.find((a) => a.id === "ts-server")!;
|
|
211
|
+
|
|
212
|
+
expect(tsServer.outputDir).toBe(
|
|
213
|
+
path.join(dir, "dist", "sdk", "auth", "ts-server"),
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("resolves the root-level publishing block per artifact, interpolating branch/repositoryUrl/registries but not the tag template", async () => {
|
|
218
|
+
const configPath = await writeConfig(baseConfig());
|
|
219
|
+
const config = loadConfig(configPath);
|
|
220
|
+
|
|
221
|
+
const auth = config.contracts.find((c) => c.name === "auth")!;
|
|
222
|
+
const tsClient = auth.artifacts.find((a) => a.id === "ts-client")!;
|
|
223
|
+
|
|
224
|
+
expect(tsClient.branch).toBe("sdk/svc-auth/ts-client");
|
|
225
|
+
expect(tsClient.publishing.branch).toBe("sdk/svc-auth/ts-client");
|
|
226
|
+
expect(tsClient.publishing.repositoryUrl).toBe(
|
|
227
|
+
"https://github.com/OctalMesh/ows-contracts",
|
|
228
|
+
);
|
|
229
|
+
expect(tsClient.publishing.npmRegistry).toBe("https://npm.pkg.github.com");
|
|
230
|
+
expect(tsClient.publishing.npmAccess).toBe("public");
|
|
231
|
+
expect(tsClient.publishing.mavenRepositoryId).toBe("github");
|
|
232
|
+
expect(tsClient.publishing.tagTemplate).toBe(
|
|
233
|
+
"svc-{service}-{id}-v{version}",
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("applies an artifact-ref's 'as' to rename the artifact id (and its branch/outputDir)", async () => {
|
|
238
|
+
const configPath = await writeConfig(baseConfig());
|
|
239
|
+
const config = loadConfig(configPath);
|
|
240
|
+
|
|
241
|
+
const catalog = config.contracts.find((c) => c.name === "catalog")!;
|
|
242
|
+
|
|
243
|
+
expect(catalog.artifacts).toHaveLength(1);
|
|
244
|
+
expect(catalog.artifacts[0]!.id).toBe("ts-client-legacy");
|
|
245
|
+
expect(catalog.artifacts[0]!.branch).toBe(
|
|
246
|
+
"sdk/svc-catalog/ts-client-legacy",
|
|
247
|
+
);
|
|
248
|
+
expect(catalog.artifacts[0]!.outputDir).toBe(
|
|
249
|
+
path.join(dir, "dist", "sdk", "catalog", "ts-client-legacy"),
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("merges an artifact-ref's overrides onto the base generator (own fields win)", async () => {
|
|
254
|
+
const configPath = await writeConfig(baseConfig());
|
|
255
|
+
const config = loadConfig(configPath);
|
|
256
|
+
|
|
257
|
+
const catalog = config.contracts.find((c) => c.name === "catalog")!;
|
|
258
|
+
const legacy = catalog.artifacts[0]!;
|
|
259
|
+
|
|
260
|
+
expect(legacy.package).toBe("@octalmesh/catalog-client-legacy");
|
|
261
|
+
expect(legacy.additionalProperties).toEqual({
|
|
262
|
+
supportsES6: true,
|
|
263
|
+
legacy: true,
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("throws a descriptive error when a contract references an unknown generator id", async () => {
|
|
268
|
+
const cfg = baseConfig();
|
|
269
|
+
|
|
270
|
+
(cfg.contracts as Record<string, unknown>[])[0]!.artifacts = [
|
|
271
|
+
"does-not-exist",
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
const configPath = await writeConfig(cfg);
|
|
275
|
+
|
|
276
|
+
expect(() => loadConfig(configPath)).toThrow(
|
|
277
|
+
/Contract "auth" references unknown generator "does-not-exist" \(available: java-client, ts-client, ts-server\)/,
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("throws a descriptive error when a resolved branch would start with '-'", async () => {
|
|
282
|
+
const cfg = baseConfig();
|
|
283
|
+
|
|
284
|
+
cfg.vars = { ...(cfg.vars as Record<string, unknown>), org: "octalmesh" };
|
|
285
|
+
(cfg.publishing as Record<string, unknown>).branch =
|
|
286
|
+
"-{vars.org}/{service}/{id}";
|
|
287
|
+
|
|
288
|
+
const configPath = await writeConfig(cfg);
|
|
289
|
+
|
|
290
|
+
expect(() => loadConfig(configPath)).toThrow(
|
|
291
|
+
/Invalid git publishing\.branch for artifact "auth\/ts-client" "-octalmesh\/auth\/ts-client": must not start with "-"/,
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("throws when a template placeholder can't be resolved", async () => {
|
|
296
|
+
const cfg = baseConfig();
|
|
297
|
+
|
|
298
|
+
(cfg.generators as Record<string, Record<string, unknown>>)[
|
|
299
|
+
"ts-client"
|
|
300
|
+
]!.package = "@{vars.missing}/{service}-client";
|
|
301
|
+
|
|
302
|
+
const configPath = await writeConfig(cfg);
|
|
303
|
+
|
|
304
|
+
expect(() => loadConfig(configPath)).toThrow(
|
|
305
|
+
/Unknown template placeholder "\{vars\.missing\}"/,
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe("publishing overrides", () => {
|
|
310
|
+
it("lets a generator-level 'publishing' override win over the root block", async () => {
|
|
311
|
+
const cfg = baseConfig();
|
|
312
|
+
|
|
313
|
+
(cfg.generators as Record<string, Record<string, unknown>>)[
|
|
314
|
+
"ts-client"
|
|
315
|
+
]!.publishing = {
|
|
316
|
+
npm: { registry: "https://registry.internal.example.com" },
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const configPath = await writeConfig(cfg);
|
|
320
|
+
const config = loadConfig(configPath);
|
|
321
|
+
|
|
322
|
+
const auth = config.contracts.find((c) => c.name === "auth")!;
|
|
323
|
+
const tsClient = auth.artifacts.find((a) => a.id === "ts-client")!;
|
|
324
|
+
const tsServer = auth.artifacts.find((a) => a.id === "ts-server")!;
|
|
325
|
+
|
|
326
|
+
expect(tsClient.publishing.npmRegistry).toBe(
|
|
327
|
+
"https://registry.internal.example.com",
|
|
328
|
+
);
|
|
329
|
+
expect(tsClient.publishing.npmAccess).toBe("public");
|
|
330
|
+
expect(tsServer.publishing.npmRegistry).toBe(
|
|
331
|
+
"https://npm.pkg.github.com",
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("lets an artifact-ref override's 'publishing' win over both the generator's and the root's", async () => {
|
|
336
|
+
const cfg = baseConfig();
|
|
337
|
+
|
|
338
|
+
(cfg.generators as Record<string, Record<string, unknown>>)[
|
|
339
|
+
"ts-client"
|
|
340
|
+
]!.publishing = { branch: "generator-level/{service}/{id}" };
|
|
341
|
+
|
|
342
|
+
(cfg.contracts as Record<string, unknown>[])[1]!.artifacts = [
|
|
343
|
+
{
|
|
344
|
+
generator: "ts-client",
|
|
345
|
+
overrides: {
|
|
346
|
+
publishing: { branch: "artifact-level/{service}/{id}" },
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
];
|
|
350
|
+
|
|
351
|
+
const configPath = await writeConfig(cfg);
|
|
352
|
+
const config = loadConfig(configPath);
|
|
353
|
+
|
|
354
|
+
const catalog = config.contracts.find((c) => c.name === "catalog")!;
|
|
355
|
+
|
|
356
|
+
expect(catalog.artifacts[0]!.branch).toBe(
|
|
357
|
+
"artifact-level/catalog/ts-client",
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("deep-merges 'maven' when an artifact-ref override's own 'maven' partially overlaps the generator's", async () => {
|
|
362
|
+
const cfg = baseConfig();
|
|
363
|
+
|
|
364
|
+
(cfg.contracts as Record<string, unknown>[])[1]!.artifacts = [
|
|
365
|
+
{
|
|
366
|
+
generator: "java-client",
|
|
367
|
+
overrides: {
|
|
368
|
+
maven: {
|
|
369
|
+
groupId: "com.{vars.org}.{service}",
|
|
370
|
+
artifactId: "{service}-client-v2",
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
const configPath = await writeConfig(cfg);
|
|
377
|
+
const config = loadConfig(configPath);
|
|
378
|
+
|
|
379
|
+
const catalog = config.contracts.find((c) => c.name === "catalog")!;
|
|
380
|
+
|
|
381
|
+
expect(catalog.artifacts[0]!.maven).toEqual({
|
|
382
|
+
groupId: "com.octalmesh.catalog",
|
|
383
|
+
artifactId: "catalog-client-v2",
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("deep-merges 'npm' and 'maven' when both a generator-level and an artifact-level publishing override set them", async () => {
|
|
388
|
+
const cfg = baseConfig();
|
|
389
|
+
|
|
390
|
+
(cfg.generators as Record<string, Record<string, unknown>>)[
|
|
391
|
+
"ts-client"
|
|
392
|
+
]!.publishing = {
|
|
393
|
+
npm: {
|
|
394
|
+
registry: "https://generator-level.example.com",
|
|
395
|
+
access: "public",
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
(cfg.contracts as Record<string, unknown>[])[1]!.artifacts = [
|
|
400
|
+
{
|
|
401
|
+
generator: "ts-client",
|
|
402
|
+
overrides: {
|
|
403
|
+
publishing: { npm: { access: "restricted" } },
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
];
|
|
407
|
+
|
|
408
|
+
const configPath = await writeConfig(cfg);
|
|
409
|
+
const config = loadConfig(configPath);
|
|
410
|
+
|
|
411
|
+
const catalog = config.contracts.find((c) => c.name === "catalog")!;
|
|
412
|
+
|
|
413
|
+
expect(catalog.artifacts[0]!.publishing.npmRegistry).toBe(
|
|
414
|
+
"https://generator-level.example.com",
|
|
415
|
+
);
|
|
416
|
+
expect(catalog.artifacts[0]!.publishing.npmAccess).toBe("restricted");
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("deep-merges 'maven' in a publishing override on top of the root's maven repository config", async () => {
|
|
420
|
+
const cfg = baseConfig();
|
|
421
|
+
|
|
422
|
+
(cfg.contracts as Record<string, unknown>[])[1]!.artifacts = [
|
|
423
|
+
{
|
|
424
|
+
generator: "ts-client",
|
|
425
|
+
overrides: {
|
|
426
|
+
publishing: { maven: { repositoryId: "internal" } },
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
];
|
|
430
|
+
|
|
431
|
+
const configPath = await writeConfig(cfg);
|
|
432
|
+
const config = loadConfig(configPath);
|
|
433
|
+
|
|
434
|
+
const catalog = config.contracts.find((c) => c.name === "catalog")!;
|
|
435
|
+
|
|
436
|
+
expect(catalog.artifacts[0]!.publishing.mavenRepositoryId).toBe(
|
|
437
|
+
"internal",
|
|
438
|
+
);
|
|
439
|
+
expect(catalog.artifacts[0]!.publishing.mavenRepositoryUrl).toBe(
|
|
440
|
+
"https://maven.pkg.github.com/OctalMesh/ows-contracts",
|
|
441
|
+
);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe("readme templates", () => {
|
|
446
|
+
it("resolves 'readme' relative to the config's rootDir when set", async () => {
|
|
447
|
+
const cfg = baseConfig();
|
|
448
|
+
|
|
449
|
+
(cfg.generators as Record<string, Record<string, unknown>>)[
|
|
450
|
+
"ts-client"
|
|
451
|
+
]!.readme = "readme-templates/ts-client.md";
|
|
452
|
+
|
|
453
|
+
const configPath = await writeConfig(cfg);
|
|
454
|
+
const config = loadConfig(configPath);
|
|
455
|
+
|
|
456
|
+
const auth = config.contracts.find((c) => c.name === "auth")!;
|
|
457
|
+
const tsClient = auth.artifacts.find((a) => a.id === "ts-client")!;
|
|
458
|
+
|
|
459
|
+
expect(tsClient.readmeTemplate).toBe(
|
|
460
|
+
path.join(dir, "readme-templates/ts-client.md"),
|
|
461
|
+
);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("leaves readmeTemplate undefined when no 'readme' is configured", async () => {
|
|
465
|
+
const configPath = await writeConfig(baseConfig());
|
|
466
|
+
const config = loadConfig(configPath);
|
|
467
|
+
|
|
468
|
+
const auth = config.contracts.find((c) => c.name === "auth")!;
|
|
469
|
+
const tsServer = auth.artifacts.find((a) => a.id === "ts-server")!;
|
|
470
|
+
|
|
471
|
+
expect(tsServer.readmeTemplate).toBeUndefined();
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
describe("allArtifacts", () => {
|
|
476
|
+
it("flattens every (contract, artifact) pair across all contracts, in config order", async () => {
|
|
477
|
+
const configPath = await writeConfig(baseConfig());
|
|
478
|
+
const config = loadConfig(configPath);
|
|
479
|
+
|
|
480
|
+
expect(
|
|
481
|
+
config.allArtifacts.map((e) => `${e.contract.name}/${e.artifact.id}`),
|
|
482
|
+
).toEqual([
|
|
483
|
+
"auth/ts-client",
|
|
484
|
+
"auth/ts-server",
|
|
485
|
+
"catalog/ts-client-legacy",
|
|
486
|
+
]);
|
|
487
|
+
|
|
488
|
+
for (const entry of config.allArtifacts) {
|
|
489
|
+
expect(entry.contract).toBe(
|
|
490
|
+
config.contracts.find((c) => c.name === entry.contract.name),
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
describe("config file discovery filenames", () => {
|
|
497
|
+
it("loads correctly regardless of which recognized filename is used", async () => {
|
|
498
|
+
await mkdir(dir, { recursive: true });
|
|
499
|
+
const configPath = await writeConfig(baseConfig(), ".seagull.yaml");
|
|
500
|
+
const config = loadConfig(configPath);
|
|
501
|
+
|
|
502
|
+
expect(config.contracts).toHaveLength(2);
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
});
|
package/src/config/loader.ts
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
|
|
7
|
+
import { assertSafeRefName } from "../git/git";
|
|
7
8
|
import {
|
|
8
9
|
type ArtifactRefInput,
|
|
9
10
|
type ContractInput,
|
|
@@ -29,7 +30,9 @@ import type {
|
|
|
29
30
|
* @returns The validated (but not yet resolved) raw config.
|
|
30
31
|
*/
|
|
31
32
|
function readRawConfig(configPath: string): RootConfigInput {
|
|
32
|
-
const raw: unknown = parseYaml(readFileSync(configPath, "utf8")
|
|
33
|
+
const raw: unknown = parseYaml(readFileSync(configPath, "utf8"), {
|
|
34
|
+
merge: true,
|
|
35
|
+
});
|
|
33
36
|
const result = rootConfigSchema.safeParse(raw);
|
|
34
37
|
|
|
35
38
|
if (!result.success) {
|
|
@@ -170,18 +173,29 @@ function resolveArtifactRef(
|
|
|
170
173
|
* @param rootPublishing - The required root-level `publishing:` config
|
|
171
174
|
* from the config file.
|
|
172
175
|
* @param context - The flattened template context for this artifact
|
|
173
|
-
* (`service`, `id`, `
|
|
176
|
+
* (`service`, `id`, `vars.*`).
|
|
177
|
+
* @param artifactLabel - `"<contract>/<artifact id>"`, used only to
|
|
178
|
+
* identify the offending artifact in the error
|
|
179
|
+
* thrown when `branch` resolves unsafely - see
|
|
180
|
+
* {@link assertSafeRefName}.
|
|
174
181
|
* @returns The fully resolved publishing conventions for this artifact.
|
|
175
182
|
*/
|
|
176
183
|
function resolvePublishing(
|
|
177
184
|
generatorPublishing: PublishingOverrideInput | undefined,
|
|
178
185
|
rootPublishing: PublishingInput,
|
|
179
186
|
context: Record<string, string>,
|
|
187
|
+
artifactLabel: string,
|
|
180
188
|
): ResolvedPublishing {
|
|
181
189
|
const merged = applyPublishingOverride(rootPublishing, generatorPublishing);
|
|
190
|
+
const branch = interpolate(merged.branch, context);
|
|
191
|
+
|
|
192
|
+
assertSafeRefName(
|
|
193
|
+
branch,
|
|
194
|
+
`publishing.branch for artifact "${artifactLabel}"`,
|
|
195
|
+
);
|
|
182
196
|
|
|
183
197
|
return {
|
|
184
|
-
branch
|
|
198
|
+
branch,
|
|
185
199
|
tagTemplate: merged.tag,
|
|
186
200
|
repositoryUrl: interpolate(merged.repositoryUrl, context),
|
|
187
201
|
npmRegistry: interpolate(merged.npm.registry, context),
|
|
@@ -203,7 +217,7 @@ function resolvePublishing(
|
|
|
203
217
|
* @param sdkDir - Absolute path to the SDK output root (`<dist>/sdk`).
|
|
204
218
|
* @param contractName - The owning contract's name.
|
|
205
219
|
* @param contractContext - The flattened template context for this contract
|
|
206
|
-
* (`service`, `
|
|
220
|
+
* (`service`, `vars.*` - not yet `id`).
|
|
207
221
|
* @param rootPublishing - The required root-level `publishing:` config from
|
|
208
222
|
* the config file.
|
|
209
223
|
* @returns The fully resolved artifact.
|
|
@@ -219,7 +233,12 @@ function resolveArtifact(
|
|
|
219
233
|
): ResolvedArtifact {
|
|
220
234
|
const context = { ...contractContext, id };
|
|
221
235
|
const resolved = interpolateDeep(def, context);
|
|
222
|
-
const publishing = resolvePublishing(
|
|
236
|
+
const publishing = resolvePublishing(
|
|
237
|
+
def.publishing,
|
|
238
|
+
rootPublishing,
|
|
239
|
+
context,
|
|
240
|
+
`${contractName}/${id}`,
|
|
241
|
+
);
|
|
223
242
|
|
|
224
243
|
return {
|
|
225
244
|
id,
|
|
@@ -250,8 +269,6 @@ function resolveArtifact(
|
|
|
250
269
|
* resolved relative to this.
|
|
251
270
|
* @param sdkDir - Absolute path to the SDK output root (`<dist>/sdk`).
|
|
252
271
|
* @param generators - The full `generators:` map from the raw config.
|
|
253
|
-
* @param githubCtx - `{ owner, repo }`, exposed to templates as
|
|
254
|
-
* `{github.owner}`/`{github.repo}`.
|
|
255
272
|
* @param vars - The `vars:` tree from the raw config, exposed as
|
|
256
273
|
* `{vars.*}`.
|
|
257
274
|
* @param rootPublishing - The required root-level `publishing:` config from the
|
|
@@ -263,13 +280,11 @@ function resolveContract(
|
|
|
263
280
|
rootDir: string,
|
|
264
281
|
sdkDir: string,
|
|
265
282
|
generators: RootConfigInput["generators"],
|
|
266
|
-
githubCtx: { owner: string; repo: string },
|
|
267
283
|
vars: RootConfigInput["vars"],
|
|
268
284
|
rootPublishing: PublishingInput,
|
|
269
285
|
): ResolvedContract {
|
|
270
286
|
const context = buildTemplateContext({
|
|
271
287
|
service: input.name,
|
|
272
|
-
github: githubCtx,
|
|
273
288
|
vars,
|
|
274
289
|
});
|
|
275
290
|
|
|
@@ -332,7 +347,6 @@ export function loadConfig(configPath: string): ResolvedConfig {
|
|
|
332
347
|
rootDir,
|
|
333
348
|
sdkDir,
|
|
334
349
|
raw.generators,
|
|
335
|
-
raw.github,
|
|
336
350
|
raw.vars,
|
|
337
351
|
raw.publishing,
|
|
338
352
|
),
|
|
@@ -341,8 +355,13 @@ export function loadConfig(configPath: string): ResolvedConfig {
|
|
|
341
355
|
return {
|
|
342
356
|
configVersion: raw.configVersion,
|
|
343
357
|
rootDir,
|
|
344
|
-
paths: {
|
|
345
|
-
|
|
358
|
+
paths: {
|
|
359
|
+
dist: distDir,
|
|
360
|
+
specs: specsDir,
|
|
361
|
+
docs: docsDir,
|
|
362
|
+
sdk: sdkDir,
|
|
363
|
+
specFormat: raw.paths.specFormat,
|
|
364
|
+
},
|
|
346
365
|
vars: raw.vars,
|
|
347
366
|
docs: raw.docs,
|
|
348
367
|
contracts,
|