import * as ts from "typescript"; import * as fs from "fs"; import trtransformer from "./tools/trgen/ts_transformer"; import {exec} from "child_process"; import * as util from "util"; import * as path from "path"; import { DefinePlugin, Configuration } from "webpack"; import { Plugin as SvgSpriteGenerator } from "webpack-svg-sprite-generator"; const ManifestGenerator = require("./webpack/ManifestPlugin"); const HtmlWebpackInlineSourcePlugin = require("./webpack/HtmlWebpackInlineSource"); import HtmlWebpackPlugin from "html-webpack-plugin"; const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin'); const { CleanWebpackPlugin } = require("clean-webpack-plugin"); const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); export let isDevelopment = process.env.NODE_ENV === 'development'; console.log("Webpacking for %s (%s)", isDevelopment ? "development" : "production", process.env.NODE_ENV || "NODE_ENV not specified"); const generateDefinitions = async (target: string) => { const gitRevision = fs.readFileSync(path.join(__dirname, ".git", "HEAD")).toString(); let version; if(gitRevision.indexOf("/") === -1) { version = (gitRevision || "0000000").substr(0, 7); } else { version = fs.readFileSync(path.join(__dirname, ".git", gitRevision.substr(5).trim())).toString().substr(0, 7); } let timestamp; try { const { stdout } = await util.promisify(exec)("git show -s --format=%ct"); timestamp = parseInt(stdout.toString()); if(isNaN(timestamp)) { throw "failed to parse timestamp '" + stdout.toString() + "'"; } } catch (error) { console.error("Failed to get commit timestamp: %o", error); throw "failed to get commit timestamp"; } return { "__build": { target: JSON.stringify(target), mode: JSON.stringify(isDevelopment ? "debug" : "release"), version: JSON.stringify(version), timestamp: timestamp, entry_chunk_name: JSON.stringify(target === "web" ? "shared-app" : "client-app") } as BuildDefinitions } as any; }; const generateIndexPlugin = (target: "web" | "client"): HtmlWebpackPlugin => { const options: HtmlWebpackPlugin.Options & { inlineSource?: RegExp | string } = {}; options.cache = true; options.chunks = ["loader"]; options.inject = false; options.template = path.join(__dirname, "loader", "index.ejs"); options.templateParameters = { buildTarget: target }; options.scriptLoading = "defer"; if(!isDevelopment) { options.minify = { html5: true, collapseWhitespace: true, removeComments: true, removeRedundantAttributes: true, removeScriptTypeAttributes: true, removeTagWhitespace: true, minifyCSS: true, minifyJS: true, minifyURLs: true, }; options.inlineSource = /\.(js|css)$/; } return new HtmlWebpackPlugin(options); } export const config = async (target: "web" | "client"): Promise => ({ entry: { "loader": ["./loader/app/index.ts"], "modal-external": ["./shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts"], //"devel-main": "./shared/js/devel_main.ts" }, devtool: isDevelopment ? "inline-source-map" : "source-map", mode: isDevelopment ? "development" : "production", plugins: [ new CleanWebpackPlugin(), new DefinePlugin(await generateDefinitions(target)), new MiniCssExtractPlugin({ filename: isDevelopment ? "[name].[contenthash].css" : "[contenthash].css", chunkFilename: isDevelopment ? "[name].[contenthash].css" : "[contenthash].css", ignoreOrder: true }), new ManifestGenerator({ outputFileName: "manifest.json", context: __dirname }), new SvgSpriteGenerator({ dtsOutputFolder: path.join(__dirname, "shared", "svg-sprites"), publicPath: "js/", configurations: { "client-icons": { folder: path.join(__dirname, "shared", "img", "client-icons"), cssClassPrefix: "client-", cssOptions: [ { scale: 1, selector: ".icon", unit: "px" }, { scale: 1.5, selector: ".icon_x24", unit: "px" }, { scale: 2, selector: ".icon_x32", unit: "px" }, { scale: 1, selector: ".icon_em", unit: "em" } ], dtsOptions: { enumName: "ClientIcon", classUnionName: "ClientIconClass", module: false } } } }), generateIndexPlugin(target), new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin), //new BundleAnalyzerPlugin(), ].filter(e => !!e), module: { rules: [ { test: /\.(s[ac]|c)ss$/, use: [ //'style-loader', { loader: MiniCssExtractPlugin.loader, options: { esModule: false } }, { loader: 'css-loader', options: { modules: { mode: "local", localIdentName: isDevelopment ? "[path][name]__[local]--[hash:base64:5]" : "[hash:base64]", }, sourceMap: isDevelopment } }, { loader: 'sass-loader', options: { sourceMap: isDevelopment } } ] }, { test: /\.tsx?$/, exclude: /node_modules/, use: [ { loader: "babel-loader", options: { presets: ["@babel/preset-env"] } }, { loader: "ts-loader", options: { context: __dirname, colors: true, getCustomTransformers: (prog: ts.Program) => { return { before: [trtransformer(prog, { optimized: false, verbose: true, target_file: path.join(__dirname, "dist", "translations.json") })] }; }, transpileOnly: isDevelopment } } ] }, { test: /\.was?t$/, use: [ "./webpack/WatLoader.js" ] }, { test: /ChangeLog\.md$|\.html$/i, type: "asset/source", }, { test: /\.svg$/, use: 'svg-inline-loader' }, { test: /\.(png|jpg|jpeg|gif)?$/, type: "asset/resource" }, ] } as any, resolve: { extensions: ['.tsx', '.ts', '.js', ".scss"], alias: { "vendor/xbbcode": path.resolve(__dirname, "vendor/xbbcode/src"), "tc-events": path.resolve(__dirname, "vendor/TeaEventBus/src/index.ts"), "tc-services": path.resolve(__dirname, "vendor/TeaClientServices/src/index.ts"), }, }, externals: [ {"tc-loader": "window loader"} ], output: { filename: isDevelopment ? "[name].[contenthash].js" : "[contenthash].js", chunkFilename: isDevelopment ? "[name].[contenthash].js" : "[contenthash].js", path: path.resolve(__dirname, 'dist'), publicPath: "js/" }, performance: { hints: false }, optimization: { splitChunks: { chunks: "all", maxSize: 512 * 1024 }, minimize: !isDevelopment, minimizer: [ new TerserPlugin(), new CssMinimizerPlugin() ] }, devServer: { publicPath: "/", contentBase: path.join(__dirname, 'dist'), writeToDisk: true, compress: true }, });