deforge_adapter_dc/
lib.rs1use deforge_core::ProjectConfig;
2use std::fs;
3use std::path::Path;
4use walkdir::WalkDir;
5
6pub fn generate(output_dir: &Path, config: &ProjectConfig) -> anyhow::Result<()> {
7 let mut routes = Vec::new();
9 let src_dir = output_dir;
10
11 for entry in WalkDir::new(src_dir) {
12 let entry = entry?;
13 if entry.file_type().is_file() {
14 let path = entry.path();
15 if let Some(ext) = path.extension() {
16 if ext == "ts" || ext == "js" {
17 if path.to_string_lossy().contains("/adapters/") {
19 continue;
20 }
21
22 let relative = path.strip_prefix(src_dir).unwrap();
23 let route = filename_to_route(relative.to_string_lossy().as_ref());
24 let import_path = format!(
25 "./{}",
26 relative
27 .to_string_lossy()
28 .to_string()
29 .trim_end_matches(".ts")
30 .trim_end_matches(".js")
31 );
32 routes.push((route, import_path));
33 }
34 }
35 }
36 }
37
38 generate_router(output_dir, &routes, config)?;
40
41 let deno_json = serde_json::json!({
43 "tasks": {
44 "start": "deno run -A main.ts",
45 "dev": "deno run --watch -A main.ts"
46 },
47 "imports": {
48 "std/": "https://deno.land/std@0.208.0/"
49 }
50 });
51
52 fs::write(
53 output_dir.join("deno.json"),
54 serde_json::to_string_pretty(&deno_json)?,
55 )?;
56
57 let kv_adapter = r#"/**
59 * Deno KV Adapter
60 */
61
62export function createKVAdapter(kv: Deno.Kv) {
63 return {
64 async get(key: string): Promise<string | null> {
65 const result = await kv.get([key]);
66 return result.value ? String(result.value) : null;
67 },
68
69 async getJSON<T = any>(key: string): Promise<T | null> {
70 const result = await kv.get([key]);
71 return result.value as T | null;
72 },
73
74 async put(key: string, value: string, options?: any): Promise<void> {
75 const kvOptions: any = {};
76 if (options?.expirationTtl) {
77 kvOptions.expireIn = options.expirationTtl * 1000;
78 }
79 await kv.set([key], value, kvOptions);
80 },
81
82 async putJSON<T = any>(key: string, value: T, options?: any): Promise<void> {
83 const kvOptions: any = {};
84 if (options?.expirationTtl) {
85 kvOptions.expireIn = options.expirationTtl * 1000;
86 }
87 await kv.set([key], value, kvOptions);
88 },
89
90 async delete(key: string): Promise<void> {
91 await kv.delete([key]);
92 },
93
94 async list(options?: any) {
95 const entries = kv.list(options?.prefix ? { prefix: [options.prefix] } : {});
96 const keys: any[] = [];
97
98 for await (const entry of entries) {
99 keys.push({
100 name: entry.key[0],
101 metadata: entry.versionstamp
102 });
103 }
104
105 return {
106 keys,
107 list_complete: true,
108 cursor: undefined
109 };
110 },
111
112 async exists(key: string): Promise<boolean> {
113 const result = await kv.get([key]);
114 return result.value !== null;
115 }
116 };
117}
118"#;
119
120 let adapters_dir = output_dir.join("adapters");
121 fs::create_dir_all(&adapters_dir)?;
122 fs::write(adapters_dir.join("kv-adapter.ts"), kv_adapter)?;
123
124 Ok(())
125}
126
127fn generate_router(
128 output_dir: &Path,
129 routes: &[(String, String)],
130 config: &ProjectConfig,
131) -> anyhow::Result<()> {
132 let has_kv = config
133 .services
134 .as_ref()
135 .and_then(|s| s.kv.as_ref())
136 .map(|kv| kv.enabled)
137 .unwrap_or(false);
138
139 let mut imports = String::new();
140 let mut route_handlers = String::new();
141
142 for (i, (route, import_path)) in routes.iter().enumerate() {
143 imports.push_str(&format!("import handler{i} from '{import_path}';\n"));
144
145 route_handlers.push_str(&format!(
146 r#" if (matchRoute(url.pathname, '{route}')) {{
147 return handler{i}(request);
148 }}
149"#
150 ));
151 }
152
153 let kv_import = if has_kv {
154 "import { createKVAdapter } from './adapters/kv-adapter.ts';\n\n"
155 } else {
156 ""
157 };
158 let kv_comment = if has_kv { "" } else { "undefined; // " };
159
160 let router = format!(
161 r#"/**
162 * Deno Deploy Main Entry
163 * Auto-generated router
164 */
165
166{imports}{kv_import}
167
168const kv = {kv_comment}await Deno.openKv() : undefined;
169
170Deno.serve(async (request: Request): Promise<Response> => {{
171 const url = new URL(request.url);
172
173{route_handlers}
174 // 404 Not Found
175 return new Response('Not Found', {{ status: 404 }});
176}});
177
178function matchRoute(pathname: string, route: string): boolean {{
179 // Exact match
180 if (pathname === route) return true;
181
182 // Dynamic parameter match
183 const routeParts = route.split('/');
184 const pathParts = pathname.split('/');
185
186 if (routeParts.length !== pathParts.length) return false;
187
188 return routeParts.every((part, i) => {{
189 if (part.startsWith(':')) return true;
190 return part === pathParts[i];
191 }});
192}}
193
194console.log('🚀 Deno Deploy server running...');
195"#
196 );
197
198 fs::write(output_dir.join("main.ts"), router)?;
199
200 Ok(())
201}
202
203fn filename_to_route(filename: &str) -> String {
204 let without_ext = filename.trim_end_matches(".ts").trim_end_matches(".js");
205
206 let route = if without_ext == "index" {
207 "/"
208 } else if without_ext.ends_with("/index") {
209 without_ext.trim_end_matches("/index")
210 } else {
211 without_ext
212 };
213
214 let route = route.replace("[", ":").replace("]", "");
215
216 if route.starts_with('/') {
217 route.to_string()
218 } else {
219 format!("/{route}")
220 }
221}