using my boilerplate yuh

This commit is contained in:
Sriram Hariharan
2023-02-22 22:51:38 -06:00
parent 21d7056aae
commit bce2717088
91 changed files with 32400 additions and 0 deletions

41
webpack/development.ts Normal file
View File

@@ -0,0 +1,41 @@
import webpack from 'webpack';
import WebpackDevServer from 'webpack-dev-server';
import path from 'path';
import { Server } from 'socket.io';
import config from './webpack.config';
import { version } from '../package.json';
import { getManifest } from './manifest.config';
import { initializeHotReloading } from './plugins/custom/hotReloadServer';
const HOT_RELOAD_PORT = 9090;
const MODE: Environment = 'development';
const manifest = getManifest(MODE, version);
const compiler = webpack(config(MODE, manifest));
initializeHotReloading(HOT_RELOAD_PORT, compiler);
const server = new WebpackDevServer(
{
https: false,
hot: false,
client: false,
host: 'localhost',
static: {
directory: path.resolve('build'),
},
devMiddleware: {
writeToDisk: true,
},
headers: {
'Access-Control-Allow-Origin': '*',
},
allowedHosts: 'all',
watchFiles: {
paths: ['src/**/*.{ts,tsx,js,jsx,html,css,scss,json,md,png,jpg,jpeg,gif,svg}', 'public/**/*'],
},
},
compiler
);
await server.start();

52
webpack/loaders/index.ts Normal file
View File

@@ -0,0 +1,52 @@
import { RuleSetRule } from 'webpack';
import * as styleLoaders from './styleLoaders';
/** using esbuild-loader for ⚡ fast builds */
const typescriptLoader: RuleSetRule = {
test: /\.tsx?$/,
loader: 'esbuild-loader',
options: {
loader: 'tsx',
target: 'es2021',
},
};
/** convert svgs to react components automatically */
const svgLoader: RuleSetRule = {
test: /\.svg$/,
issuer: /\.tsx?$/,
loader: '@svgr/webpack',
};
/** these are files that we want to be able to be loaded into the extension folder instead of imported */
const urlLoader: RuleSetRule = {
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/, /\.mp3$/],
loader: 'url-loader',
options: {
limit: '10000',
name: 'static/media/[name].[ext]',
},
};
/** these loaders will allow us to use raw css imports, css modules, raw sass imports, and sass modules */
const { cssLoader, cssModuleLoader, sassLoader, sassModuleLoader } = styleLoaders;
// this is the default file loader, it will be used for any file that doesn't match the other loaders
const fileLoader: RuleSetRule = {
loader: 'file-loader',
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/, /\.mp3$/],
options: {
name: 'static/media/[name].[ext]',
},
};
/** the assembled list of loaders in the order that we want webpack to attempt to use them on modules */
const loaders: RuleSetRule[] = [
typescriptLoader,
{
// IMPORTANT: if you are adding a new loader, it must come before the file loader
oneOf: [svgLoader, urlLoader, cssLoader, cssModuleLoader, sassLoader, sassModuleLoader, fileLoader],
},
];
export default loaders;

View File

@@ -0,0 +1,75 @@
import { RuleSetRule, RuleSetUseItem } from 'webpack';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import getCSSModuleLocalIdent from 'react-dev-utils/getCSSModuleLocalIdent';
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
function buildStyleLoaders(cssLoaderOptions: Record<string, any>): RuleSetUseItem[] {
const loaders = [
{
loader: MiniCssExtractPlugin.loader,
},
{
loader: 'css-loader',
options: { ...cssLoaderOptions, sourceMap: false },
},
];
return loaders;
}
export const cssLoader: RuleSetRule = {
test: cssRegex,
exclude: cssModuleRegex,
sideEffects: true,
use: [
...buildStyleLoaders({
importLoaders: 1,
esModule: false,
}),
],
};
export const cssModuleLoader: RuleSetRule = {
test: cssModuleRegex,
use: [
...buildStyleLoaders({
importLoaders: 1,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
}),
],
};
export const sassLoader: RuleSetRule = {
test: sassRegex,
exclude: sassModuleRegex,
sideEffects: true,
use: [
...buildStyleLoaders({
importLoaders: 2,
}),
{
loader: 'sass-loader',
},
],
};
export const sassModuleLoader: RuleSetRule = {
test: sassModuleRegex,
use: [
...buildStyleLoaders({
importLoaders: 2,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
}),
{
loader: 'sass-loader',
},
],
};

View File

@@ -0,0 +1,57 @@
const NAME = 'UT Registration Plus';
const SHORT_NAME = 'ut-registration-plus';
const DESCRIPTION = 'Improves the course registration process at the University of Texas at Austin!';
const HOST_PERMISSIONS: string[] = [
'*://*.utdirect.utexas.edu/apps/registrar/course_schedule/*',
'*://*.utexas.collegescheduler.com/*',
'*://*.catalog.utexas.edu/ribbit/',
'*://*.registrar.utexas.edu/schedules/*',
'*://*.login.utexas.edu/login/*',
];
/**
* Creates a chrome extension manifest from the given version, mode, and
* @param mode the build mode (development or production)
* @param version a chrome extension version (not a semantic version)
* @returns a chrome extension manifest
*/
export function getManifest(mode: Environment, version: string): chrome.runtime.ManifestV3 {
let name = mode === 'development' ? `${NAME} (dev)` : NAME;
const manifest = {
name,
short_name: SHORT_NAME,
description: DESCRIPTION,
version,
manifest_version: 3,
// hardcode the key for development builds
key: process.env.MANIFEST_KEY,
host_permissions: HOST_PERMISSIONS,
permissions: ['storage', 'unlimitedStorage', 'background'],
background: {
service_worker: 'static/js/background.js',
},
content_scripts: [
{
matches: HOST_PERMISSIONS,
css: ['/static/css/content.css'],
js: ['/static/js/content.js'],
},
],
web_accessible_resources: [
{
resources: ['static/media/*', '*'],
matches: HOST_PERMISSIONS,
},
],
icons: {
16: `icons/icon_${mode}_16.png`,
48: `icons/icon_${mode}_48.png`,
128: `icons/icon_${mode}_128.png`,
},
action: {
default_popup: 'popup.html',
},
} satisfies chrome.runtime.ManifestV3;
return manifest;
}

View File

@@ -0,0 +1,116 @@
import path from 'path';
import dotenv from 'dotenv';
import webpack, { WebpackPluginInstance } from 'webpack';
import { EntryId } from 'webpack/webpack.config';
import CreateFileWebpack from 'create-file-webpack';
import CopyWebpackPlugin from 'copy-webpack-plugin';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import WebpackBuildNotifierPlugin from 'webpack-build-notifier';
import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin';
import HTMLWebpackPlugin from 'html-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import TypeErrorNotifierPlugin from './custom/TypeErrorNotifierPlugin';
/**
* Gets the plugins that are used in the build process
* @param mode the environment that the build is running in
* @param htmlEntries the entry points that need an html file
* @param manifest the manifest.json file
* @returns an array of webpack plugins
*/
export function getBuildPlugins(mode: Environment, htmlEntries: EntryId[], manifest: chrome.runtime.ManifestV3) {
let plugins: WebpackPluginInstance[] = [];
// show the progress of the build
plugins.push(new webpack.ProgressPlugin());
// make sure that the paths are case sensitive
plugins.push(new CaseSensitivePathsPlugin());
// specify how the outputed css files should be named
plugins.push(
new MiniCssExtractPlugin({
filename: 'static/css/[name].css',
chunkFilename: 'static/css/[name].chunk.css',
})
);
// create an html file for each entry point that needs one
for (const entryId of htmlEntries) {
// if (!entries[entryId]) return;
plugins.push(
new HTMLWebpackPlugin({
hash: false,
filename: `${entryId}.html`,
chunks: [entryId],
title: `${manifest.short_name} ${entryId} `,
template: path.resolve('webpack', 'plugins', 'template.html'),
})
);
}
// write the manifest.json file to the build directory
plugins.push(
new CreateFileWebpack({
path: path.resolve('build'),
fileName: 'manifest.json',
content: JSON.stringify(manifest, null, 2),
})
);
// copy the public directory to the build directory, but only copy the icons for the current mode
plugins.push(
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve('public'),
filter: path => (path.includes('icons') ? path.includes(mode) : true),
},
],
})
);
// run the typescript checker in a separate process
plugins.push(
new ForkTsCheckerWebpackPlugin({
async: false,
})
);
// notify the developer of build events when in development mode
if (mode === 'development') {
plugins.push(
new WebpackBuildNotifierPlugin({
title: `${manifest.short_name} v${manifest.version} ${mode}`,
logo: path.resolve('public', 'icons', 'icon_production_128.png'),
failureSound: 'Ping',
showDuration: true,
suppressWarning: true,
})
);
}
// notify the developer of type errors
plugins.push(new TypeErrorNotifierPlugin());
// define the environment variables that are available within the extension code
plugins.push(
new webpack.DefinePlugin({
'process.env': JSON.stringify({
SEMANTIC_VERSION: process.env.SEMANTIC_VERSION,
NODE_ENV: mode,
...dotenv.config({ path: `.env.${mode}` }).parsed,
} satisfies typeof process.env),
})
);
// provide some global nodejs variables so that nodejs libraries can be used
plugins.push(
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
process: 'process/browser',
})
);
return plugins;
}

View File

@@ -0,0 +1,84 @@
import { Compiler } from 'webpack';
import path from 'path';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import WebpackBuildNotifierPlugin from 'webpack-build-notifier';
import { Issue, IssueLocation } from 'fork-ts-checker-webpack-plugin/lib/issue';
interface Resource {
path: string;
location: IssueLocation;
}
/**
* This plugin hooks into the fork-ts-checker-webpack-plugin and
* notifies the developer of type errors using the webpack-build-notifier plugin.
*/
export default class TypeErrorNotifierPlugin {
apply(compiler: Compiler) {
// hook into the fork-ts-checker-webpack-plugin
const hooks = ForkTsCheckerWebpackPlugin.getCompilerHooks(compiler);
hooks.issues.tap('MyPlugin', issues => {
const errors = issues.filter(issue => issue.severity === 'error');
if (!errors?.[0]?.message) {
return errors;
}
let error = errors[0];
let resource = getErrorResource(error);
try {
notifyTypeError(resource, error.message, errors);
} catch (e) {
console.error(e);
}
return errors;
});
}
}
function notifyTypeError(resource: Resource, message: string, errors: Issue[]) {
const { line, column } = resource.location.start;
const buildNotifier = new WebpackBuildNotifierPlugin({
logo: path.resolve('public', 'icons', 'icon_production_128.png'),
compilationSound: 'Pop',
failureSound: 'Sosumi',
title: `TS: ${errors.length} errors`,
notifyOptions: {
open: `vscode://file/${resource.path}:${line}:${column}`,
},
});
const fakeInput = {
hasErrors: () => true,
compilation: {
children: null,
errors: [
{
message,
module: {
resource: resource.path,
},
},
],
},
};
// @ts-ignore - private method
buildNotifier.onCompilationDone(fakeInput);
}
function getErrorResource(error: Issue): Resource {
return {
path: error.file ?? '',
location: error.location ?? {
end: {
column: 0,
line: 0,
},
start: {
column: 0,
line: 0,
},
},
};
}

View File

@@ -0,0 +1,36 @@
import { Server } from 'socket.io';
import { Compiler } from 'webpack';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
// Name of the plugin
const PLUGIN_NAME = 'HotReloadServer';
// How long to wait before reloading the browser after a successful build
const RELOAD_LOCKOUT = 2000;
// we want to cache all the "reload requests" here so we don't have to keep re-compiling the app while typing
const reloads: NodeJS.Timeout[] = [];
let io: Server;
export function initializeHotReloading(port: number, compiler: Compiler) {
io = new Server(port);
const hooks = ForkTsCheckerWebpackPlugin.getCompilerHooks(compiler);
hooks.issues.tap(PLUGIN_NAME, (issues, compilation) => {
const typeErrors = issues.filter(issue => issue.severity === 'error');
const buildErrors = compilation?.errors ?? [];
// if no errors (thus successful build), lets queue up a reload for the browser
if (typeErrors.length === 0 && buildErrors.length === 0) {
reloads.push(setTimeout(() => io.emit('reload'), RELOAD_LOCKOUT));
}
return typeErrors;
});
// if a recompile is triggered, we want to clear out all the queue'd reloads
// (so we don't spam-reload the extension while we are still typing
compiler.hooks.compile.tap(PLUGIN_NAME, () => {
reloads.forEach(reload => clearTimeout(reload));
reloads.length = 0;
});
}

View File

@@ -0,0 +1,12 @@
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
import path from 'path';
import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin';
export const moduleResolutionPlugins = [
// this will make sure that webpack uses the tsconfig path aliases
new TsconfigPathsPlugin({
configFile: path.resolve('src', 'tsconfig.json'),
}),
// this will make sure that we don't import anything outside of the src directory from the src directory
new ModuleScopePlugin(path.resolve('src'), path.resolve('package.json')),
];

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
--></body>
</html>

46
webpack/production.ts Normal file
View File

@@ -0,0 +1,46 @@
import webpack from 'webpack';
import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages';
import config from './webpack.config';
import { info, success } from './utils/chalk';
import { getManifest } from './manifest.config';
import { version } from '../package.json';
import { convertSemver } from './utils/convertSemver';
import printError from './utils/printError';
import { zipProductionBuild } from './utils/zipProductionBuild';
const MODE: Environment = 'production';
// generate the manifest.json file
const semanticVersion = process.env.SEMANTIC_VERSION || version;
const manifestVersion = convertSemver(semanticVersion);
const manifest = getManifest(MODE, manifestVersion);
console.log(info(`${manifest.short_name} v${manifest.version} ${MODE} build starting...`));
// kick off the webpack build
webpack(config(MODE, manifest), async error => {
if (!error) {
await onBuildSuccess();
process.exit(0);
}
await onBuildFailure(error);
process.exit(1);
});
async function onBuildSuccess(): Promise<void> {
// zip the output directory and put it in the artifacts directory
const fileName = `${manifest.short_name} v${manifestVersion}`;
await zipProductionBuild(fileName);
console.log(success(`${fileName} built and zipped into build/artifacts/${fileName}.zip!`));
}
function onBuildFailure(error: Error): void {
if (!error.message) {
return printError(error);
}
const messages = formatWebpackMessages({ errors: [error.message], warnings: [] });
if (messages.errors.length > 1) {
messages.errors.length = 1;
}
return printError(new Error(messages.errors.join('\n\n')));
}

52
webpack/release.ts Normal file
View File

@@ -0,0 +1,52 @@
import { simpleGit } from 'simple-git';
import prompts from 'prompts';
import { error, info } from './utils/chalk';
import { getSourceRef } from './utils/git/getSourceRef';
const git = simpleGit();
const status = await git.status();
if (status.files.length) {
console.log(error('Working directory is not clean, please commit or stash changes before releasing.'));
process.exit(1);
}
const { destinationBranch } = await prompts({
type: 'select',
name: 'destinationBranch',
message: 'What kind of release do you want to create?',
choices: ['preview', 'production'].map(releaseType => ({
title: releaseType,
value: releaseType,
})),
});
const sourceRef = await getSourceRef(destinationBranch);
const { confirm } = await prompts({
type: 'confirm',
name: 'confirm',
message: `Are you sure you want to create a ${destinationBranch} release from ${sourceRef}?`,
});
if (!confirm) {
console.log(error('Aborting release.'));
process.exit(1);
}
// we fetch the latest changes from the remote
await git.fetch();
// we checkout the source ref, pull the latest changes and then checkout the destination branch
console.info(`Checking out and updating ${sourceRef}...`);
await git.checkout(sourceRef);
await git.pull('origin', sourceRef);
console.info(`Checking out and updating ${destinationBranch}...`);
await git.checkout(destinationBranch);
await git.pull('origin', destinationBranch);
// we trigger the release github action by merging the source ref into the destination branch
console.info(`Merging ${sourceRef} into ${destinationBranch}...`);
await git.merge([sourceRef, '--ff-only']);
await git.push('origin', destinationBranch);
console.info(info(`Release to ${destinationBranch} created! Check github for status`));

15
webpack/tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"module": "esnext",
"esModuleInterop": true,
"composite": true,
"lib": [
"es2021"
],
"types": [
"chrome",
"node"
],
},
}

7
webpack/utils/chalk.ts Normal file
View File

@@ -0,0 +1,7 @@
import chalk from 'chalk';
export const error = chalk.bold.red;
export const { bold } = chalk;
export const info = chalk.bgHex('#673AB7').rgb(255, 255, 255);
export const warning = chalk.bgHex('#FF9800').rgb(255, 255, 255);
export const success = chalk.bgHex('#4CAF50').rgb(255, 255, 255);

View File

@@ -0,0 +1,22 @@
import { parse } from 'semver';
/**
* Converts npm semver-style strings (including pre-releases) to a release version compatible
* with the extension stores.
*
* @example
* semverVersionTo('1.0.0-beta.1`) returns 1.0.0.100
*/
export function convertSemver(version: string): string {
const semver = parse(version);
if (!semver) {
throw new Error(`Couldn't parse ${version}!`);
}
const { major, minor, patch, prerelease } = semver;
let manifestVersion = `${major}.${minor}.${patch}`;
if (prerelease.length) {
manifestVersion += `.${prerelease[1]}00`;
}
return manifestVersion;
}

View File

@@ -0,0 +1,28 @@
import prompts from 'prompts';
import { simpleGit } from 'simple-git';
import { error } from '../chalk';
const git = simpleGit();
export async function getSourceRef(destinationBranch: 'preview' | 'production'): Promise<string> {
if (destinationBranch === 'preview') {
return 'main';
}
const tags = await git.tags(['--sort=-committerdate']);
const alphaTags = tags.all.filter((tag: string) => tag.includes('alpha'));
if (!alphaTags.length) {
console.log(error('No preview builds found, please create one before releasing a production build.'));
process.exit(1);
}
const { sourceTag } = await prompts({
message: 'Which preview tag do you want to create a production build from?',
type: 'select',
name: 'sourceTag',
choices: alphaTags.map(tag => ({ title: tag, value: tag })),
});
return sourceTag;
}

View File

@@ -0,0 +1,16 @@
import printBuildError from 'react-dev-utils/printBuildError';
import { error } from './chalk';
/**
* Print Errors that we got back from webpack
* @param e the error provided by webpacxk
*/
export default function printError(e: Error) {
console.log('printBuildError -> e', e);
if (process.env.TSC_COMPILE_ON_ERROR === 'true') {
printBuildError(e);
} else {
// console.log(error('Failed to compile.\n'));
printBuildError(e);
process.exit(1);
}
}

View File

@@ -0,0 +1,35 @@
import fs, { mkdirSync } from 'fs';
import archiver from 'archiver';
import chalk from 'chalk';
import path from 'path';
/**
* Creates a zip file from the given source directory
* @param fileName the name of the zip file to create
* @param outDir the directory to zip up
* @param globOptions the glob options to use when finding files to zip
* @returns
*/
export async function zipProductionBuild(fileName: string) {
const outDirectory = path.resolve('build');
const artifactsDir = path.join(outDirectory, 'artifacts');
mkdirSync(artifactsDir, { recursive: true });
const output = fs.createWriteStream(`${artifactsDir}/${fileName}.zip`);
const archive = archiver('zip', {
zlib: { level: 9 },
});
archive.pipe(output);
const promise = new Promise((resolve, reject) => {
output.on('close', resolve);
archive.on('warning', warn => console.log(chalk.red(warn)));
archive.on('error', err => reject(err));
});
archive.glob('**/*', { cwd: outDirectory, ignore: ['*.zip', 'artifacts'] });
// eslint-disable-next-line no-void
void archive.finalize(); // The promise returned is what's `await-ed`, not the call to `finalize()`
return promise;
}

119
webpack/webpack.config.ts Normal file
View File

@@ -0,0 +1,119 @@
import { Configuration, EntryObject } from 'webpack';
import path from 'path';
import TerserPlugin from 'terser-webpack-plugin';
import { moduleResolutionPlugins } from './plugins/moduleResolutionPlugins';
import loaders from './loaders';
import { getBuildPlugins } from './plugins/buildProcessPlugins';
export interface Entries {
content: string[];
background: string[];
popup: string[];
// only used in development
debug?: string[];
}
export type EntryId = keyof Entries;
/**
* This function will generate the webpack configuration for the extension
* @param mode the mode that webpack is running in (development or production)
* * @param manifest the manifest.json object for the extension
* @returns the webpack configuration
*/
export default function config(mode: Environment, manifest: chrome.runtime.ManifestV3): Configuration {
const outDirectory = path.resolve('build');
// the entry points for the extension (the files that webpack will start bundling from)
const entry: Entries = {
content: [path.resolve('src', 'views', 'content', 'content')],
background: [path.resolve('src', 'background', 'background')],
popup: [path.resolve('src', 'views', 'popup', 'popup')],
};
// the entries that need an html file to be generated
const htmlEntries: EntryId[] = mode === 'development' ? ['popup', 'debug'] : ['popup'];
if (mode === 'development') {
// TODO: add hot reloading script to the debug entry
entry.debug = [path.resolve('src', 'debug')];
// we need to import react-devtools before the react code in development
entry.content = [path.resolve('src', 'views', 'reactDevtools'), ...entry.content];
entry.popup = [path.resolve('src', 'views', 'reactDevtools'), ...entry.popup];
}
/** @see https://webpack.js.org/configuration for documentation */
const config: Configuration = {
mode,
devtool: mode === 'development' ? 'cheap-module-source-map' : undefined,
bail: true,
cache: true,
// entry and resolve is what webpack uses for figuring out where to start bundling and how to resolve modules
entry: entry as unknown as EntryObject,
resolve: {
modules: ['node_modules'],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
plugins: moduleResolutionPlugins,
// this is to polyfill some node-only modules
fallback: {
crypto: 'crypto-browserify',
stream: 'stream-browserify',
buffer: 'buffer',
},
},
// this is where we define the loaders for different file types
module: {
strictExportPresence: true,
rules: loaders,
},
output: {
clean: true,
path: outDirectory,
pathinfo: mode === 'development',
filename: 'static/js/[name].js',
publicPath: '/',
// this is for windows support (which uses backslashes in paths)
devtoolModuleFilenameTemplate: info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'),
// this is to make sure that the global chunk loading function name is unique
chunkLoadingGlobal: `webpackJsonp${manifest.short_name}`,
globalObject: 'this',
},
stats: {
errorDetails: true,
errorsCount: true,
},
// this is where we define the plugins that webpack will use
plugins: getBuildPlugins(mode, htmlEntries, manifest),
};
if (mode === 'production') {
config.optimization = {
minimize: true,
minimizer: [
new TerserPlugin({
extractComments: false,
parallel: false,
terserOptions: {
compress: {
ecma: 2020,
drop_console: true,
drop_debugger: true,
comparisons: false,
inline: 2,
},
keep_classnames: false,
keep_fnames: false,
output: {
ecma: 2020,
comments: false,
ascii_only: true,
},
},
}),
],
};
}
return config;
}