import HtmlWebpackPlugin from "html-webpack-plugin";
import HtmlInlineScriptPlugin from "html-inline-script-webpack-plugin";
import FaviconsWebpackPlugin from "favicons-webpack-plugin";
import { CleanWebpackPlugin } from "clean-webpack-plugin";
import WorkboxPlugin from "workbox-webpack-plugin";
import ReactRefreshTypeScript from "react-refresh-typescript";
import ReactRefreshWebpackPlugin from "@pmmmwh/react-refresh-webpack-plugin";
import webpack from "webpack";
import { execSync } from "child_process";
import path from "path";
import { fileURLToPath } from "url";
import {
defineReactCompilerLoaderOption,
reactCompilerLoader,
} from "react-compiler-webpack";
function parseChangelog() {
return execSync('git log --grep "^Changelog: " -10')
.toString()
.split(/^commit /m)
.slice(1)
.map((commit) => {
const changelogIndex = commit.indexOf(" Changelog: ");
if (changelogIndex === -1) {
throw `Changelog not found in commit:\n${commit}`;
}
const dateString = commit.match(/^Date: (.*)$/m)?.[1];
if (!dateString) {
throw `Date not found in commit:\n${commit}`;
}
const dateValue = new Date(dateString);
const date = dateValue.toISOString().split("T")[0];
const id = commit.substring(0, commit.indexOf("\n"));
const message = commit
.substring(changelogIndex + 15)
.replaceAll("\n ", "\n")
.trim();
return {
id,
message,
date,
};
})
.filter((changelog) => changelog.message);
}
export default async (env, argv) => {
const getContributorsForRepo = async (repoName) => {
const contributorsData = await fetch(
`https://api.github.com/repos/LiveSplit/${repoName}/contributors`,
{
headers: {
Authorization: env.GITHUB_TOKEN
? `Bearer ${env.GITHUB_TOKEN}`
: undefined,
},
},
);
return contributorsData.json();
};
const lsoContributorsList = await getContributorsForRepo("LiveSplitOne");
const coreContributorsList = await getContributorsForRepo("livesplit-core");
const coreContributorsMap = {};
for (const coreContributor of coreContributorsList) {
if (
coreContributor.type === "User" &&
!coreContributor.login.includes("dependabot")
) {
coreContributorsMap[coreContributor.login] = coreContributor;
}
}
for (let lsoContributor of lsoContributorsList) {
const existingContributor = coreContributorsMap[lsoContributor.login];
if (existingContributor) {
existingContributor.contributions += lsoContributor.contributions;
} else if (
lsoContributor.type === "User" &&
!lsoContributor.login.includes("dependabot")
) {
coreContributorsMap[lsoContributor.login] = lsoContributor;
}
}
const contributorsList = Object.values(coreContributorsMap)
// Sort by contributions, but fallback to alphabetical order for the
// same amount of contributions
.sort((a, b) => (a.login > b.login ? 1 : b.login > a.login ? -1 : 0))
.sort((a, b) => b.contributions - a.contributions)
.map((user) => {
return { id: user.id, name: user.login };
});
const commitHash = execSync("git rev-parse --short HEAD").toString();
const date = new Date()
.toISOString()
.replace("T", " ")
.replace(/\..+/, " UTC");
const changelog = parseChangelog();
const basePath = path.dirname(fileURLToPath(import.meta.url));
const isProduction = argv.mode === "production";
const isTauri = env.TAURI === "true";
const distPath = path.join(
...[
basePath,
...(isTauri ? ["src-tauri", "target", "dist"] : ["dist"]),
],
);
return {
entry: {
bundle: ["./src/index.tsx"],
},
output: {
filename: "[name].js",
path: distPath,
publicPath: "",
},
devtool: isProduction ? undefined : "source-map",
devServer: {
port: 8080,
hot: true,
},
resolve: {
extensions: [
".webpack.js",
".web.js",
".ts",
".tsx",
".js",
".json",
".wasm",
],
},
plugins: [
...(isProduction
? [
new CleanWebpackPlugin({
protectWebpackAssets: false,
cleanAfterEveryBuildPatterns: ["*.LICENSE.txt"],
}),
]
: []),
...(isTauri
? []
: [
new FaviconsWebpackPlugin({
logo: path.resolve("src/assets/icon.svg"),
inject: true,
logoMaskable: path.resolve("src/assets/maskable.svg"),
favicons: {
appName: "LiveSplit One",
appDescription:
"A version of LiveSplit that works on a lot of platforms.",
developerName: "CryZe",
developerURL: "https://livesplit.org",
background: "#171717",
theme_color: "#232323",
appleStatusBarStyle: "black-translucent",
icons: {
appleIcon: {
offset: 10,
},
appleStartup: {
offset: 15,
},
windows: false,
coast: false,
yandex: false,
},
start_url: "/",
},
}),
]),
new HtmlWebpackPlugin({
template: "./src/index.html",
}),
new webpack.DefinePlugin({
BUILD_DATE: JSON.stringify(date),
COMMIT_HASH: JSON.stringify(commitHash),
CONTRIBUTORS_LIST: JSON.stringify(contributorsList),
CHANGELOG: JSON.stringify(changelog),
}),
...(isProduction
? [
new HtmlInlineScriptPlugin({
scriptMatchPattern: ["^bundle.js$"],
}),
...(isTauri
? []
: [
new WorkboxPlugin.GenerateSW({
clientsClaim: true,
skipWaiting: true,
maximumFileSizeToCacheInBytes:
100 * 1024 * 1024,
exclude: [/^assets/, /\.LICENSE\.txt$/],
runtimeCaching: [
{
urlPattern: (context) => {
return (
self.origin ===
context.url.origin &&
context.url.pathname.startsWith(
"/assets/",
)
);
},
handler: "CacheFirst",
},
],
}),
]),
]
: []),
...(!isProduction ? [new ReactRefreshWebpackPlugin()] : []),
],
module: {
rules: [
{
test: /\.tsx?$/,
use: [
...(isProduction
? [
{
loader: reactCompilerLoader,
options: defineReactCompilerLoaderOption({
babelTransFormOpt: {
plugins: [
"@babel/plugin-proposal-explicit-resource-management",
],
},
}),
},
]
: []),
{
loader: "ts-loader",
options: {
getCustomTransformers: () => ({
before: [
!isProduction &&
ReactRefreshTypeScript(),
].filter(Boolean),
}),
transpileOnly: !isProduction,
},
},
],
exclude: "/node_modules",
},
{
test: /\.(s?)css$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
importLoaders: 1,
modules: {
auto: true,
// In development mode, we want to know
// which classes are being used in the DOM.
localIdentName: isProduction
? "[hash:base64]"
: "[name]_[local]_[hash:base64]",
},
},
},
"sass-loader",
],
},
{
test: /\.(png|jpg|gif|woff|ico|svg)$/,
type: "asset/resource",
},
],
},
mode: isProduction ? "production" : "development",
};
};