// Sol Compiler - Rolldown-based bundler for Islands
// ============================================================================
// Rolldown Bindings
// ============================================================================
///|
extern "js" fn ffi_rolldown_build(
options : @js.Any,
) -> @js.Promise[RolldownOutput] =
#| async (options) => {
#| const { build } = await import('rolldown');
#| return await build(options);
#| }
///|
/// Rolldown build output
#external
pub type RolldownOutput
///|
pub fn RolldownOutput::as_any(self : RolldownOutput) -> @js.Any = "%identity"
///|
/// Get output chunks from build result
pub fn RolldownOutput::output(self : RolldownOutput) -> Array[RolldownChunk] {
let output_arr = self.as_any()._get("output")
if @js.is_undefined(output_arr) || @js.is_null(output_arr) {
return []
}
let len : Int = @js.identity(output_arr._get("length"))
let result : Array[RolldownChunk] = []
for i = 0; i < len; i = i + 1 {
result.push(output_arr._get(i.to_string()).cast())
}
result
}
///|
/// Rolldown output chunk
#external
pub type RolldownChunk
///|
pub fn RolldownChunk::as_any(self : RolldownChunk) -> @js.Any = "%identity"
///|
/// Get the file name
pub fn RolldownChunk::file_name(self : RolldownChunk) -> String {
@js.identity(self.as_any()._get("fileName"))
}
///|
/// Get the chunk code
pub fn RolldownChunk::code(self : RolldownChunk) -> String {
let code = self.as_any()._get("code")
if @js.is_undefined(code) {
""
} else {
@js.identity(code)
}
}
///|
/// Check if this is an entry chunk
pub fn RolldownChunk::is_entry(self : RolldownChunk) -> Bool {
let is_entry = self.as_any()._get("isEntry")
if @js.is_undefined(is_entry) {
false
} else {
@js.identity(is_entry)
}
}
// ============================================================================
// Build Configuration
// ============================================================================
///|
/// Output format for bundled code
pub enum OutputFormat {
Esm
}
///|
pub fn OutputFormat::to_string(self : OutputFormat) -> String {
match self {
Esm => "esm"
}
}
///|
/// Bundle configuration
pub struct BundleConfig {
/// Input entries: Map of output name -> input path
input : Array[(String, String)]
/// Output directory
output_dir : String
/// Output format (default: esm)
format : OutputFormat
/// Entry file name pattern (default: "[name].js")
entry_file_names : String
/// Chunk file name pattern (default: "_shared/[name]-[hash].js")
chunk_file_names : String
/// Whether to minify output
minify : Bool
/// Whether to generate source maps
sourcemap : Bool
}
///|
/// Create default bundle configuration
pub fn BundleConfig::new(
input : Array[(String, String)],
output_dir : String,
) -> BundleConfig {
{
input,
output_dir,
format: Esm,
entry_file_names: "[name].js",
chunk_file_names: "_shared/[name]-[hash].js",
minify: false,
sourcemap: false,
}
}
///|
/// Builder: set output format
pub fn BundleConfig::with_format(
self : BundleConfig,
format : OutputFormat,
) -> BundleConfig {
{ ..self, format, }
}
///|
/// Builder: set minification
pub fn BundleConfig::with_minify(
self : BundleConfig,
minify : Bool,
) -> BundleConfig {
{ ..self, minify, }
}
///|
/// Builder: set source map generation
pub fn BundleConfig::with_sourcemap(
self : BundleConfig,
sourcemap : Bool,
) -> BundleConfig {
{ ..self, sourcemap, }
}
// ============================================================================
// Build API
// ============================================================================
///|
fn make_rolldown_options(config : BundleConfig) -> @js.Any {
let opts = @js.new_object()
// Build input object
let input_obj = @js.new_object()
for entry in config.input {
let (name, path) = entry
input_obj._set(name, @js.any(path))
}
opts._set("input", input_obj)
// Build output object
let output_obj = @js.new_object()
output_obj._set("dir", @js.any(config.output_dir))
output_obj._set("format", @js.any(config.format.to_string()))
output_obj._set("entryFileNames", @js.any(config.entry_file_names))
output_obj._set("chunkFileNames", @js.any(config.chunk_file_names))
if config.sourcemap {
output_obj._set("sourcemap", @js.any(true))
}
opts._set("output", output_obj)
opts
}
///|
/// Build with rolldown (async)
pub async fn build(config : BundleConfig) -> RolldownOutput {
let opts = make_rolldown_options(config)
ffi_rolldown_build(opts).wait()
}
///|
/// Build islands from configuration
/// Takes a list of (island_name, entry_path) pairs and outputs to static directory
pub async fn bundle_islands(
islands : Array[(String, String)],
output_dir : String,
) -> RolldownOutput {
let config = BundleConfig::new(islands, output_dir)
build(config)
}
///|
/// Build islands with minification for production
pub async fn bundle_islands_prod(
islands : Array[(String, String)],
output_dir : String,
) -> RolldownOutput {
let config = BundleConfig::new(islands, output_dir).with_minify(true)
build(config)
}
// ============================================================================
// Result helpers
// ============================================================================
///|
/// Bundle result with success/failure info
pub struct BundleResult {
success : Bool
output : Array[String]
errors : Array[String]
}
///|
/// Build islands and return simplified result
pub async fn bundle_islands_result(
islands : Array[(String, String)],
output_dir : String,
) -> BundleResult {
try {
let result = bundle_islands(islands, output_dir)
let chunks = result.output()
let files : Array[String] = []
for chunk in chunks {
files.push(chunk.file_name())
}
{ success: true, output: files, errors: [] }
} catch {
e => { success: false, output: [], errors: [e.to_string()] }
}
}
// ============================================================================
// Manifest-based build
// ============================================================================
///|
/// Island manifest entry
pub struct IslandManifestEntry {
name : String
entry_path : String
}
///|
/// Island manifest (written by generate, read by compiler)
pub struct IslandManifest {
islands : Array[IslandManifestEntry]
output_dir : String
}
///|
/// Parse island manifest from JSON string
pub fn parse_island_manifest(json_str : String) -> IslandManifest? {
try {
let json = @json.parse(json_str.view())
guard json is Object(obj) else { return None }
let output_dir = match obj.get("output_dir") {
Some(String(s)) => s
_ => "static"
}
let islands : Array[IslandManifestEntry] = []
match obj.get("islands") {
Some(Array(arr)) =>
for item in arr {
guard item is Object(island_obj) else { continue }
let name = match island_obj.get("name") {
Some(String(s)) => s
_ => continue
}
let entry_path = match island_obj.get("entry_path") {
Some(String(s)) => s
_ => continue
}
islands.push({ name, entry_path })
}
_ => ()
}
Some({ islands, output_dir })
} catch {
_ => None
}
}
///|
/// Generate island manifest JSON
pub fn generate_island_manifest(
islands : Array[(String, String)],
output_dir : String,
) -> String {
let buf = StringBuilder::new()
buf.write_string("{\n")
buf.write_string(" \"output_dir\": \"")
buf.write_string(output_dir)
buf.write_string("\",\n")
buf.write_string(" \"islands\": [\n")
for i, entry in islands {
let (name, entry_path) = entry
buf.write_string(" { \"name\": \"")
buf.write_string(name)
buf.write_string("\", \"entry_path\": \"")
buf.write_string(entry_path)
buf.write_string("\" }")
if i < islands.length() - 1 {
buf.write_string(",")
}
buf.write_string("\n")
}
buf.write_string(" ]\n")
buf.write_string("}\n")
buf.to_string()
}
///|
/// Build islands from manifest
pub async fn build_from_manifest(manifest : IslandManifest) -> RolldownOutput {
let entries : Array[(String, String)] = []
for island in manifest.islands {
entries.push((island.name, island.entry_path))
}
bundle_islands(entries, manifest.output_dir)
}
///|
/// Build islands from manifest file path (convenience for CLI)
/// Returns a promise that resolves when build is complete
/// cwd: working directory where rolldown should be resolved from
pub fn build_from_manifest_file(
manifest_path : String,
cwd : String,
on_success : (Array[String]) -> Unit,
on_error : (String) -> Unit,
) -> Unit {
let _ = ffi_build_from_manifest(manifest_path, cwd, on_success, on_error)
}
///|
extern "js" fn ffi_build_from_manifest(
manifest_path : String,
cwd : String,
on_success : (Array[String]) -> Unit,
on_error : (String) -> Unit,
) -> @js.Promise[Unit] =
#| async (manifestPath, cwd, onSuccess, onError) => {
#| try {
#| const importer = globalThis.__SOL_DYNAMIC_IMPORT__ ||
#| ((specifier) => import(specifier));
#| const fs = await importer('node:fs');
#| const path = await importer('node:path');
#| const { createRequire } = await importer('node:module');
#| const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
#| const manifest = JSON.parse(manifestContent);
#| // Resolve rolldown from the project's node_modules
#| const require = createRequire(path.join(cwd, 'package.json'));
#| const { build } = require('rolldown');
#| const input = {};
#| for (const island of manifest.islands) {
#| input[island.name] = island.entry_path;
#| }
#| // Change to cwd for relative path resolution
#| const originalCwd = process.cwd();
#| process.chdir(cwd);
#| try {
#| const result = await build({
#| input,
#| output: {
#| dir: manifest.output_dir,
#| format: 'esm',
#| entryFileNames: '[name].js',
#| chunkFileNames: '_shared/[name]-[hash].js',
#| },
#| });
#| const files = result.output.map(chunk => chunk.fileName);
#| onSuccess(files);
#| } finally {
#| process.chdir(originalCwd);
#| }
#| } catch (e) {
#| onError(e.message || String(e));
#| }
#| }