feat: cleanup webpack deps (#14530)

#### PR Dependency Tree


* **PR #14530** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Breaking Changes**
  * Webpack bundler support removed from the build system
* Bundler selection parameter removed from build and development
commands

* **Refactor**
  * Build configuration consolidated to a single bundler approach
* Webpack-specific build paths and workflows removed; development server
simplified

* **Chores**
  * Removed webpack-related dev dependencies and tooling
  * Updated package build scripts for a unified bundle command

* **Dependencies**
* Upgraded Sentry packages across frontend packages
(react/electron/esbuild plugin)
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-02-28 00:24:08 +08:00
committed by GitHub
parent a4e2242b8d
commit 2cb171f553
30 changed files with 588 additions and 1929 deletions
+100
View File
@@ -0,0 +1,100 @@
function testPackageName(regexp: RegExp): (module: any) => boolean {
return (module: any) =>
module.nameForCondition && regexp.test(module.nameForCondition());
}
// https://hackernoon.com/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758
export const productionCacheGroups = {
i18n: {
test: /frontend[\\/]i18n[\\/]/,
name: (module: any) => {
const name = module.resource.match(/[\\/]([^\\/]+)\.json$/)?.[1];
if (name && name !== 'en') {
return `i18n-langs.${name}`;
}
return 'i18n';
},
priority: 200,
enforce: true,
},
asyncVendor: {
test: /[\\/]node_modules[\\/]/,
name(module: any) {
const modulePath =
module?.nameForCondition?.() || module?.context || module?.resource;
if (!modulePath || typeof modulePath !== 'string') {
return 'app-async';
}
// monorepo linked in node_modules, so it's not a npm package
if (!modulePath.includes('node_modules')) {
return `app-async`;
}
const name = modulePath.match(
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
)?.[1];
return `npm-async-${name}`;
},
priority: Number.MAX_SAFE_INTEGER,
chunks: 'async' as const,
},
blocksuite: {
name: `npm-blocksuite`,
test: testPackageName(/[\\/]node_modules[\\/](@blocksuite)[\\/]/),
priority: 200,
enforce: true,
},
react: {
name: `npm-react`,
test: testPackageName(
/[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/
),
priority: 200,
enforce: true,
},
jotai: {
name: `npm-jotai`,
test: testPackageName(/[\\/]node_modules[\\/](jotai)[\\/]/),
priority: 200,
enforce: true,
},
rxjs: {
name: `npm-rxjs`,
test: testPackageName(/[\\/]node_modules[\\/]rxjs[\\/]/),
priority: 200,
enforce: true,
},
lodash: {
name: `npm-lodash`,
test: testPackageName(/[\\/]node_modules[\\/]lodash[\\/]/),
priority: 200,
enforce: true,
},
emotion: {
name: `npm-emotion`,
test: testPackageName(/[\\/]node_modules[\\/](@emotion)[\\/]/),
priority: 200,
enforce: true,
},
vendor: {
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
priority: 190,
enforce: true,
},
styles: {
name: 'styles',
test: (module: any) =>
module.nameForCondition &&
module.nameForCondition()?.endsWith('.css') &&
!module.type.startsWith('javascript'),
chunks: 'all' as const,
minSize: 1,
minChunks: 1,
reuseExistingChunk: true,
priority: 1000,
enforce: true,
},
};
@@ -0,0 +1,116 @@
(function () {
let errorEl = null;
function showGlobalErrorPage() {
if (errorEl) {
return;
}
errorEl = document.createElement('div');
errorEl.innerHTML = [
'<style>',
'.gue {display:flex;flex-direction:column;align-items:center;justify-content:center;width:380px;}',
'.gue img{width:380px;}',
'.gue div{padding:16px 40px 0 40px;text-align:center;}',
'.gue .p1{color:#141414;line-height:24px;font-weight:500;}',
'.gue .p2{color:#7A7A7A;line-height:22px;}',
'</style>',
'<div class="gue">',
'<img src="https://cdn.affine.pro/error.png" />',
'<div>',
'<p class="p1">Unsupported Environment</p>',
'<p class="p2">',
'It looks like AFFiNE cannot run in this environment.',
"Please ensure you are using a supported browser or update your device's operating system to the latest version.",
'If the issue persists, visit our <a href="https://github.com/toeverything/AFFiNE/issues">support page</a> for further assistance.',
'</p>',
'</div>',
'</div>',
].join('');
errorEl.setAttribute(
'style',
'position:absolute;top:0;left:0;height:100vh;width:100vw;display:flex;flex-direction:column;align-items:center;justify-content:center;background:white;z-index:999;'
);
document.body.append(errorEl);
}
/**
* @param event {PromiseRejectionEvent|ErrorEvent}
*/
function handler(event) {
let error;
if ('error' in event) {
error =
event.error ||
(event.message === 'Script error.'
? new SyntaxError(event.message)
: new Error(event.message));
} else {
error = event.reason;
}
console.error('unhandled unrecoverable error', error);
const shouldCache =
// syntax error
error && error instanceof SyntaxError;
if (!shouldCache) {
return;
}
event.stopImmediatePropagation();
showGlobalErrorPage();
}
function registerGlobalErrorHandler() {
if (typeof document !== 'undefined') {
globalThis.addEventListener('unhandledrejection', handler);
globalThis.addEventListener('error', handler);
return function () {
globalThis.removeEventListener('unhandledrejection', handler);
globalThis.removeEventListener('error', handler);
};
}
return null;
}
function unregisterRegisterGlobalErrorHandler(fn) {
if (typeof fn === 'function') {
const app = document.getElementById('app');
if (app) {
let ob = new MutationObserver(function () {
fn();
ob.disconnect();
ob = null;
});
ob.observe(app, { childList: true });
}
}
}
function ensureBasicEnvironment() {
const globals = [
'Promise',
'Map',
'fetch',
'customElements',
'MutationObserver',
];
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < globals.length; i++) {
if (!(globals[i] in globalThis)) {
showGlobalErrorPage();
return;
}
}
}
ensureBasicEnvironment();
const goodtogo = registerGlobalErrorHandler();
unregisterRegisterGlobalErrorHandler(goodtogo);
})();
+292
View File
@@ -0,0 +1,292 @@
import { execSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { Path, ProjectRoot } from '@affine-tools/utils/path';
import { Repository } from '@napi-rs/simple-git';
import HTMLPlugin from 'html-webpack-plugin';
import { once } from 'lodash-es';
type PluginLike = {
apply: (compiler: CompilerLike) => void;
};
type CompilerLike = {
webpack?: {
sources?: {
RawSource?: new (source: string) => unknown;
};
};
hooks: {
compilation: {
tap: (name: string, callback: (compilation: any) => void) => void;
};
};
};
function createRawSource(compiler: CompilerLike, source: string) {
const RawSource = compiler.webpack?.sources?.RawSource;
if (!RawSource) {
throw new Error(
'compiler.webpack.sources.RawSource is required for html plugin assets emission'
);
}
return new RawSource(source);
}
export const getPublicPath = (BUILD_CONFIG: BUILD_CONFIG_TYPE) => {
const { BUILD_TYPE } = process.env;
if (typeof process.env.PUBLIC_PATH === 'string') {
return process.env.PUBLIC_PATH;
}
if (
BUILD_CONFIG.debug ||
BUILD_CONFIG.distribution === 'desktop' ||
BUILD_CONFIG.distribution === 'ios' ||
BUILD_CONFIG.distribution === 'android'
) {
return '/';
}
switch (BUILD_TYPE) {
case 'stable':
return 'https://prod.affineassets.com/';
case 'beta':
return 'https://beta.affineassets.com/';
default:
return 'https://dev.affineassets.com/';
}
};
const DESCRIPTION = `There can be more than Notion and Miro. AFFiNE is a next-gen knowledge base that brings planning, sorting and creating all together.`;
const gitShortHash = once(() => {
const { GITHUB_SHA } = process.env;
if (GITHUB_SHA) {
return GITHUB_SHA.substring(0, 9);
}
const repo = new Repository(ProjectRoot.value);
const shortSha = repo.head().target()?.substring(0, 9);
if (shortSha) {
return shortSha;
}
const sha = execSync(`git rev-parse --short HEAD`, {
encoding: 'utf-8',
}).trim();
return sha;
});
const currentDir = Path.dir(import.meta.url);
export interface CreateHTMLPluginConfig {
filename?: string;
additionalEntryForSelfhost?: boolean;
selfhostPublicPath?: string;
injectGlobalErrorHandler?: boolean;
emitAssetsManifest?: boolean;
}
function getHTMLPluginOptions(BUILD_CONFIG: BUILD_CONFIG_TYPE) {
const publicPath = getPublicPath(BUILD_CONFIG);
const cdnOrigin = publicPath.startsWith('/')
? undefined
: new URL(publicPath).origin;
const templateParams = {
GIT_SHORT_SHA: gitShortHash(),
DESCRIPTION,
PRECONNECT: cdnOrigin
? `<link rel="preconnect" href="${cdnOrigin}" />`
: '',
VIEWPORT_FIT: BUILD_CONFIG.isMobileEdition ? 'cover' : 'auto',
};
return {
template: currentDir.join('template.html').toString(),
inject: 'body',
minify: false,
templateParameters: templateParams,
chunks: ['app'],
scriptLoading: 'blocking',
} satisfies HTMLPlugin.Options;
}
const AssetsManifestPlugin = {
apply(compiler: CompilerLike) {
compiler.hooks.compilation.tap('assets-manifest-plugin', compilation => {
HTMLPlugin.getHooks(compilation).beforeAssetTagGeneration.tap(
'assets-manifest-plugin',
arg => {
if (!compilation.getAsset('assets-manifest.json')) {
compilation.emitAsset(
`assets-manifest.json`,
createRawSource(
compiler,
JSON.stringify(
{
...arg.assets,
js: arg.assets.js.map(file =>
file.substring(arg.assets.publicPath.length)
),
css: arg.assets.css.map(file =>
file.substring(arg.assets.publicPath.length)
),
gitHash: gitShortHash(),
description: DESCRIPTION,
},
null,
2
)
),
{
immutable: false,
}
);
}
return arg;
}
);
});
},
};
const GlobalErrorHandlerPlugin = {
apply(compiler: CompilerLike) {
const globalErrorHandler = [
'js/global-error-handler.js',
readFileSync(currentDir.join('./error-handler.js').toString(), 'utf-8'),
];
compiler.hooks.compilation.tap(
'global-error-handler-plugin',
compilation => {
HTMLPlugin.getHooks(compilation).beforeAssetTagGeneration.tap(
'global-error-handler-plugin',
arg => {
if (!compilation.getAsset(globalErrorHandler[0])) {
compilation.emitAsset(
globalErrorHandler[0],
createRawSource(compiler, globalErrorHandler[1])
);
arg.assets.js.unshift(
arg.assets.publicPath + globalErrorHandler[0]
);
}
return arg;
}
);
}
);
},
};
const CorsPlugin = {
apply(compiler: CompilerLike) {
compiler.hooks.compilation.tap('html-js-cors-plugin', compilation => {
HTMLPlugin.getHooks(compilation).alterAssetTags.tap(
'html-js-cors-plugin',
options => {
if (options.publicPath !== '/') {
options.assetTags.scripts.forEach(script => {
script.attributes.crossorigin = true;
});
options.assetTags.styles.forEach(style => {
style.attributes.crossorigin = true;
});
}
return options;
}
);
});
},
};
export function createHTMLPlugins(
BUILD_CONFIG: BUILD_CONFIG_TYPE,
config: CreateHTMLPluginConfig
): (HTMLPlugin | PluginLike)[] {
const publicPath = getPublicPath(BUILD_CONFIG);
const htmlPluginOptions = getHTMLPluginOptions(BUILD_CONFIG);
const selfhostPublicPath = config.selfhostPublicPath ?? '/';
const plugins: (HTMLPlugin | PluginLike)[] = [];
plugins.push(
new HTMLPlugin({
...htmlPluginOptions,
chunks: ['index'],
filename: config.filename,
publicPath,
meta: {
'env:publicPath': publicPath,
},
})
);
if (BUILD_CONFIG.isElectron) {
plugins.push(
new HTMLPlugin({
...htmlPluginOptions,
chunks: ['shell'],
filename: 'shell.html',
publicPath,
meta: {
'env:publicPath': publicPath,
},
}),
new HTMLPlugin({
...htmlPluginOptions,
filename: 'popup.html',
chunks: ['popup'],
publicPath,
meta: {
'env:publicPath': publicPath,
},
}),
new HTMLPlugin({
...htmlPluginOptions,
filename: 'background-worker.html',
chunks: ['backgroundWorker'],
publicPath,
meta: {
'env:publicPath': publicPath,
},
})
);
}
if (!BUILD_CONFIG.isElectron) {
plugins.push(CorsPlugin);
}
if (config.emitAssetsManifest) {
plugins.push(AssetsManifestPlugin);
}
if (config.injectGlobalErrorHandler) {
plugins.push(GlobalErrorHandlerPlugin);
}
if (config.additionalEntryForSelfhost) {
plugins.push(
new HTMLPlugin({
...htmlPluginOptions,
chunks: ['index'],
publicPath: selfhostPublicPath,
meta: {
'env:isSelfHosted': 'true',
'env:publicPath': selfhostPublicPath,
},
filename: 'selfhost.html',
templateParameters: {
...htmlPluginOptions.templateParameters,
PRECONNECT: '',
},
})
);
}
return plugins;
}
@@ -0,0 +1,18 @@
import { parse } from 'node:path';
export const raw = true;
/**
* @type {import('webpack').LoaderDefinitionFunction}
*/
export default function loader(content) {
const name = parse(this.resourcePath).base;
this.emitFile(name, content);
return `
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
const binding = require('./${name}')
export default binding
`;
}
+141
View File
@@ -0,0 +1,141 @@
import { readdir, readFile } from 'node:fs/promises';
import { join, relative, sep } from 'node:path';
import { createS3CompatClient } from '@affine/s3-compat';
import { lookup } from 'mime-types';
export const R2_BUCKET =
process.env.R2_BUCKET ??
(process.env.BUILD_TYPE === 'canary' ? 'assets-dev' : 'assets-prod');
const S3_UPLOAD_PACKAGE_NAMES = new Set([
'@affine/web',
'@affine/mobile',
'@affine/admin',
]);
const MAX_UPLOAD_RETRIES = 3;
const UPLOAD_RETRY_BASE_DELAY_MS = 500;
function createR2Client() {
const { R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY } = process.env;
if (!R2_ACCOUNT_ID || !R2_ACCESS_KEY_ID || !R2_SECRET_ACCESS_KEY) {
throw new Error('Missing R2 credentials for uploading release assets');
}
return createS3CompatClient(
{
region: 'auto',
bucket: R2_BUCKET,
forcePathStyle: true,
endpoint: `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
},
{
accessKeyId: R2_ACCESS_KEY_ID,
secretAccessKey: R2_SECRET_ACCESS_KEY,
}
);
}
async function collectFiles(dir: string): Promise<string[]> {
const dirs = [dir];
const files: string[] = [];
while (dirs.length > 0) {
const current = dirs.pop()!;
const entries = await readdir(current, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(current, entry.name);
if (entry.isDirectory()) {
dirs.push(fullPath);
} else if (entry.isFile()) {
files.push(fullPath);
}
}
}
return files;
}
function toAssetKey(outputPath: string, filePath: string): string {
return relative(outputPath, filePath).split(sep).join('/');
}
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function putObjectWithRetry(
s3: ReturnType<typeof createR2Client>,
asset: string,
assetSource: Buffer,
contentType: string | false | undefined
) {
let retries = 0;
while (true) {
try {
await s3.putObject(asset, assetSource, {
contentType: contentType || undefined,
contentLength: assetSource.byteLength,
});
return;
} catch (error) {
if (retries >= MAX_UPLOAD_RETRIES) {
throw error;
}
retries += 1;
const delay = UPLOAD_RETRY_BASE_DELAY_MS * 2 ** (retries - 1);
const errorMessage =
error instanceof Error ? error.message : String(error);
console.warn(
`[s3-upload] Retry ${retries}/${MAX_UPLOAD_RETRIES} for ${asset}: ${errorMessage}`
);
await sleep(delay);
}
}
}
async function runInParallel<T>(
values: T[],
worker: (value: T) => Promise<void>,
concurrency = 16
) {
if (values.length === 0) {
return;
}
let nextIndex = 0;
const workers = Array.from(
{ length: Math.min(concurrency, values.length) },
async () => {
while (true) {
const index = nextIndex++;
if (index >= values.length) {
return;
}
await worker(values[index]!);
}
}
);
await Promise.all(workers);
}
export function shouldUploadReleaseAssets(pkgName: string): boolean {
return S3_UPLOAD_PACKAGE_NAMES.has(pkgName);
}
export async function uploadDistAssetsToS3(outputPath: string) {
const allFiles = await collectFiles(outputPath);
const uploadFiles = allFiles.filter(file => !file.endsWith('.html'));
if (uploadFiles.length === 0) {
return;
}
const s3 = createR2Client();
await runInParallel(uploadFiles, async filePath => {
const asset = toAssetKey(outputPath, filePath);
const assetSource = await readFile(filePath);
const contentType = lookup(asset);
await putObjectWithRetry(s3, asset, assetSource, contentType);
});
}
+48
View File
@@ -0,0 +1,48 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=<%= VIEWPORT_FIT %>"
/>
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta
name="apple-mobile-web-app-status-bar-style"
content="black-translucent"
/>
<title>AFFiNE</title>
<meta name="theme-color" content="#fafafa" />
<%= PRECONNECT %>
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" sizes="192x192" href="/favicon-192.png" />
<link rel="shortcut icon" href="/favicon.ico?v=2" />
<meta name="emotion-insertion-point" content="" />
<meta property="description" content="<%= DESCRIPTION %>" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content="https://app.affine.pro/" />
<meta
name="twitter:title"
content="AFFiNE: There can be more than Notion and Miro."
/>
<meta name="twitter:description" content="<%= DESCRIPTION %>" />
<meta name="twitter:site" content="@AffineOfficial" />
<meta name="twitter:image" content="https://affine.pro/og.jpeg" />
<meta
property="og:title"
content="AFFiNE: There can be more than Notion and Miro."
/>
<meta property="og:type" content="website" />
<meta property="og:description" content="<%= DESCRIPTION %>" />
<meta property="og:url" content="https://app.affine.pro/" />
<meta property="og:image" content="https://affine.pro/og.jpeg" />
</head>
<body>
<div id="app" data-version="<%= GIT_SHORT_SHA %>"></div>
</body>
</html>