@typokit/cli 0.1.4
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/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +13 -0
- package/dist/bin.js.map +1 -0
- package/dist/commands/build.d.ts +42 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +302 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/dev.d.ts +106 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +536 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/generate.d.ts +65 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +430 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/inspect.d.ts +26 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +579 -0
- package/dist/commands/inspect.js.map +1 -0
- package/dist/commands/migrate.d.ts +70 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +570 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/scaffold.d.ts +70 -0
- package/dist/commands/scaffold.d.ts.map +1 -0
- package/dist/commands/scaffold.js +483 -0
- package/dist/commands/scaffold.js.map +1 -0
- package/dist/commands/test.d.ts +56 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +248 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +69 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +245 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +33 -0
- package/dist/logger.js.map +1 -0
- package/package.json +33 -0
- package/src/bin.ts +22 -0
- package/src/commands/build.ts +433 -0
- package/src/commands/dev.ts +822 -0
- package/src/commands/generate.ts +640 -0
- package/src/commands/inspect.ts +885 -0
- package/src/commands/migrate.ts +800 -0
- package/src/commands/scaffold.ts +627 -0
- package/src/commands/test.ts +353 -0
- package/src/config.ts +93 -0
- package/src/dev.test.ts +285 -0
- package/src/env.d.ts +86 -0
- package/src/generate.test.ts +304 -0
- package/src/index.test.ts +217 -0
- package/src/index.ts +397 -0
- package/src/inspect.test.ts +411 -0
- package/src/logger.ts +49 -0
- package/src/migrate.test.ts +205 -0
- package/src/scaffold.test.ts +256 -0
- package/src/test.test.ts +230 -0
package/src/dev.test.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// @typokit/cli — Dev Command Tests
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from "@rstest/core";
|
|
4
|
+
import {
|
|
5
|
+
createDevState,
|
|
6
|
+
detectChangedFiles,
|
|
7
|
+
updateTrackedFiles,
|
|
8
|
+
buildDepGraph,
|
|
9
|
+
getAffectedOutputs,
|
|
10
|
+
isCacheValid,
|
|
11
|
+
updateCache,
|
|
12
|
+
} from "./commands/dev.js";
|
|
13
|
+
import { parseArgs } from "./index.js";
|
|
14
|
+
|
|
15
|
+
// ─── createDevState ─────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
describe("createDevState", () => {
|
|
18
|
+
it("creates initial state with defaults", () => {
|
|
19
|
+
const state = createDevState();
|
|
20
|
+
expect(state.running).toBe(false);
|
|
21
|
+
expect(state.stopWatcher).toBe(null);
|
|
22
|
+
expect(state.trackedFiles.size).toBe(0);
|
|
23
|
+
expect(state.depGraph.size).toBe(0);
|
|
24
|
+
expect(state.astCache.size).toBe(0);
|
|
25
|
+
expect(state.rebuildCount).toBe(0);
|
|
26
|
+
expect(state.lastRebuildMs).toBe(0);
|
|
27
|
+
expect(state.serverPid).toBe(null);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ─── detectChangedFiles ─────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
describe("detectChangedFiles", () => {
|
|
34
|
+
it("detects added files", () => {
|
|
35
|
+
const state = createDevState();
|
|
36
|
+
const currentFiles = [
|
|
37
|
+
{ path: "/src/a.ts", mtime: 1000 },
|
|
38
|
+
{ path: "/src/b.ts", mtime: 2000 },
|
|
39
|
+
];
|
|
40
|
+
const result = detectChangedFiles(state, currentFiles);
|
|
41
|
+
expect(result.added.length).toBe(2);
|
|
42
|
+
expect(result.changed.length).toBe(0);
|
|
43
|
+
expect(result.removed.length).toBe(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("detects changed files by mtime", () => {
|
|
47
|
+
const state = createDevState();
|
|
48
|
+
state.trackedFiles.set("/src/a.ts", { path: "/src/a.ts", mtime: 1000 });
|
|
49
|
+
state.trackedFiles.set("/src/b.ts", { path: "/src/b.ts", mtime: 2000 });
|
|
50
|
+
|
|
51
|
+
const currentFiles = [
|
|
52
|
+
{ path: "/src/a.ts", mtime: 1500 }, // changed
|
|
53
|
+
{ path: "/src/b.ts", mtime: 2000 }, // unchanged
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const result = detectChangedFiles(state, currentFiles);
|
|
57
|
+
expect(result.changed.length).toBe(1);
|
|
58
|
+
expect(result.changed[0].path).toBe("/src/a.ts");
|
|
59
|
+
expect(result.added.length).toBe(0);
|
|
60
|
+
expect(result.removed.length).toBe(0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("detects removed files", () => {
|
|
64
|
+
const state = createDevState();
|
|
65
|
+
state.trackedFiles.set("/src/a.ts", { path: "/src/a.ts", mtime: 1000 });
|
|
66
|
+
state.trackedFiles.set("/src/b.ts", { path: "/src/b.ts", mtime: 2000 });
|
|
67
|
+
|
|
68
|
+
const currentFiles = [{ path: "/src/a.ts", mtime: 1000 }];
|
|
69
|
+
|
|
70
|
+
const result = detectChangedFiles(state, currentFiles);
|
|
71
|
+
expect(result.removed.length).toBe(1);
|
|
72
|
+
expect(result.removed[0]).toBe("/src/b.ts");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("handles mixed changes", () => {
|
|
76
|
+
const state = createDevState();
|
|
77
|
+
state.trackedFiles.set("/src/a.ts", { path: "/src/a.ts", mtime: 1000 });
|
|
78
|
+
state.trackedFiles.set("/src/b.ts", { path: "/src/b.ts", mtime: 2000 });
|
|
79
|
+
|
|
80
|
+
const currentFiles = [
|
|
81
|
+
{ path: "/src/a.ts", mtime: 1500 }, // changed
|
|
82
|
+
{ path: "/src/c.ts", mtime: 3000 }, // added
|
|
83
|
+
// /src/b.ts removed
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const result = detectChangedFiles(state, currentFiles);
|
|
87
|
+
expect(result.changed.length).toBe(1);
|
|
88
|
+
expect(result.added.length).toBe(1);
|
|
89
|
+
expect(result.removed.length).toBe(1);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ─── updateTrackedFiles ─────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
describe("updateTrackedFiles", () => {
|
|
96
|
+
it("replaces all tracked files", () => {
|
|
97
|
+
const state = createDevState();
|
|
98
|
+
state.trackedFiles.set("/old.ts", { path: "/old.ts", mtime: 100 });
|
|
99
|
+
|
|
100
|
+
updateTrackedFiles(state, [
|
|
101
|
+
{ path: "/src/a.ts", mtime: 1000 },
|
|
102
|
+
{ path: "/src/b.ts", mtime: 2000 },
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
expect(state.trackedFiles.size).toBe(2);
|
|
106
|
+
expect(state.trackedFiles.has("/old.ts")).toBe(false);
|
|
107
|
+
expect(state.trackedFiles.has("/src/a.ts")).toBe(true);
|
|
108
|
+
expect(state.trackedFiles.has("/src/b.ts")).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ─── buildDepGraph ──────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
describe("buildDepGraph", () => {
|
|
115
|
+
it("maps type files to validator and schema outputs", () => {
|
|
116
|
+
const graph = buildDepGraph(["/src/types.ts"], []);
|
|
117
|
+
const entry = graph.get("/src/types.ts");
|
|
118
|
+
expect(entry).toBeDefined();
|
|
119
|
+
expect(entry!.category).toBe("type");
|
|
120
|
+
expect(entry!.affectedOutputs).toContain("validators");
|
|
121
|
+
expect(entry!.affectedOutputs).toContain("schemas/openapi.json");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("maps route files to router, schema, and test outputs", () => {
|
|
125
|
+
const graph = buildDepGraph([], ["/src/routes.ts"]);
|
|
126
|
+
const entry = graph.get("/src/routes.ts");
|
|
127
|
+
expect(entry).toBeDefined();
|
|
128
|
+
expect(entry!.category).toBe("route");
|
|
129
|
+
expect(entry!.affectedOutputs).toContain("routes/compiled-router.ts");
|
|
130
|
+
expect(entry!.affectedOutputs).toContain("schemas/openapi.json");
|
|
131
|
+
expect(entry!.affectedOutputs).toContain("tests/contract.test.ts");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("handles both type and route files", () => {
|
|
135
|
+
const graph = buildDepGraph(["/src/types.ts"], ["/src/routes.ts"]);
|
|
136
|
+
expect(graph.size).toBe(2);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ─── getAffectedOutputs ────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
describe("getAffectedOutputs", () => {
|
|
143
|
+
it("returns affected outputs for changed type file", () => {
|
|
144
|
+
const graph = buildDepGraph(["/src/types.ts"], ["/src/routes.ts"]);
|
|
145
|
+
const affected = getAffectedOutputs(graph, ["/src/types.ts"]);
|
|
146
|
+
expect(affected.has("validators")).toBe(true);
|
|
147
|
+
expect(affected.has("schemas/openapi.json")).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("returns affected outputs for changed route file", () => {
|
|
151
|
+
const graph = buildDepGraph(["/src/types.ts"], ["/src/routes.ts"]);
|
|
152
|
+
const affected = getAffectedOutputs(graph, ["/src/routes.ts"]);
|
|
153
|
+
expect(affected.has("routes/compiled-router.ts")).toBe(true);
|
|
154
|
+
expect(affected.has("tests/contract.test.ts")).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("merges outputs for multiple changed files", () => {
|
|
158
|
+
const graph = buildDepGraph(["/src/types.ts"], ["/src/routes.ts"]);
|
|
159
|
+
const affected = getAffectedOutputs(graph, [
|
|
160
|
+
"/src/types.ts",
|
|
161
|
+
"/src/routes.ts",
|
|
162
|
+
]);
|
|
163
|
+
expect(affected.has("validators")).toBe(true);
|
|
164
|
+
expect(affected.has("routes/compiled-router.ts")).toBe(true);
|
|
165
|
+
expect(affected.has("schemas/openapi.json")).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("returns empty set for unknown files", () => {
|
|
169
|
+
const graph = buildDepGraph(["/src/types.ts"], []);
|
|
170
|
+
const affected = getAffectedOutputs(graph, ["/src/unknown.ts"]);
|
|
171
|
+
expect(affected.size).toBe(0);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ─── AST Cache ──────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
describe("isCacheValid", () => {
|
|
178
|
+
it("returns false for uncached file", () => {
|
|
179
|
+
const cache = new Map();
|
|
180
|
+
expect(isCacheValid(cache, "/src/a.ts", 1000)).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("returns true when mtime matches", () => {
|
|
184
|
+
const cache = new Map();
|
|
185
|
+
updateCache(cache, "/src/a.ts", 1000);
|
|
186
|
+
expect(isCacheValid(cache, "/src/a.ts", 1000)).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("returns false when mtime differs", () => {
|
|
190
|
+
const cache = new Map();
|
|
191
|
+
updateCache(cache, "/src/a.ts", 1000);
|
|
192
|
+
expect(isCacheValid(cache, "/src/a.ts", 2000)).toBe(false);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("updateCache", () => {
|
|
197
|
+
it("stores cache entry with mtime and hash", () => {
|
|
198
|
+
const cache = new Map();
|
|
199
|
+
updateCache(cache, "/src/a.ts", 1000);
|
|
200
|
+
const entry = cache.get("/src/a.ts");
|
|
201
|
+
expect(entry).toBeDefined();
|
|
202
|
+
expect(entry.mtime).toBe(1000);
|
|
203
|
+
expect(typeof entry.hash).toBe("string");
|
|
204
|
+
expect(entry.hash.length).toBeGreaterThan(0);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("overwrites existing cache entry", () => {
|
|
208
|
+
const cache = new Map();
|
|
209
|
+
updateCache(cache, "/src/a.ts", 1000);
|
|
210
|
+
updateCache(cache, "/src/a.ts", 2000);
|
|
211
|
+
const entry = cache.get("/src/a.ts");
|
|
212
|
+
expect(entry.mtime).toBe(2000);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ─── parseArgs: dev command ─────────────────────────────────
|
|
217
|
+
|
|
218
|
+
describe("parseArgs dev command", () => {
|
|
219
|
+
it("parses dev command", () => {
|
|
220
|
+
const result = parseArgs(["node", "typokit", "dev"]);
|
|
221
|
+
expect(result.command).toBe("dev");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("parses --debug-port flag", () => {
|
|
225
|
+
const result = parseArgs([
|
|
226
|
+
"node",
|
|
227
|
+
"typokit",
|
|
228
|
+
"dev",
|
|
229
|
+
"--debug-port",
|
|
230
|
+
"9900",
|
|
231
|
+
]);
|
|
232
|
+
expect(result.command).toBe("dev");
|
|
233
|
+
expect(result.flags["debug-port"]).toBe("9900");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("parses dev with --verbose", () => {
|
|
237
|
+
const result = parseArgs(["node", "typokit", "dev", "--verbose"]);
|
|
238
|
+
expect(result.command).toBe("dev");
|
|
239
|
+
expect(result.flags["verbose"]).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("parses dev with multiple flags", () => {
|
|
243
|
+
const result = parseArgs([
|
|
244
|
+
"node",
|
|
245
|
+
"typokit",
|
|
246
|
+
"dev",
|
|
247
|
+
"--verbose",
|
|
248
|
+
"--debug-port",
|
|
249
|
+
"8080",
|
|
250
|
+
"--root",
|
|
251
|
+
"/my/project",
|
|
252
|
+
]);
|
|
253
|
+
expect(result.command).toBe("dev");
|
|
254
|
+
expect(result.flags["verbose"]).toBe(true);
|
|
255
|
+
expect(result.flags["debug-port"]).toBe("8080");
|
|
256
|
+
expect(result.flags["root"]).toBe("/my/project");
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// ─── DevServerState ─────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
describe("DevServerState lifecycle", () => {
|
|
263
|
+
it("tracks rebuild count", () => {
|
|
264
|
+
const state = createDevState();
|
|
265
|
+
expect(state.rebuildCount).toBe(0);
|
|
266
|
+
state.rebuildCount++;
|
|
267
|
+
expect(state.rebuildCount).toBe(1);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("tracks last rebuild duration", () => {
|
|
271
|
+
const state = createDevState();
|
|
272
|
+
expect(state.lastRebuildMs).toBe(0);
|
|
273
|
+
state.lastRebuildMs = 42;
|
|
274
|
+
expect(state.lastRebuildMs).toBe(42);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("running flag controls watcher lifecycle", () => {
|
|
278
|
+
const state = createDevState();
|
|
279
|
+
expect(state.running).toBe(false);
|
|
280
|
+
state.running = true;
|
|
281
|
+
expect(state.running).toBe(true);
|
|
282
|
+
state.running = false;
|
|
283
|
+
expect(state.running).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
});
|
package/src/env.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Minimal type declarations for Node.js APIs used by @typokit/cli
|
|
2
|
+
// Avoids adding @types/node as a dependency
|
|
3
|
+
|
|
4
|
+
declare module "module" {
|
|
5
|
+
export function createRequire(url: string | URL): (id: string) => unknown;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
declare module "path" {
|
|
9
|
+
export function join(...paths: string[]): string;
|
|
10
|
+
export function dirname(p: string): string;
|
|
11
|
+
export function resolve(...paths: string[]): string;
|
|
12
|
+
export function relative(from: string, to: string): string;
|
|
13
|
+
export function basename(p: string, ext?: string): string;
|
|
14
|
+
export function extname(p: string): string;
|
|
15
|
+
export function isAbsolute(p: string): boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
declare module "fs" {
|
|
19
|
+
export function existsSync(path: string): boolean;
|
|
20
|
+
export function mkdirSync(
|
|
21
|
+
path: string,
|
|
22
|
+
options?: { recursive?: boolean },
|
|
23
|
+
): void;
|
|
24
|
+
export function readFileSync(path: string, encoding: string): string;
|
|
25
|
+
export function writeFileSync(
|
|
26
|
+
path: string,
|
|
27
|
+
data: string,
|
|
28
|
+
encoding?: string,
|
|
29
|
+
): void;
|
|
30
|
+
export function readdirSync(
|
|
31
|
+
path: string,
|
|
32
|
+
options?: { recursive?: boolean; withFileTypes?: boolean },
|
|
33
|
+
): string[];
|
|
34
|
+
export function statSync(path: string): {
|
|
35
|
+
isDirectory(): boolean;
|
|
36
|
+
isFile(): boolean;
|
|
37
|
+
mtimeMs: number;
|
|
38
|
+
};
|
|
39
|
+
export function rmSync(
|
|
40
|
+
path: string,
|
|
41
|
+
options?: { recursive?: boolean; force?: boolean },
|
|
42
|
+
): void;
|
|
43
|
+
export function watch(
|
|
44
|
+
path: string,
|
|
45
|
+
options: { recursive?: boolean },
|
|
46
|
+
listener: (event: string, filename: string | null) => void,
|
|
47
|
+
): { close(): void };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
declare module "child_process" {
|
|
51
|
+
interface SpawnSyncResult {
|
|
52
|
+
status: number | null;
|
|
53
|
+
stdout: string;
|
|
54
|
+
stderr: string;
|
|
55
|
+
error?: Error;
|
|
56
|
+
}
|
|
57
|
+
export function spawnSync(
|
|
58
|
+
command: string,
|
|
59
|
+
args: string[],
|
|
60
|
+
options?: { cwd?: string; encoding?: string; stdio?: string | string[] },
|
|
61
|
+
): SpawnSyncResult;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
declare module "url" {
|
|
65
|
+
export function pathToFileURL(path: string): URL;
|
|
66
|
+
export function fileURLToPath(url: string | URL): string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface ImportMeta {
|
|
70
|
+
url: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
declare module "http" {
|
|
74
|
+
interface IncomingMessage {
|
|
75
|
+
statusCode?: number;
|
|
76
|
+
on(event: string, cb: (data?: unknown) => void): void;
|
|
77
|
+
setEncoding(enc: string): void;
|
|
78
|
+
}
|
|
79
|
+
interface ClientRequest {
|
|
80
|
+
on(event: string, cb: (err: Error) => void): void;
|
|
81
|
+
}
|
|
82
|
+
export function get(
|
|
83
|
+
url: string,
|
|
84
|
+
cb: (res: IncomingMessage) => void,
|
|
85
|
+
): ClientRequest;
|
|
86
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// @typokit/cli — Generate Command Tests
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from "@rstest/core";
|
|
4
|
+
import { createLogger } from "./index.js";
|
|
5
|
+
import type {
|
|
6
|
+
GenerateCommandOptions,
|
|
7
|
+
GenerateResult,
|
|
8
|
+
} from "./commands/generate.js";
|
|
9
|
+
|
|
10
|
+
// ─── executeGenerate dispatcher ─────────────────────────────
|
|
11
|
+
|
|
12
|
+
describe("executeGenerate", () => {
|
|
13
|
+
it("returns error for unknown subcommand", async () => {
|
|
14
|
+
const { executeGenerate } = await import("./commands/generate.js");
|
|
15
|
+
const logger = createLogger({ verbose: false });
|
|
16
|
+
|
|
17
|
+
const result = await executeGenerate({
|
|
18
|
+
rootDir: "/nonexistent",
|
|
19
|
+
config: {
|
|
20
|
+
typeFiles: [],
|
|
21
|
+
routeFiles: [],
|
|
22
|
+
outputDir: ".typokit",
|
|
23
|
+
distDir: "dist",
|
|
24
|
+
compiler: "tsc",
|
|
25
|
+
compilerArgs: [],
|
|
26
|
+
},
|
|
27
|
+
logger,
|
|
28
|
+
subcommand: "nonexistent",
|
|
29
|
+
flags: {},
|
|
30
|
+
verbose: false,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(result.success).toBe(false);
|
|
34
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
35
|
+
expect(result.errors[0]).toContain("Unknown generate subcommand");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("dispatches to generate:db", async () => {
|
|
39
|
+
const { executeGenerate } = await import("./commands/generate.js");
|
|
40
|
+
const logger = createLogger({ verbose: false });
|
|
41
|
+
|
|
42
|
+
const result = await executeGenerate({
|
|
43
|
+
rootDir: "/nonexistent/empty",
|
|
44
|
+
config: {
|
|
45
|
+
typeFiles: [],
|
|
46
|
+
routeFiles: [],
|
|
47
|
+
outputDir: ".typokit",
|
|
48
|
+
distDir: "dist",
|
|
49
|
+
compiler: "tsc",
|
|
50
|
+
compilerArgs: [],
|
|
51
|
+
},
|
|
52
|
+
logger,
|
|
53
|
+
subcommand: "db",
|
|
54
|
+
flags: {},
|
|
55
|
+
verbose: false,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// With no type files, should succeed with no output
|
|
59
|
+
expect(result.success).toBe(true);
|
|
60
|
+
expect(result.filesWritten).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("dispatches to generate:client", async () => {
|
|
64
|
+
const { executeGenerate } = await import("./commands/generate.js");
|
|
65
|
+
const logger = createLogger({ verbose: false });
|
|
66
|
+
|
|
67
|
+
const result = await executeGenerate({
|
|
68
|
+
rootDir: "/nonexistent/empty",
|
|
69
|
+
config: {
|
|
70
|
+
typeFiles: [],
|
|
71
|
+
routeFiles: [],
|
|
72
|
+
outputDir: ".typokit",
|
|
73
|
+
distDir: "dist",
|
|
74
|
+
compiler: "tsc",
|
|
75
|
+
compilerArgs: [],
|
|
76
|
+
},
|
|
77
|
+
logger,
|
|
78
|
+
subcommand: "client",
|
|
79
|
+
flags: {},
|
|
80
|
+
verbose: false,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(result.success).toBe(true);
|
|
84
|
+
expect(result.filesWritten).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("dispatches to generate:openapi", async () => {
|
|
88
|
+
const { executeGenerate } = await import("./commands/generate.js");
|
|
89
|
+
const logger = createLogger({ verbose: false });
|
|
90
|
+
|
|
91
|
+
const result = await executeGenerate({
|
|
92
|
+
rootDir: "/nonexistent/empty",
|
|
93
|
+
config: {
|
|
94
|
+
typeFiles: [],
|
|
95
|
+
routeFiles: [],
|
|
96
|
+
outputDir: ".typokit",
|
|
97
|
+
distDir: "dist",
|
|
98
|
+
compiler: "tsc",
|
|
99
|
+
compilerArgs: [],
|
|
100
|
+
},
|
|
101
|
+
logger,
|
|
102
|
+
subcommand: "openapi",
|
|
103
|
+
flags: {},
|
|
104
|
+
verbose: false,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(result.success).toBe(true);
|
|
108
|
+
expect(result.filesWritten).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("dispatches to generate:tests", async () => {
|
|
112
|
+
const { executeGenerate } = await import("./commands/generate.js");
|
|
113
|
+
const logger = createLogger({ verbose: false });
|
|
114
|
+
|
|
115
|
+
const result = await executeGenerate({
|
|
116
|
+
rootDir: "/nonexistent/empty",
|
|
117
|
+
config: {
|
|
118
|
+
typeFiles: [],
|
|
119
|
+
routeFiles: [],
|
|
120
|
+
outputDir: ".typokit",
|
|
121
|
+
distDir: "dist",
|
|
122
|
+
compiler: "tsc",
|
|
123
|
+
compilerArgs: [],
|
|
124
|
+
},
|
|
125
|
+
logger,
|
|
126
|
+
subcommand: "tests",
|
|
127
|
+
flags: {},
|
|
128
|
+
verbose: false,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(result.success).toBe(true);
|
|
132
|
+
expect(result.filesWritten).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ─── generateClientCode ──────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
describe("generateClientCode", () => {
|
|
139
|
+
it("generates a valid TypeScript client module", async () => {
|
|
140
|
+
const { generateClientCode } = await import("./commands/generate.js");
|
|
141
|
+
|
|
142
|
+
const code = generateClientCode("// compiled routes here");
|
|
143
|
+
expect(code).toContain("export interface ClientOptions");
|
|
144
|
+
expect(code).toContain("export interface RequestOptions");
|
|
145
|
+
expect(code).toContain("export function createClient");
|
|
146
|
+
expect(code).toContain("baseUrl");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("includes auto-generated header comment", async () => {
|
|
150
|
+
const { generateClientCode } = await import("./commands/generate.js");
|
|
151
|
+
|
|
152
|
+
const code = generateClientCode("// routes");
|
|
153
|
+
expect(code).toContain("Auto-generated by @typokit/cli");
|
|
154
|
+
expect(code).toContain("do not edit manually");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("includes HTTP methods (get, post, put, patch, delete)", async () => {
|
|
158
|
+
const { generateClientCode } = await import("./commands/generate.js");
|
|
159
|
+
|
|
160
|
+
const code = generateClientCode("// routes");
|
|
161
|
+
expect(code).toContain('"GET"');
|
|
162
|
+
expect(code).toContain('"POST"');
|
|
163
|
+
expect(code).toContain('"PUT"');
|
|
164
|
+
expect(code).toContain('"PATCH"');
|
|
165
|
+
expect(code).toContain('"DELETE"');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("includes query string serialization", async () => {
|
|
169
|
+
const { generateClientCode } = await import("./commands/generate.js");
|
|
170
|
+
|
|
171
|
+
const code = generateClientCode("// routes");
|
|
172
|
+
expect(code).toContain("encodeURIComponent");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("includes param substitution", async () => {
|
|
176
|
+
const { generateClientCode } = await import("./commands/generate.js");
|
|
177
|
+
|
|
178
|
+
const code = generateClientCode("// routes");
|
|
179
|
+
expect(code).toContain("params");
|
|
180
|
+
expect(code).toContain("replace");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("includes compiled route reference", async () => {
|
|
184
|
+
const { generateClientCode } = await import("./commands/generate.js");
|
|
185
|
+
|
|
186
|
+
const code = generateClientCode("// my-compiled-routes");
|
|
187
|
+
expect(code).toContain("my-compiled-routes");
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ─── GenerateResult interface shape ──────────────────────────
|
|
192
|
+
|
|
193
|
+
describe("GenerateResult", () => {
|
|
194
|
+
it("has expected shape", () => {
|
|
195
|
+
const result: GenerateResult = {
|
|
196
|
+
success: true,
|
|
197
|
+
filesWritten: ["/some/path/file.ts"],
|
|
198
|
+
duration: 42,
|
|
199
|
+
errors: [],
|
|
200
|
+
};
|
|
201
|
+
expect(result.success).toBe(true);
|
|
202
|
+
expect(result.filesWritten.length).toBe(1);
|
|
203
|
+
expect(result.duration).toBe(42);
|
|
204
|
+
expect(result.errors.length).toBe(0);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("works with errors", () => {
|
|
208
|
+
const result: GenerateResult = {
|
|
209
|
+
success: false,
|
|
210
|
+
filesWritten: [],
|
|
211
|
+
duration: 10,
|
|
212
|
+
errors: ["Something went wrong"],
|
|
213
|
+
};
|
|
214
|
+
expect(result.success).toBe(false);
|
|
215
|
+
expect(result.errors[0]).toBe("Something went wrong");
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ─── GenerateCommandOptions interface shape ──────────────────
|
|
220
|
+
|
|
221
|
+
describe("GenerateCommandOptions", () => {
|
|
222
|
+
it("accepts all required fields", () => {
|
|
223
|
+
const opts: GenerateCommandOptions = {
|
|
224
|
+
rootDir: "/my/project",
|
|
225
|
+
config: {
|
|
226
|
+
typeFiles: ["src/**/*.types.ts"],
|
|
227
|
+
routeFiles: ["src/**/*.routes.ts"],
|
|
228
|
+
outputDir: ".typokit",
|
|
229
|
+
distDir: "dist",
|
|
230
|
+
compiler: "tsc",
|
|
231
|
+
compilerArgs: [],
|
|
232
|
+
},
|
|
233
|
+
logger: createLogger({ verbose: false }),
|
|
234
|
+
subcommand: "openapi",
|
|
235
|
+
flags: { output: "./dist/openapi.json" },
|
|
236
|
+
verbose: false,
|
|
237
|
+
};
|
|
238
|
+
expect(opts.subcommand).toBe("openapi");
|
|
239
|
+
expect(opts.flags["output"]).toBe("./dist/openapi.json");
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// ─── CLI integration: parseArgs for generate commands ────────
|
|
244
|
+
|
|
245
|
+
describe("parseArgs with generate commands", () => {
|
|
246
|
+
it("parses generate:openapi command", async () => {
|
|
247
|
+
const { parseArgs } = await import("./index.js");
|
|
248
|
+
const result = parseArgs([
|
|
249
|
+
"node",
|
|
250
|
+
"typokit",
|
|
251
|
+
"generate:openapi",
|
|
252
|
+
"--output",
|
|
253
|
+
"./dist/api.json",
|
|
254
|
+
]);
|
|
255
|
+
expect(result.command).toBe("generate:openapi");
|
|
256
|
+
expect(result.flags["output"]).toBe("./dist/api.json");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("parses generate:db command", async () => {
|
|
260
|
+
const { parseArgs } = await import("./index.js");
|
|
261
|
+
const result = parseArgs(["node", "typokit", "generate:db", "--verbose"]);
|
|
262
|
+
expect(result.command).toBe("generate:db");
|
|
263
|
+
expect(result.flags["verbose"]).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("parses generate:client command", async () => {
|
|
267
|
+
const { parseArgs } = await import("./index.js");
|
|
268
|
+
const result = parseArgs(["node", "typokit", "generate:client"]);
|
|
269
|
+
expect(result.command).toBe("generate:client");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("parses generate:tests command", async () => {
|
|
273
|
+
const { parseArgs } = await import("./index.js");
|
|
274
|
+
const result = parseArgs([
|
|
275
|
+
"node",
|
|
276
|
+
"typokit",
|
|
277
|
+
"generate:tests",
|
|
278
|
+
"--root",
|
|
279
|
+
"/my/project",
|
|
280
|
+
]);
|
|
281
|
+
expect(result.command).toBe("generate:tests");
|
|
282
|
+
expect(result.flags["root"]).toBe("/my/project");
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// ─── resolveFilePatterns ─────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
describe("resolveGenerateFilePatterns", () => {
|
|
289
|
+
it("returns empty array for nonexistent dir patterns", async () => {
|
|
290
|
+
const { resolveGenerateFilePatterns } =
|
|
291
|
+
await import("./commands/generate.js");
|
|
292
|
+
const files = await resolveGenerateFilePatterns("/nonexistent/dir", [
|
|
293
|
+
"src/**/*.ts",
|
|
294
|
+
]);
|
|
295
|
+
expect(files).toEqual([]);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("returns empty for empty patterns", async () => {
|
|
299
|
+
const { resolveGenerateFilePatterns } =
|
|
300
|
+
await import("./commands/generate.js");
|
|
301
|
+
const files = await resolveGenerateFilePatterns("/nonexistent", []);
|
|
302
|
+
expect(files).toEqual([]);
|
|
303
|
+
});
|
|
304
|
+
});
|