const HtmlWebpackPlugin = require('html-webpack-plugin'); const CopyPlugin = require("copy-webpack-plugin"); const HtmlMinimizerPlugin = require("html-minimizer-webpack-plugin"); const { CleanWebpackPlugin } = require("clean-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const CompressionPlugin = require("compression-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin"); const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin"); const webpack = require("webpack"); const path = require("path"); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const globSync = require("glob").sync; const glob = require('glob'); const { merge } = require('webpack-merge'); const devserver = require('./webpack/webpack.dev.js'); const fs = require('fs'); const zlib = require("zlib"); const PurgeCSSPlugin = require('purgecss-webpack-plugin') const whitelister = require('purgecss-whitelister'); const PATHS = { src: path.join(__dirname, 'src') } class BuildEventsHook { constructor(name, fn, stage = 'afterEmit') { this.name = name; this.stage = stage; this.function = fn; } apply(compiler) { compiler.hooks[this.stage].tap(this.name, this.function); } } module.exports = (env, options) => ( merge( env.WEBPACK_SERVE ? devserver : {}, env.ANALYZE_SIZE?{ plugins: [ new BundleAnalyzerPlugin( { analyzerMode: 'static', generateStatsFile: true, statsFilename: 'stats.json', } ) ]}:{}, { entry: { index: './src/index.ts' }, devtool:"source-map", module: { rules: [ { test: /\.ejs$/, loader: 'ejs-loader', options: { variable: 'data', interpolate : '\\{\\{(.+?)\\}\\}', evaluate : '\\[\\[(.+?)\\]\\]' } }, { test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/, use: [ { loader: 'file-loader', options: { name: '[name].[ext]', outputPath: 'fonts/' } } ] }, // { // test: /\.s[ac]ss$/i, // use: [{ // loader: 'style-loader', // inject CSS to page // }, // { // loader: MiniCssExtractPlugin.loader, // options: { // publicPath: "../", // }, // }, // "css-loader", // { // loader: "postcss-loader", // options: { // postcssOptions: { // plugins: [["autoprefixer"]], // }, // }, // }, // "sass-loader", // ] // }, { test: /\.(scss)$/, use: [ { loader: MiniCssExtractPlugin.loader, options: { publicPath: "../", }, }, // { // // inject CSS to page // loader: 'style-loader' // }, { // translates CSS into CommonJS modules loader: 'css-loader' }, { // Run postcss actions loader: 'postcss-loader', options: { // `postcssOptions` is needed for postcss 8.x; // if you use postcss 7.x skip the key postcssOptions: { // postcss plugins, can be exported to postcss.config.js plugins: function () { return [ require('autoprefixer') ]; } } } }, { // compiles Sass to CSS loader: 'sass-loader' }] }, { test: /\.js$/, exclude: /(node_modules|bower_components)/, use: { loader: "babel-loader", options: { presets: ["@babel/preset-env"], plugins: ['@babel/plugin-transform-runtime'] }, }, }, { test: /\.(jpe?g|png|gif|svg)$/i, type: "asset", }, // { // test: /\.html$/i, // type: "asset/resource", // }, { test: /\.html$/i, loader: "html-loader", options: { minimize: true, } }, { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, plugins: [ new HtmlWebpackPlugin({ title: 'SqueezeESP32', template: './src/index.ejs', filename: 'index.html', inject: 'body', minify: { html5 : true, collapseWhitespace : true, minifyCSS : true, minifyJS : true, minifyURLs : false, removeAttributeQuotes : true, removeComments : true, // false for Vue SSR to find app placeholder removeEmptyAttributes : true, removeOptionalTags : true, removeRedundantAttributes : true, removeScriptTypeAttributes : true, removeStyleLinkTypeAttributese : true, useShortDoctype : true }, favicon: "./src/assets/images/favicon-32x32.png", excludeChunks: ['test'], }), // new CompressionPlugin({ // test: /\.(js|css|html|svg)$/, // //filename: '[path].br[query]', // filename: "[path][base].br", // algorithm: 'brotliCompress', // compressionOptions: { level: 11 }, // threshold: 100, // minRatio: 0.8, // deleteOriginalAssets: false // }), new MiniCssExtractPlugin({ filename: "css/[name].[contenthash].css", }), new PurgeCSSPlugin({ keyframes: false, paths: glob.sync(`${path.join(__dirname, 'src')}/**/*`, { nodir: true }), whitelist: whitelister('bootstrap/dist/css/bootstrap.css') }), new webpack.ProvidePlugin({ $: "jquery", // jQuery: "jquery", // "window.jQuery": "jquery", // Popper: ["popper.js", "default"], // Util: "exports-loader?Util!bootstrap/js/dist/util", // Dropdown: "exports-loader?Dropdown!bootstrap/js/dist/dropdown", }), new CompressionPlugin({ //filename: '[path].gz[query]', test: /\.js$|\.css$|\.html$/, filename: "[path][base].gz", algorithm: 'gzip', threshold: 100, minRatio: 0.8, }), new BuildEventsHook('Update C App', function (stats, arguments) { if (options.mode !== "production") return; let buildRootPath = path.join(process.cwd(),'..','..','..'); let wifiManagerPath=glob.sync(path.join(buildRootPath,'components/**/wifi-manager*'))[0]; let buildCRootPath=glob.sync(buildRootPath)[0]; fs.appendFileSync('./dist/index.html.gz', zlib.gzipSync(fs.readFileSync('./dist/index.html'), { chunckSize: 65536, level: zlib.constants.Z_BEST_COMPRESSION })); var getDirectories = function (src, callback) { var searchPath = path.posix.join(src, '/**/*(*.gz|favicon-32x32.png)'); console.log(`Post build: Getting file list from ${searchPath}`); glob(searchPath, callback); }; var cleanUpPath = path.posix.join(buildCRootPath, '/build/*.S'); console.log(`Post build: Cleaning up previous builds in ${cleanUpPath}`); glob(cleanUpPath, function (err, list) { if (err) { console.error('Error', err); } else { list.forEach(fileName => { try { console.log(`Post build: Purging old binary file ${fileName} from C project.`); fs.unlinkSync(fileName) //file removed } catch (ferr) { console.error(ferr) } }); } }, 'afterEmit' ); console.log('Generating C include files from webpack build output'); getDirectories('./dist', function (err, list) { console.log(`Post build: found ${list.length} files. Relative path: ${wifiManagerPath}.`); if (err) { console.log('Error', err); } else { let exportDefHead = `/*********************************** webpack_headers ${arguments[1]} ***********************************/ #pragma once #include extern const char * resource_lookups[]; extern const uint8_t * resource_map_start[]; extern const uint8_t * resource_map_end[];`; let exportDef = '// Automatically generated. Do not edit manually!.\n' + '#include \n'; let lookupDef = 'const char * resource_lookups[] = {\n'; let lookupMapStart = 'const uint8_t * resource_map_start[] = {\n'; let lookupMapEnd = 'const uint8_t * resource_map_end[] = {\n'; let cMake=''; list.forEach(foundFile => { let exportName = path.basename(foundFile).replace(/[\. \-]/gm, '_'); //take the full path of the file and make it relative to the build directory let cmakeFileName = path.posix.relative(wifiManagerPath,glob.sync(path.resolve(foundFile))[0]); let httpRelativePath=path.posix.join('/',path.posix.relative('dist',foundFile)); exportDef += `extern const uint8_t _${exportName}_start[] asm("_binary_${exportName}_start");\nextern const uint8_t _${exportName}_end[] asm("_binary_${exportName}_end");\n`; lookupDef += `\t"${httpRelativePath}",\n`; lookupMapStart += '\t_' + exportName + '_start,\n'; lookupMapEnd += '\t_' + exportName + '_end,\n'; cMake += `target_add_binary_data( __idf_wifi-manager ${cmakeFileName} BINARY)\n`; console.log(`Post build: adding cmake file reference to ${cmakeFileName} from C project, with web path ${httpRelativePath}.`); }); lookupDef += '""\n};\n'; lookupMapStart = lookupMapStart.substring(0, lookupMapStart.length - 2) + '\n};\n'; lookupMapEnd = lookupMapEnd.substring(0, lookupMapEnd.length - 2) + '\n};\n'; try { fs.writeFileSync('webapp.cmake', cMake); fs.writeFileSync('webpack.c', exportDef + lookupDef + lookupMapStart + lookupMapEnd); fs.writeFileSync('webpack.h', exportDefHead); //file written successfully } catch (e) { console.error(e); } } }); console.log('Post build completed.'); }) ], optimization: { minimize: true, providedExports: true, usedExports: true, minimizer: [ new TerserPlugin({ terserOptions: { format: { comments: false, }, }, extractComments: false, // enable parallel running parallel: true, }), new HtmlMinimizerPlugin({ minimizerOptions: { removeComments: true, removeOptionalTags: true, } } ), new CssMinimizerPlugin(), new ImageMinimizerPlugin({ minimizer: { implementation: ImageMinimizerPlugin.imageminMinify, options: { // Lossless optimization with custom option // Feel free to experiment with options for better result for you plugins: [ ["gifsicle", { interlaced: true }], ["jpegtran", { progressive: true }], ["optipng", { optimizationLevel: 5 }], // Svgo configuration here https://github.com/svg/svgo#configuration [ "svgo", { plugins: [ { name: 'preset-default', params: { overrides: { // customize default plugin options inlineStyles: { onlyMatchedOnce: false, }, // or disable plugins removeDoctype: false, }, }, } ], }, ], ], }, }, }), ], splitChunks: { cacheGroups: { vendor: { name: "node_vendors", test: /[\\/]node_modules[\\/]/, chunks: "all", } } } }, // output: { // filename: "[name].js", // path: path.resolve(__dirname, "dist"), // publicPath: "", // }, resolve: { extensions: ['.tsx', '.ts', '.js', '.ejs' ], }, output: { path: path.resolve(__dirname, 'dist'), filename: './js/[name].[fullhash:6].bundle.js', clean: true }, } ) );