@octalmesh/seagull-cli 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 +49 -0
- package/README.md +157 -20
- package/dist/index.mjs +18 -13
- package/package.json +5 -5
- package/src/commands/bundle.test.ts +192 -0
- package/src/commands/bundle.ts +18 -8
- package/src/commands/clean.test.ts +67 -0
- package/src/commands/generate-docs.test.ts +30 -0
- package/src/commands/generate-sdk.test.ts +382 -0
- package/src/commands/generate-sdk.ts +13 -5
- package/src/commands/lint.test.ts +125 -0
- package/src/commands/publish-registries.test.ts +160 -0
- package/src/commands/publish-sdk.test.ts +383 -0
- package/src/commands/publish-sdk.ts +1 -2
- package/src/commands/serve-docs.test.ts +30 -0
- package/src/program.test.ts +282 -0
- package/src/test-support/fixtures.ts +93 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, stat, 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, vi } from "vitest";
|
|
6
|
+
import type { MockInstance } from "vitest";
|
|
7
|
+
|
|
8
|
+
import { makeConfig } from "../test-support/fixtures";
|
|
9
|
+
import { cleanCommand } from "./clean";
|
|
10
|
+
|
|
11
|
+
describe("cleanCommand", () => {
|
|
12
|
+
let dir: string;
|
|
13
|
+
let logSpy: MockInstance;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
dir = await mkdtemp(path.join(tmpdir(), "seagull-clean-"));
|
|
17
|
+
logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
await rm(dir, { recursive: true, force: true });
|
|
22
|
+
logSpy.mockRestore();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("removes the dist directory recursively", async () => {
|
|
26
|
+
const config = makeConfig(dir);
|
|
27
|
+
|
|
28
|
+
await mkdir(path.join(config.paths.dist, "sdk", "auth"), {
|
|
29
|
+
recursive: true,
|
|
30
|
+
});
|
|
31
|
+
await writeFile(
|
|
32
|
+
path.join(config.paths.dist, "sdk", "auth", "file.txt"),
|
|
33
|
+
"hi",
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
await cleanCommand(config);
|
|
37
|
+
|
|
38
|
+
await expect(stat(config.paths.dist)).rejects.toThrow();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("does not throw when dist doesn't exist (force: true)", async () => {
|
|
42
|
+
await expect(cleanCommand(makeConfig(dir))).resolves.toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("logs the cleaned path", async () => {
|
|
46
|
+
const config = makeConfig(dir);
|
|
47
|
+
|
|
48
|
+
await mkdir(config.paths.dist, { recursive: true });
|
|
49
|
+
|
|
50
|
+
await cleanCommand(config);
|
|
51
|
+
|
|
52
|
+
expect(logSpy).toHaveBeenCalledWith(`Cleaned ${config.paths.dist}`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("only removes 'dist', leaving sibling files in rootDir untouched", async () => {
|
|
56
|
+
const config = makeConfig(dir);
|
|
57
|
+
|
|
58
|
+
await mkdir(config.paths.dist, { recursive: true });
|
|
59
|
+
await writeFile(path.join(dir, "seagull.yaml"), "configVersion: 1");
|
|
60
|
+
|
|
61
|
+
await cleanCommand(config);
|
|
62
|
+
|
|
63
|
+
await expect(stat(path.join(dir, "seagull.yaml"))).resolves.toMatchObject(
|
|
64
|
+
{},
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { makeConfig } from "../test-support/fixtures";
|
|
4
|
+
|
|
5
|
+
const generateDocsSiteMock = vi.fn((..._args: unknown[]) => Promise.resolve());
|
|
6
|
+
|
|
7
|
+
vi.mock("@octalmesh/seagull-docs", () => ({
|
|
8
|
+
generateDocsSite: (...a: unknown[]) => generateDocsSiteMock(...a),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const { generateDocsCommand } = await import("./generate-docs");
|
|
12
|
+
|
|
13
|
+
describe("generateDocsCommand", () => {
|
|
14
|
+
it("delegates straight to generateDocsSite with the resolved config", async () => {
|
|
15
|
+
const config = makeConfig("/repo");
|
|
16
|
+
|
|
17
|
+
await generateDocsCommand(config);
|
|
18
|
+
|
|
19
|
+
expect(generateDocsSiteMock).toHaveBeenCalledTimes(1);
|
|
20
|
+
expect(generateDocsSiteMock).toHaveBeenCalledWith(config);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("propagates errors from generateDocsSite", async () => {
|
|
24
|
+
generateDocsSiteMock.mockRejectedValueOnce(new Error("boom"));
|
|
25
|
+
|
|
26
|
+
await expect(generateDocsCommand(makeConfig("/repo"))).rejects.toThrow(
|
|
27
|
+
"boom",
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import type { GenerateContext } from "@octalmesh/seagull-core";
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import type { MockInstance } from "vitest";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
makeArtifact,
|
|
11
|
+
makeConfig,
|
|
12
|
+
makeContract,
|
|
13
|
+
} from "../test-support/fixtures";
|
|
14
|
+
|
|
15
|
+
const syncRedoclyConfigMock = vi.fn((..._args: unknown[]) => Promise.resolve());
|
|
16
|
+
const resolveVersionMock = vi.fn((..._args: unknown[]) => "1.0.0");
|
|
17
|
+
const hashSpecMock = vi.fn((..._args: unknown[]) => "some-hash");
|
|
18
|
+
const renderReadmeMock = vi.fn((..._args: unknown[]) =>
|
|
19
|
+
Promise.resolve("# readme\n"),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const openApiGeneratorPrepare = vi.fn((..._args: unknown[]) =>
|
|
23
|
+
Promise.resolve(),
|
|
24
|
+
);
|
|
25
|
+
const openApiGeneratorGenerate = vi.fn(async (ctx: GenerateContext) => {
|
|
26
|
+
await mkdir(ctx.artifact.outputDir, { recursive: true });
|
|
27
|
+
});
|
|
28
|
+
const openApiTypescriptPrepare = vi.fn((..._args: unknown[]) =>
|
|
29
|
+
Promise.resolve(),
|
|
30
|
+
);
|
|
31
|
+
const openApiTypescriptGenerate = vi.fn(async (ctx: GenerateContext) => {
|
|
32
|
+
await mkdir(ctx.artifact.outputDir, { recursive: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
vi.mock("@octalmesh/seagull-core", async (importOriginal) => {
|
|
36
|
+
const actual =
|
|
37
|
+
await importOriginal<typeof import("@octalmesh/seagull-core")>();
|
|
38
|
+
|
|
39
|
+
class FakeOpenApiGeneratorCli {
|
|
40
|
+
readonly tool = "openapi-generator";
|
|
41
|
+
prepare = openApiGeneratorPrepare;
|
|
42
|
+
generate = openApiGeneratorGenerate;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class FakeOpenApiTypescriptGenerator {
|
|
46
|
+
readonly tool = "openapi-typescript";
|
|
47
|
+
prepare = openApiTypescriptPrepare;
|
|
48
|
+
generate = openApiTypescriptGenerate;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
...actual,
|
|
53
|
+
syncRedoclyConfig: (...a: unknown[]) => syncRedoclyConfigMock(...a),
|
|
54
|
+
resolveVersion: (...a: unknown[]) => resolveVersionMock(...a),
|
|
55
|
+
hashSpec: (...a: unknown[]) => hashSpecMock(...a),
|
|
56
|
+
renderReadme: (...a: unknown[]) => renderReadmeMock(...a),
|
|
57
|
+
OpenApiGeneratorCli: FakeOpenApiGeneratorCli,
|
|
58
|
+
OpenApiTypescriptGenerator: FakeOpenApiTypescriptGenerator,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const { generateSdkCommand } = await import("./generate-sdk");
|
|
63
|
+
|
|
64
|
+
describe("generateSdkCommand", () => {
|
|
65
|
+
let dir: string;
|
|
66
|
+
let logSpy: MockInstance;
|
|
67
|
+
|
|
68
|
+
beforeEach(async () => {
|
|
69
|
+
dir = await mkdtemp(path.join(tmpdir(), "seagull-gensdk-"));
|
|
70
|
+
logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
71
|
+
|
|
72
|
+
syncRedoclyConfigMock.mockClear();
|
|
73
|
+
resolveVersionMock.mockClear().mockReturnValue("1.0.0");
|
|
74
|
+
hashSpecMock.mockClear().mockReturnValue("some-hash");
|
|
75
|
+
renderReadmeMock.mockClear().mockResolvedValue("# readme\n");
|
|
76
|
+
openApiGeneratorPrepare.mockClear();
|
|
77
|
+
openApiGeneratorGenerate.mockClear();
|
|
78
|
+
openApiTypescriptPrepare.mockClear();
|
|
79
|
+
openApiTypescriptGenerate.mockClear();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
afterEach(async () => {
|
|
83
|
+
await rm(dir, { recursive: true, force: true });
|
|
84
|
+
logSpy.mockRestore();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
async function writeSpec(
|
|
88
|
+
specsDir: string,
|
|
89
|
+
name: string,
|
|
90
|
+
raw = '{"info":{"version":"1.0.0"}}',
|
|
91
|
+
): Promise<void> {
|
|
92
|
+
await mkdir(specsDir, { recursive: true });
|
|
93
|
+
await writeFile(path.join(specsDir, `${name}.json`), raw);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
it("syncs redocly.yaml before generating", async () => {
|
|
97
|
+
const config = makeConfig(dir, { contracts: [] });
|
|
98
|
+
|
|
99
|
+
await writeSpec(config.paths.specs, "auth");
|
|
100
|
+
|
|
101
|
+
await generateSdkCommand(config);
|
|
102
|
+
|
|
103
|
+
expect(syncRedoclyConfigMock).toHaveBeenCalledOnce();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("recreates the sdk output directory", async () => {
|
|
107
|
+
const config = makeConfig(dir, { contracts: [] });
|
|
108
|
+
|
|
109
|
+
await mkdir(config.paths.sdk, { recursive: true });
|
|
110
|
+
await writeFile(path.join(config.paths.sdk, "stale.txt"), "x");
|
|
111
|
+
|
|
112
|
+
await generateSdkCommand(config);
|
|
113
|
+
|
|
114
|
+
await expect(
|
|
115
|
+
readFile(path.join(config.paths.sdk, "stale.txt"), "utf8"),
|
|
116
|
+
).rejects.toThrow();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("calls prepare() once per distinct tool present among the artifacts, with only its own entries", async () => {
|
|
120
|
+
const config = makeConfig(dir, {
|
|
121
|
+
contracts: [
|
|
122
|
+
makeContract({
|
|
123
|
+
name: "auth",
|
|
124
|
+
artifacts: [
|
|
125
|
+
makeArtifact({
|
|
126
|
+
id: "ts-client",
|
|
127
|
+
tool: "openapi-generator",
|
|
128
|
+
outputDir: path.join(dir, "dist", "sdk", "auth", "ts-client"),
|
|
129
|
+
}),
|
|
130
|
+
makeArtifact({
|
|
131
|
+
id: "ts-server",
|
|
132
|
+
tool: "openapi-typescript",
|
|
133
|
+
outputDir: path.join(dir, "dist", "sdk", "auth", "ts-server"),
|
|
134
|
+
}),
|
|
135
|
+
],
|
|
136
|
+
}),
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await writeSpec(config.paths.specs, "auth");
|
|
141
|
+
|
|
142
|
+
await generateSdkCommand(config);
|
|
143
|
+
|
|
144
|
+
expect(openApiGeneratorPrepare).toHaveBeenCalledOnce();
|
|
145
|
+
expect(openApiGeneratorPrepare.mock.calls[0]![0]).toMatchObject({
|
|
146
|
+
rootDir: config.rootDir,
|
|
147
|
+
entries: [{ artifact: { id: "ts-client" } }],
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(openApiTypescriptPrepare).toHaveBeenCalledOnce();
|
|
151
|
+
expect(openApiTypescriptPrepare.mock.calls[0]![0]).toMatchObject({
|
|
152
|
+
rootDir: config.rootDir,
|
|
153
|
+
entries: [{ artifact: { id: "ts-server" } }],
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("calls generate() once per artifact, with the right generator dispatched by tool", async () => {
|
|
158
|
+
const config = makeConfig(dir, {
|
|
159
|
+
contracts: [
|
|
160
|
+
makeContract({
|
|
161
|
+
name: "auth",
|
|
162
|
+
artifacts: [
|
|
163
|
+
makeArtifact({
|
|
164
|
+
id: "ts-client",
|
|
165
|
+
tool: "openapi-generator",
|
|
166
|
+
outputDir: path.join(dir, "dist", "sdk", "auth", "ts-client"),
|
|
167
|
+
}),
|
|
168
|
+
],
|
|
169
|
+
}),
|
|
170
|
+
],
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await writeSpec(config.paths.specs, "auth");
|
|
174
|
+
await mkdir(path.dirname(config.contracts[0]!.artifacts[0]!.outputDir), {
|
|
175
|
+
recursive: true,
|
|
176
|
+
});
|
|
177
|
+
await mkdir(config.contracts[0]!.artifacts[0]!.outputDir, {
|
|
178
|
+
recursive: true,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await generateSdkCommand(config);
|
|
182
|
+
|
|
183
|
+
expect(openApiGeneratorGenerate).toHaveBeenCalledOnce();
|
|
184
|
+
expect(openApiTypescriptGenerate).not.toHaveBeenCalled();
|
|
185
|
+
|
|
186
|
+
const ctx = openApiGeneratorGenerate.mock.calls[0]![0];
|
|
187
|
+
|
|
188
|
+
expect(ctx).toMatchObject({
|
|
189
|
+
rootDir: config.rootDir,
|
|
190
|
+
version: "1.0.0",
|
|
191
|
+
specInputPath: path.join(config.paths.specs, "auth.json"),
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("resolves version/hash from the bundled spec once per contract, caching across its artifacts", async () => {
|
|
196
|
+
const config = makeConfig(dir, {
|
|
197
|
+
contracts: [
|
|
198
|
+
makeContract({
|
|
199
|
+
name: "auth",
|
|
200
|
+
artifacts: [
|
|
201
|
+
makeArtifact({
|
|
202
|
+
id: "ts-client",
|
|
203
|
+
tool: "openapi-generator",
|
|
204
|
+
outputDir: path.join(dir, "dist", "sdk", "auth", "ts-client"),
|
|
205
|
+
}),
|
|
206
|
+
makeArtifact({
|
|
207
|
+
id: "ts-server",
|
|
208
|
+
tool: "openapi-typescript",
|
|
209
|
+
outputDir: path.join(dir, "dist", "sdk", "auth", "ts-server"),
|
|
210
|
+
}),
|
|
211
|
+
],
|
|
212
|
+
}),
|
|
213
|
+
],
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await writeSpec(config.paths.specs, "auth", '{"info":{"version":"2.0.0"}}');
|
|
217
|
+
for (const artifact of config.allArtifacts) {
|
|
218
|
+
await mkdir(artifact.artifact.outputDir, { recursive: true });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
await generateSdkCommand(config);
|
|
222
|
+
|
|
223
|
+
expect(resolveVersionMock).toHaveBeenCalledTimes(1);
|
|
224
|
+
expect(hashSpecMock).toHaveBeenCalledTimes(1);
|
|
225
|
+
expect(resolveVersionMock).toHaveBeenCalledWith(
|
|
226
|
+
{ info: { version: "2.0.0" } },
|
|
227
|
+
"auth",
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("writes VERSION, SPEC_HASH, and README.md into each artifact's outputDir", async () => {
|
|
232
|
+
const outputDir = path.join(dir, "dist", "sdk", "auth", "ts-client");
|
|
233
|
+
const config = makeConfig(dir, {
|
|
234
|
+
contracts: [
|
|
235
|
+
makeContract({
|
|
236
|
+
name: "auth",
|
|
237
|
+
artifacts: [makeArtifact({ id: "ts-client", outputDir })],
|
|
238
|
+
}),
|
|
239
|
+
],
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
resolveVersionMock.mockReturnValue("3.4.5");
|
|
243
|
+
hashSpecMock.mockReturnValue("abc123");
|
|
244
|
+
renderReadmeMock.mockResolvedValue("# Hello\n");
|
|
245
|
+
|
|
246
|
+
await writeSpec(config.paths.specs, "auth");
|
|
247
|
+
await mkdir(outputDir, { recursive: true });
|
|
248
|
+
|
|
249
|
+
await generateSdkCommand(config);
|
|
250
|
+
|
|
251
|
+
await expect(
|
|
252
|
+
readFile(path.join(outputDir, "VERSION"), "utf8"),
|
|
253
|
+
).resolves.toBe("3.4.5\n");
|
|
254
|
+
await expect(
|
|
255
|
+
readFile(path.join(outputDir, "SPEC_HASH"), "utf8"),
|
|
256
|
+
).resolves.toBe("abc123\n");
|
|
257
|
+
await expect(
|
|
258
|
+
readFile(path.join(outputDir, "README.md"), "utf8"),
|
|
259
|
+
).resolves.toBe("# Hello\n");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("passes contract/artifact/version/vars through to renderReadme", async () => {
|
|
263
|
+
const outputDir = path.join(dir, "dist", "sdk", "auth", "ts-client");
|
|
264
|
+
const config = makeConfig(dir, {
|
|
265
|
+
vars: { org: "octalmesh" },
|
|
266
|
+
contracts: [
|
|
267
|
+
makeContract({
|
|
268
|
+
name: "auth",
|
|
269
|
+
artifacts: [makeArtifact({ id: "ts-client", outputDir })],
|
|
270
|
+
}),
|
|
271
|
+
],
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
await writeSpec(config.paths.specs, "auth");
|
|
275
|
+
await mkdir(outputDir, { recursive: true });
|
|
276
|
+
|
|
277
|
+
await generateSdkCommand(config);
|
|
278
|
+
|
|
279
|
+
expect(renderReadmeMock).toHaveBeenCalledWith(
|
|
280
|
+
expect.objectContaining({
|
|
281
|
+
version: "1.0.0",
|
|
282
|
+
vars: { org: "octalmesh" },
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("logs a summary listing each contract's resolved version", async () => {
|
|
288
|
+
const outputDir = path.join(dir, "dist", "sdk", "auth", "ts-client");
|
|
289
|
+
const config = makeConfig(dir, {
|
|
290
|
+
contracts: [
|
|
291
|
+
makeContract({
|
|
292
|
+
name: "auth",
|
|
293
|
+
artifacts: [makeArtifact({ id: "ts-client", outputDir })],
|
|
294
|
+
}),
|
|
295
|
+
],
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
resolveVersionMock.mockReturnValue("9.9.9");
|
|
299
|
+
|
|
300
|
+
await writeSpec(config.paths.specs, "auth");
|
|
301
|
+
await mkdir(outputDir, { recursive: true });
|
|
302
|
+
|
|
303
|
+
await generateSdkCommand(config);
|
|
304
|
+
|
|
305
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("auth@9.9.9"));
|
|
306
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("Generated 1"));
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("reads <specs>/<name>.yaml and parses it as YAML when paths.specFormat is 'yaml'", async () => {
|
|
310
|
+
const outputDir = path.join(dir, "dist", "sdk", "auth", "ts-client");
|
|
311
|
+
const config = makeConfig(dir, {
|
|
312
|
+
contracts: [
|
|
313
|
+
makeContract({
|
|
314
|
+
name: "auth",
|
|
315
|
+
artifacts: [makeArtifact({ id: "ts-client", outputDir })],
|
|
316
|
+
}),
|
|
317
|
+
],
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
config.paths.specFormat = ["yaml"];
|
|
321
|
+
|
|
322
|
+
await mkdir(config.paths.specs, { recursive: true });
|
|
323
|
+
await writeFile(
|
|
324
|
+
path.join(config.paths.specs, "auth.yaml"),
|
|
325
|
+
'info:\n version: "7.0.0"\n',
|
|
326
|
+
);
|
|
327
|
+
await mkdir(outputDir, { recursive: true });
|
|
328
|
+
|
|
329
|
+
await generateSdkCommand(config);
|
|
330
|
+
|
|
331
|
+
expect(resolveVersionMock).toHaveBeenCalledWith(
|
|
332
|
+
{ info: { version: "7.0.0" } },
|
|
333
|
+
"auth",
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const ctx = openApiGeneratorGenerate.mock.calls[0]![0];
|
|
337
|
+
|
|
338
|
+
expect(ctx).toMatchObject({
|
|
339
|
+
specInputPath: path.join(config.paths.specs, "auth.yaml"),
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("uses the first entry in paths.specFormat as the primary format when several are configured", async () => {
|
|
344
|
+
const outputDir = path.join(dir, "dist", "sdk", "auth", "ts-client");
|
|
345
|
+
const config = makeConfig(dir, {
|
|
346
|
+
contracts: [
|
|
347
|
+
makeContract({
|
|
348
|
+
name: "auth",
|
|
349
|
+
artifacts: [makeArtifact({ id: "ts-client", outputDir })],
|
|
350
|
+
}),
|
|
351
|
+
],
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
config.paths.specFormat = ["yaml", "json"];
|
|
355
|
+
|
|
356
|
+
await mkdir(config.paths.specs, { recursive: true });
|
|
357
|
+
// Both bundled formats exist on disk (as a real `bundle` run would leave
|
|
358
|
+
// them) - only the primary (first) one, "yaml", should actually get read.
|
|
359
|
+
await writeFile(
|
|
360
|
+
path.join(config.paths.specs, "auth.yaml"),
|
|
361
|
+
'info:\n version: "1.2.3"\n',
|
|
362
|
+
);
|
|
363
|
+
await writeFile(
|
|
364
|
+
path.join(config.paths.specs, "auth.json"),
|
|
365
|
+
'{"info":{"version":"9.9.9"}}',
|
|
366
|
+
);
|
|
367
|
+
await mkdir(outputDir, { recursive: true });
|
|
368
|
+
|
|
369
|
+
await generateSdkCommand(config);
|
|
370
|
+
|
|
371
|
+
expect(resolveVersionMock).toHaveBeenCalledWith(
|
|
372
|
+
{ info: { version: "1.2.3" } },
|
|
373
|
+
"auth",
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
const ctx = openApiGeneratorGenerate.mock.calls[0]![0];
|
|
377
|
+
|
|
378
|
+
expect(ctx).toMatchObject({
|
|
379
|
+
specInputPath: path.join(config.paths.specs, "auth.yaml"),
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
});
|
|
@@ -8,8 +8,11 @@ import {
|
|
|
8
8
|
OpenApiTypescriptGenerator,
|
|
9
9
|
type ResolvedConfig,
|
|
10
10
|
hashSpec,
|
|
11
|
+
parseBundledSpec,
|
|
12
|
+
primarySpecFormat,
|
|
11
13
|
renderReadme,
|
|
12
14
|
resolveVersion,
|
|
15
|
+
specFilename,
|
|
13
16
|
syncRedoclyConfig,
|
|
14
17
|
} from "@octalmesh/seagull-core";
|
|
15
18
|
|
|
@@ -48,6 +51,7 @@ export async function generateSdkCommand(
|
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
const versionCache = new Map<string, VersionInfo>();
|
|
54
|
+
const specFormat = primarySpecFormat(config.paths.specFormat);
|
|
51
55
|
|
|
52
56
|
async function getVersionInfo(contractName: string): Promise<VersionInfo> {
|
|
53
57
|
const cached = versionCache.get(contractName);
|
|
@@ -56,9 +60,12 @@ export async function generateSdkCommand(
|
|
|
56
60
|
return cached;
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
const specPath = path.join(
|
|
63
|
+
const specPath = path.join(
|
|
64
|
+
config.paths.specs,
|
|
65
|
+
specFilename(contractName, specFormat),
|
|
66
|
+
);
|
|
60
67
|
const raw = await readFile(specPath, "utf8");
|
|
61
|
-
const spec =
|
|
68
|
+
const spec = parseBundledSpec(raw, specFormat) as BundledSpec;
|
|
62
69
|
|
|
63
70
|
const info: VersionInfo = {
|
|
64
71
|
version: resolveVersion(spec, contractName),
|
|
@@ -79,8 +86,10 @@ export async function generateSdkCommand(
|
|
|
79
86
|
contract,
|
|
80
87
|
artifact,
|
|
81
88
|
version,
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
specInputPath: path.join(
|
|
90
|
+
config.paths.specs,
|
|
91
|
+
specFilename(contract.name, specFormat),
|
|
92
|
+
),
|
|
84
93
|
});
|
|
85
94
|
|
|
86
95
|
await writeFile(path.join(artifact.outputDir, "VERSION"), `${version}\n`);
|
|
@@ -91,7 +100,6 @@ export async function generateSdkCommand(
|
|
|
91
100
|
contract,
|
|
92
101
|
artifact,
|
|
93
102
|
version,
|
|
94
|
-
github: config.github,
|
|
95
103
|
vars: config.vars,
|
|
96
104
|
}),
|
|
97
105
|
);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { MockInstance } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { makeConfig, makeContract } from "../test-support/fixtures";
|
|
5
|
+
|
|
6
|
+
const runSyncMock = vi.fn((..._args: unknown[]) => 0);
|
|
7
|
+
const resolveBinPathMock = vi.fn(
|
|
8
|
+
(..._args: unknown[]) => "/fake/bin/redocly.js",
|
|
9
|
+
);
|
|
10
|
+
const syncRedoclyConfigMock = vi.fn((..._args: unknown[]) => Promise.resolve());
|
|
11
|
+
|
|
12
|
+
vi.mock("@octalmesh/seagull-core", async (importOriginal) => {
|
|
13
|
+
const actual =
|
|
14
|
+
await importOriginal<typeof import("@octalmesh/seagull-core")>();
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
runSync: (...a: unknown[]) => runSyncMock(...a),
|
|
19
|
+
resolveBinPath: (...a: unknown[]) => resolveBinPathMock(...a),
|
|
20
|
+
syncRedoclyConfig: (...a: unknown[]) => syncRedoclyConfigMock(...a),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const { lintCommand } = await import("./lint");
|
|
25
|
+
|
|
26
|
+
describe("lintCommand", () => {
|
|
27
|
+
let logSpy: MockInstance;
|
|
28
|
+
const originalExitCode = process.exitCode;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
32
|
+
runSyncMock.mockClear().mockReturnValue(0);
|
|
33
|
+
resolveBinPathMock.mockClear();
|
|
34
|
+
syncRedoclyConfigMock.mockClear();
|
|
35
|
+
process.exitCode = undefined;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
logSpy.mockRestore();
|
|
40
|
+
process.exitCode = originalExitCode;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("syncs redocly.yaml before linting", async () => {
|
|
44
|
+
await lintCommand(makeConfig("/repo", { contracts: [] }));
|
|
45
|
+
|
|
46
|
+
expect(syncRedoclyConfigMock).toHaveBeenCalledOnce();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("runs 'redocly lint <entrypoint>' once per contract via runSync", async () => {
|
|
50
|
+
const config = makeConfig("/repo", {
|
|
51
|
+
contracts: [
|
|
52
|
+
makeContract({
|
|
53
|
+
name: "auth",
|
|
54
|
+
entrypoint: "/repo/specs/auth/openapi.yaml",
|
|
55
|
+
}),
|
|
56
|
+
makeContract({
|
|
57
|
+
name: "catalog",
|
|
58
|
+
entrypoint: "/repo/specs/catalog/openapi.yaml",
|
|
59
|
+
}),
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await lintCommand(config);
|
|
64
|
+
|
|
65
|
+
expect(runSyncMock).toHaveBeenCalledTimes(2);
|
|
66
|
+
expect(runSyncMock).toHaveBeenNthCalledWith(
|
|
67
|
+
1,
|
|
68
|
+
"node",
|
|
69
|
+
["/fake/bin/redocly.js", "lint", "/repo/specs/auth/openapi.yaml"],
|
|
70
|
+
config.rootDir,
|
|
71
|
+
);
|
|
72
|
+
expect(runSyncMock).toHaveBeenNthCalledWith(
|
|
73
|
+
2,
|
|
74
|
+
"node",
|
|
75
|
+
["/fake/bin/redocly.js", "lint", "/repo/specs/catalog/openapi.yaml"],
|
|
76
|
+
config.rootDir,
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("leaves process.exitCode unset when every contract lints cleanly", async () => {
|
|
81
|
+
runSyncMock.mockReturnValue(0);
|
|
82
|
+
|
|
83
|
+
await lintCommand(
|
|
84
|
+
makeConfig("/repo", {
|
|
85
|
+
contracts: [
|
|
86
|
+
makeContract({ name: "auth" }),
|
|
87
|
+
makeContract({ name: "catalog" }),
|
|
88
|
+
],
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
expect(process.exitCode).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("sets process.exitCode = 1 if any single contract fails", async () => {
|
|
96
|
+
runSyncMock.mockReturnValueOnce(0).mockReturnValueOnce(1);
|
|
97
|
+
|
|
98
|
+
await lintCommand(
|
|
99
|
+
makeConfig("/repo", {
|
|
100
|
+
contracts: [
|
|
101
|
+
makeContract({ name: "auth" }),
|
|
102
|
+
makeContract({ name: "catalog" }),
|
|
103
|
+
],
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(process.exitCode).toBe(1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("keeps checking every contract even after an earlier one fails", async () => {
|
|
111
|
+
runSyncMock.mockReturnValueOnce(1).mockReturnValueOnce(0);
|
|
112
|
+
|
|
113
|
+
await lintCommand(
|
|
114
|
+
makeConfig("/repo", {
|
|
115
|
+
contracts: [
|
|
116
|
+
makeContract({ name: "auth" }),
|
|
117
|
+
makeContract({ name: "catalog" }),
|
|
118
|
+
],
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(runSyncMock).toHaveBeenCalledTimes(2);
|
|
123
|
+
expect(process.exitCode).toBe(1);
|
|
124
|
+
});
|
|
125
|
+
});
|