@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/dist/index.d.ts +148 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +290 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
- package/src/env.d.ts +40 -0
- package/src/index.d.ts +135 -0
- package/src/index.test.ts +878 -0
- package/src/index.ts +437 -0
- package/src/lib.rs +388 -0
- package/src/openapi_generator.rs +525 -0
- package/src/output_pipeline.rs +234 -0
- package/src/parser.rs +105 -0
- package/src/route_compiler.rs +615 -0
- package/src/schema_differ.rs +393 -0
- package/src/test_stub_generator.rs +318 -0
- package/src/type_extractor.rs +370 -0
- package/src/typia_bridge.rs +179 -0
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
|
+
}
|