const fs = require('fs'); const path = require('path'); const glob = require('glob'); const terser = require('terser'); const csso = require('csso'); const htmlmin = require('html-minifier'); // Configuration const config = { rootJs: { src: 'app.js', exclude: ['*.min.js', 'build*.js'], outputExt: '.min.js' }, js: { src: 'src/**/*.js', exclude: 'src/**/*.min.js', outputExt: '.min.js' }, css: { src: 'src/**/*.css', exclude: 'src/**/*.min.css', outputExt: '.min.css' }, json: { src: ['src/**/*.json', './package.json', './package-lock.json'] }, html: { src: 'src/**/*.html' }, images: { src: 'src/**/*.+(png|jpg|jpeg|gif|svg|ico)' }, typescript: { src: 'src/**/*.+(ts|ts.map|d.ts)' }, // Add all files that might contain references allFiles: { src: ['./*.js', 'src/**/*.js', 'src/**/*.html', 'src/**/*.css', 'src/**/*.json'] } }; // Ensure directory exists function ensureDirectoryExistence(filePath) { const dirname = path.dirname(filePath); if (fs.existsSync(dirname)) return; ensureDirectoryExistence(dirname); fs.mkdirSync(dirname); } // Clean public directory function cleanPublicDir() { console.log('Cleaning public directory...'); if (fs.existsSync('public')) { fs.rmSync('public', { recursive: true, force: true }); } fs.mkdirSync('public'); console.log('✓ Public directory cleaned'); } // Create output path preserving directory structure function createOutputPath(file, baseDir, outputExt = null) { // Get relative path from the source root const relativePath = path.relative(baseDir, file); // Create public path with the same relative structure let outputPath = path.join('public', relativePath); // Apply output extension if provided if (outputExt) { outputPath = outputPath.replace(path.extname(outputPath), outputExt); } return outputPath; } // Minify JavaScript files async function minifyJS() { console.log('Minifying JavaScript files...'); // Minify root-level JS files (like app.js) const rootFiles = glob.sync(config.rootJs.src, { ignore: config.rootJs.exclude }); for (const file of rootFiles) { // Skip already minified files if (file.endsWith('.min.js')) continue; try { const content = fs.readFileSync(file, 'utf8'); const result = await terser.minify(content, { compress: true, mangle: true }); const outputPath = createOutputPath(file, '.', config.rootJs.outputExt); ensureDirectoryExistence(outputPath); fs.writeFileSync(outputPath, result.code); console.log(`✓ Minified: ${file} -> ${outputPath}`); } catch (err) { console.error(`Error minifying ${file}:`, err); } } // Minify JavaScript files in src directory const srcFiles = glob.sync(config.js.src, { ignore: config.js.exclude }); for (const file of srcFiles) { // Skip already minified files if (file.endsWith('.min.js')) continue; try { const content = fs.readFileSync(file, 'utf8'); const result = await terser.minify(content, { compress: true, mangle: true }); const outputPath = createOutputPath(file, '.', config.js.outputExt); ensureDirectoryExistence(outputPath); fs.writeFileSync(outputPath, result.code); console.log(`✓ Minified: ${file} -> ${outputPath}`); } catch (err) { console.error(`Error minifying ${file}:`, err); } } } // Minify CSS files function minifyCSS() { console.log('Minifying CSS files...'); const files = glob.sync(config.css.src, { ignore: config.css.exclude }); for (const file of files) { if (file.endsWith('.min.css')) continue; try { const content = fs.readFileSync(file, 'utf8'); const result = csso.minify(content); const outputPath = createOutputPath(file, '.', config.css.outputExt); ensureDirectoryExistence(outputPath); fs.writeFileSync(outputPath, result.css); console.log(`✓ Minified: ${file} -> ${outputPath}`); } catch (err) { console.error(`Error minifying ${file}:`, err); } } } // Minify JSON files function minifyJSON() { console.log('Minifying JSON files...'); const files = glob.sync(config.json.src); for (const file of files) { try { const content = fs.readFileSync(file, 'utf8'); const jsonData = JSON.parse(content); const minified = JSON.stringify(jsonData); const outputPath = createOutputPath(file, '.'); ensureDirectoryExistence(outputPath); fs.writeFileSync(outputPath, minified); console.log(`✓ Minified: ${file} -> ${outputPath}`); } catch (err) { console.error(`Error minifying ${file}:`, err); } } } // Minify HTML files function minifyHTML() { console.log('Minifying HTML files...'); const files = glob.sync(config.html.src); for (const file of files) { try { let content = fs.readFileSync(file, 'utf8'); // Minify HTML const minified = htmlmin.minify(content, { collapseWhitespace: true, removeComments: true, minifyJS: true, minifyCSS: true, removeRedundantAttributes: true, removeEmptyAttributes: true, removeOptionalTags: true }); const outputPath = createOutputPath(file, '.'); ensureDirectoryExistence(outputPath); fs.writeFileSync(outputPath, minified); console.log(`✓ Minified: ${file} -> ${outputPath}`); } catch (err) { console.error(`Error minifying ${file}:`, err); } } } // Copy images function copyImages() { console.log('Copying images...'); const files = glob.sync(config.images.src); for (const file of files) { try { const outputPath = createOutputPath(file, '.'); ensureDirectoryExistence(outputPath); fs.copyFileSync(file, outputPath); console.log(`✓ Copied: ${file} -> ${outputPath}`); } catch (err) { console.error(`Error copying ${file}:`, err); } } } // Copy TypeScript files function copyTypeScriptFiles() { console.log('Copying TypeScript files...'); const files = glob.sync(config.typescript.src); for (const file of files) { try { const outputPath = createOutputPath(file, '.'); ensureDirectoryExistence(outputPath); fs.copyFileSync(file, outputPath); console.log(`✓ Copied: ${file} -> ${outputPath}`); } catch (err) { console.error(`Error copying ${file}:`, err); } } } // Update all file references in all files function updateAllReferences() { console.log('Updating file references in all files...'); // Get all files that might contain references const files = glob.sync(config.allFiles.src); // Create a list of file mappings (original to minified) const jsFiles = [ ...glob.sync(config.rootJs.src, { ignore: config.rootJs.exclude }), ...glob.sync(config.js.src, { ignore: config.js.exclude }) ] .filter(file => !file.endsWith('.min.js') && !file.endsWith('.d.ts') && !file.endsWith('.map') && file !== 'build.js'); const cssFiles = glob.sync(config.css.src, { ignore: config.css.exclude }) .filter(file => !file.endsWith('.min.css')); for (const targetFile of files) { // Skip build.js if (targetFile === 'build.js' || targetFile.includes('build_v')) continue; // Skip files that likely don't have references or are already processed if (targetFile.endsWith('.min.js') || targetFile.endsWith('.d.ts') || targetFile.endsWith('.map') || targetFile.endsWith('.min.css')) continue; try { let content = fs.readFileSync(targetFile, 'utf8'); let modified = false; // Replace JS file references for (const jsFile of jsFiles) { // Get different forms of the path for replacement const normalizedPath = jsFile.replace(/\\/g, '/'); // Convert backslashes to forward slashes const relativePath = './' + normalizedPath; // With leading ./ const plainPath = normalizedPath; // Without leading ./ const fileNameOnly = path.basename(normalizedPath); // Just the filename // Create minified versions of each path form const normalizedPathMin = normalizedPath.replace('.js', '.min.js'); const relativePathMin = relativePath.replace('.js', '.min.js'); const plainPathMin = plainPath.replace('.js', '.min.js'); const fileNameOnlyMin = fileNameOnly.replace('.js', '.min.js'); // Attempt different path styles replacements if (content.includes(relativePath)) { content = content.replace(new RegExp(escapeRegExp(relativePath), 'g'), relativePathMin); modified = true; } if (content.includes(plainPath)) { content = content.replace(new RegExp(escapeRegExp(plainPath), 'g'), plainPathMin); modified = true; } if (content.includes(fileNameOnly) && fileNameOnly !== 'index.js') { // Be careful with just filenames content = content.replace(new RegExp(`(['"\\s])${escapeRegExp(fileNameOnly)}(['"\\s])`, 'g'), `$1${fileNameOnlyMin}$2`); modified = true; } } // Replace CSS file references for (const cssFile of cssFiles) { const normalizedPath = cssFile.replace(/\\/g, '/'); const relativePath = './' + normalizedPath; const plainPath = normalizedPath; const fileNameOnly = path.basename(normalizedPath); const normalizedPathMin = normalizedPath.replace('.css', '.min.css'); const relativePathMin = relativePath.replace('.css', '.min.css'); const plainPathMin = plainPath.replace('.css', '.min.css'); const fileNameOnlyMin = fileNameOnly.replace('.css', '.min.css'); if (content.includes(relativePath)) { content = content.replace(new RegExp(escapeRegExp(relativePath), 'g'), relativePathMin); modified = true; } if (content.includes(plainPath)) { content = content.replace(new RegExp(escapeRegExp(plainPath), 'g'), plainPathMin); modified = true; } if (content.includes(fileNameOnly) && fileNameOnly !== 'styles.css') { // Be careful with just filenames content = content.replace(new RegExp(`(['"\\s])${escapeRegExp(fileNameOnly)}(['"\\s])`, 'g'), `$1${fileNameOnlyMin}$2`); modified = true; } } if (modified) { // Write updated content to the public version const outputPath = createOutputPath(targetFile, '.', targetFile.endsWith('.js') ? config.js.outputExt : targetFile.endsWith('.css') ? config.css.outputExt : null); fs.writeFileSync(outputPath, content); console.log(`✓ Updated references in: ${outputPath}`); } } catch (err) { console.error(`Error updating references in ${targetFile}:`, err); } } } // Helper function to escape special characters in a string for use in RegExp function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // Main function async function build() { console.log('Starting build process...'); cleanPublicDir(); await minifyJS(); minifyCSS(); minifyJSON(); minifyHTML(); copyImages(); copyTypeScriptFiles(); updateAllReferences(); console.log('Build completed successfully!'); } // Run the build build().catch(err => { console.error('Build failed:', err); process.exit(1); });