using my boilerplate yuh
This commit is contained in:
41
webpack/development.ts
Normal file
41
webpack/development.ts
Normal 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
52
webpack/loaders/index.ts
Normal 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;
|
||||
75
webpack/loaders/styleLoaders.ts
Normal file
75
webpack/loaders/styleLoaders.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
};
|
||||
57
webpack/manifest.config.ts
Normal file
57
webpack/manifest.config.ts
Normal 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;
|
||||
}
|
||||
116
webpack/plugins/buildProcessPlugins.ts
Normal file
116
webpack/plugins/buildProcessPlugins.ts
Normal 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;
|
||||
}
|
||||
84
webpack/plugins/custom/TypeErrorNotifierPlugin.ts
Normal file
84
webpack/plugins/custom/TypeErrorNotifierPlugin.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
36
webpack/plugins/custom/hotReloadServer.ts
Normal file
36
webpack/plugins/custom/hotReloadServer.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
12
webpack/plugins/moduleResolutionPlugins.ts
Normal file
12
webpack/plugins/moduleResolutionPlugins.ts
Normal 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')),
|
||||
];
|
||||
32
webpack/plugins/template.html
Normal file
32
webpack/plugins/template.html
Normal 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
46
webpack/production.ts
Normal 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
52
webpack/release.ts
Normal 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
15
webpack/tsconfig.json
Normal 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
7
webpack/utils/chalk.ts
Normal 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);
|
||||
22
webpack/utils/convertSemver.ts
Normal file
22
webpack/utils/convertSemver.ts
Normal 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;
|
||||
}
|
||||
28
webpack/utils/git/getSourceRef.ts
Normal file
28
webpack/utils/git/getSourceRef.ts
Normal 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;
|
||||
}
|
||||
16
webpack/utils/printError.ts
Normal file
16
webpack/utils/printError.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
35
webpack/utils/zipProductionBuild.ts
Normal file
35
webpack/utils/zipProductionBuild.ts
Normal 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
119
webpack/webpack.config.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user