use crate::parser::{Item, Program};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct CodeSplitConfig {
pub min_chunk_size: usize,
pub max_chunk_size: usize,
pub dynamic_imports: bool,
}
impl Default for CodeSplitConfig {
fn default() -> Self {
Self {
min_chunk_size: 20_000, max_chunk_size: 500_000, dynamic_imports: true,
}
}
}
#[derive(Debug, Clone)]
pub struct CodeChunk {
pub name: String,
pub code: String,
pub dependencies: Vec<String>,
pub is_entry: bool,
}
pub struct CodeSplitter {
config: CodeSplitConfig,
chunks: Vec<CodeChunk>,
}
impl CodeSplitter {
pub fn new(config: CodeSplitConfig) -> Self {
Self {
config,
chunks: Vec::new(),
}
}
pub fn split(&mut self, program: &Program, main_code: &str) -> Vec<CodeChunk> {
self.chunks.clear();
let dep_graph = self.build_dependency_graph(program);
let main_chunk = CodeChunk {
name: "main".to_string(),
code: main_code.to_string(),
dependencies: Vec::new(),
is_entry: true,
};
self.chunks.push(main_chunk);
self.split_by_module(program, &dep_graph);
if self.config.dynamic_imports {
self.split_by_dynamic_imports(program);
}
self.chunks.clone()
}
fn build_dependency_graph(&self, program: &Program) -> HashMap<String, HashSet<String>> {
let mut graph = HashMap::new();
for item in &program.items {
if let Item::Function { decl: func, .. } = item {
let deps = HashSet::new();
graph.insert(func.name.clone(), deps);
}
}
graph
}
fn split_by_module(
&mut self,
program: &Program,
_dep_graph: &HashMap<String, HashSet<String>>,
) {
let mut current_code = String::new();
let mut current_size = 0;
for item in &program.items {
if let Item::Function { decl: func, .. } = item {
let func_code = format!("function {}() {{}}\n", func.name);
let func_size = func_code.len();
if current_size + func_size > self.config.max_chunk_size && current_size > 0 {
if current_size >= self.config.min_chunk_size {
self.chunks.push(CodeChunk {
name: format!("chunk_{}", self.chunks.len()),
code: current_code.clone(),
dependencies: vec!["main".to_string()],
is_entry: false,
});
}
current_code.clear();
current_size = 0;
}
current_code.push_str(&func_code);
current_size += func_size;
}
}
if current_size >= self.config.min_chunk_size {
self.chunks.push(CodeChunk {
name: format!("chunk_{}", self.chunks.len()),
code: current_code,
dependencies: vec!["main".to_string()],
is_entry: false,
});
}
}
fn split_by_dynamic_imports(&mut self, _program: &Program) {
}
pub fn generate_chunk_loader(&self) -> String {
String::from(
r#"
// Windjammer Code Splitting Runtime
const __wj_chunks = {};
const __wj_loaded = new Set();
async function __wj_load_chunk(name) {
if (__wj_loaded.has(name)) {
return __wj_chunks[name];
}
try {
const module = await import(`./${name}.js`);
__wj_chunks[name] = module;
__wj_loaded.add(name);
return module;
} catch (e) {
console.error(`Failed to load chunk: ${name}`, e);
throw e;
}
}
"#,
)
}
}
pub fn split_code(program: &Program, main_code: &str, config: CodeSplitConfig) -> Vec<CodeChunk> {
let mut splitter = CodeSplitter::new(config);
splitter.split(program, main_code)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_code_splitter_creation() {
let config = CodeSplitConfig::default();
let splitter = CodeSplitter::new(config);
assert!(splitter.chunks.is_empty());
}
#[test]
fn test_default_config() {
let config = CodeSplitConfig::default();
assert_eq!(config.min_chunk_size, 20_000);
assert_eq!(config.max_chunk_size, 500_000);
assert!(config.dynamic_imports);
}
#[test]
fn test_chunk_loader_generation() {
let config = CodeSplitConfig::default();
let splitter = CodeSplitter::new(config);
let loader = splitter.generate_chunk_loader();
assert!(loader.contains("__wj_load_chunk"));
assert!(loader.contains("async function"));
}
}