// Route Analyzer
//
// Analyzes SolRoutes to extract dynamic routes for Cloudflare Workers
// run_worker_first configuration.
// ============================================================================
// Route Classification
// ============================================================================
///|
/// Classification of a route for static/dynamic handling
pub(all) enum RouteKind {
/// Static route - can be pre-rendered at build time
Static
/// Dynamic route - requires worker handling at runtime
Dynamic
}
///|
/// Analyzed route information
pub(all) struct AnalyzedRoute {
/// URL pattern (e.g., "/api/users/:id")
pattern : String
/// Whether this route is dynamic
kind : RouteKind
/// Reason for classification
reason : String
}
// ============================================================================
// Route Analysis
// ============================================================================
///|
/// Analyze SolRoutes and extract all routes with their classification
pub fn analyze_routes(routes : Array[SolRoutes]) -> Array[AnalyzedRoute] {
let result : Array[AnalyzedRoute] = []
analyze_routes_inner(routes, "", result)
result
}
///|
fn analyze_routes_inner(
routes : Array[SolRoutes],
prefix : String,
result : Array[AnalyzedRoute],
) -> Unit {
for route in routes {
match route {
Page(path~, handler~, title~, meta~, revalidate~, cache~) => {
let _ = (handler, title, meta, cache)
let full_path = normalize_path(prefix + path)
let (kind, reason) = classify_page_route(full_path, revalidate)
result.push({ pattern: full_path, kind, reason })
}
Layout(segment~, layout~, children~) => {
let _ = layout
let new_prefix = normalize_path(prefix + segment)
analyze_routes_inner(children, new_prefix, result)
}
Get(path~, handler~) => {
let _ = handler
let full_path = normalize_path(prefix + path)
result.push({
pattern: full_path,
kind: RouteKind::Dynamic,
reason: "API GET endpoint",
})
}
Post(path~, handler~) => {
let _ = handler
let full_path = normalize_path(prefix + path)
result.push({
pattern: full_path,
kind: RouteKind::Dynamic,
reason: "API POST endpoint",
})
}
Put(path~, handler~) => {
let _ = handler
let full_path = normalize_path(prefix + path)
result.push({
pattern: full_path,
kind: RouteKind::Dynamic,
reason: "API PUT endpoint",
})
}
Delete(path~, handler~) => {
let _ = handler
let full_path = normalize_path(prefix + path)
result.push({
pattern: full_path,
kind: RouteKind::Dynamic,
reason: "API DELETE endpoint",
})
}
Patch(path~, handler~) => {
let _ = handler
let full_path = normalize_path(prefix + path)
result.push({
pattern: full_path,
kind: RouteKind::Dynamic,
reason: "API PATCH endpoint",
})
}
WithMiddleware(middleware~, children~) => {
let _ = middleware
analyze_routes_inner(children, prefix, result)
}
}
}
}
///|
/// Classify a page route as static or dynamic
fn classify_page_route(path : String, revalidate : Int?) -> (RouteKind, String) {
// ISR routes are dynamic (need worker for revalidation)
if revalidate is Some(_) {
return (RouteKind::Dynamic, "ISR enabled (revalidate)")
}
// Routes with path parameters are dynamic
// Unless they have staticParams (handled at build time)
if has_dynamic_segment(path) {
return (RouteKind::Dynamic, "Dynamic path parameter")
}
// Catch-all routes are dynamic
if has_catch_all(path) {
return (RouteKind::Dynamic, "Catch-all route")
}
// Static route
(RouteKind::Static, "Static page")
}
///|
/// Check if path contains dynamic segments (:id, [id])
fn has_dynamic_segment(path : String) -> Bool {
let segments = path.split("/")
for segment in segments {
if segment.has_prefix(":") {
return true
}
if segment.has_prefix("[") &&
!(segment.has_prefix("[...")) &&
!(segment.has_prefix("[[...")) &&
segment.has_suffix("]") {
return true
}
}
false
}
///|
/// Check if path contains catch-all segments ([...slug], [[...slug]])
fn has_catch_all(path : String) -> Bool {
path.contains("[...") || path.contains("[[...")
}
// ============================================================================
// Cloudflare Configuration Generation
// ============================================================================
///|
/// Extract dynamic route patterns for run_worker_first
pub fn extract_dynamic_patterns(routes : Array[SolRoutes]) -> Array[String] {
let analyzed = analyze_routes(routes)
let patterns : Array[String] = []
for route in analyzed {
if route.kind is Dynamic {
// Convert to glob pattern for Cloudflare
let pattern = to_cloudflare_pattern(route.pattern)
if !(patterns.contains(pattern)) {
patterns.push(pattern)
}
}
}
// Sort for consistency
patterns.sort_by(fn(a, b) { a.compare(b) })
patterns
}
///|
/// Convert Sol route pattern to Cloudflare glob pattern
fn to_cloudflare_pattern(path : String) -> String {
let result = StringBuilder::new()
let segments = path.split("/")
let mut first = true
for segment in segments {
if segment.is_empty() {
continue
}
if !(first) {
result.write_string("/")
}
first = false
if segment.has_prefix(":") {
// :id -> * (single segment wildcard)
result.write_string("*")
} else if segment.has_prefix("[[...") && segment.has_suffix("]]") {
// [[...slug]] -> * (optional catch-all)
result.write_string("*")
} else if segment.has_prefix("[...") && segment.has_suffix("]") {
// [...slug] -> * (required catch-all)
result.write_string("*")
} else if segment.has_prefix("[") && segment.has_suffix("]") {
// [id] -> * (single segment)
result.write_string("*")
} else {
result.write_string(segment.to_string())
}
}
"/" + result.to_string()
}
///|
/// Generate wrangler.json assets config fragment
pub fn generate_wrangler_assets_config(
routes : Array[SolRoutes],
output_dir : String,
) -> String {
let patterns = extract_dynamic_patterns(routes)
if patterns.is_empty() {
// Pure static site
return "{\n \"directory\": \"\{output_dir}\",\n \"html_handling\": \"auto-trailing-slash\",\n \"not_found_handling\": \"404-page\"\n}"
}
// Site with dynamic routes
let patterns_json = StringBuilder::new()
patterns_json.write_string("[")
for i, pattern in patterns {
if i > 0 {
patterns_json.write_string(", ")
}
patterns_json.write_string("\"")
patterns_json.write_string(pattern)
patterns_json.write_string("\"")
}
patterns_json.write_string("]")
"{\n \"directory\": \"\{output_dir}\",\n \"html_handling\": \"auto-trailing-slash\",\n \"not_found_handling\": \"single-page-application\",\n \"run_worker_first\": \{patterns_json.to_string()}\n}"
}