const defaultHtml = ` macchiato.dev ${ '<' + 'script type="module" src="/app.js"><' + '/script>' } `.trim() const defaultIntro = ` window.Macchiato = { location: new MLocation(), } `.trim() export class Builder { constructor(files) { this.files = files } buildModule(file) { const script = document.createElement('script') script.setAttribute('type', 'module') let initAppend = "" let append = "" const data = file.data.replaceAll( /^\s*export\s+(?:class|function|async\s+function|const)\s+(\S+)/gms, (match, p1) => { const path = JSON.stringify(file.name) const mref = `Macchiato.modules[${path}]` const pref = `[${JSON.stringify(p1)}]` initAppend = `\n\n${mref} = {}` const s = `${mref}${pref} = ${p1}` append += "\n" + s return `// append: ${s}\n${match}` } ).replaceAll( /^\s*import\s+(\{[^}]+\})\s+from\s+("[^"]+"|')/gms, (match, p1, p2) => { const vars = p1.replaceAll(' as ', ': ') const importPath = p2.slice(1, -1) const path = JSON.stringify( importPath.slice( importPath.indexOf('/') + 1 ) ) const ref = `Macchiato.modules[${path}]` return `const ${vars} = ${ref}` } ) script.textContent = ( "\n" + data + initAppend + append + "\n" ) return script.outerHTML } buildReplace(filesMap) { if ('_replace.js' in filesMap) { const rSrc = filesMap['_replace.js'] return new Function( rSrc.match(/\((\w+)\)/)[1], rSrc.slice( rSrc.indexOf('{') + 1, rSrc.lastIndexOf('}') ) ) } else { return s => s } } build() { const filesMap = Object.fromEntries( this.files.map( ({name, data}) => ([name, data]) ) ) const intro = this.buildModule({ name: '_intro.js', data: ( '_intro.js' in filesMap ? filesMap['_intro.js'] : defaultIntro ), }) const replace = this.buildReplace(filesMap) const modules = this.files.filter(({name}) => ( name.endsWith('.js') && !name.startsWith('_') )).map(file => ( this.buildModule({ ...file, data: replace(file.data), }) )) const html = ( 'index.html' in filesMap ? filesMap['index.html'] : defaultHtml ) return html.replace( '<' + 'script type="module" src="/app.js"><' + '/script>', intro + "\n" + modules.join("\n") ) } }