import path from "path"; import { readdirSync } from "fs"; import { Compilation, sources } from "webpack"; import type { Configuration, RuleSetRule, Compiler, EntryObject, } from "webpack"; interface FindReplacePluginOptions { replace: { from: RegExp; to: string }[]; } /** * Simple find and replace in assets plugin for Webpack */ class FindReplacePlugin { options: FindReplacePluginOptions; constructor(options: FindReplacePluginOptions) { this.options = options; } apply(compiler: Compiler) { const pluginName = FindReplacePlugin.name; const logger = compiler.getInfrastructureLogger(pluginName); compiler.hooks.compilation.tap({ name: pluginName }, (compilation) => { compilation.hooks.processAssets.tap( { name: pluginName, stage: Compilation.PROCESS_ASSETS_STAGE_ANALYSE, }, (assets) => { Object.entries(assets).forEach(([pathname, source]) => { if (path.extname(pathname) !== ".js") return; let replaced = source.source().toString(); // For every regex pattern, replace it in the source content for (const { from, to } of this.options.replace) { const matches = replaced.match(from); if (matches) { logger.info(`${from} | Found ${matches.length} matches`); } replaced = replaced.replace(from, to); } const newSource = new sources.RawSource(replaced); const previousSize = source.size(); compilation.updateAsset(pathname, newSource); const newSize = newSource.size(); if (newSize !== previousSize) { logger.info( `Found and replaced in ${pathname}: ${previousSize} -> ${newSize}`, ); } }); }, ); }); } } const babelRule: RuleSetRule = { test: /\.(ts|js|mjs)$/, // https://webpack.js.org/loaders/babel-loader/#exclude-libraries-that-should-not-be-transpiled exclude: [ // \\ for Windows, / for macOS and Linux /node_modules[\\/]core-js/, ], use: { loader: "babel-loader", options: { targets: "defaults", sourceType: "unambiguous", presets: [ // Parse TypeScript ["@babel/preset-typescript"], [ "@babel/preset-env", { // Use this to cover the lowest possible syntax requirement for QtScript // See: https://browsersl.ist/ targets: "cover 100%", // Adds at the top of each file imports of polyfills only for features used in the current and not supported by target environments useBuiltIns: "usage", corejs: "3.46.0", }, ], ], }, }, }; /** * Dynamically returns an object of script entries from src/scripts/*.ts files */ function getScriptsEntryObject(): EntryObject { const entries: EntryObject = {}; const scriptsPath = path.resolve(__dirname, "src", "scripts"); const scripts = readdirSync(scriptsPath).filter((file) => file.endsWith(".ts"), ); scripts.forEach((script) => { entries[path.parse(script).name] = path.resolve(scriptsPath, script); }); return entries; } const config: Configuration = { mode: "production", module: { rules: [babelRule], }, entry: getScriptsEntryObject(), resolve: { extensions: [".ts", ".js", ".mjs"], // Explicitly define where node_modules is in this project for corejs imports // See: https://stackoverflow.com/a/62129649 modules: [path.resolve(__dirname, "node_modules")], }, output: { filename: "[name].js", path: path.resolve(__dirname, "dist"), environment: { arrowFunction: false }, }, plugins: [ new FindReplacePlugin({ replace: [ /** * Replaces reserved words as property names * For example obj.for = 0 -> obj["for"] = 0 */ { from: /\.(for|return)([^\w])/g, to: '["$1"]$2' }, /** * Replaces reserved words as object literal property names * See: https://eslint.style/rules/quote-props#quote-props * For example { for: 0 } -> { "for": 0 } */ { from: /(for|return):/g, to: '"$1":' }, /** * Some Regexp uses "/" character in character sets but must be escaped in QtScript syntax (don't know why) */ { from: /\[\^\\s\(\/\]/g, to: "[^\\s(\\/]" }, { from: /\[\\w\.\/\]/g, to: "[\\w.\\/]" }, { from: /\[a-z_\+-\/\]/g, to: "[a-z_+-\\/]" }, ], }), ], optimization: { minimize: false, }, }; export default config;