@typokit/plugin-axum 0.2.1
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/README.md +81 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +186 -0
- package/dist/index.js.map +1 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.linux-x64-musl.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +57 -0
- package/src/index.ts +309 -0
- package/src/lib.rs +80 -0
- package/src/rust_codegen/database.rs +898 -0
- package/src/rust_codegen/handlers.rs +1111 -0
- package/src/rust_codegen/middleware.rs +156 -0
- package/src/rust_codegen/mod.rs +91 -0
- package/src/rust_codegen/project.rs +593 -0
- package/src/rust_codegen/router.rs +385 -0
- package/src/rust_codegen/services.rs +476 -0
- package/src/rust_codegen/structs.rs +1363 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// @typokit/plugin-axum — Axum Server Code Generation Plugin
|
|
2
|
+
|
|
3
|
+
import type { TypoKitPlugin, BuildPipeline } from "@typokit/core";
|
|
4
|
+
import type { CompileContext } from "@typokit/types";
|
|
5
|
+
|
|
6
|
+
// ─── Native Binding Types ────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
interface JsRustGeneratedOutput {
|
|
9
|
+
path: string;
|
|
10
|
+
content: string;
|
|
11
|
+
overwrite: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface NativeBindings {
|
|
15
|
+
generateRustCodegen(
|
|
16
|
+
typeFilePaths: string[],
|
|
17
|
+
routeFilePaths: string[],
|
|
18
|
+
): JsRustGeneratedOutput[];
|
|
19
|
+
computeContentHash(filePaths: string[]): string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─── Native Addon Loader ─────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
async function loadNativeAddon(): Promise<NativeBindings> {
|
|
25
|
+
const g = globalThis as Record<string, unknown>;
|
|
26
|
+
const proc = g["process"] as { platform: string; arch: string } | undefined;
|
|
27
|
+
const platform = proc?.platform ?? "unknown";
|
|
28
|
+
const arch = proc?.arch ?? "unknown";
|
|
29
|
+
|
|
30
|
+
const triples: Record<string, Record<string, string>> = {
|
|
31
|
+
win32: { x64: "win32-x64-msvc" },
|
|
32
|
+
darwin: { x64: "darwin-x64", arm64: "darwin-arm64" },
|
|
33
|
+
linux: { x64: "linux-x64-gnu", arm64: "linux-arm64-gnu" },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const triple = triples[platform]?.[arch];
|
|
37
|
+
if (!triple) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`@typokit/plugin-axum: unsupported platform ${platform}-${arch}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { createRequire } = (await import(/* @vite-ignore */ "module")) as {
|
|
44
|
+
createRequire: (url: string) => (id: string) => unknown;
|
|
45
|
+
};
|
|
46
|
+
const req = createRequire(import.meta.url);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
return req(`../index.${triple}.node`) as NativeBindings;
|
|
50
|
+
} catch {
|
|
51
|
+
try {
|
|
52
|
+
return req(`@typokit/plugin-axum-${triple}`) as NativeBindings;
|
|
53
|
+
} catch {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`@typokit/plugin-axum: failed to load native addon for ${triple}. ` +
|
|
56
|
+
`Make sure the native addon is built.`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let _native: NativeBindings | undefined;
|
|
63
|
+
let _loading: Promise<NativeBindings> | undefined;
|
|
64
|
+
|
|
65
|
+
async function getNative(): Promise<NativeBindings> {
|
|
66
|
+
if (_native) return _native;
|
|
67
|
+
if (!_loading) {
|
|
68
|
+
_loading = loadNativeAddon().then((n) => {
|
|
69
|
+
_native = n;
|
|
70
|
+
return n;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return _loading;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Plugin Options ──────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
/** Configuration options for the Axum code generation plugin */
|
|
79
|
+
export interface AxumPluginOptions {
|
|
80
|
+
/** Database adapter (currently only 'sqlx' is supported, default: 'sqlx') */
|
|
81
|
+
db?: string;
|
|
82
|
+
/** Output directory for the generated Rust project (default: project root) */
|
|
83
|
+
outDir?: string;
|
|
84
|
+
/** Path to cache hash file (defaults to ".typokit/.cache-hash" within outDir) */
|
|
85
|
+
cacheFile?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Plugin Factory ──────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create an Axum server code generation plugin.
|
|
92
|
+
*
|
|
93
|
+
* Generates a complete Axum server (structs, router, sqlx DB layer, handlers,
|
|
94
|
+
* services, middleware, and project scaffold) from TypeScript schema types
|
|
95
|
+
* and route contracts during the build pipeline.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* import { axumPlugin } from '@typokit/plugin-axum';
|
|
100
|
+
*
|
|
101
|
+
* export default {
|
|
102
|
+
* plugins: [axumPlugin({ db: 'sqlx' })],
|
|
103
|
+
* };
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export function axumPlugin(options: AxumPluginOptions = {}): TypoKitPlugin {
|
|
107
|
+
const { db = "sqlx", outDir, cacheFile } = options;
|
|
108
|
+
|
|
109
|
+
if (db !== "sqlx") {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`@typokit/plugin-axum: unsupported database adapter '${db}'. Only 'sqlx' is currently supported.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
name: "plugin-axum",
|
|
117
|
+
|
|
118
|
+
onBuild(pipeline: BuildPipeline) {
|
|
119
|
+
// Tap the emit hook to generate Rust code from the parsed schemas
|
|
120
|
+
pipeline.hooks.emit.tap("plugin-axum", async (outputs, ctx) => {
|
|
121
|
+
const { join, dirname } = (await import(/* @vite-ignore */ "path")) as {
|
|
122
|
+
join: (...args: string[]) => string;
|
|
123
|
+
dirname: (p: string) => string;
|
|
124
|
+
};
|
|
125
|
+
const nodeFs = (await import(/* @vite-ignore */ "fs")) as {
|
|
126
|
+
existsSync: (p: string) => boolean;
|
|
127
|
+
mkdirSync: (p: string, opts?: { recursive?: boolean }) => void;
|
|
128
|
+
readFileSync: (p: string, encoding: string) => string;
|
|
129
|
+
writeFileSync: (p: string, data: string, encoding?: string) => void;
|
|
130
|
+
};
|
|
131
|
+
const native = await getNative();
|
|
132
|
+
const resolvedOutDir = outDir ?? ctx.rootDir;
|
|
133
|
+
const resolvedCacheFile =
|
|
134
|
+
cacheFile ?? join(resolvedOutDir, ".typokit", ".cache-hash");
|
|
135
|
+
|
|
136
|
+
// Resolve type and route files from the build context
|
|
137
|
+
const fsAdapter = nodeFs as unknown as {
|
|
138
|
+
existsSync: (p: string) => boolean;
|
|
139
|
+
readdirSync: (p: string) => string[];
|
|
140
|
+
statSync: (p: string) => {
|
|
141
|
+
isFile(): boolean;
|
|
142
|
+
isDirectory(): boolean;
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
const typeFiles = resolveTypeFiles(ctx.rootDir, fsAdapter, join);
|
|
146
|
+
const routeFiles = resolveRouteFiles(ctx.rootDir, fsAdapter, join);
|
|
147
|
+
|
|
148
|
+
if (typeFiles.length === 0 && routeFiles.length === 0) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check content hash cache
|
|
153
|
+
const allPaths = [...typeFiles, ...routeFiles];
|
|
154
|
+
const contentHash = native.computeContentHash(allPaths);
|
|
155
|
+
|
|
156
|
+
if (nodeFs.existsSync(resolvedCacheFile)) {
|
|
157
|
+
const cachedHash = nodeFs
|
|
158
|
+
.readFileSync(resolvedCacheFile, "utf-8")
|
|
159
|
+
.trim();
|
|
160
|
+
if (cachedHash === contentHash) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Generate Rust codegen outputs
|
|
166
|
+
const rustOutputs = native.generateRustCodegen(typeFiles, routeFiles);
|
|
167
|
+
|
|
168
|
+
// Write generated files
|
|
169
|
+
for (const output of rustOutputs) {
|
|
170
|
+
const fullPath = join(resolvedOutDir, output.path);
|
|
171
|
+
const dir = dirname(fullPath);
|
|
172
|
+
nodeFs.mkdirSync(dir, { recursive: true });
|
|
173
|
+
|
|
174
|
+
// Respect overwrite flag: skip existing files when overwrite is false
|
|
175
|
+
if (!output.overwrite && nodeFs.existsSync(fullPath)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
nodeFs.writeFileSync(fullPath, output.content, "utf-8");
|
|
180
|
+
outputs.push({
|
|
181
|
+
filePath: fullPath,
|
|
182
|
+
content: output.content,
|
|
183
|
+
overwrite: output.overwrite,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Write cache hash
|
|
188
|
+
nodeFs.mkdirSync(dirname(resolvedCacheFile), { recursive: true });
|
|
189
|
+
nodeFs.writeFileSync(resolvedCacheFile, contentHash, "utf-8");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Tap the compile hook to run cargo build instead of the TypeScript compiler
|
|
193
|
+
pipeline.hooks.compile.tap(
|
|
194
|
+
"plugin-axum",
|
|
195
|
+
async (compileCtx: CompileContext, ctx) => {
|
|
196
|
+
const { spawnSync } = (await import(
|
|
197
|
+
/* @vite-ignore */ "child_process"
|
|
198
|
+
)) as {
|
|
199
|
+
spawnSync: (
|
|
200
|
+
cmd: string,
|
|
201
|
+
args: string[],
|
|
202
|
+
opts: { cwd?: string; encoding?: string },
|
|
203
|
+
) => {
|
|
204
|
+
status: number | null;
|
|
205
|
+
stdout: string;
|
|
206
|
+
stderr: string;
|
|
207
|
+
error?: Error;
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const resolvedOutDir = outDir ?? ctx.rootDir;
|
|
212
|
+
const result = spawnSync("cargo", ["build"], {
|
|
213
|
+
cwd: resolvedOutDir,
|
|
214
|
+
encoding: "utf-8",
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
compileCtx.handled = true;
|
|
218
|
+
compileCtx.compiler = "cargo";
|
|
219
|
+
|
|
220
|
+
if (result.error) {
|
|
221
|
+
compileCtx.result = {
|
|
222
|
+
success: false,
|
|
223
|
+
errors: [result.error.message],
|
|
224
|
+
};
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (result.status !== 0) {
|
|
229
|
+
const errorOutput =
|
|
230
|
+
result.stderr || result.stdout || "cargo build failed";
|
|
231
|
+
compileCtx.result = {
|
|
232
|
+
success: false,
|
|
233
|
+
errors: [errorOutput.trim()],
|
|
234
|
+
};
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
compileCtx.result = { success: true, errors: [] };
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ─── File Resolution Helpers ─────────────────────────────────
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Resolve TypeScript type definition files from the project root.
|
|
249
|
+
* Looks for files matching common TypoKit type patterns.
|
|
250
|
+
*/
|
|
251
|
+
function resolveTypeFiles(
|
|
252
|
+
rootDir: string,
|
|
253
|
+
fs: {
|
|
254
|
+
existsSync: (p: string) => boolean;
|
|
255
|
+
readdirSync: (p: string) => string[];
|
|
256
|
+
statSync: (p: string) => { isFile(): boolean; isDirectory(): boolean };
|
|
257
|
+
},
|
|
258
|
+
join: (...args: string[]) => string,
|
|
259
|
+
): string[] {
|
|
260
|
+
return resolvePatternFiles(rootDir, "types", fs, join);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Resolve TypeScript route contract files from the project root.
|
|
265
|
+
* Looks for files matching common TypoKit route patterns.
|
|
266
|
+
*/
|
|
267
|
+
function resolveRouteFiles(
|
|
268
|
+
rootDir: string,
|
|
269
|
+
fs: {
|
|
270
|
+
existsSync: (p: string) => boolean;
|
|
271
|
+
readdirSync: (p: string) => string[];
|
|
272
|
+
statSync: (p: string) => { isFile(): boolean; isDirectory(): boolean };
|
|
273
|
+
},
|
|
274
|
+
join: (...args: string[]) => string,
|
|
275
|
+
): string[] {
|
|
276
|
+
return resolvePatternFiles(rootDir, "routes", fs, join);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Resolve files from a subdirectory matching .ts extension.
|
|
281
|
+
*/
|
|
282
|
+
function resolvePatternFiles(
|
|
283
|
+
rootDir: string,
|
|
284
|
+
subDir: string,
|
|
285
|
+
fs: {
|
|
286
|
+
existsSync: (p: string) => boolean;
|
|
287
|
+
readdirSync: (p: string) => string[];
|
|
288
|
+
statSync: (p: string) => { isFile(): boolean; isDirectory(): boolean };
|
|
289
|
+
},
|
|
290
|
+
join: (...args: string[]) => string,
|
|
291
|
+
): string[] {
|
|
292
|
+
const dir = join(rootDir, subDir);
|
|
293
|
+
if (!fs.existsSync(dir)) return [];
|
|
294
|
+
|
|
295
|
+
const results: string[] = [];
|
|
296
|
+
const entries = fs.readdirSync(dir);
|
|
297
|
+
for (const entry of entries) {
|
|
298
|
+
const fullPath = join(dir, entry);
|
|
299
|
+
try {
|
|
300
|
+
const stat = fs.statSync(fullPath);
|
|
301
|
+
if (stat.isFile() && entry.endsWith(".ts")) {
|
|
302
|
+
results.push(fullPath);
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
// Skip files that can't be stat'd
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return results.sort();
|
|
309
|
+
}
|
package/src/lib.rs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
mod rust_codegen;
|
|
2
|
+
|
|
3
|
+
use std::collections::HashMap;
|
|
4
|
+
use napi::bindgen_prelude::*;
|
|
5
|
+
use napi_derive::napi;
|
|
6
|
+
use typokit_transform_native::{parser, route_compiler};
|
|
7
|
+
|
|
8
|
+
/// A single generated Rust code output file
|
|
9
|
+
#[napi(object)]
|
|
10
|
+
#[derive(Debug, Clone)]
|
|
11
|
+
pub struct JsRustGeneratedOutput {
|
|
12
|
+
/// Relative path for the generated file (e.g., ".typokit/models/user.rs")
|
|
13
|
+
pub path: String,
|
|
14
|
+
/// Generated file content
|
|
15
|
+
pub content: String,
|
|
16
|
+
/// Whether to overwrite an existing file at this path
|
|
17
|
+
pub overwrite: bool,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Generate Rust (Axum) server code from TypeScript schema type files and route contract files.
|
|
21
|
+
///
|
|
22
|
+
/// Parses type definitions and route contracts from the given files, then generates
|
|
23
|
+
/// a complete Axum server: structs, router, sqlx DB layer, handlers, services,
|
|
24
|
+
/// middleware, and project scaffold (Cargo.toml, main.rs, lib.rs, app.rs, error.rs).
|
|
25
|
+
///
|
|
26
|
+
/// Returns an array of GeneratedOutput objects specifying the file path, content,
|
|
27
|
+
/// and whether to overwrite existing files.
|
|
28
|
+
#[napi]
|
|
29
|
+
pub fn generate_rust_codegen(
|
|
30
|
+
type_file_paths: Vec<String>,
|
|
31
|
+
route_file_paths: Vec<String>,
|
|
32
|
+
) -> Result<Vec<JsRustGeneratedOutput>> {
|
|
33
|
+
// 1. Parse and extract types (with full JSDoc metadata)
|
|
34
|
+
let type_map = parser::parse_and_extract_types(&type_file_paths)
|
|
35
|
+
.map_err(|e| Error::from_reason(e))?;
|
|
36
|
+
|
|
37
|
+
// 2. Extract route entries from route contract files
|
|
38
|
+
let mut all_route_entries = Vec::new();
|
|
39
|
+
for path in &route_file_paths {
|
|
40
|
+
let source = std::fs::read_to_string(path)
|
|
41
|
+
.map_err(|e| Error::from_reason(format!("Failed to read file {}: {}", path, e)))?;
|
|
42
|
+
let parsed = parser::parse_typescript(path, &source)
|
|
43
|
+
.map_err(|e| Error::from_reason(e))?;
|
|
44
|
+
let entries = route_compiler::extract_route_contracts(&parsed.module);
|
|
45
|
+
all_route_entries.extend(entries);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 3. Generate Rust codegen outputs
|
|
49
|
+
let outputs = rust_codegen::generate(&type_map, &all_route_entries);
|
|
50
|
+
|
|
51
|
+
Ok(outputs
|
|
52
|
+
.into_iter()
|
|
53
|
+
.map(|o| JsRustGeneratedOutput {
|
|
54
|
+
path: o.path,
|
|
55
|
+
content: o.content,
|
|
56
|
+
overwrite: o.overwrite,
|
|
57
|
+
})
|
|
58
|
+
.collect())
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Compute a SHA-256 content hash of the given file paths and their contents.
|
|
62
|
+
///
|
|
63
|
+
/// Used for cache invalidation: if the hash matches a previous build, outputs
|
|
64
|
+
/// can be reused without regeneration.
|
|
65
|
+
#[napi]
|
|
66
|
+
pub fn compute_content_hash(file_paths: Vec<String>) -> Result<String> {
|
|
67
|
+
use sha2::{Sha256, Digest};
|
|
68
|
+
let mut hasher = Sha256::new();
|
|
69
|
+
let mut sorted_paths = file_paths.clone();
|
|
70
|
+
sorted_paths.sort();
|
|
71
|
+
|
|
72
|
+
for path in &sorted_paths {
|
|
73
|
+
let content = std::fs::read_to_string(path)
|
|
74
|
+
.map_err(|e| Error::from_reason(format!("Failed to read file {}: {}", path, e)))?;
|
|
75
|
+
hasher.update(path.as_bytes());
|
|
76
|
+
hasher.update(content.as_bytes());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Ok(hex::encode(hasher.finalize()))
|
|
80
|
+
}
|