Skip to main content

deforge_adapter_dc/
lib.rs

1use 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    // Collect all handlers for router
8    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                    // Skip adapters directory
18                    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 main router
39    generate_router(output_dir, &routes, config)?;
40
41    // Generate deno.json
42    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    // Generate KV adapter
58    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}