@typokit/transform-native 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/src/index.ts ADDED
@@ -0,0 +1,437 @@
1
+ // @typokit/transform-native — Rust-native AST transform (napi-rs)
2
+ import type { SchemaTypeMap } from "@typokit/types";
3
+
4
+ interface JsPropertyMetadata {
5
+ type: string;
6
+ optional: boolean;
7
+ }
8
+
9
+ interface JsTypeMetadata {
10
+ name: string;
11
+ properties: Record<string, JsPropertyMetadata>;
12
+ }
13
+
14
+ interface JsSchemaChange {
15
+ type: string;
16
+ entity: string;
17
+ field?: string;
18
+ details?: Record<string, string>;
19
+ }
20
+
21
+ interface JsMigrationDraft {
22
+ name: string;
23
+ sql: string;
24
+ destructive: boolean;
25
+ changes: JsSchemaChange[];
26
+ }
27
+
28
+ interface JsTypeValidatorInput {
29
+ name: string;
30
+ properties: Record<string, JsPropertyMetadata>;
31
+ }
32
+
33
+ interface JsPipelineResult {
34
+ contentHash: string;
35
+ types: Record<string, JsTypeMetadata>;
36
+ compiledRoutes: string;
37
+ openapiSpec: string;
38
+ testStubs: string;
39
+ validatorInputs: JsTypeValidatorInput[];
40
+ }
41
+
42
+ interface NativeBindings {
43
+ parseAndExtractTypes(filePaths: string[]): Record<string, JsTypeMetadata>;
44
+ compileRoutes(filePaths: string[]): string;
45
+ generateOpenApi(routeFilePaths: string[], typeFilePaths: string[]): string;
46
+ diffSchemas(
47
+ oldTypes: Record<string, JsTypeMetadata>,
48
+ newTypes: Record<string, JsTypeMetadata>,
49
+ migrationName: string,
50
+ ): JsMigrationDraft;
51
+ generateTestStubs(filePaths: string[]): string;
52
+ prepareValidatorInputs(typeFilePaths: string[]): JsTypeValidatorInput[];
53
+ collectValidatorOutputs(results: string[][]): Record<string, string>;
54
+ computeContentHash(filePaths: string[]): string;
55
+ runPipeline(
56
+ typeFilePaths: string[],
57
+ routeFilePaths: string[],
58
+ ): JsPipelineResult;
59
+ }
60
+
61
+ /** Options for the output pipeline */
62
+ export interface PipelineOptions {
63
+ /** Paths to TypeScript files containing type definitions */
64
+ typeFiles: string[];
65
+ /** Paths to TypeScript files containing route contracts */
66
+ routeFiles: string[];
67
+ /** Output directory (defaults to ".typokit") */
68
+ outputDir?: string;
69
+ /** Optional validator callback — receives type inputs, returns [name, code] pairs */
70
+ validatorCallback?: (
71
+ inputs: JsTypeValidatorInput[],
72
+ ) => Promise<[string, string][]> | [string, string][];
73
+ /** Path to cache hash file (defaults to ".typokit/.cache-hash") */
74
+ cacheFile?: string;
75
+ }
76
+
77
+ /** Result of a full pipeline run */
78
+ export interface PipelineOutput {
79
+ /** Whether outputs were regenerated (false = cache hit) */
80
+ regenerated: boolean;
81
+ /** Content hash of source files */
82
+ contentHash: string;
83
+ /** Extracted type metadata */
84
+ types: SchemaTypeMap;
85
+ /** Files written to outputDir */
86
+ filesWritten: string[];
87
+ }
88
+
89
+ // Load the platform-specific native addon
90
+ async function loadNativeAddon(): Promise<NativeBindings> {
91
+ const g = globalThis as Record<string, unknown>;
92
+ const proc = g["process"] as { platform: string; arch: string } | undefined;
93
+ const platform = proc?.platform ?? "unknown";
94
+ const arch = proc?.arch ?? "unknown";
95
+
96
+ const triples: Record<string, Record<string, string>> = {
97
+ win32: { x64: "win32-x64-msvc" },
98
+ darwin: { x64: "darwin-x64", arm64: "darwin-arm64" },
99
+ linux: { x64: "linux-x64-gnu", arm64: "linux-arm64-gnu" },
100
+ };
101
+
102
+ const triple = triples[platform]?.[arch];
103
+ if (!triple) {
104
+ throw new Error(
105
+ `@typokit/transform-native: unsupported platform ${platform}-${arch}`,
106
+ );
107
+ }
108
+
109
+ // In ESM, require() is not global. Use createRequire from 'module' built-in.
110
+ const { createRequire } = (await import(/* @vite-ignore */ "module")) as {
111
+ createRequire: (url: string) => (id: string) => unknown;
112
+ };
113
+ const req = createRequire(import.meta.url);
114
+
115
+ // Try loading the platform-specific native addon
116
+ try {
117
+ return req(`../index.${triple}.node`) as NativeBindings;
118
+ } catch {
119
+ try {
120
+ return req(`@typokit/transform-native-${triple}`) as NativeBindings;
121
+ } catch {
122
+ throw new Error(
123
+ `@typokit/transform-native: failed to load native addon for ${triple}. ` +
124
+ `Make sure the native addon is built.`,
125
+ );
126
+ }
127
+ }
128
+ }
129
+
130
+ let _native: NativeBindings | undefined;
131
+ let _loading: Promise<NativeBindings> | undefined;
132
+
133
+ async function getNative(): Promise<NativeBindings> {
134
+ if (_native) return _native;
135
+ if (!_loading) {
136
+ _loading = loadNativeAddon().then((n) => {
137
+ _native = n;
138
+ return n;
139
+ });
140
+ }
141
+ return _loading;
142
+ }
143
+
144
+ /**
145
+ * Parse TypeScript source files and extract type metadata.
146
+ *
147
+ * Uses the Rust-native SWC parser for high-performance AST parsing and
148
+ * type extraction. Extracts interface definitions including JSDoc tags
149
+ * (@table, @id, @generated, @format, @unique, @minLength, @maxLength,
150
+ * @default, @onUpdate).
151
+ *
152
+ * @param filePaths - Array of file paths to parse
153
+ * @returns SchemaTypeMap mapping type names to their metadata
154
+ */
155
+ export async function parseAndExtractTypes(
156
+ filePaths: string[],
157
+ ): Promise<SchemaTypeMap> {
158
+ const native = await getNative();
159
+ const raw = native.parseAndExtractTypes(filePaths);
160
+
161
+ // Convert JsTypeMetadata to TypeMetadata (compatible shape)
162
+ const result: SchemaTypeMap = {};
163
+ for (const [name, meta] of Object.entries(raw)) {
164
+ result[name] = {
165
+ name: meta.name,
166
+ properties: {},
167
+ };
168
+ for (const [propName, prop] of Object.entries(meta.properties)) {
169
+ result[name].properties[propName] = {
170
+ type: prop.type,
171
+ optional: prop.optional,
172
+ };
173
+ }
174
+ }
175
+ return result;
176
+ }
177
+
178
+ /**
179
+ * Compile route contracts from TypeScript files into a radix tree.
180
+ *
181
+ * Parses interfaces with route contract keys (e.g., "GET /users") and
182
+ * builds a compiled radix tree serialized as TypeScript source code.
183
+ *
184
+ * @param filePaths - Array of file paths containing route contract interfaces
185
+ * @returns TypeScript source code for the compiled route table
186
+ */
187
+ export async function compileRoutes(filePaths: string[]): Promise<string> {
188
+ const native = await getNative();
189
+ return native.compileRoutes(filePaths);
190
+ }
191
+
192
+ /**
193
+ * Generate an OpenAPI 3.1.0 specification from route contracts and type definitions.
194
+ *
195
+ * @param routeFilePaths - Array of file paths containing route contract interfaces
196
+ * @param typeFilePaths - Array of file paths containing type definitions
197
+ * @returns OpenAPI 3.1.0 specification as a JSON string
198
+ */
199
+ export async function generateOpenApi(
200
+ routeFilePaths: string[],
201
+ typeFilePaths: string[],
202
+ ): Promise<string> {
203
+ const native = await getNative();
204
+ return native.generateOpenApi(routeFilePaths, typeFilePaths);
205
+ }
206
+
207
+ /**
208
+ * Diff two schema versions and produce a migration draft.
209
+ *
210
+ * Compares old types against new types to detect added, removed, and
211
+ * modified entities and fields. Generates SQL DDL stubs for the changes.
212
+ *
213
+ * @param oldTypes - Previous schema version
214
+ * @param newTypes - New schema version
215
+ * @param migrationName - Name for the migration draft
216
+ * @returns MigrationDraft with SQL, changes, and destructive flag
217
+ */
218
+ export async function diffSchemas(
219
+ oldTypes: SchemaTypeMap,
220
+ newTypes: SchemaTypeMap,
221
+ migrationName: string,
222
+ ): Promise<JsMigrationDraft> {
223
+ const native = await getNative();
224
+ const oldJs = schemaTypeMapToJs(oldTypes);
225
+ const newJs = schemaTypeMapToJs(newTypes);
226
+ return native.diffSchemas(oldJs, newJs, migrationName);
227
+ }
228
+
229
+ /**
230
+ * Generate contract test scaffolding from route contract files.
231
+ *
232
+ * Parses route contracts and generates TypeScript test stubs with
233
+ * describe/it blocks for each route.
234
+ *
235
+ * @param filePaths - Array of file paths containing route contract interfaces
236
+ * @returns TypeScript test code string
237
+ */
238
+ export async function generateTestStubs(filePaths: string[]): Promise<string> {
239
+ const native = await getNative();
240
+ return native.generateTestStubs(filePaths);
241
+ }
242
+
243
+ /**
244
+ * Prepare type metadata for Typia validator generation.
245
+ *
246
+ * Converts parsed type metadata into a format suitable for passing
247
+ * to the @typokit/transform-typia bridge callback.
248
+ *
249
+ * @param typeFilePaths - Array of file paths containing type definitions
250
+ * @returns Array of type validator inputs
251
+ */
252
+ export async function prepareValidatorInputs(
253
+ typeFilePaths: string[],
254
+ ): Promise<JsTypeValidatorInput[]> {
255
+ const native = await getNative();
256
+ return native.prepareValidatorInputs(typeFilePaths);
257
+ }
258
+
259
+ /**
260
+ * Collect validator code results into a file path map.
261
+ *
262
+ * Maps type names and their generated code to output file paths
263
+ * under .typokit/validators/.
264
+ *
265
+ * @param results - Array of [typeName, code] pairs
266
+ * @returns Map of file paths to validator code
267
+ */
268
+ export async function collectValidatorOutputs(
269
+ results: [string, string][],
270
+ ): Promise<Record<string, string>> {
271
+ const native = await getNative();
272
+ return native.collectValidatorOutputs(results);
273
+ }
274
+
275
+ /**
276
+ * Compute a SHA-256 content hash of source files.
277
+ *
278
+ * Used for cache invalidation: if the hash matches a previous build,
279
+ * outputs can be reused without regeneration.
280
+ *
281
+ * @param filePaths - Array of file paths to hash
282
+ * @returns Hex-encoded SHA-256 hash string
283
+ */
284
+ export async function computeContentHash(filePaths: string[]): Promise<string> {
285
+ const native = await getNative();
286
+ return native.computeContentHash(filePaths);
287
+ }
288
+
289
+ /**
290
+ * Run the full output pipeline with content-hash caching.
291
+ *
292
+ * Orchestrates all transform steps: parse types, compile routes, generate
293
+ * OpenAPI spec, generate test stubs, and prepare validator inputs. Writes
294
+ * all outputs to the `.typokit/` directory structure:
295
+ *
296
+ * - `.typokit/routes/compiled-router.ts` — Compiled radix tree
297
+ * - `.typokit/schemas/openapi.json` — OpenAPI 3.1.0 spec
298
+ * - `.typokit/tests/contract.test.ts` — Contract test stubs
299
+ * - `.typokit/validators/*.ts` — Typia validators (if callback provided)
300
+ *
301
+ * Content-hash caching: If the hash of all source files matches the cached
302
+ * hash, no outputs are regenerated. Force a rebuild by deleting `.typokit/.cache-hash`.
303
+ *
304
+ * @param options - Pipeline configuration
305
+ * @returns Pipeline output with metadata about what was generated
306
+ */
307
+ export async function buildPipeline(
308
+ options: PipelineOptions,
309
+ ): Promise<PipelineOutput> {
310
+ const { join, dirname } = (await import(/* @vite-ignore */ "path")) as {
311
+ join: (...args: string[]) => string;
312
+ dirname: (p: string) => string;
313
+ };
314
+ const nodeFs = (await import(/* @vite-ignore */ "fs")) as {
315
+ existsSync: (p: string) => boolean;
316
+ mkdirSync: (p: string, opts?: { recursive?: boolean }) => void;
317
+ readFileSync: (p: string, encoding: string) => string;
318
+ writeFileSync: (p: string, data: string, encoding?: string) => void;
319
+ };
320
+
321
+ const native = await getNative();
322
+ const outputDir = options.outputDir ?? ".typokit";
323
+ const cacheFile = options.cacheFile ?? join(outputDir, ".cache-hash");
324
+
325
+ // 1. Compute content hash of all input files
326
+ const allPaths = [...options.typeFiles, ...options.routeFiles];
327
+ const contentHash = native.computeContentHash(allPaths);
328
+
329
+ // 2. Check cache
330
+ if (nodeFs.existsSync(cacheFile)) {
331
+ const cachedHash = nodeFs.readFileSync(cacheFile, "utf-8").trim();
332
+ if (cachedHash === contentHash) {
333
+ return {
334
+ regenerated: false,
335
+ contentHash,
336
+ types: {},
337
+ filesWritten: [],
338
+ };
339
+ }
340
+ }
341
+
342
+ // 3. Run native pipeline
343
+ const result = native.runPipeline(options.typeFiles, options.routeFiles);
344
+
345
+ // 4. Ensure output directories exist
346
+ const dirs = [
347
+ join(outputDir, "routes"),
348
+ join(outputDir, "schemas"),
349
+ join(outputDir, "tests"),
350
+ join(outputDir, "validators"),
351
+ join(outputDir, "client"),
352
+ ];
353
+ for (const dir of dirs) {
354
+ nodeFs.mkdirSync(dir, { recursive: true });
355
+ }
356
+
357
+ const filesWritten: string[] = [];
358
+
359
+ // 5. Write compiled routes
360
+ const routesPath = join(outputDir, "routes", "compiled-router.ts");
361
+ nodeFs.writeFileSync(routesPath, result.compiledRoutes, "utf-8");
362
+ filesWritten.push(routesPath);
363
+
364
+ // 6. Write OpenAPI spec
365
+ const openapiPath = join(outputDir, "schemas", "openapi.json");
366
+ nodeFs.writeFileSync(openapiPath, result.openapiSpec, "utf-8");
367
+ filesWritten.push(openapiPath);
368
+
369
+ // 7. Write test stubs
370
+ const testsPath = join(outputDir, "tests", "contract.test.ts");
371
+ nodeFs.writeFileSync(testsPath, result.testStubs, "utf-8");
372
+ filesWritten.push(testsPath);
373
+
374
+ // 8. Generate and write validators (if callback provided)
375
+ if (options.validatorCallback && result.validatorInputs.length > 0) {
376
+ const validatorResults = await options.validatorCallback(
377
+ result.validatorInputs,
378
+ );
379
+ const validatorOutputs = native.collectValidatorOutputs(validatorResults);
380
+ for (const [filePath, code] of Object.entries(validatorOutputs)) {
381
+ const fullPath = filePath.startsWith(outputDir)
382
+ ? filePath
383
+ : join(outputDir, filePath.replace(/^\.typokit\//, ""));
384
+ const dir = dirname(fullPath);
385
+ nodeFs.mkdirSync(dir, { recursive: true });
386
+ nodeFs.writeFileSync(fullPath, code, "utf-8");
387
+ filesWritten.push(fullPath);
388
+ }
389
+ }
390
+
391
+ // 9. Write cache hash
392
+ nodeFs.mkdirSync(dirname(cacheFile), { recursive: true });
393
+ nodeFs.writeFileSync(cacheFile, contentHash, "utf-8");
394
+ filesWritten.push(cacheFile);
395
+
396
+ // 10. Convert types to SchemaTypeMap
397
+ const types: SchemaTypeMap = {};
398
+ for (const [name, meta] of Object.entries(result.types)) {
399
+ types[name] = {
400
+ name: meta.name,
401
+ properties: {},
402
+ };
403
+ for (const [propName, prop] of Object.entries(meta.properties)) {
404
+ types[name].properties[propName] = {
405
+ type: prop.type,
406
+ optional: prop.optional,
407
+ };
408
+ }
409
+ }
410
+
411
+ return {
412
+ regenerated: true,
413
+ contentHash,
414
+ types,
415
+ filesWritten,
416
+ };
417
+ }
418
+
419
+ /** Convert SchemaTypeMap to JsTypeMetadata format for native binding */
420
+ function schemaTypeMapToJs(
421
+ types: SchemaTypeMap,
422
+ ): Record<string, JsTypeMetadata> {
423
+ const result: Record<string, JsTypeMetadata> = {};
424
+ for (const [name, meta] of Object.entries(types)) {
425
+ result[name] = {
426
+ name: meta.name,
427
+ properties: {},
428
+ };
429
+ for (const [propName, prop] of Object.entries(meta.properties)) {
430
+ result[name].properties[propName] = {
431
+ type: prop.type,
432
+ optional: prop.optional,
433
+ };
434
+ }
435
+ }
436
+ return result;
437
+ }