// Router configuration and chunk manifest
///|
/// Chunk manifest entry (from Rolldown/Vite manifest.json)
/// Maps entry files to their output chunks and dependencies
pub struct ChunkManifestEntry {
/// Output file path (e.g., "counter.js")
file : String
/// Import dependencies (e.g., ["_shared/client-abc123.js"])
imports : Array[String]
}
///|
/// Create a chunk manifest entry
pub fn ChunkManifestEntry::new(
file : String,
imports : Array[String],
) -> ChunkManifestEntry {
{ file, imports }
}
///|
/// Chunk manifest - maps entry names to their output info
/// Used to resolve shared chunk dependencies for preloading
pub struct ChunkManifest {
/// Map of entry name -> manifest entry
entries : Map[String, ChunkManifestEntry]
/// Base URL prefix for chunks (e.g., "/static/")
base_url : String
}
///|
/// Create empty chunk manifest
pub fn ChunkManifest::empty() -> ChunkManifest {
{ entries: {}, base_url: "/static/" }
}
///|
/// Create chunk manifest with base URL
pub fn ChunkManifest::new(base_url : String) -> ChunkManifest {
{ entries: {}, base_url }
}
///|
/// Add entry to manifest
pub fn ChunkManifest::add(
self : ChunkManifest,
name : String,
entry : ChunkManifestEntry,
) -> Unit {
self.entries[name] = entry
}
///|
/// Get all chunk URLs for an entry (including dependencies)
pub fn ChunkManifest::get_chunks(
self : ChunkManifest,
entry_url : String,
) -> Array[String] {
// Extract entry name from URL (e.g., "/static/counter.js" -> "counter.js")
let entry_name = extract_entry_name(entry_url, self.base_url)
match self.resolve_entry(entry_name) {
Some(entry) => {
let chunks : Array[String] = []
self.collect_import_chunks(entry, chunks, [])
chunks
}
None => []
}
}
///|
fn is_manifest_url(path : String) -> Bool {
path.has_prefix("http://") ||
path.has_prefix("https://") ||
path.has_prefix("//") ||
path.has_prefix("/")
}
///|
fn ChunkManifest::manifest_path_to_url(
self : ChunkManifest,
path : String,
) -> String {
if is_manifest_url(path) {
path
} else {
self.base_url + path
}
}
///|
fn ChunkManifest::resolve_entry(
self : ChunkManifest,
reference : String,
) -> ChunkManifestEntry? {
let entry_name = extract_entry_name(reference, self.base_url)
match self.entries.get(reference) {
Some(entry) => Some(entry)
None =>
match self.entries.get(entry_name) {
Some(entry) => Some(entry)
None => {
for item in self.entries {
let entry = item.1
if entry.file == reference || entry.file == entry_name {
return Some(entry)
}
}
None
}
}
}
}
///|
fn ChunkManifest::collect_import_chunks(
self : ChunkManifest,
entry : ChunkManifestEntry,
chunks : Array[String],
visited : Array[String],
) -> Unit {
if visited.contains(entry.file) {
return
}
let next_visited = visited.copy()
next_visited.push(entry.file)
for imp in entry.imports {
match self.resolve_entry(imp) {
Some(dep) => {
let url = self.manifest_path_to_url(dep.file)
if !chunks.contains(url) {
chunks.push(url)
}
self.collect_import_chunks(dep, chunks, next_visited)
}
None => {
let url = self.manifest_path_to_url(imp)
if !chunks.contains(url) {
chunks.push(url)
}
}
}
}
}
///|
fn extract_entry_name(url : String, base_url : String) -> String {
if url.has_prefix(base_url) {
// Remove base_url prefix
let result = url[base_url.length():].to_owned()
result
} else {
// Find last slash and get filename
let mut last_slash = -1
let chars = url.to_array()
for i = 0; i < chars.length(); i = i + 1 {
if chars[i] == '/' {
last_slash = i
}
}
if last_slash >= 0 && last_slash < url.length() - 1 {
let result = url[last_slash + 1:].to_owned()
result
} else {
url
}
}
}
///|
/// Parse chunk manifest from JSON string (Rolldown/Vite format)
pub fn ChunkManifest::parse(
json_str : String,
base_url : String,
) -> ChunkManifest? {
let json : Json = @json.parse(json_str) catch { _ => return None }
let obj = match json {
Object(o) => o
_ => return None
}
let manifest = ChunkManifest::new(base_url)
for entry in obj {
let name = entry.0
let entry_obj = match entry.1 {
Object(o) => o
_ => continue
}
// Get "file" field
let file = match entry_obj.get("file") {
Some(String(s)) => s
_ => continue
}
// Get "imports" field (optional)
let imports : Array[String] = match entry_obj.get("imports") {
Some(Array(arr)) => {
let result : Array[String] = []
for item in arr {
match item {
String(s) => result.push(s)
_ => continue
}
}
result
}
_ => []
}
manifest.add(name, { file, imports })
}
Some(manifest)
}
///|
/// Router configuration for customizing routing behavior
pub struct RouterConfig {
/// Prefix for page titles (e.g., "MyApp - ")
title_prefix : String
/// Default title when page has no title
default_title : String
/// Default head HTML (styles, meta tags, etc.)
default_head : String
/// Stylesheet asset paths attached to every page.
styles : Array[String]
/// Client-side module script asset paths attached to every page.
client_scripts : Array[String]
/// URL for the island loader script
loader_url : String
/// URL for the Web Components loader script
wc_loader_url : String
/// Enable modulepreload for island scripts (default: true)
enable_preload : Bool
/// Enable streaming SSR for full-page responses (default: false)
/// Note: fragment responses and ISR(with cache) are always non-streaming.
/// Note: streaming uses `root_template` when `__LUNA_MAIN__` exists;
/// otherwise it falls back to built-in page shell.
enable_streaming : Bool
/// Additional library URLs to preload (e.g., ["/static/lib.js"])
preload_libs : Array[String]
/// Chunk manifest for resolving shared chunk dependencies
chunk_manifest : ChunkManifest?
/// Root HTML template with placeholders:
/// - __LUNA_TITLE__ : Page title
/// - __LUNA_PRELOAD__ : Modulepreload tags
/// - __LUNA_HEAD__ : Custom head HTML
/// - __LUNA_MAIN__ : Main content
root_template : String
/// ISR cache for incremental static regeneration (None = disabled)
/// Currently uses MemoryCache; can be replaced with KVCache for production
isr_cache : @isr.MemoryCache?
/// Cache strategy for ISR pages (CDN SWR, Hybrid, or App-only ISR)
/// Controls Cache-Control headers and CDN/app layer interaction.
/// None = use AppIsr (default, backward compatible)
cache_strategy : @isr.CDNCacheStrategy?
/// Server island registry for deferred server-side rendering
/// None = no server islands (default)
server_islands : ServerIslandRegistry?
}
///|
/// Default root HTML template
fn default_root_template() -> String {
"__LUNA_TITLE____LUNA_PRELOAD____LUNA_HEAD____LUNA_MAIN__
"
}
///|
/// Create default router configuration
pub fn RouterConfig::default() -> RouterConfig {
{
title_prefix: "",
default_title: "App",
default_head: "",
styles: [],
client_scripts: [],
loader_url: "/__sol__/loader.js",
wc_loader_url: "/__sol__/wc-loader.js",
enable_preload: true,
enable_streaming: false,
preload_libs: [],
chunk_manifest: None,
root_template: default_root_template(),
isr_cache: None,
cache_strategy: None,
server_islands: None,
}
}
///|
/// Set ISR cache for incremental static regeneration
pub fn RouterConfig::with_isr_cache(
self : RouterConfig,
cache : @isr.MemoryCache,
) -> RouterConfig {
{ ..self, isr_cache: Some(cache) }
}
///|
/// Set chunk manifest for resolving shared chunk dependencies
pub fn RouterConfig::with_chunk_manifest(
self : RouterConfig,
manifest : ChunkManifest,
) -> RouterConfig {
{ ..self, chunk_manifest: Some(manifest) }
}
///|
/// Parse an external bundler manifest JSON and use it for SSR modulepreload.
/// This accepts Vite/Rolldown-style manifest objects keyed either by source
/// entry or by emitted file.
pub fn RouterConfig::with_client_manifest_json(
self : RouterConfig,
json : String,
base_url? : String = "/static/",
) -> RouterConfig {
match ChunkManifest::parse(json, base_url) {
Some(manifest) => self.with_chunk_manifest(manifest)
None => self
}
}
///|
/// Add library URLs to preload
pub fn RouterConfig::with_preload_libs(
self : RouterConfig,
libs : Array[String],
) -> RouterConfig {
{ ..self, preload_libs: libs }
}
///|
/// Enable/disable streaming SSR for full-page responses.
pub fn RouterConfig::with_streaming_ssr(
self : RouterConfig,
enabled? : Bool = true,
) -> RouterConfig {
{ ..self, enable_streaming: enabled }
}
///|
/// Set default head HTML
pub fn RouterConfig::with_default_head(
self : RouterConfig,
head : String,
) -> RouterConfig {
{ ..self, default_head: head }
}
///|
/// Attach a stylesheet asset to every page.
pub fn RouterConfig::with_style(
self : RouterConfig,
style : String,
) -> RouterConfig {
self.with_styles([style])
}
///|
/// Attach stylesheet assets to every page.
pub fn RouterConfig::with_styles(
self : RouterConfig,
styles : Array[String],
) -> RouterConfig {
{ ..self, styles: merge_asset_paths(self.styles, styles) }
}
///|
/// Attach a client module script asset to every page.
pub fn RouterConfig::with_client_script(
self : RouterConfig,
script : String,
) -> RouterConfig {
self.with_client_scripts([script])
}
///|
/// Attach client module script assets to every page.
pub fn RouterConfig::with_client_scripts(
self : RouterConfig,
scripts : Array[String],
) -> RouterConfig {
{ ..self, client_scripts: merge_asset_paths(self.client_scripts, scripts) }
}
///|
/// Set loader URL
pub fn RouterConfig::with_loader_url(
self : RouterConfig,
url : String,
) -> RouterConfig {
{ ..self, loader_url: url }
}
///|
/// Set Web Components loader URL
pub fn RouterConfig::with_wc_loader_url(
self : RouterConfig,
url : String,
) -> RouterConfig {
{ ..self, wc_loader_url: url }
}
///|
/// Set title prefix
pub fn RouterConfig::with_title_prefix(
self : RouterConfig,
prefix : String,
) -> RouterConfig {
{ ..self, title_prefix: prefix }
}
///|
/// Set default title
pub fn RouterConfig::with_default_title(
self : RouterConfig,
title : String,
) -> RouterConfig {
{ ..self, default_title: title }
}
///|
/// Set cache strategy for ISR pages
pub fn RouterConfig::with_cache_strategy(
self : RouterConfig,
strategy : @isr.CDNCacheStrategy,
) -> RouterConfig {
{ ..self, cache_strategy: Some(strategy) }
}
///|
/// Set server island registry for deferred rendering
pub fn RouterConfig::with_server_islands(
self : RouterConfig,
registry : ServerIslandRegistry,
) -> RouterConfig {
{ ..self, server_islands: Some(registry) }
}
///|
/// Set root HTML template
/// Template placeholders:
/// - __LUNA_TITLE__ : Page title
/// - __LUNA_PRELOAD__ : Modulepreload tags
/// - __LUNA_HEAD__ : Custom head HTML
/// - __LUNA_MAIN__ : Main content
pub fn RouterConfig::with_root_template(
self : RouterConfig,
template : String,
) -> RouterConfig {
{ ..self, root_template: template }
}
///|
fn resolve_router_config(
config : RouterConfig?,
title_prefix? : String = "",
default_title? : String = "App",
default_head? : String = "",
loader_url? : String = "/__sol__/loader.js",
wc_loader_url? : String = "/__sol__/wc-loader.js",
) -> RouterConfig {
match config {
Some(existing) => existing
None =>
RouterConfig::default()
.with_title_prefix(title_prefix)
.with_default_title(default_title)
.with_default_head(default_head)
.with_loader_url(loader_url)
.with_wc_loader_url(wc_loader_url)
}
}