@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.
Files changed (64) hide show
  1. package/dist/bin.d.ts +3 -0
  2. package/dist/bin.d.ts.map +1 -0
  3. package/dist/bin.js +13 -0
  4. package/dist/bin.js.map +1 -0
  5. package/dist/commands/build.d.ts +42 -0
  6. package/dist/commands/build.d.ts.map +1 -0
  7. package/dist/commands/build.js +302 -0
  8. package/dist/commands/build.js.map +1 -0
  9. package/dist/commands/dev.d.ts +106 -0
  10. package/dist/commands/dev.d.ts.map +1 -0
  11. package/dist/commands/dev.js +536 -0
  12. package/dist/commands/dev.js.map +1 -0
  13. package/dist/commands/generate.d.ts +65 -0
  14. package/dist/commands/generate.d.ts.map +1 -0
  15. package/dist/commands/generate.js +430 -0
  16. package/dist/commands/generate.js.map +1 -0
  17. package/dist/commands/inspect.d.ts +26 -0
  18. package/dist/commands/inspect.d.ts.map +1 -0
  19. package/dist/commands/inspect.js +579 -0
  20. package/dist/commands/inspect.js.map +1 -0
  21. package/dist/commands/migrate.d.ts +70 -0
  22. package/dist/commands/migrate.d.ts.map +1 -0
  23. package/dist/commands/migrate.js +570 -0
  24. package/dist/commands/migrate.js.map +1 -0
  25. package/dist/commands/scaffold.d.ts +70 -0
  26. package/dist/commands/scaffold.d.ts.map +1 -0
  27. package/dist/commands/scaffold.js +483 -0
  28. package/dist/commands/scaffold.js.map +1 -0
  29. package/dist/commands/test.d.ts +56 -0
  30. package/dist/commands/test.d.ts.map +1 -0
  31. package/dist/commands/test.js +248 -0
  32. package/dist/commands/test.js.map +1 -0
  33. package/dist/config.d.ts +20 -0
  34. package/dist/config.d.ts.map +1 -0
  35. package/dist/config.js +69 -0
  36. package/dist/config.js.map +1 -0
  37. package/dist/index.d.ts +30 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +245 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/logger.d.ts +12 -0
  42. package/dist/logger.d.ts.map +1 -0
  43. package/dist/logger.js +33 -0
  44. package/dist/logger.js.map +1 -0
  45. package/package.json +33 -0
  46. package/src/bin.ts +22 -0
  47. package/src/commands/build.ts +433 -0
  48. package/src/commands/dev.ts +822 -0
  49. package/src/commands/generate.ts +640 -0
  50. package/src/commands/inspect.ts +885 -0
  51. package/src/commands/migrate.ts +800 -0
  52. package/src/commands/scaffold.ts +627 -0
  53. package/src/commands/test.ts +353 -0
  54. package/src/config.ts +93 -0
  55. package/src/dev.test.ts +285 -0
  56. package/src/env.d.ts +86 -0
  57. package/src/generate.test.ts +304 -0
  58. package/src/index.test.ts +217 -0
  59. package/src/index.ts +397 -0
  60. package/src/inspect.test.ts +411 -0
  61. package/src/logger.ts +49 -0
  62. package/src/migrate.test.ts +205 -0
  63. package/src/scaffold.test.ts +256 -0
  64. package/src/test.test.ts +230 -0
@@ -0,0 +1,353 @@
1
+ // @typokit/cli — Test Commands
2
+
3
+ import type { CliLogger } from "../logger.js";
4
+ import type { TypoKitConfig } from "../config.js";
5
+
6
+ export type TestRunner = "jest" | "vitest" | "rstest";
7
+
8
+ export interface TestCommandOptions {
9
+ /** Project root directory */
10
+ rootDir: string;
11
+ /** Resolved configuration */
12
+ config: Required<TypoKitConfig>;
13
+ /** Logger instance */
14
+ logger: CliLogger;
15
+ /** Test subcommand: "all" | "contracts" | "integration" */
16
+ subcommand: string;
17
+ /** CLI flags */
18
+ flags: Record<string, string | boolean>;
19
+ /** Whether verbose mode is enabled */
20
+ verbose: boolean;
21
+ }
22
+
23
+ export interface TestResult {
24
+ /** Whether all tests passed */
25
+ success: boolean;
26
+ /** Which test runner was used */
27
+ runner: TestRunner;
28
+ /** Duration in milliseconds */
29
+ duration: number;
30
+ /** Errors encountered */
31
+ errors: string[];
32
+ /** Whether contract tests were regenerated before running */
33
+ contractsRegenerated: boolean;
34
+ }
35
+
36
+ /**
37
+ * Config file patterns used to auto-detect test runners.
38
+ */
39
+ const RUNNER_CONFIG_PATTERNS: Record<TestRunner, string[]> = {
40
+ jest: [
41
+ "jest.config.js",
42
+ "jest.config.ts",
43
+ "jest.config.mjs",
44
+ "jest.config.cjs",
45
+ ],
46
+ vitest: [
47
+ "vitest.config.js",
48
+ "vitest.config.ts",
49
+ "vitest.config.mjs",
50
+ "vitest.config.cjs",
51
+ ],
52
+ rstest: [
53
+ "rstest.config.js",
54
+ "rstest.config.ts",
55
+ "rstest.config.mjs",
56
+ "rstest.config.cjs",
57
+ ],
58
+ };
59
+
60
+ /**
61
+ * Auto-detect the test runner by checking for config files in the project root.
62
+ * Returns the first match found, or "vitest" as default.
63
+ */
64
+ export async function detectTestRunner(rootDir: string): Promise<TestRunner> {
65
+ const { join } = (await import(/* @vite-ignore */ "path")) as {
66
+ join: (...args: string[]) => string;
67
+ };
68
+ const { existsSync } = (await import(/* @vite-ignore */ "fs")) as {
69
+ existsSync: (p: string) => boolean;
70
+ };
71
+
72
+ for (const [runner, patterns] of Object.entries(RUNNER_CONFIG_PATTERNS) as [
73
+ TestRunner,
74
+ string[],
75
+ ][]) {
76
+ for (const pattern of patterns) {
77
+ if (existsSync(join(rootDir, pattern))) {
78
+ return runner;
79
+ }
80
+ }
81
+ }
82
+
83
+ return "vitest";
84
+ }
85
+
86
+ /**
87
+ * Build the command and arguments for each test runner.
88
+ */
89
+ export function buildRunnerCommand(
90
+ runner: TestRunner,
91
+ subcommand: string,
92
+ rootDir: string,
93
+ verbose: boolean,
94
+ ): { command: string; args: string[] } {
95
+ const args: string[] = [];
96
+
97
+ switch (runner) {
98
+ case "jest": {
99
+ const cmd = "jest";
100
+ if (subcommand === "contracts") {
101
+ args.push("--testPathPattern", "__generated__/.*\\.contract\\.test");
102
+ } else if (subcommand === "integration") {
103
+ args.push("--testPathPattern", "integration");
104
+ }
105
+ if (verbose) {
106
+ args.push("--verbose");
107
+ }
108
+ args.push("--passWithNoTests");
109
+ return { command: cmd, args };
110
+ }
111
+ case "vitest": {
112
+ const cmd = "vitest";
113
+ args.push("run");
114
+ if (subcommand === "contracts") {
115
+ args.push("__generated__/");
116
+ } else if (subcommand === "integration") {
117
+ args.push("--dir", "tests/integration");
118
+ }
119
+ if (verbose) {
120
+ args.push("--reporter", "verbose");
121
+ }
122
+ args.push("--passWithNoTests");
123
+ return { command: cmd, args };
124
+ }
125
+ case "rstest": {
126
+ const cmd = "rstest";
127
+ args.push("run");
128
+ if (subcommand === "contracts") {
129
+ args.push("--testPathPattern", "__generated__/.*\\.contract\\.test");
130
+ } else if (subcommand === "integration") {
131
+ args.push("--testPathPattern", "integration");
132
+ }
133
+ args.push("--passWithNoTests");
134
+ return { command: cmd, args };
135
+ }
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Check whether schemas have changed since last contract test generation.
141
+ * Compares the content hash in .typokit/build-cache.json against current type files.
142
+ */
143
+ export async function schemasChanged(
144
+ rootDir: string,
145
+ config: Required<TypoKitConfig>,
146
+ ): Promise<boolean> {
147
+ const { join } = (await import(/* @vite-ignore */ "path")) as {
148
+ join: (...args: string[]) => string;
149
+ };
150
+ const { existsSync } = (await import(/* @vite-ignore */ "fs")) as {
151
+ existsSync: (p: string) => boolean;
152
+ };
153
+
154
+ const cacheFile = join(rootDir, config.outputDir, "build-cache.json");
155
+
156
+ // If no cache exists, schemas have effectively "changed" (never built)
157
+ if (!existsSync(cacheFile)) {
158
+ return true;
159
+ }
160
+
161
+ // If the generated contracts directory doesn't exist, need to regenerate
162
+ const generatedDir = join(rootDir, "__generated__");
163
+ if (!existsSync(generatedDir)) {
164
+ return true;
165
+ }
166
+
167
+ // Cache exists and generated dir exists — assume up to date
168
+ // A full implementation would compare file hashes, but for now
169
+ // we rely on the build pipeline's cache mechanism
170
+ return false;
171
+ }
172
+
173
+ /**
174
+ * Regenerate contract tests by invoking the generate:tests pipeline.
175
+ */
176
+ async function regenerateContracts(
177
+ options: TestCommandOptions,
178
+ ): Promise<{ success: boolean; errors: string[] }> {
179
+ const { logger, rootDir, config, verbose } = options;
180
+
181
+ logger.step("test", "Regenerating contract tests from schemas...");
182
+
183
+ try {
184
+ const { executeGenerate } = await import("./generate.js");
185
+ const result = await executeGenerate({
186
+ rootDir,
187
+ config,
188
+ logger,
189
+ subcommand: "tests",
190
+ flags: options.flags,
191
+ verbose,
192
+ });
193
+
194
+ if (!result.success) {
195
+ return { success: false, errors: result.errors };
196
+ }
197
+
198
+ logger.success(
199
+ `Contract tests regenerated — ${result.filesWritten.length} files`,
200
+ );
201
+ return { success: true, errors: [] };
202
+ } catch (err: unknown) {
203
+ const message = err instanceof Error ? err.message : String(err);
204
+ return {
205
+ success: false,
206
+ errors: [`Failed to regenerate contracts: ${message}`],
207
+ };
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Execute test commands.
213
+ *
214
+ * Subcommands:
215
+ * "all" — runs all tests
216
+ * "contracts" — runs only contract tests from __generated__/
217
+ * "integration" — runs integration tests with in-memory database
218
+ */
219
+ export async function executeTest(
220
+ options: TestCommandOptions,
221
+ ): Promise<TestResult> {
222
+ const startTime = Date.now();
223
+ const { logger, rootDir, config, flags, verbose } = options;
224
+ const subcommand = options.subcommand || "all";
225
+ const errors: string[] = [];
226
+ let contractsRegenerated = false;
227
+
228
+ // Determine test runner: --runner flag overrides auto-detection
229
+ let runner: TestRunner;
230
+ if (typeof flags["runner"] === "string") {
231
+ const requested = flags["runner"] as string;
232
+ if (
233
+ requested === "jest" ||
234
+ requested === "vitest" ||
235
+ requested === "rstest"
236
+ ) {
237
+ runner = requested;
238
+ logger.step("test", `Using runner: ${runner} (from --runner flag)`);
239
+ } else {
240
+ logger.error(
241
+ `Unknown test runner: ${requested}. Use jest, vitest, or rstest.`,
242
+ );
243
+ return {
244
+ success: false,
245
+ runner: "vitest",
246
+ duration: Date.now() - startTime,
247
+ errors: [`Unknown test runner: ${requested}`],
248
+ contractsRegenerated: false,
249
+ };
250
+ }
251
+ } else {
252
+ runner = await detectTestRunner(rootDir);
253
+ logger.step("test", `Auto-detected runner: ${runner}`);
254
+ }
255
+
256
+ // Regenerate contract tests if schemas have changed
257
+ if (subcommand === "all" || subcommand === "contracts") {
258
+ const changed = await schemasChanged(rootDir, config);
259
+ if (changed) {
260
+ const regenResult = await regenerateContracts(options);
261
+ contractsRegenerated = true;
262
+ if (!regenResult.success) {
263
+ logger.warn(
264
+ "Contract test regeneration failed — running existing tests",
265
+ );
266
+ for (const e of regenResult.errors) {
267
+ errors.push(e);
268
+ }
269
+ }
270
+ }
271
+ }
272
+
273
+ // Build runner command
274
+ const { command, args } = buildRunnerCommand(
275
+ runner,
276
+ subcommand,
277
+ rootDir,
278
+ verbose,
279
+ );
280
+
281
+ logger.step("test", `Running: ${command} ${args.join(" ")}`);
282
+
283
+ // Execute the test runner
284
+ const { spawnSync } = (await import(/* @vite-ignore */ "child_process")) as {
285
+ spawnSync: (
286
+ cmd: string,
287
+ args: string[],
288
+ opts: {
289
+ cwd?: string;
290
+ encoding?: string;
291
+ stdio?: string;
292
+ },
293
+ ) => {
294
+ status: number | null;
295
+ stdout: string;
296
+ stderr: string;
297
+ error?: Error;
298
+ };
299
+ };
300
+
301
+ const result = spawnSync(command, args, {
302
+ cwd: rootDir,
303
+ encoding: "utf-8",
304
+ stdio: "pipe",
305
+ });
306
+
307
+ if (result.error) {
308
+ logger.error(`Failed to start test runner: ${result.error.message}`);
309
+ return {
310
+ success: false,
311
+ runner,
312
+ duration: Date.now() - startTime,
313
+ errors: [`Failed to start ${runner}: ${result.error.message}`],
314
+ contractsRegenerated,
315
+ };
316
+ }
317
+
318
+ // Output test results
319
+ if (result.stdout) {
320
+ // Print test output lines
321
+ for (const line of result.stdout.split("\n")) {
322
+ if (line.trim()) {
323
+ logger.info(line);
324
+ }
325
+ }
326
+ }
327
+
328
+ if (result.stderr && (verbose || result.status !== 0)) {
329
+ for (const line of result.stderr.split("\n")) {
330
+ if (line.trim()) {
331
+ logger.error(line);
332
+ }
333
+ }
334
+ }
335
+
336
+ const success = result.status === 0;
337
+ const duration = Date.now() - startTime;
338
+
339
+ if (success) {
340
+ logger.success(`Tests passed in ${duration}ms`);
341
+ } else {
342
+ logger.error(`Tests failed (exit code: ${result.status})`);
343
+ errors.push(`Test runner exited with code ${result.status}`);
344
+ }
345
+
346
+ return {
347
+ success,
348
+ runner,
349
+ duration,
350
+ errors,
351
+ contractsRegenerated,
352
+ };
353
+ }
package/src/config.ts ADDED
@@ -0,0 +1,93 @@
1
+ // @typokit/cli — Configuration loading
2
+
3
+ export interface TypoKitConfig {
4
+ /** Glob patterns or paths for type definition files */
5
+ typeFiles?: string[];
6
+ /** Glob patterns or paths for route contract files */
7
+ routeFiles?: string[];
8
+ /** Output directory for generated files (default: ".typokit") */
9
+ outputDir?: string;
10
+ /** Output directory for compiled output (default: "dist") */
11
+ distDir?: string;
12
+ /** TypeScript compiler to use: "tsc" | "tsup" | "swc" (default: "tsc") */
13
+ compiler?: "tsc" | "tsup" | "swc";
14
+ /** Additional compiler args */
15
+ compilerArgs?: string[];
16
+ }
17
+
18
+ const DEFAULT_CONFIG: Required<TypoKitConfig> = {
19
+ typeFiles: ["src/**/*.types.ts", "src/**/types.ts"],
20
+ routeFiles: ["src/**/*.routes.ts", "src/**/routes.ts", "src/**/contracts.ts"],
21
+ outputDir: ".typokit",
22
+ distDir: "dist",
23
+ compiler: "tsc",
24
+ compilerArgs: [],
25
+ };
26
+
27
+ /**
28
+ * Load TypoKit configuration from typokit.config.ts or package.json.
29
+ * Searches in the given root directory.
30
+ */
31
+ export async function loadConfig(
32
+ rootDir: string,
33
+ ): Promise<Required<TypoKitConfig>> {
34
+ const { join } = (await import(/* @vite-ignore */ "path")) as {
35
+ join: (...args: string[]) => string;
36
+ };
37
+ const { existsSync, readFileSync } = (await import(
38
+ /* @vite-ignore */ "fs"
39
+ )) as {
40
+ existsSync: (p: string) => boolean;
41
+ readFileSync: (p: string, encoding: string) => string;
42
+ };
43
+
44
+ // Try typokit.config.ts (compiled to .js)
45
+ const configTsPath = join(rootDir, "typokit.config.ts");
46
+ const configJsPath = join(rootDir, "typokit.config.js");
47
+
48
+ if (existsSync(configJsPath)) {
49
+ try {
50
+ const { pathToFileURL } = (await import(/* @vite-ignore */ "url")) as {
51
+ pathToFileURL: (p: string) => { href: string };
52
+ };
53
+ const mod = (await import(pathToFileURL(configJsPath).href)) as {
54
+ default?: TypoKitConfig;
55
+ };
56
+ return mergeConfig(mod.default ?? {});
57
+ } catch {
58
+ // Fall through to package.json
59
+ }
60
+ }
61
+
62
+ if (existsSync(configTsPath)) {
63
+ // Config exists as TS but not compiled — return defaults with a note
64
+ // Users should compile it or use package.json field
65
+ }
66
+
67
+ // Try package.json "typokit" field
68
+ const pkgPath = join(rootDir, "package.json");
69
+ if (existsSync(pkgPath)) {
70
+ try {
71
+ const pkgContent = readFileSync(pkgPath, "utf-8");
72
+ const pkg = JSON.parse(pkgContent) as Record<string, unknown>;
73
+ if (pkg["typokit"] && typeof pkg["typokit"] === "object") {
74
+ return mergeConfig(pkg["typokit"] as TypoKitConfig);
75
+ }
76
+ } catch {
77
+ // Fall through to defaults
78
+ }
79
+ }
80
+
81
+ return { ...DEFAULT_CONFIG };
82
+ }
83
+
84
+ function mergeConfig(partial: TypoKitConfig): Required<TypoKitConfig> {
85
+ return {
86
+ typeFiles: partial.typeFiles ?? DEFAULT_CONFIG.typeFiles,
87
+ routeFiles: partial.routeFiles ?? DEFAULT_CONFIG.routeFiles,
88
+ outputDir: partial.outputDir ?? DEFAULT_CONFIG.outputDir,
89
+ distDir: partial.distDir ?? DEFAULT_CONFIG.distDir,
90
+ compiler: partial.compiler ?? DEFAULT_CONFIG.compiler,
91
+ compilerArgs: partial.compilerArgs ?? DEFAULT_CONFIG.compilerArgs,
92
+ };
93
+ }