@chkit/plugin-codegen 0.1.0-beta.7 → 0.1.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,852 +1,4 @@
1
- import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
- import { dirname, join, relative, resolve } from 'node:path';
3
- import { canonicalizeDefinitions, loadSchemaDefinitions, } from '@chkit/core';
4
- const DEFAULT_OPTIONS = {
5
- outFile: './src/generated/chkit-types.ts',
6
- emitZod: false,
7
- tableNameStyle: 'pascal',
8
- bigintMode: 'string',
9
- includeViews: false,
10
- runOnGenerate: true,
11
- failOnUnsupportedType: true,
12
- emitIngest: false,
13
- ingestOutFile: './src/generated/chkit-ingest.ts',
14
- };
15
- const LARGE_INTEGER_TYPES = new Set([
16
- 'Int64',
17
- 'UInt64',
18
- 'Int128',
19
- 'UInt128',
20
- 'Int256',
21
- 'UInt256',
22
- ]);
23
- const NUMBER_TYPES = new Set([
24
- 'Int8',
25
- 'Int16',
26
- 'Int32',
27
- 'UInt8',
28
- 'UInt16',
29
- 'UInt32',
30
- 'Float32',
31
- 'Float64',
32
- 'BFloat16',
33
- ]);
34
- const STRING_TYPES = new Set([
35
- 'String',
36
- 'FixedString',
37
- 'Date',
38
- 'Date32',
39
- 'DateTime',
40
- 'DateTime64',
41
- 'UUID',
42
- 'IPv4',
43
- 'IPv6',
44
- 'Enum',
45
- 'Enum8',
46
- 'Enum16',
47
- 'Decimal',
48
- 'Decimal32',
49
- 'Decimal64',
50
- 'Decimal128',
51
- 'Decimal256',
52
- ]);
53
- const BOOLEAN_TYPES = new Set(['Bool', 'Boolean']);
54
- class CodegenConfigError extends Error {
55
- constructor(message) {
56
- super(message);
57
- this.name = 'CodegenConfigError';
58
- }
59
- }
60
- class UnsupportedTypeError extends Error {
61
- path;
62
- sourceType;
63
- constructor(path, sourceType) {
64
- super(`Unsupported column type "${sourceType}" at ${path}. Set failOnUnsupportedType=false to emit unknown.`);
65
- this.name = 'UnsupportedTypeError';
66
- this.path = path;
67
- this.sourceType = sourceType;
68
- }
69
- }
70
- function parseBooleanOption(options, key) {
71
- const value = options[key];
72
- if (value === undefined)
73
- return undefined;
74
- if (typeof value === 'boolean')
75
- return value;
76
- throw new CodegenConfigError(`Invalid plugin option "${key}". Expected boolean.`);
77
- }
78
- function parseStringOption(options, key) {
79
- const value = options[key];
80
- if (value === undefined)
81
- return undefined;
82
- if (typeof value === 'string' && value.length > 0)
83
- return value;
84
- throw new CodegenConfigError(`Invalid plugin option "${key}". Expected non-empty string.`);
85
- }
86
- function normalizeRuntimeOptions(options) {
87
- const rawTableNameStyle = options.tableNameStyle;
88
- if (rawTableNameStyle !== undefined &&
89
- rawTableNameStyle !== 'pascal' &&
90
- rawTableNameStyle !== 'camel' &&
91
- rawTableNameStyle !== 'raw') {
92
- throw new CodegenConfigError('Invalid plugin option "tableNameStyle". Expected one of: pascal, camel, raw.');
93
- }
94
- const rawBigintMode = options.bigintMode;
95
- if (rawBigintMode !== undefined && rawBigintMode !== 'string' && rawBigintMode !== 'bigint') {
96
- throw new CodegenConfigError('Invalid plugin option "bigintMode". Expected one of: string, bigint.');
97
- }
98
- const normalized = {};
99
- const outFile = parseStringOption(options, 'outFile');
100
- const emitZod = parseBooleanOption(options, 'emitZod');
101
- const includeViews = parseBooleanOption(options, 'includeViews');
102
- const runOnGenerate = parseBooleanOption(options, 'runOnGenerate');
103
- const failOnUnsupportedType = parseBooleanOption(options, 'failOnUnsupportedType');
104
- const emitIngest = parseBooleanOption(options, 'emitIngest');
105
- const ingestOutFile = parseStringOption(options, 'ingestOutFile');
106
- if (outFile !== undefined)
107
- normalized.outFile = outFile;
108
- if (emitZod !== undefined)
109
- normalized.emitZod = emitZod;
110
- if (rawTableNameStyle !== undefined)
111
- normalized.tableNameStyle = rawTableNameStyle;
112
- if (rawBigintMode !== undefined)
113
- normalized.bigintMode = rawBigintMode;
114
- if (includeViews !== undefined)
115
- normalized.includeViews = includeViews;
116
- if (runOnGenerate !== undefined)
117
- normalized.runOnGenerate = runOnGenerate;
118
- if (failOnUnsupportedType !== undefined)
119
- normalized.failOnUnsupportedType = failOnUnsupportedType;
120
- if (emitIngest !== undefined)
121
- normalized.emitIngest = emitIngest;
122
- if (ingestOutFile !== undefined)
123
- normalized.ingestOutFile = ingestOutFile;
124
- return normalized;
125
- }
126
- export function normalizeCodegenOptions(options = {}) {
127
- return {
128
- ...DEFAULT_OPTIONS,
129
- ...options,
130
- };
131
- }
132
- function toWords(input) {
133
- return input
134
- .split(/[^a-zA-Z0-9]+/)
135
- .map((part) => part.trim())
136
- .filter((part) => part.length > 0);
137
- }
138
- function pascalCase(input) {
139
- const words = toWords(input);
140
- if (words.length === 0)
141
- return 'Item';
142
- return words.map((part) => part[0]?.toUpperCase() + part.slice(1)).join('');
143
- }
144
- function camelCase(input) {
145
- const words = toWords(input);
146
- if (words.length === 0)
147
- return 'item';
148
- const [head, ...tail] = words;
149
- return `${head?.toLowerCase() ?? 'item'}${tail
150
- .map((part) => part[0]?.toUpperCase() + part.slice(1))
151
- .join('')}`;
152
- }
153
- function rawCase(input) {
154
- const sanitized = input.replace(/[^a-zA-Z0-9_]/g, '_').replace(/_+/g, '_').replace(/^_+|_+$/g, '');
155
- return sanitized.length > 0 ? sanitized : 'item';
156
- }
157
- function isValidIdentifier(input) {
158
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(input);
159
- }
160
- function renderPropertyName(name) {
161
- if (isValidIdentifier(name))
162
- return name;
163
- return JSON.stringify(name);
164
- }
165
- function baseRowTypeName(definition, style) {
166
- const combined = `${definition.database}_${definition.name}`;
167
- if (style === 'raw') {
168
- const candidate = `${rawCase(combined)}_row`;
169
- return isValidIdentifier(candidate) ? candidate : `_${candidate}`;
170
- }
171
- if (style === 'camel') {
172
- return `${camelCase(combined)}Row`;
173
- }
174
- return `${pascalCase(combined)}Row`;
175
- }
176
- function resolveTableNames(definitions, style) {
177
- const baseNames = definitions.map((definition) => ({
178
- definition,
179
- base: baseRowTypeName(definition, style),
180
- }));
181
- const counts = new Map();
182
- return baseNames.map((item) => {
183
- const seen = counts.get(item.base) ?? 0;
184
- const nextSeen = seen + 1;
185
- counts.set(item.base, nextSeen);
186
- return {
187
- definition: item.definition,
188
- interfaceName: nextSeen === 1 ? item.base : `${item.base}_${nextSeen}`,
189
- };
190
- });
191
- }
192
- function splitTopLevelArgs(input) {
193
- const args = [];
194
- let depth = 0;
195
- let start = 0;
196
- for (let i = 0; i < input.length; i++) {
197
- const ch = input[i];
198
- if (ch === '(')
199
- depth++;
200
- else if (ch === ')')
201
- depth--;
202
- else if (ch === ',' && depth === 0) {
203
- args.push(input.slice(start, i).trim());
204
- start = i + 1;
205
- }
206
- }
207
- const last = input.slice(start).trim();
208
- if (last.length > 0)
209
- args.push(last);
210
- return args;
211
- }
212
- function parseClickHouseType(typeStr) {
213
- const trimmed = typeStr.trim();
214
- const parenIndex = trimmed.indexOf('(');
215
- if (parenIndex < 0)
216
- return { base: trimmed, args: [] };
217
- const base = trimmed.slice(0, parenIndex);
218
- const closingParen = trimmed.lastIndexOf(')');
219
- if (closingParen <= parenIndex)
220
- return { base: trimmed, args: [] };
221
- const inner = trimmed.slice(parenIndex + 1, closingParen);
222
- if (inner.trim().length === 0)
223
- return { base, args: [] };
224
- return { base, args: splitTopLevelArgs(inner) };
225
- }
226
- function mapScalarType(base, bigintMode) {
227
- if (STRING_TYPES.has(base))
228
- return 'string';
229
- if (BOOLEAN_TYPES.has(base))
230
- return 'boolean';
231
- if (NUMBER_TYPES.has(base))
232
- return 'number';
233
- if (LARGE_INTEGER_TYPES.has(base))
234
- return bigintMode;
235
- return null;
236
- }
237
- function scalarToZod(mapped) {
238
- if (mapped === 'string')
239
- return 'z.string()';
240
- if (mapped === 'number')
241
- return 'z.number()';
242
- if (mapped === 'boolean')
243
- return 'z.boolean()';
244
- if (mapped === 'bigint')
245
- return 'z.bigint()';
246
- return 'z.unknown()';
247
- }
248
- function resolveInnerType(typeStr, bigintMode) {
249
- const parsed = parseClickHouseType(typeStr);
250
- const scalar = mapScalarType(parsed.base, bigintMode);
251
- if (scalar)
252
- return { tsType: scalar, zodType: scalarToZod(scalar) };
253
- switch (parsed.base) {
254
- case 'Nullable': {
255
- const arg = parsed.args[0];
256
- if (!arg)
257
- return null;
258
- const inner = resolveInnerType(arg, bigintMode);
259
- if (!inner)
260
- return null;
261
- return { tsType: `${inner.tsType} | null`, zodType: `${inner.zodType}.nullable()` };
262
- }
263
- case 'LowCardinality': {
264
- const arg = parsed.args[0];
265
- if (!arg)
266
- return null;
267
- return resolveInnerType(arg, bigintMode);
268
- }
269
- case 'Array': {
270
- const arg = parsed.args[0];
271
- if (!arg)
272
- return null;
273
- const inner = resolveInnerType(arg, bigintMode);
274
- if (!inner)
275
- return null;
276
- const needsWrap = inner.tsType.includes('|');
277
- const tsType = needsWrap ? `(${inner.tsType})[]` : `${inner.tsType}[]`;
278
- return { tsType, zodType: `z.array(${inner.zodType})` };
279
- }
280
- case 'Map': {
281
- const keyArg = parsed.args[0];
282
- const valueArg = parsed.args[1];
283
- if (!keyArg || !valueArg)
284
- return null;
285
- const key = resolveInnerType(keyArg, bigintMode);
286
- const value = resolveInnerType(valueArg, bigintMode);
287
- if (!key || !value)
288
- return null;
289
- return {
290
- tsType: `Record<${key.tsType}, ${value.tsType}>`,
291
- zodType: `z.record(${key.zodType}, ${value.zodType})`,
292
- };
293
- }
294
- case 'Tuple': {
295
- if (parsed.args.length === 0)
296
- return null;
297
- const elements = parsed.args.map((a) => resolveInnerType(a, bigintMode));
298
- if (elements.some((e) => e === null))
299
- return null;
300
- const valid = elements;
301
- return {
302
- tsType: `[${valid.map((e) => e.tsType).join(', ')}]`,
303
- zodType: `z.tuple([${valid.map((e) => e.zodType).join(', ')}])`,
304
- };
305
- }
306
- case 'SimpleAggregateFunction': {
307
- const lastArg = parsed.args[parsed.args.length - 1];
308
- if (parsed.args.length < 2 || !lastArg)
309
- return null;
310
- return resolveInnerType(lastArg, bigintMode);
311
- }
312
- case 'JSON': {
313
- return { tsType: 'Record<string, unknown>', zodType: 'z.record(z.string(), z.unknown())' };
314
- }
315
- default:
316
- return null;
317
- }
318
- }
319
- function resolveColumnType(typeStr, bigintMode) {
320
- const parsed = parseClickHouseType(typeStr);
321
- const firstArg = parsed.args[0];
322
- if (parsed.base === 'LowCardinality' && firstArg) {
323
- return resolveColumnType(firstArg, bigintMode);
324
- }
325
- if (parsed.base === 'Nullable' && firstArg) {
326
- const inner = resolveColumnType(firstArg, bigintMode);
327
- if (!inner)
328
- return null;
329
- return { tsType: inner.tsType, zodType: inner.zodType, nullable: true };
330
- }
331
- const resolved = resolveInnerType(typeStr, bigintMode);
332
- if (!resolved)
333
- return null;
334
- return { tsType: resolved.tsType, zodType: resolved.zodType, nullable: false };
335
- }
336
- export function mapColumnType(input, options) {
337
- const resolved = resolveColumnType(input.column.type, options.bigintMode);
338
- const columnNullable = input.column.nullable === true;
339
- if (!resolved) {
340
- if (options.failOnUnsupportedType) {
341
- throw new UnsupportedTypeError(input.path, input.column.type);
342
- }
343
- const unknownType = columnNullable ? 'unknown | null' : 'unknown';
344
- return {
345
- tsType: unknownType,
346
- zodType: 'z.unknown()',
347
- nullable: columnNullable,
348
- finding: {
349
- code: 'codegen_unsupported_type',
350
- message: `Unsupported type "${input.column.type}" at ${input.path}; emitted unknown.`,
351
- severity: 'warn',
352
- path: input.path,
353
- },
354
- };
355
- }
356
- const nullable = columnNullable || resolved.nullable;
357
- const tsType = nullable ? `${resolved.tsType} | null` : resolved.tsType;
358
- return { tsType, zodType: resolved.zodType, nullable };
359
- }
360
- function renderHeader(toolVersion) {
361
- const lines = ['// Generated by chkit codegen plugin'];
362
- lines.push(`// chkit-codegen-version: ${toolVersion}`);
363
- return lines;
364
- }
365
- function renderTableInterface(table, interfaceName, options) {
366
- const lines = [`export interface ${interfaceName} {`];
367
- const findings = [];
368
- const zodFields = [];
369
- for (const column of table.columns) {
370
- const path = `${table.database}.${table.name}.${column.name}`;
371
- const mapped = mapColumnType({ column, path }, options);
372
- if (mapped.finding)
373
- findings.push(mapped.finding);
374
- lines.push(` ${renderPropertyName(column.name)}: ${mapped.tsType}`);
375
- const zodExpr = mapped.nullable ? `${mapped.zodType}.nullable()` : mapped.zodType;
376
- zodFields.push(` ${renderPropertyName(column.name)}: ${zodExpr},`);
377
- }
378
- lines.push('}');
379
- if (options.emitZod) {
380
- lines.push('');
381
- lines.push(`export const ${interfaceName}Schema = z.object({`);
382
- lines.push(...zodFields);
383
- lines.push('})');
384
- lines.push('');
385
- lines.push(`export type ${interfaceName}Input = z.input<typeof ${interfaceName}Schema>`);
386
- lines.push(`export type ${interfaceName}Output = z.output<typeof ${interfaceName}Schema>`);
387
- }
388
- return { lines, findings };
389
- }
390
- function renderViewInterface(definition, interfaceName) {
391
- const kind = definition.kind === 'view' ? 'view' : 'materialized_view';
392
- return {
393
- lines: [
394
- `export interface ${interfaceName} {`,
395
- ' [key: string]: unknown',
396
- '}',
397
- '',
398
- `// ${kind} ${definition.database}.${definition.name} is emitted as unknown-key row shape in v1.`,
399
- ],
400
- findings: [],
401
- };
402
- }
403
- export function generateTypeArtifacts(input) {
404
- const normalized = normalizeCodegenOptions(input.options);
405
- const definitions = canonicalizeDefinitions(input.definitions);
406
- const sortedDefinitions = definitions
407
- .filter((definition) => {
408
- if (definition.kind === 'table')
409
- return true;
410
- if (!normalized.includeViews)
411
- return false;
412
- return definition.kind === 'view' || definition.kind === 'materialized_view';
413
- })
414
- .sort((a, b) => {
415
- if (a.database !== b.database)
416
- return a.database.localeCompare(b.database);
417
- return a.name.localeCompare(b.name);
418
- });
419
- const resolved = resolveTableNames(sortedDefinitions, normalized.tableNameStyle);
420
- const findings = [];
421
- const bodyLines = [];
422
- for (const entry of resolved) {
423
- const rendered = entry.definition.kind === 'table'
424
- ? renderTableInterface(entry.definition, entry.interfaceName, normalized)
425
- : renderViewInterface(entry.definition, entry.interfaceName);
426
- findings.push(...rendered.findings);
427
- bodyLines.push(...rendered.lines);
428
- bodyLines.push('');
429
- }
430
- const header = renderHeader(input.toolVersion ?? '0.1.0');
431
- const lines = [
432
- ...header,
433
- ...(normalized.emitZod ? ['', "import { z } from 'zod'"] : []),
434
- '',
435
- ...bodyLines,
436
- ];
437
- const content = `${lines.join('\n').trimEnd()}\n`;
438
- return {
439
- content,
440
- outFile: normalized.outFile,
441
- declarationCount: resolved.length,
442
- findings,
443
- };
444
- }
445
- function computeRelativeImportPath(fromFile, toFile) {
446
- const fromDir = dirname(fromFile);
447
- let rel = relative(fromDir, toFile);
448
- if (!rel.startsWith('.'))
449
- rel = `./${rel}`;
450
- // Replace .ts extension with .js for ESM imports
451
- return rel.replace(/\.ts$/, '.js');
452
- }
453
- function stripRowSuffix(name) {
454
- if (name.endsWith('Row'))
455
- return name.slice(0, -3);
456
- if (name.endsWith('_row'))
457
- return name.slice(0, -4);
458
- return name;
459
- }
460
- function renderIngestFunction(table, interfaceName, emitZod) {
461
- const funcName = `ingest${stripRowSuffix(interfaceName)}`;
462
- const tableFqn = `${table.database}.${table.name}`;
463
- const lines = [];
464
- if (emitZod) {
465
- lines.push(`export async function ${funcName}(`);
466
- lines.push(` ingestor: Ingestor,`);
467
- lines.push(` rows: ${interfaceName}[],`);
468
- lines.push(` options?: { validate?: boolean }`);
469
- lines.push(`): Promise<void> {`);
470
- lines.push(` const data = options?.validate ? rows.map(row => ${interfaceName}Schema.parse(row)) : rows`);
471
- lines.push(` await ingestor.insert({ table: '${tableFqn}', values: data })`);
472
- lines.push(`}`);
473
- }
474
- else {
475
- lines.push(`export async function ${funcName}(`);
476
- lines.push(` ingestor: Ingestor,`);
477
- lines.push(` rows: ${interfaceName}[]`);
478
- lines.push(`): Promise<void> {`);
479
- lines.push(` await ingestor.insert({ table: '${tableFqn}', values: rows })`);
480
- lines.push(`}`);
481
- }
482
- return lines;
483
- }
484
- export function generateIngestArtifacts(input) {
485
- const normalized = normalizeCodegenOptions(input.options);
486
- const definitions = canonicalizeDefinitions(input.definitions);
487
- const tables = definitions
488
- .filter((definition) => definition.kind === 'table')
489
- .sort((a, b) => {
490
- if (a.database !== b.database)
491
- return a.database.localeCompare(b.database);
492
- return a.name.localeCompare(b.name);
493
- });
494
- const resolved = resolveTableNames(tables, normalized.tableNameStyle);
495
- const importPath = computeRelativeImportPath(normalized.ingestOutFile, normalized.outFile);
496
- const typeImports = [];
497
- const valueImports = [];
498
- for (const entry of resolved) {
499
- typeImports.push(entry.interfaceName);
500
- if (normalized.emitZod) {
501
- valueImports.push(`${entry.interfaceName}Schema`);
502
- }
503
- }
504
- const header = renderHeader(input.toolVersion ?? '0.1.0');
505
- const lines = [...header, ''];
506
- if (typeImports.length > 0) {
507
- lines.push(`import type { ${typeImports.join(', ')} } from '${importPath}'`);
508
- }
509
- if (valueImports.length > 0) {
510
- lines.push(`import { ${valueImports.join(', ')} } from '${importPath}'`);
511
- }
512
- lines.push('');
513
- lines.push('export interface Ingestor {');
514
- lines.push(' insert(params: { table: string; values: Record<string, unknown>[] }): Promise<void>');
515
- lines.push('}');
516
- for (const entry of resolved) {
517
- if (entry.definition.kind !== 'table')
518
- continue;
519
- lines.push('');
520
- lines.push(...renderIngestFunction(entry.definition, entry.interfaceName, normalized.emitZod));
521
- }
522
- const content = `${lines.join('\n').trimEnd()}\n`;
523
- return {
524
- content,
525
- outFile: normalized.ingestOutFile,
526
- functionCount: resolved.length,
527
- };
528
- }
529
- function parseArgs(args) {
530
- const parsed = {
531
- check: false,
532
- };
533
- for (let i = 0; i < args.length; i += 1) {
534
- const token = args[i];
535
- if (!token)
536
- continue;
537
- if (token === '--check') {
538
- parsed.check = true;
539
- continue;
540
- }
541
- if (token === '--include-views') {
542
- parsed.includeViews = true;
543
- continue;
544
- }
545
- if (token === '--emit-zod') {
546
- parsed.emitZod = true;
547
- continue;
548
- }
549
- if (token === '--no-emit-zod') {
550
- parsed.emitZod = false;
551
- continue;
552
- }
553
- if (token === '--emit-ingest') {
554
- parsed.emitIngest = true;
555
- continue;
556
- }
557
- if (token === '--no-emit-ingest') {
558
- parsed.emitIngest = false;
559
- continue;
560
- }
561
- if (token === '--out-file') {
562
- const value = args[i + 1];
563
- if (!value || value.startsWith('--')) {
564
- throw new CodegenConfigError('Missing value for --out-file');
565
- }
566
- parsed.outFile = value;
567
- i += 1;
568
- continue;
569
- }
570
- if (token === '--ingest-out-file') {
571
- const value = args[i + 1];
572
- if (!value || value.startsWith('--')) {
573
- throw new CodegenConfigError('Missing value for --ingest-out-file');
574
- }
575
- parsed.ingestOutFile = value;
576
- i += 1;
577
- continue;
578
- }
579
- if (token === '--bigint-mode') {
580
- const value = args[i + 1];
581
- if (value !== 'string' && value !== 'bigint') {
582
- throw new CodegenConfigError('Invalid value for --bigint-mode. Expected string or bigint.');
583
- }
584
- parsed.bigintMode = value;
585
- i += 1;
586
- }
587
- }
588
- return parsed;
589
- }
590
- function mergeOptions(baseOptions, runtimeOptions, argOptions) {
591
- const fromRuntime = normalizeRuntimeOptions(runtimeOptions);
592
- const withRuntime = normalizeCodegenOptions({ ...baseOptions, ...fromRuntime });
593
- return normalizeCodegenOptions({
594
- ...withRuntime,
595
- outFile: argOptions.outFile ?? withRuntime.outFile,
596
- emitZod: argOptions.emitZod ?? withRuntime.emitZod,
597
- bigintMode: argOptions.bigintMode ?? withRuntime.bigintMode,
598
- includeViews: argOptions.includeViews ?? withRuntime.includeViews,
599
- emitIngest: argOptions.emitIngest ?? withRuntime.emitIngest,
600
- ingestOutFile: argOptions.ingestOutFile ?? withRuntime.ingestOutFile,
601
- });
602
- }
603
- export function isRunOnGenerateEnabled(baseOptions, runtimeOptions) {
604
- const fromRuntime = normalizeRuntimeOptions(runtimeOptions);
605
- const effective = normalizeCodegenOptions({ ...baseOptions, ...fromRuntime });
606
- return effective.runOnGenerate;
607
- }
608
- async function writeAtomic(targetPath, content) {
609
- await mkdir(dirname(targetPath), { recursive: true });
610
- const tempPath = join(dirname(targetPath), `.${Date.now()}-${Math.random().toString(16).slice(2)}.tmp`);
611
- await writeFile(tempPath, content, 'utf8');
612
- await rename(tempPath, targetPath);
613
- }
614
- async function readMaybe(path) {
615
- try {
616
- return await readFile(path, 'utf8');
617
- }
618
- catch {
619
- return null;
620
- }
621
- }
622
- function checkGeneratedOutput(input) {
623
- if (input.current === null) {
624
- return {
625
- plugin: 'codegen',
626
- evaluated: true,
627
- ok: false,
628
- findings: [
629
- {
630
- code: input.missingCode,
631
- message: `${input.label} output file is missing: ${input.outFile}`,
632
- severity: 'error',
633
- metadata: { outFile: input.outFile },
634
- },
635
- ],
636
- metadata: {
637
- outFile: input.outFile,
638
- },
639
- };
640
- }
641
- if (input.current !== input.expected) {
642
- return {
643
- plugin: 'codegen',
644
- evaluated: true,
645
- ok: false,
646
- findings: [
647
- {
648
- code: input.staleCode,
649
- message: `${input.label} output is stale: ${input.outFile}`,
650
- severity: 'error',
651
- metadata: { outFile: input.outFile },
652
- },
653
- ],
654
- metadata: {
655
- outFile: input.outFile,
656
- },
657
- };
658
- }
659
- return {
660
- plugin: 'codegen',
661
- evaluated: true,
662
- ok: true,
663
- findings: [],
664
- metadata: {
665
- outFile: input.outFile,
666
- },
667
- };
668
- }
669
- function mergeCheckResults(results) {
670
- const allFindings = results.flatMap((r) => r.findings);
671
- const allOk = results.every((r) => r.ok);
672
- return {
673
- plugin: 'codegen',
674
- evaluated: true,
675
- ok: allOk,
676
- findings: allFindings,
677
- metadata: results.reduce((acc, r) => ({ ...acc, ...r.metadata }), {}),
678
- };
679
- }
680
- export function createCodegenPlugin(options = {}) {
681
- const base = normalizeCodegenOptions(options);
682
- return {
683
- manifest: {
684
- name: 'codegen',
685
- apiVersion: 1,
686
- },
687
- commands: [
688
- {
689
- name: 'codegen',
690
- description: 'Generate TypeScript artifacts from chkit schema definitions',
691
- async run({ args, jsonMode, print, options: runtimeOptions, config, configPath, }) {
692
- try {
693
- const parsedArgs = parseArgs(args);
694
- const effectiveOptions = mergeOptions(base, runtimeOptions, parsedArgs);
695
- const configDir = resolve(configPath, '..');
696
- const outFile = resolve(configDir, effectiveOptions.outFile);
697
- const definitions = await loadSchemaDefinitions(config.schema, { cwd: configDir });
698
- const generated = generateTypeArtifacts({
699
- definitions,
700
- options: effectiveOptions,
701
- });
702
- let ingestGenerated = null;
703
- let ingestOutFile = null;
704
- if (effectiveOptions.emitIngest) {
705
- ingestGenerated = generateIngestArtifacts({
706
- definitions,
707
- options: effectiveOptions,
708
- });
709
- ingestOutFile = resolve(configDir, effectiveOptions.ingestOutFile);
710
- }
711
- if (parsedArgs.check) {
712
- const current = await readMaybe(outFile);
713
- const typeCheckResult = checkGeneratedOutput({
714
- label: 'Codegen',
715
- outFile,
716
- expected: generated.content,
717
- current,
718
- missingCode: 'codegen_missing_output',
719
- staleCode: 'codegen_stale_output',
720
- });
721
- const results = [typeCheckResult];
722
- if (ingestGenerated && ingestOutFile) {
723
- const ingestCurrent = await readMaybe(ingestOutFile);
724
- results.push(checkGeneratedOutput({
725
- label: 'Codegen ingest',
726
- outFile: ingestOutFile,
727
- expected: ingestGenerated.content,
728
- current: ingestCurrent,
729
- missingCode: 'codegen_missing_ingest_output',
730
- staleCode: 'codegen_stale_ingest_output',
731
- }));
732
- }
733
- const checkResult = mergeCheckResults(results);
734
- const payload = {
735
- ok: checkResult.ok,
736
- findingCodes: checkResult.findings.map((finding) => finding.code),
737
- outFile,
738
- mode: 'check',
739
- };
740
- if (jsonMode) {
741
- print(payload);
742
- }
743
- else {
744
- if (checkResult.ok) {
745
- print(`Codegen up-to-date: ${outFile}`);
746
- }
747
- else {
748
- const firstCode = checkResult.findings[0]?.code ?? 'codegen_stale_output';
749
- print(`Codegen check failed (${firstCode}): ${outFile}`);
750
- }
751
- }
752
- return checkResult.ok ? 0 : 1;
753
- }
754
- await writeAtomic(outFile, generated.content);
755
- if (ingestGenerated && ingestOutFile) {
756
- await writeAtomic(ingestOutFile, ingestGenerated.content);
757
- }
758
- const payload = {
759
- ok: true,
760
- outFile,
761
- declarationCount: generated.declarationCount,
762
- findingCodes: generated.findings.map((finding) => finding.code),
763
- mode: 'write',
764
- };
765
- if (jsonMode) {
766
- print(payload);
767
- }
768
- else {
769
- print(`Codegen wrote ${outFile} (${generated.declarationCount} declarations)`);
770
- }
771
- return 0;
772
- }
773
- catch (error) {
774
- const message = error instanceof Error ? error.message : String(error);
775
- if (jsonMode) {
776
- print({
777
- ok: false,
778
- error: message,
779
- });
780
- }
781
- else {
782
- print(`Codegen failed: ${message}`);
783
- }
784
- if (error instanceof CodegenConfigError) {
785
- return 2;
786
- }
787
- return 1;
788
- }
789
- },
790
- },
791
- ],
792
- hooks: {
793
- onConfigLoaded({ options: runtimeOptions }) {
794
- normalizeRuntimeOptions(runtimeOptions);
795
- },
796
- async onCheck({ config, configPath, options: runtimeOptions }) {
797
- const effectiveOptions = mergeOptions(base, runtimeOptions, { check: false });
798
- const configDir = resolve(configPath, '..');
799
- const outFile = resolve(configDir, effectiveOptions.outFile);
800
- const definitions = await loadSchemaDefinitions(config.schema, { cwd: configDir });
801
- const generated = generateTypeArtifacts({
802
- definitions,
803
- options: effectiveOptions,
804
- });
805
- const current = await readMaybe(outFile);
806
- const typeResult = checkGeneratedOutput({
807
- label: 'Codegen',
808
- outFile,
809
- expected: generated.content,
810
- current,
811
- missingCode: 'codegen_missing_output',
812
- staleCode: 'codegen_stale_output',
813
- });
814
- if (!effectiveOptions.emitIngest) {
815
- return typeResult;
816
- }
817
- const ingestOutFile = resolve(configDir, effectiveOptions.ingestOutFile);
818
- const ingestGenerated = generateIngestArtifacts({
819
- definitions,
820
- options: effectiveOptions,
821
- });
822
- const ingestCurrent = await readMaybe(ingestOutFile);
823
- const ingestResult = checkGeneratedOutput({
824
- label: 'Codegen ingest',
825
- outFile: ingestOutFile,
826
- expected: ingestGenerated.content,
827
- current: ingestCurrent,
828
- missingCode: 'codegen_missing_ingest_output',
829
- staleCode: 'codegen_stale_ingest_output',
830
- });
831
- return mergeCheckResults([typeResult, ingestResult]);
832
- },
833
- onCheckReport({ result, print }) {
834
- const findingCodes = result.findings.map((finding) => finding.code);
835
- if (result.ok) {
836
- print(`codegen check: ok`);
837
- return;
838
- }
839
- print(`codegen check: failed${findingCodes.length > 0 ? ` (${findingCodes.join(', ')})` : ''}`);
840
- },
841
- },
842
- };
843
- }
844
- export function codegen(options = {}) {
845
- return {
846
- plugin: createCodegenPlugin(),
847
- name: 'codegen',
848
- enabled: true,
849
- options,
850
- };
851
- }
1
+ export { createCodegenPlugin, codegen } from './plugin.js';
2
+ export { normalizeCodegenOptions, isRunOnGenerateEnabled } from './options.js';
3
+ export { mapColumnType, generateTypeArtifacts, generateIngestArtifacts } from './generators.js';
852
4
  //# sourceMappingURL=index.js.map