const fs = require('fs'); const path = require('path'); const axios = require('axios'); const { minify } = require('html-minifier-terser'); async function inlineAsset(html, params) { let asset; const filePath = params.path.startsWith('http') ? params.path : path.join(params.sourceFolder, params.path); if (fs.existsSync(filePath)) { if (!params.inlineLocalAssets) { if (filePath.includes('assets')) { const assetPath = path.join(filePath.split('assets')[1]); const targetPath = path.join(params.targetFolder, assetPath); const targetFolder = path.dirname(targetPath); const htmlAssetPath = `.${assetPath.replace(/\\/g, '/')}`; if (!fs.existsSync(targetFolder)) { fs.mkdirSync(targetFolder, { recursive: true }); } fs.copyFileSync(filePath, targetPath); if (params.originalTag.toLowerCase() === 'link') { html = html.replace(params.regex, ``); } else if (params.originalTag.toLowerCase() === 'script') { html = html.replace(params.regex, ``); } } return html; } asset = fs.readFileSync(filePath, 'utf8'); } else { if (!params.inlineRemoteAssets) { return html; } asset = (await axios.get(filePath)).data; } return html.replace(params.regex, `<${params.tag}>\n${asset}\n`); } function cleanTargetPath(targetDir) { if (!fs.existsSync(targetDir)) { return; } fs.readdirSync(targetDir).forEach((file) => { const entryPath = path.join(targetDir, file); const stat = fs.lstatSync(entryPath); if (stat.isFile()) { fs.unlinkSync(entryPath); } else if (stat.isDirectory()) { cleanTargetPath(entryPath); } }); fs.rmdirSync(targetDir); } async function inlineAssets(params) { const sourceFolder = params.sourceFolder; const targetFolder = params.targetFolder; console.log(`Inlining assets from ${sourceFolder} to ${targetFolder}`); const inputFile = path.join(sourceFolder, params.inputFile); const outputFile = path.join(targetFolder, 'index.html'); let html = fs.readFileSync(inputFile, 'utf8'); const inlineLocalAssets = params.inlineLocalAssets || false; const inlineRemoteAssets = params.inlineRemoteAssets || false; cleanTargetPath(targetFolder); if (!fs.existsSync(targetFolder)) { fs.mkdirSync(targetFolder, { recursive: true }); } // Match and inline CSS links const cssLinks = [...html.matchAll(/]*?\s+href=["'](.*?)["'][^>]*?>/gs)]; for (const match of cssLinks) { html = await inlineAsset(html, { path: match[1], regex: match[0], originalTag: 'link', tag: 'style', sourceFolder, targetFolder, inlineLocalAssets, inlineRemoteAssets, }); } // Match and inline JavaScript src const scriptLinks = [...html.matchAll(/]*?\s+src=["'](.*?)["'][^>]*?>\s*<\/script>/gs)]; for (const match of scriptLinks) { html = await inlineAsset(html, { path: match[1], regex: match[0], originalTag: 'script', tag: 'script', sourceFolder, targetFolder, inlineLocalAssets, inlineRemoteAssets, }); } // Minify the HTML const minifiedHtml = await minify(html, { collapseWhitespace: true, removeComments: true, removeRedundantAttributes: true, removeEmptyAttributes: true, minifyCSS: true, minifyJS: true, }); // Save the minified HTML file fs.writeFileSync(outputFile, minifiedHtml, 'utf8'); console.log(`Saved minified HTML to ${outputFile}`); } const defaultBuild = { sourceFolder: './src', inputFile: './index.html', targetFolder: './dist/default', inlineLocalAssets: false, inlineRemoteAssets: false, }; const singleFileWithRemoteAssets = { sourceFolder: './src', inputFile: './index.html', targetFolder: './dist/single-file-with-remote-assets', inlineLocalAssets: true, inlineRemoteAssets: false, }; const singleFile = { sourceFolder: './src', inputFile: './index.html', targetFolder: './dist/single-file', inlineLocalAssets: true, inlineRemoteAssets: true, }; inlineAssets(defaultBuild); inlineAssets(singleFileWithRemoteAssets); inlineAssets(singleFile);