import gulp from 'gulp'; import cleanCSS from 'gulp-clean-css'; import htmlmin from 'gulp-htmlmin'; import concat from 'gulp-concat'; import sourcemaps from 'gulp-sourcemaps'; import gulpif from 'gulp-if'; import rename from 'gulp-rename'; import { deleteAsync } from 'del'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import replace from 'gulp-replace'; import jsonMinify from 'gulp-json-minify'; import crypto from 'crypto'; import { rollup } from 'rollup'; import { nodeResolve } from '@rollup/plugin-node-resolve'; import rollupTerser from '@rollup/plugin-terser'; import fs from 'fs/promises'; import path from 'path'; import { glob } from 'glob'; // Get command line arguments const argv = yargs(hideBin(process.argv)).argv; const isProduction = argv.production || process.env.NODE_ENV === 'production'; // Paths configuration const paths = { src: { js: { entry: 'js/core.js', all: 'js/**/*.js' }, css: ['css/main.css', 'css/finetune.css'], html: { main: 'index.html', templates: 'templates/**/*.html' }, lang: 'lang/**/*.json', assets: [ 'favicon.ico', 'favicon.svg', 'favicon-16x16.png', 'favicon-32x32.png', 'favicon-96x96.png', 'apple-touch-icon.png', 'web-app-manifest-192x192.png', 'web-app-manifest-512x512.png', 'site.webmanifest', 'assets/donate.png', 'googlec4c2e36a49e62fa3.html', 'fa.min.css' ], svg: 'assets/**/*.svg' }, dist: 'dist', temp: '.tmp' }; // Clean task function clean() { return deleteAsync([paths.dist, paths.temp]); } // JavaScript bundling with Rollup async function scripts() { const inputOptions = { input: paths.src.js.entry, plugins: [ nodeResolve(), ...(isProduction ? [ rollupTerser({ compress: { drop_console: false, drop_debugger: true, pure_funcs: ['console.debug', 'console.trace'], passes: 2, unsafe: true, unsafe_comps: true, unsafe_math: true, unsafe_proto: true }, mangle: { reserved: [ 'gboot', 'connect', 'disconnect', 'disconnectSync', 'show_faq_modal', 'calibrate_stick_centers', 'calibrate_range', 'ds5_finetune', 'auto_calibrate_stick_centers', 'flash_all_changes', 'reboot_controller', 'welcome_accepted', 'show_info_tab', 'nvslock', 'nvsunlock', 'refresh_nvstatus', 'show_edge_modal', 'show_donate_modal' ], properties: { regex: /^_/ } }, format: { comments: false } }) ] : []) ] }; let filename = 'app.js'; if (isProduction) { const bundle = await rollup(inputOptions); const { output } = await bundle.generate({ format: 'es' }); const code = output[0].code; const hash = crypto.createHash('md5').update(code).digest('hex').substring(0, 8); filename = `app-${hash}.js`; await fs.writeFile(path.join(paths.dist, filename), code); await bundle.close(); } else { const outputOptions = { file: path.join(paths.dist, filename), format: 'es', sourcemap: true }; const bundle = await rollup(inputOptions); await bundle.write(outputOptions); await bundle.close(); } // Store the filename for HTML processing global.jsFilename = filename; return Promise.resolve(); } // CSS processing function styles() { let stream = gulp.src(paths.src.css) .pipe(gulpif(!isProduction, sourcemaps.init())) .pipe(concat('app.css')); if (isProduction) { stream = stream.pipe(cleanCSS({ level: 2 })); // Add hash to filename in production stream = stream.pipe(rename(function(path) { const hash = crypto.createHash('md5').update(Date.now().toString()).digest('hex').substring(0, 8); path.basename = `app-${hash}`; global.cssFilename = `${path.basename}.css`; })); } else { stream = stream.pipe(sourcemaps.write('.')); global.cssFilename = 'app.css'; } return stream.pipe(gulp.dest(paths.dist)); } // Bundle templates and SVG assets into HTML for production async function bundleAssets() { if (!isProduction) { return Promise.resolve(); } try { // Read all template files const templateFiles = await glob('templates/**/*.html'); const templates = {}; for (const templateFile of templateFiles) { const content = await fs.readFile(templateFile, 'utf8'); const templateName = path.basename(templateFile, '.html'); templates[templateName] = content; } // Read SVG assets const svgFiles = await glob('assets/**/*.svg'); const svgAssets = {}; for (const svgFile of svgFiles) { const content = await fs.readFile(svgFile, 'utf8'); const assetName = path.relative('assets', svgFile); svgAssets[assetName] = content; } // Create the bundled assets object const bundledAssets = { templates, svg: svgAssets }; // Store for use in HTML processing global.bundledAssets = bundledAssets; return Promise.resolve(); } catch (error) { console.error('Error bundling assets:', error); throw error; } } // HTML processing async function html() { const jsFile = global.jsFilename || 'app.js'; const cssFile = global.cssFilename || 'app.css'; let htmlContent = await fs.readFile(paths.src.html.main, 'utf8'); // Replace script and CSS references htmlContent = htmlContent.replace('', ``); htmlContent = htmlContent.replace('', ''); htmlContent = htmlContent.replace('', ``); // In production, inject bundled assets if (isProduction && global.bundledAssets) { const bundledAssetsScript = ` `; // Insert the bundled assets script before the main app script htmlContent = htmlContent.replace( ``, `${bundledAssetsScript}\n` ); } if (isProduction) { // Use htmlmin to minify the content const htmlMinify = (await import('html-minifier-terser')).minify; htmlContent = await htmlMinify(htmlContent, { caseSensitive: false, collapseBooleanAttributes: true, collapseInlineTagWhitespace: false, collapseWhitespace: true, conservativeCollapse: false, decodeEntities: true, html5: true, includeAutoGeneratedTags: true, keepClosingSlash: false, minifyCSS: true, minifyJS: true, minifyURLs: false, preserveLineBreaks: false, preventAttributesEscaping: false, processConditionalComments: false, removeAttributeQuotes: true, removeComments: true, removeEmptyAttributes: true, removeEmptyElements: false, removeOptionalTags: true, removeRedundantAttributes: true, removeScriptTypeAttributes: true, removeStyleLinkTypeAttributes: true, removeTagWhitespace: false, sortAttributes: true, sortClassName: true, trimCustomFragments: true, useShortDoctype: true }); } // Write the processed HTML file await fs.writeFile(path.join(paths.dist, 'index.html'), htmlContent); return Promise.resolve(); } // Template processing (only for development builds) function templates() { if (isProduction) { // In production, templates are bundled into the HTML file return Promise.resolve(); } return gulp.src(paths.src.html.templates) .pipe(gulp.dest(`${paths.dist}/templates`)); } // Language files processing function languages() { return gulp.src(paths.src.lang) .pipe(gulpif(isProduction, jsonMinify())) .pipe(gulp.dest(`${paths.dist}/lang`)); } // Copy assets (SVGs are bundled in production) function assets() { if (isProduction) { // In production, SVGs are bundled into the HTML file, so only copy other assets return gulp.src(paths.src.assets, { base: '.', encoding: false }) .pipe(gulp.dest(paths.dist)); } return gulp.src([...paths.src.assets, paths.src.svg], { base: '.', encoding: false }) .pipe(gulp.dest(paths.dist)); } // Watch task function watch() { gulp.watch(paths.src.js.all, scripts); gulp.watch(paths.src.css, styles); gulp.watch([paths.src.html.main, paths.src.html.templates], gulp.series(html, templates)); gulp.watch(paths.src.lang, languages); gulp.watch([...paths.src.assets, paths.src.svg], assets); } // Development task function dev() { console.log('🚀 Development mode - watching files for changes...'); return watch(); } // Build task const build = gulp.series( clean, gulp.parallel(scripts, styles), bundleAssets, gulp.parallel(html, templates, languages, assets) ); // Export tasks export { clean, scripts, styles, bundleAssets, html, templates, languages, assets, watch, dev, build }; export default build;