package deno import ( "bufio" "fmt" "os" "path/filepath" "regexp" "strings" "text/template" ) // RouteEntry represents a parsed API endpoint from a TypeScript file. type RouteEntry struct { Method string // GET, POST, PUT, DELETE, PATCH Path string // e.g., "/api/weather/current" ImportPath string // relative import path, e.g., "./api/weather.ts" FunctionName string // original export name, e.g., "API_get_current" } // validMethods lists the HTTP methods recognized by the convention. var validMethods = map[string]bool{ "get": true, "post": true, "put": true, "delete": true, "patch": true, } // apiExportPattern matches "export async function API_..." or "export function API_..." var apiExportPattern = regexp.MustCompile(`^export\s+(?:async\s+)?function\s+(API_\w+)`) // ScanRoutes scans all .ts files in the api/ subdirectory for API_ prefixed exports // and returns a list of RouteEntry for each discovered endpoint. func ScanRoutes(apiDir string) ([]RouteEntry, error) { var routes []RouteEntry err := filepath.Walk(apiDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } ext := strings.ToLower(filepath.Ext(path)) if ext != ".ts" && ext != ".js" { return nil } // Derive the base route from file path relative to apiDir. relPath, err := filepath.Rel(apiDir, path) if err != nil { return fmt.Errorf("rel path: %w", err) } // Convert "weather.ts" → "weather", "sub/weather.ts" → "sub/weather" baseName := strings.TrimSuffix(relPath, filepath.Ext(relPath)) baseName = filepath.ToSlash(baseName) baseRoute := "/api/" + baseName // Import path relative to the generated router. importPath := "./api/" + filepath.ToSlash(relPath) // Scan file for API_ exports. fileRoutes, err := scanFileExports(path, baseRoute, importPath) if err != nil { return fmt.Errorf("scan %s: %w", relPath, err) } routes = append(routes, fileRoutes...) return nil }) return routes, err } // scanFileExports reads a TypeScript file and extracts API_ prefixed exports. func scanFileExports(filePath, baseRoute, importPath string) ([]RouteEntry, error) { file, err := os.Open(filePath) if err != nil { return nil, fmt.Errorf("open file: %w", err) } defer file.Close() var routes []RouteEntry scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) matches := apiExportPattern.FindStringSubmatch(line) if len(matches) < 2 { continue } funcName := matches[1] // e.g., "API_get_current" entry, ok := parseAPIFunctionName(funcName, baseRoute, importPath) if ok { routes = append(routes, entry) } } return routes, scanner.Err() } // parseAPIFunctionName parses "API_get_current" into a RouteEntry. // Convention: API_{method} → handles base route; API_{method}_{path} → handles sub-route. func parseAPIFunctionName(funcName, baseRoute, importPath string) (RouteEntry, bool) { // Strip "API_" prefix. rest := strings.TrimPrefix(funcName, "API_") if rest == "" { return RouteEntry{}, false } // Split on first "_" to extract method. parts := strings.SplitN(rest, "_", 2) method := strings.ToLower(parts[0]) if !validMethods[method] { return RouteEntry{}, false } path := baseRoute if len(parts) == 2 && parts[1] != "" { path = baseRoute + "/" + parts[1] } return RouteEntry{ Method: strings.ToUpper(method), Path: path, ImportPath: importPath, FunctionName: funcName, }, true } // routerTemplate is the Deno router entrypoint template. var routerTemplate = template.Must(template.New("router").Parse(`// Auto-generated by Docker Watcher — do not edit manually. import { serveDir } from "https://deno.land/std/http/file_server.ts"; {{- range .Imports}} import { {{.FunctionName}} as {{.Alias}} } from "{{.Path}}"; {{- end}} const routes: Array<{ method: string; path: string; handler: (req: Request) => Promise | Response }> = [ {{- range .Routes}} { method: "{{.Method}}", path: "{{.Path}}", handler: {{.Alias}} }, {{- end}} ]; Deno.serve({ port: 8000 }, async (req: Request): Promise => { const url = new URL(req.url); const method = req.method.toUpperCase(); // Match API routes. for (const route of routes) { if (route.method === method && url.pathname === route.path) { try { return await route.handler(req); } catch (e) { console.error("Handler error:", e); return new Response(JSON.stringify({ error: "Internal server error" }), { status: 500, headers: { "Content-Type": "application/json" }, }); } } } // Serve static files from /public. return serveDir(req, { fsRoot: "/app/public", quiet: true }); }); `)) // ImportEntry represents a single aliased import. type ImportEntry struct { FunctionName string // original name, e.g., "API_get" Alias string // unique alias, e.g., "time_API_get" Path string // import path, e.g., "./api/time.ts" } // routerData holds the data for the router template. type routerData struct { Imports []ImportEntry Routes []routeWithAlias } // routeWithAlias is a route entry using the aliased handler name. type routeWithAlias struct { Method string Path string Alias string } // GenerateRouter generates the Deno router TypeScript source from route entries. func GenerateRouter(routes []RouteEntry) (string, error) { var imports []ImportEntry var aliasedRoutes []routeWithAlias for _, r := range routes { // Derive a unique alias from the file base name + function name. // e.g., "./api/echo.ts" + "API_get" → "echo_API_get" baseName := filepath.Base(r.ImportPath) baseName = strings.TrimSuffix(baseName, filepath.Ext(baseName)) alias := baseName + "_" + r.FunctionName imports = append(imports, ImportEntry{ FunctionName: r.FunctionName, Alias: alias, Path: r.ImportPath, }) aliasedRoutes = append(aliasedRoutes, routeWithAlias{ Method: r.Method, Path: r.Path, Alias: alias, }) } data := routerData{Imports: imports, Routes: aliasedRoutes} var buf strings.Builder if err := routerTemplate.Execute(&buf, data); err != nil { return "", fmt.Errorf("execute router template: %w", err) } return buf.String(), nil } // GenerateDockerfile generates the Dockerfile for the Deno container. func GenerateDockerfile() string { return `FROM denoland/deno:latest WORKDIR /app # Copy static files. COPY public/ /app/public/ # Copy API source files and generated router. COPY api/ /app/api/ COPY router.ts /app/router.ts # Cache dependencies. RUN deno install --entrypoint router.ts EXPOSE 8000 CMD ["deno", "run", "--allow-net", "--allow-read", "--allow-env", "router.ts"] ` } // GenerateStaticDockerfile generates a minimal nginx Dockerfile for pure static sites. func GenerateStaticDockerfile() string { return `FROM nginx:alpine COPY . /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] ` }