// MBTX Chunk Splitting
//
// Extracts shared top-level declarations across multiple .mbtx JS outputs
// into a shared ESM module, reducing bundle size when multiple .mbtx
// routes share common MoonBit standard library code.
///|
/// Input entry for chunk splitting
pub(all) struct MbtxEntry {
/// Unique ID (e.g., "hello", "counter")
id : String
/// Raw JS code from moon build
code : String
}
///|
/// Result of chunk splitting
pub(all) struct MbtxChunkResult {
/// Shared module code (empty if nothing shared)
shared_code : String
/// Per-entry ESM modules with imports from shared
entries : Array[MbtxChunkEntry]
}
///|
/// A single entry after chunk splitting
pub(all) struct MbtxChunkEntry {
id : String
code : String
}
///|
/// Extract shared declarations across multiple .mbtx JS outputs.
/// Declarations appearing in 2+ files are moved to a shared module.
/// Each entry is rewritten to import from the shared module.
pub fn split_mbtx_chunks(
entries : Array[MbtxEntry],
shared_path? : String = "./_mbtx_shared.mjs",
) -> MbtxChunkResult {
if entries.length() <= 1 {
// Single entry: no sharing possible, just transform to ESM
let result_entries = entries.map(fn(e) {
MbtxChunkEntry::{ id: e.id, code: transform_mbtx_to_esm(e.code) }
})
return { shared_code: "", entries: result_entries }
}
ffi_split_mbtx_chunks(entries, shared_path)
}
///|
/// JS implementation of chunk splitting.
/// Parses top-level declarations, finds duplicates, and rewrites.
extern "js" fn ffi_split_mbtx_chunks(
entries : Array[MbtxEntry],
shared_path : String,
) -> MbtxChunkResult =
#| (entries, sharedPath) => {
#| // Parse top-level declarations from JS code
#| // Handles: function Name(...) { ... } and const Name = ...;
#| function parseDecls(code) {
#| const decls = [];
#| const lines = code.split('\n');
#| let i = 0;
#| while (i < lines.length) {
#| const line = lines[i];
#| // function declaration
#| const fnMatch = line.match(/^function\s+(_M\w+)\s*\(/);
#| if (fnMatch) {
#| let depth = 0, start = i;
#| for (; i < lines.length; i++) {
#| for (const ch of lines[i]) { if (ch === '{') depth++; if (ch === '}') depth--; }
#| if (depth === 0) { i++; break; }
#| }
#| const body = lines.slice(start, i).join('\n');
#| decls.push({ name: fnMatch[1], body, start, end: i });
#| continue;
#| }
#| // const declaration
#| const constMatch = line.match(/^const\s+(_M\w+)\s*=/);
#| if (constMatch) {
#| let depth = 0, start = i;
#| for (; i < lines.length; i++) {
#| for (const ch of lines[i]) { if (ch === '{') depth++; if (ch === '}') depth--; }
#| if (depth <= 0 && lines[i].includes(';')) { i++; break; }
#| }
#| const body = lines.slice(start, i).join('\n');
#| decls.push({ name: constMatch[1], body, start, end: i });
#| continue;
#| }
#| i++;
#| }
#| return decls;
#| }
#|
#| // Parse all entries
#| const parsed = entries.map(e => ({ id: e.id, code: e.code, decls: parseDecls(e.code) }));
#|
#| // Count occurrences of each declaration body
#| const bodyCount = new Map();
#| const bodyToName = new Map();
#| for (const p of parsed) {
#| for (const d of p.decls) {
#| const key = d.body;
#| bodyCount.set(key, (bodyCount.get(key) || 0) + 1);
#| bodyToName.set(key, d.name);
#| }
#| }
#|
#| // Shared = appears in 2+ entries
#| const sharedBodies = new Set();
#| const sharedNames = new Set();
#| for (const [body, count] of bodyCount) {
#| if (count >= 2) {
#| sharedBodies.add(body);
#| sharedNames.add(bodyToName.get(body));
#| }
#| }
#|
#| if (sharedBodies.size === 0) {
#| // Nothing shared
#| return {
#| shared_code: '',
#| entries: parsed.map(p => ({
#| id: p.id,
#| code: transformToEsm(p.code),
#| })),
#| };
#| }
#|
#| // Build shared module
#| const sharedLines = [];
#| for (const body of sharedBodies) {
#| // Replace leading 'function' or 'const' with 'export function' / 'export const'
#| const exported = body.replace(/^(function|const)\s/, 'export $1 ');
#| sharedLines.push(exported);
#| }
#| const shared_code = sharedLines.join('\n') + '\n';
#|
#| // Rewrite each entry: remove shared decls, add import
#| const resultEntries = parsed.map(p => {
#| const lines = p.code.split('\n');
#| // Remove shared declarations
#| const removeRanges = [];
#| for (const d of p.decls) {
#| if (sharedBodies.has(d.body)) {
#| removeRanges.push([d.start, d.end]);
#| }
#| }
#| // Build filtered code
#| const kept = [];
#| let skip = 0;
#| for (let i = 0; i < lines.length; i++) {
#| const inRange = removeRanges.some(([s, e]) => i >= s && i < e);
#| if (!inRange) kept.push(lines[i]);
#| }
#| // Add import statement
#| const names = p.decls.filter(d => sharedBodies.has(d.body)).map(d => d.name);
#| const importLine = `import { ${[...new Set(names)].join(', ')} } from '${sharedPath}';`;
#| const rewritten = importLine + '\n' + kept.join('\n');
#| return { id: p.id, code: transformToEsm(rewritten) };
#| });
#|
#| return { shared_code, entries: resultEntries };
#|
#| function transformToEsm(code) {
#| return code
#| .replace(/\(\(\) => \{/, 'export default function render(env) { globalThis.__MBTX_ENV__ = env; globalThis.__MBTX_RESPONSE__ = undefined;')
#| .replace(/\}\)\(\);?\s*$/, ' return globalThis.__MBTX_RESPONSE__; }\n');
#| }
#| }