// IMPORTS const fs = require('fs'); const swc = require('@swc/core'); const path = require('path'); const util = require('util'); const postcss = require('postcss'); const htmlminify = require('html-minifier-terser').minify; const childProcess = require('child_process'); const utils = require('./src/utils.js'); const assproc = require('./src/assproc.js'); // CONFIG const rootDir = __dirname; const configFile = path.join(rootDir, 'config/config.json'); const srcDir = path.join(rootDir, 'src'); const pubDir = path.join(rootDir, 'public'); const pubAssetsDir = path.join(pubDir, 'assets'); const isDebugBuild = fs.existsSync(path.join(rootDir, '.debug')); const buildTasks = { js: [ { source: 'proto.js', target: '/assets', name: 'proto.{hash}.js', buildVar: 'MAMI_PROTO_JS', buildVarsTarget: 'self' }, { source: 'mami.js', target: '/assets', name: 'mami.{hash}.js', buildVar: 'MAMI_MAIN_JS' }, { source: 'init.js', target: '/assets', name: 'init.{hash}.js', es: 'es5' }, ], css: [ { source: 'mami.css', target: '/assets', name: 'mami.{hash}.css' }, ], html: [ { source: 'mami.html', target: '/', name: 'index.html' }, ], webmanifest: [ { source: 'mami.webmanifest', target: '/', name: 'mami.webmanifest', icons: '/icons' } ], }; // PREP const config = JSON.parse(fs.readFileSync(configFile)); const postcssPlugins = [ require('autoprefixer')({ remove: false }) ]; if(!isDebugBuild) postcssPlugins.push(require('cssnano')({ preset: [ 'cssnano-preset-default', { minifyGradients: false, reduceIdents: false, zindex: true, } ], })); const swcJscOptions = { target: 'es2020', loose: false, externalHelpers: false, keepClassNames: true, preserveAllComments: false, transform: {}, parser: { syntax: 'ecmascript', jsx: true, dynamicImport: false, privateMethod: false, functionBind: false, exportDefaultFrom: false, exportNamespaceFrom: false, decorators: false, decoratorsBeforeExport: false, topLevelAwait: true, importMeta: false, }, transform: { react: { runtime: 'classic', pragma: '$er', }, }, }; const htmlMinifyOptions = { collapseBooleanAttributes: true, collapseWhitespace: true, conservativeCollapse: false, decodeEntities: false, quoteCharacter: '"', removeAttributeQuotes: true, removeComments: true, removeEmptyAttributes: true, removeOptionalTags: true, removeScriptTypeAttributes: true, removeStyleLinkTypeAttributes: true, sortAttributes: true, sortClassName: true, }; // BUILD (async () => { const htmlVars = { 'title': config.title }; const buildVars = { FUTAMI_DEBUG: isDebugBuild, FUTAMI_URL: config.common_url, MAMI_URL: config.modern_url, AMI_URL: config.compat_url, GIT_HASH: await (() => { return new Promise((resolve, reject) => { childProcess.exec('git log --pretty="%H" -n1 HEAD', (err, stdout) => { if(err) reject(err); else resolve(stdout.trim()); }); }); })(), }; console.log('Ensuring assets directory exists...'); fs.mkdirSync(pubAssetsDir, { recursive: true }); console.log(); console.log('JS assets'); for(const info of buildTasks.js) { console.log(`=> Building ${info.source}...`); let origTarget = undefined; if('es' in info) { origTarget = swcJscOptions.target; swcJscOptions.target = info.es; } const assprocOpts = { prefix: '#', entry: info.entry || 'main.js', buildVars: buildVars, }; const swcOpts = { filename: info.source, sourceMaps: false, isModule: false, minify: !isDebugBuild, jsc: swcJscOptions, }; const pubName = await assproc.process(path.join(srcDir, info.source), assprocOpts) .then(output => swc.transform(output, swcOpts)) .then(output => { const name = utils.strtr(info.name, { hash: utils.shortHash(output.code) }); const pubName = path.join(info.target || '', name); console.log(` Saving to ${pubName}...`); fs.writeFileSync(path.join(pubDir, pubName), output.code); return pubName; }); if(origTarget !== undefined) swcJscOptions.target = origTarget; htmlVars[info.source] = pubName; if(typeof info.buildVar === 'string') buildVars[info.buildVar] = pubName; } console.log(); console.log('CSS assets'); for(const info of buildTasks.css) { console.log(`=> Building ${info.source}...`); const sourcePath = path.join(srcDir, info.source); const assprocOpts = { prefix: '@', entry: info.entry || 'main.css', }; const postcssOpts = { from: sourcePath }; htmlVars[info.source] = await assproc.process(sourcePath, assprocOpts) .then(output => postcss(postcssPlugins).process(output, postcssOpts) .then(output => { const name = utils.strtr(info.name, { hash: utils.shortHash(output.css) }); const pubName = path.join(info.target || '', name); console.log(` Saving to ${pubName}...`); fs.writeFileSync(path.join(pubDir, pubName), output.css); return pubName; })); } console.log(); console.log('Webmanifest assets...'); for(const info of buildTasks.webmanifest) { console.log(`=> Building ${info.source}...`); try { const body = JSON.parse(fs.readFileSync(path.join(srcDir, info.source))); body.name = config.title; body.short_name = config.title; if(typeof info.icons === 'string') { const iconsDir = path.join(pubDir, info.icons); if(fs.existsSync(iconsDir)) { const files = (await fs.promises.readdir(iconsDir)).sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base', })); body.icons = []; for(const file of files) { if(!file.endsWith('.png')) continue; const icon = { src: path.join(info.icons, file), type: 'image/png', }; if(file[0] !== 'c') { if(file[0] === 'm') icon.purpose = 'maskable'; else if(file[0] === 'w') icon.purpose = 'monochrome'; else continue; } let res = ''; for(let i = 1; i < file.length; ++i) { if(file[i] === 'x') break; res += file[i]; } if(res.length > 0) icon.sizes = `${res}x${res}`; body.icons.push(icon); } } } const data = JSON.stringify(body); const name = utils.strtr(info.name, { hash: utils.shortHash(data) }); const pubName = path.join(info.target || '', name); console.log(` Saving to ${pubName}...`); const fullPath = path.join(pubDir, pubName); const dirPath = path.dirname(fullPath); if(!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true }); fs.writeFileSync(fullPath, data); htmlVars[info.source] = pubName; } catch(err) { console.error(err); } } console.log(); console.log('HTML assets'); for(const info of buildTasks.html) { console.log(`=> Building ${info.source}...`); try { let data = fs.readFileSync(path.join(srcDir, info.source)); data = utils.strtr(data, htmlVars); if(!isDebugBuild) data = await htmlminify(data, htmlMinifyOptions); const name = utils.strtr(info.name, { hash: utils.shortHash(data) }); const pubName = path.join(info.target || '', name); console.log(` Saving to ${pubName}...`); const fullPath = path.join(pubDir, pubName); const dirPath = path.dirname(fullPath); if(!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true }); fs.writeFileSync(fullPath, data); htmlVars[info.source] = pubName; } catch(err) { console.error(err); } } console.log(); console.log('Cleaning up old builds...'); assproc.housekeep(pubAssetsDir); })();