refactor: webpack config (#11421)

This commit is contained in:
forehalo
2025-04-03 11:55:56 +00:00
parent 565d7b2b1e
commit c8d22d97d5
51 changed files with 766 additions and 547 deletions

View File

@@ -17,7 +17,7 @@ if (typeof window !== 'undefined') {
debug.enable('*');
console.warn('Debug logs enabled');
}
if (process.env.NODE_ENV === 'development') {
if (BUILD_CONFIG.debug) {
debug.enable('*,-micromark');
console.warn('Debug logs enabled');
}

View File

@@ -12,7 +12,8 @@
"./workspace": "./src/workspace.ts",
"./workspace/legacy-cloud": "./src/workspace/legacy-cloud/index.ts",
"./filter": "./src/filter.ts",
"./blocksuite": "./src/blocksuite/index.ts"
"./blocksuite": "./src/blocksuite/index.ts",
"./worker": "./src/worker.ts"
},
"peerDependencies": {
"@affine/templates": "workspace:*",

View File

@@ -17,6 +17,7 @@ export function setupGlobal() {
isMobile: false,
isSelfHosted: false,
publicPath: '/',
workerPath: '/js/',
};
if (globalThis.navigator) {

7
packages/common/env/src/worker.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
export function getWorkerUrl(name: string) {
if (BUILD_CONFIG.debug && !name.endsWith('.worker.js')) {
throw new Error(`worker should be named with '.worker.js', get ${name}`);
}
return environment.workerPath + name + '?v=' + BUILD_CONFIG.appVersion;
}

View File

@@ -8,6 +8,7 @@
"./utils": "./src/utils/index.ts",
"./app-config-storage": "./src/app-config-storage.ts",
"./op": "./src/op/index.ts",
"./atom": "./src/atom/index.ts",
".": "./src/index.ts"
},
"dependencies": {

View File

@@ -10,6 +10,7 @@
"dependencies": {
"@affine/component": "workspace:*",
"@affine/core": "workspace:*",
"@affine/env": "workspace:*",
"@affine/i18n": "workspace:*",
"@affine/nbstore": "workspace:*",
"@blocksuite/affine": "workspace:*",

View File

@@ -23,6 +23,7 @@ import { ClientSchemeProvider } from '@affine/core/modules/url/providers/client-
import { configureBrowserWorkbenchModule } from '@affine/core/modules/workbench';
import { WorkspacesService } from '@affine/core/modules/workspace';
import { configureBrowserWorkspaceFlavours } from '@affine/core/modules/workspace-engine';
import { getWorkerUrl } from '@affine/env/worker';
import { I18n } from '@affine/i18n';
import { StoreManagerClient } from '@affine/nbstore/worker/client';
import { defaultBlockMarkdownAdapterMatchers } from '@blocksuite/affine/adapters';
@@ -51,11 +52,7 @@ import { AffineTheme } from './plugins/affine-theme';
import { AIButton } from './plugins/ai-button';
const storeManagerClient = new StoreManagerClient(
new OpClient(
new Worker(
new URL(/* webpackChunkName: "nbstore" */ './nbstore.ts', import.meta.url)
)
)
new OpClient(new Worker(getWorkerUrl('nbstore.worker.js')))
);
window.addEventListener('beforeunload', () => {
storeManagerClient.dispose();

View File

@@ -9,6 +9,7 @@
"references": [
{ "path": "../../component" },
{ "path": "../../core" },
{ "path": "../../../common/env" },
{ "path": "../../i18n" },
{ "path": "../../../common/nbstore" },
{ "path": "../../../../blocksuite/affine/all" },

View File

@@ -1,8 +0,0 @@
export const config = {
entry: {
app: './src/app/index.tsx',
shell: './src/shell/index.tsx',
backgroundWorker: './src/background-worker/index.ts',
popup: './src/popup/index.tsx',
},
};

View File

@@ -2,7 +2,13 @@ import path from 'node:path';
import type { _AsyncVersionOf } from 'async-call-rpc';
import { AsyncCall } from 'async-call-rpc';
import type { UtilityProcess, WebContents } from 'electron';
import type {
BaseWindow,
OpenDialogOptions,
SaveDialogOptions,
UtilityProcess,
WebContents,
} from 'electron';
import {
app,
dialog,
@@ -57,7 +63,6 @@ class HelperProcessManager {
this.ready = new Promise((resolve, reject) => {
helperProcess.once('spawn', () => {
try {
this.#connectMain();
logger.info('[helper] forked', helperProcess.pid);
resolve();
} catch (err) {
@@ -91,11 +96,15 @@ class HelperProcessManager {
// bridge main <-> helper process
// also set up the RPC to the helper process
#connectMain() {
const dialogMethods = pickAndBind(dialog, [
'showOpenDialog',
'showSaveDialog',
]);
connectMain(window: BaseWindow) {
const dialogMethods = {
showOpenDialog: async (opts: OpenDialogOptions) => {
return dialog.showOpenDialog(window, opts);
},
showSaveDialog: async (opts: SaveDialogOptions) => {
return dialog.showSaveDialog(window, opts);
},
};
const shellMethods = pickAndBind(shell, [
'openExternal',
'showItemInFolder',

View File

@@ -63,8 +63,6 @@ export class MainWindowManager {
defaultHeight: 800,
});
await ensureHelperProcess();
const browserWindow = new BrowserWindow({
titleBarStyle: isMacOS()
? 'hiddenInset'
@@ -88,6 +86,8 @@ export class MainWindowManager {
sandbox: false,
},
});
const helper = await ensureHelperProcess();
helper.connectMain(browserWindow);
if (isLinux()) {
browserWindow.setIcon(

View File

@@ -16,6 +16,7 @@
"dependencies": {
"@affine/component": "workspace:*",
"@affine/core": "workspace:*",
"@affine/env": "workspace:*",
"@affine/i18n": "workspace:*",
"@affine/nbstore": "workspace:*",
"@blocksuite/affine": "workspace:*",

View File

@@ -35,6 +35,7 @@ import {
WorkspacesService,
} from '@affine/core/modules/workspace';
import { configureBrowserWorkspaceFlavours } from '@affine/core/modules/workspace-engine';
import { getWorkerUrl } from '@affine/env/worker';
import { I18n } from '@affine/i18n';
import { StoreManagerClient } from '@affine/nbstore/worker/client';
import { defaultBlockMarkdownAdapterMatchers } from '@blocksuite/affine/adapters';
@@ -414,12 +415,7 @@ export function App() {
}
function createStoreManagerClient() {
const worker = new Worker(
new URL(
/* webpackChunkName: "nbstore-worker" */ './worker.ts',
import.meta.url
)
);
const worker = new Worker(getWorkerUrl('nbstore.worker.js'));
const { port1: nativeDBApiChannelServer, port2: nativeDBApiChannelClient } =
new MessageChannel();
AsyncCall<typeof NbStoreNativeDBApis>(NbStoreNativeDBApis, {

View File

@@ -9,6 +9,7 @@
"references": [
{ "path": "../../component" },
{ "path": "../../core" },
{ "path": "../../../common/env" },
{ "path": "../../i18n" },
{ "path": "../../../common/nbstore" },
{ "path": "../../../../blocksuite/affine/all" },

View File

@@ -11,6 +11,7 @@
"dependencies": {
"@affine/component": "workspace:*",
"@affine/core": "workspace:*",
"@affine/env": "workspace:*",
"@affine/i18n": "workspace:*",
"@affine/nbstore": "workspace:*",
"@blocksuite/affine": "workspace:*",

View File

@@ -14,6 +14,7 @@ import {
import { PopupWindowProvider } from '@affine/core/modules/url';
import { configureBrowserWorkbenchModule } from '@affine/core/modules/workbench';
import { configureBrowserWorkspaceFlavours } from '@affine/core/modules/workspace-engine';
import { getWorkerUrl } from '@affine/env/worker';
import { StoreManagerClient } from '@affine/nbstore/worker/client';
import { Framework, FrameworkRoot, getCurrentStore } from '@toeverything/infra';
import { OpClient } from '@toeverything/infra/op';
@@ -22,16 +23,12 @@ import { RouterProvider } from 'react-router-dom';
let storeManagerClient: StoreManagerClient;
const workerUrl = getWorkerUrl('nbstore.worker.js');
if (window.SharedWorker) {
const worker = new SharedWorker(
new URL(/* webpackChunkName: "nbstore" */ './nbstore.ts', import.meta.url),
{ name: 'affine-shared-worker' }
);
const worker = new SharedWorker(workerUrl, { name: 'affine-shared-worker' });
storeManagerClient = new StoreManagerClient(new OpClient(worker.port));
} else {
const worker = new Worker(
new URL(/* webpackChunkName: "nbstore" */ './nbstore.ts', import.meta.url)
);
const worker = new Worker(workerUrl);
storeManagerClient = new StoreManagerClient(new OpClient(worker));
}
window.addEventListener('beforeunload', () => {

View File

@@ -9,6 +9,7 @@
"references": [
{ "path": "../../component" },
{ "path": "../../core" },
{ "path": "../../../common/env" },
{ "path": "../../i18n" },
{ "path": "../../../common/nbstore" },
{ "path": "../../../../blocksuite/affine/all" },

View File

@@ -11,6 +11,7 @@
"dependencies": {
"@affine/component": "workspace:*",
"@affine/core": "workspace:*",
"@affine/env": "workspace:*",
"@affine/i18n": "workspace:*",
"@affine/nbstore": "workspace:*",
"@affine/track": "workspace:*",

View File

@@ -12,6 +12,7 @@ import { PopupWindowProvider } from '@affine/core/modules/url';
import { configureBrowserWorkbenchModule } from '@affine/core/modules/workbench';
import { configureBrowserWorkspaceFlavours } from '@affine/core/modules/workspace-engine';
import createEmotionCache from '@affine/core/utils/create-emotion-cache';
import { getWorkerUrl } from '@affine/env/worker';
import { StoreManagerClient } from '@affine/nbstore/worker/client';
import { CacheProvider } from '@emotion/react';
import { Framework, FrameworkRoot, getCurrentStore } from '@toeverything/infra';
@@ -23,19 +24,16 @@ const cache = createEmotionCache();
let storeManagerClient: StoreManagerClient;
const workerUrl = getWorkerUrl('nbstore.worker.js');
if (
window.SharedWorker &&
localStorage.getItem('disableSharedWorker') !== 'true'
) {
const worker = new SharedWorker(
new URL(/* webpackChunkName: "nbstore" */ './nbstore.ts', import.meta.url),
{ name: 'affine-shared-worker' }
);
const worker = new SharedWorker(workerUrl);
storeManagerClient = new StoreManagerClient(new OpClient(worker.port));
} else {
const worker = new Worker(
new URL(/* webpackChunkName: "nbstore" */ './nbstore.ts', import.meta.url)
);
const worker = new Worker(workerUrl);
storeManagerClient = new StoreManagerClient(new OpClient(worker));
}
window.addEventListener('beforeunload', () => {

View File

@@ -9,6 +9,7 @@
"references": [
{ "path": "../../component" },
{ "path": "../../core" },
{ "path": "../../../common/env" },
{ "path": "../../i18n" },
{ "path": "../../../common/nbstore" },
{ "path": "../../track" },

View File

@@ -54,18 +54,15 @@ export default {
inlineSourcesContent: true,
}),
],
define: {
'process.env.CAPTCHA_SITE_KEY': `"${process.env.CAPTCHA_SITE_KEY}"`,
...Object.entries(
getBuildConfig(new Package('@affine/web'), {
mode: 'development',
channel: 'canary',
})
).reduce((envs, [key, value]) => {
envs[`BUILD_CONFIG.${key}`] = JSON.stringify(value);
return envs;
}, {}),
},
define: Object.entries(
getBuildConfig(new Package('@affine/web'), {
mode: 'development',
channel: 'canary',
})
).reduce((envs, [key, value]) => {
envs[`BUILD_CONFIG.${key}`] = JSON.stringify(value);
return envs;
}, {}),
});
},

View File

@@ -7,7 +7,6 @@ import { useDebugValue, useEffect, useState } from 'react';
/* eslint-disable react-hooks/exhaustive-deps */
// the `process.env.NODE_ENV !== 'production'` condition is resolved by the build tool
/* eslint-disable react-hooks/rules-of-hooks */
const noop: (...args: any[]) => any = () => {};
@@ -67,7 +66,7 @@ export const useRefEffect = <T>(
// Show the current ref value in development
// in react dev tools
if (process.env.NODE_ENV !== 'production') {
if (BUILD_CONFIG.debug) {
useDebugValue(internalRef.ref_.current);
}

View File

@@ -7,8 +7,6 @@
import type { PropertyDeclaration } from 'lit';
import type React from 'react';
const DEV_MODE = process.env.NODE_ENV !== 'production';
type DistributiveOmit<T, K extends string | number | symbol> = T extends any
? K extends keyof T
? Omit<T, K>
@@ -245,7 +243,7 @@ export const createComponent = <
}: Options<I, E>): ReactWebComponent<I, E> => {
const eventProps = new Set(Object.keys(events ?? {}));
if (DEV_MODE) {
if (BUILD_CONFIG.debug) {
for (const p of reservedReactProperties) {
if (p in elementClass.prototype && !(p in HTMLElement.prototype)) {
// Note, this effectively warns only for `ref` since the other

View File

@@ -1,3 +1,4 @@
import { getWorkerUrl } from '@affine/env/worker';
import { ListLayoutHandlerExtension } from '@blocksuite/affine/blocks/list';
import { ParagraphLayoutHandlerExtension } from '@blocksuite/affine/blocks/paragraph';
import {
@@ -6,15 +7,7 @@ import {
} from '@blocksuite/affine/gfx/turbo-renderer';
function createPainterWorker() {
const worker = new Worker(
/* webpackChunkName: "turbo-painter-entry" */ new URL(
'./turbo-painter-entry.worker.ts',
import.meta.url
),
{
type: 'module',
}
);
const worker = new Worker(getWorkerUrl('turbo-painter-entry.worker.js'));
return worker;
}

View File

@@ -1,5 +1,5 @@
import { mixpanel, sentry } from '@affine/track';
import { APP_SETTINGS_STORAGE_KEY } from '@toeverything/infra';
import { APP_SETTINGS_STORAGE_KEY } from '@toeverything/infra/atom';
mixpanel.init();
sentry.init();

View File

@@ -36,7 +36,7 @@ export const Captcha = () => {
return (
<Turnstile
className={style.captchaWrapper}
siteKey={process.env.CAPTCHA_SITE_KEY || '1x00000000000000000000AA'}
siteKey={BUILD_CONFIG.CAPTCHA_SITE_KEY || '1x00000000000000000000AA'}
onSuccess={handleTurnstileSuccess}
/>
);

View File

@@ -9,19 +9,24 @@ import { DebugLogger } from '@affine/debug';
import { apis } from '@affine/electron-api';
import { useI18n } from '@affine/i18n';
import { useService } from '@toeverything/infra';
import { useLayoutEffect } from 'react';
import { useLayoutEffect, useRef } from 'react';
const logger = new DebugLogger('ImportWorkspaceDialog');
export const ImportWorkspaceDialog = ({
close,
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['import-workspace']>) => {
const effectRef = useRef(false);
const t = useI18n();
const workspacesService = useService(WorkspacesService);
// TODO(@Peng): maybe refactor using xstate?
useLayoutEffect(() => {
let canceled = false;
if (effectRef.current) {
return;
}
effectRef.current = true;
// a hack for now
// when adding a workspace, we will immediately let user select a db file
// after it is done, it will effectively add a new workspace to app-data folder
@@ -32,7 +37,7 @@ export const ImportWorkspaceDialog = ({
}
logger.info('load db file');
const result = await apis.dialog.loadDBFile();
if (result.workspaceId && !canceled) {
if (result.workspaceId) {
_addLocalWorkspace(result.workspaceId);
workspacesService.list.revalidate();
close({
@@ -50,9 +55,6 @@ export const ImportWorkspaceDialog = ({
})().catch(err => {
console.error(err);
});
return () => {
canceled = true;
};
}, [close, t, workspacesService]);
return null;

View File

@@ -191,13 +191,9 @@ export const BackupSettingPanel = () => {
const t = useI18n();
const backupService = useService(BackupService);
const handlePageChange = useCallback(() => {
backupService.revalidate();
}, [backupService]);
useEffect(() => {
backupService.revalidate();
}, [backupService, handlePageChange]);
}, [backupService]);
const isLoading = useLiveData(backupService.isLoading$);
const backupWorkspaces = useLiveData(backupService.pageBackupWorkspaces$);

View File

@@ -1,3 +1,4 @@
import { getWorkerUrl } from '@affine/env/worker';
import { OpClient } from '@toeverything/infra/op';
import type { ClientOps } from './ops';
@@ -6,12 +7,7 @@ export class PDFRenderer extends OpClient<ClientOps> {
private readonly worker: Worker;
constructor() {
const worker = new Worker(
/* webpackChunkName: "pdf.worker" */ new URL(
'./worker.ts',
import.meta.url
)
);
const worker = new Worker(getWorkerUrl('pdf.worker.js'));
super(worker);
this.worker = worker;

View File

@@ -1,3 +1,4 @@
import { getWorkerUrl } from '@affine/env/worker';
import { OpClient } from '@toeverything/infra/op';
import type { WorkerOps } from './worker-ops';
@@ -9,12 +10,7 @@ export function getWorkspaceProfileWorker() {
return worker;
}
const rawWorker = new Worker(
new URL(
/* webpackChunkName: "workspace-profile-worker" */ './in-worker.ts',
import.meta.url
)
);
const rawWorker = new Worker(getWorkerUrl('workspace-profile.worker.js'));
worker = new OpClient<WorkerOps>(rawWorker);
return worker;

View File

@@ -23,8 +23,7 @@ export const schemeToChannel = {
export const channelToScheme = {
stable: 'affine',
canary:
process.env.NODE_ENV === 'development' ? 'affine-dev' : 'affine-canary',
canary: BUILD_CONFIG.debug ? 'affine-dev' : 'affine-canary',
beta: 'affine-beta',
internal: 'affine-internal',
} as Record<Channel, Scheme>;

View File

@@ -36,7 +36,7 @@ export const createIsland = () => {
Provider: ({ children }: React.PropsWithChildren) => {
const target = useLiveData(targetLiveData$);
useEffect(() => {
if (provided === true && process.env.NODE_ENV !== 'production') {
if (provided === true && BUILD_CONFIG.debug) {
throw new Error('Island should not be provided more than once');
}
provided = true;

View File

@@ -11,8 +11,8 @@ type Middleware = (
function createMixpanel() {
let mixpanel;
if (process.env.MIXPANEL_TOKEN) {
mixpanelBrowser.init(process.env.MIXPANEL_TOKEN || '', {
if (BUILD_CONFIG.MIXPANEL_TOKEN) {
mixpanelBrowser.init(BUILD_CONFIG.MIXPANEL_TOKEN || '', {
track_pageview: true,
persistence: 'localStorage',
api_host: 'https://telemetry.affine.run',

View File

@@ -14,9 +14,9 @@ function createSentry() {
if (!globalThis.SENTRY_RELEASE) {
// https://docs.sentry.io/platforms/javascript/guides/react/#configure
client = Sentry.init({
dsn: process.env.SENTRY_DSN,
dsn: BUILD_CONFIG.SENTRY_DSN,
debug: BUILD_CONFIG.debug ?? false,
environment: process.env.BUILD_TYPE ?? 'development',
environment: BUILD_CONFIG.appBuildType,
integrations: [
Sentry.reactRouterV6BrowserTracingIntegration({
useEffect,

View File

@@ -1,35 +1,26 @@
import { getConfig, start } from '@affine-tools/cli/bundle';
import { Workspace } from '@affine-tools/utils/workspace';
import webpack from 'webpack';
import { BundleCommand } from '@affine-tools/cli/bundle';
import { Package } from '@affine-tools/utils/workspace';
export default async () => {
const ws = new Workspace();
const webpackConfig = await getConfig(ws.getPackage('@affine/web'), true);
const definedPort = webpackConfig.devServer?.port ?? 8080;
await new Promise<void>((resolve, reject) => {
start(webpack(webpackConfig), {
...webpackConfig.devServer,
BundleCommand.dev(new Package('@affine/web'), {
onListening: server => {
// dev server has already started
if (server.options.port !== definedPort) {
if (server.options.port !== 8080) {
server.compiler.close(reject);
server.stop().catch(reject);
resolve();
} else {
server.middleware?.waitUntilValid?.(stats => {
if (stats?.hasErrors()) {
reject(new Error('Webpack build failed'));
} else {
resolve();
}
});
}
},
proxy: [],
})
.then(server => {
server.middleware?.waitUntilValid?.(stats => {
if (stats?.hasErrors()) {
reject(new Error('Webpack build failed'));
} else {
resolve();
}
});
})
.catch(reject);
}).catch(reject);
});
console.log('Dev server started');
};

View File

@@ -121,7 +121,7 @@ export const test = base.extend<{
electronRoot.join('package.json').value
);
// overwrite the app name
packageJson.name = 'affine-test-' + id;
packageJson.name = '@affine/electron-test-' + id;
// overwrite the path to the main script
packageJson.main = './main.js';
// write to the cloned dist

View File

@@ -33,6 +33,11 @@ declare interface BUILD_CONFIG_TYPE {
// see: tools/workers
imageProxyUrl: string;
linkPreviewUrl: string;
CAPTCHA_SITE_KEY: string;
SENTRY_DSN: string;
MIXPANEL_TOKEN: string;
DEBUG_JOTAI: string;
}
declare var BUILD_CONFIG: BUILD_CONFIG_TYPE;

View File

@@ -37,6 +37,7 @@ declare type Environment = {
// runtime configs
publicPath: string;
workerPath: string
};
var process: {

View File

@@ -1,75 +1,130 @@
import type { Package } from '@affine-tools/utils/workspace';
import webpack, { type Compiler, type Configuration } from 'webpack';
import WebpackDevServer from 'webpack-dev-server';
import { merge } from 'webpack-merge';
import { rmSync } from 'node:fs';
import { cpus } from 'node:os';
import { Logger } from '@affine-tools/utils/logger';
import { Package } from '@affine-tools/utils/workspace';
import { merge } from 'lodash-es';
import webpack from 'webpack';
import WebpackDevServer, {
type Configuration as DevServerConfiguration,
} from 'webpack-dev-server';
import { Option, PackageCommand } from './command';
import { createWebpackConfig } from './webpack';
import { createHTMLTargetConfig, createWorkerTargetConfig } from './webpack';
function getChannel() {
const channel = process.env.BUILD_TYPE ?? 'canary';
switch (channel) {
case 'canary':
case 'beta':
case 'stable':
case 'internal':
return channel;
default: {
throw new Error(
`BUILD_TYPE must be one of canary, beta, stable, internal, received [${channel}]`
function getBundleConfigs(pkg: Package) {
const core = new Package('@affine/core');
const workerConfigs = [
createWorkerTargetConfig(
pkg,
core.srcPath.join(
'modules/workspace-engine/impls/workspace-profile.worker.ts'
).value
),
createWorkerTargetConfig(
pkg,
core.srcPath.join('modules/pdf/renderer/pdf.worker.ts').value
),
createWorkerTargetConfig(
pkg,
core.srcPath.join('blocksuite/extensions/turbo-painter.worker.ts').value
),
];
switch (pkg.name) {
case '@affine/admin': {
return [createHTMLTargetConfig(pkg, pkg.srcPath.join('index.tsx').value)];
}
case '@affine/web':
case '@affine/mobile':
case '@affine/ios':
case '@affine/android': {
workerConfigs.push(
createWorkerTargetConfig(
pkg,
pkg.srcPath.join('nbstore.worker.ts').value
)
);
return [
createHTMLTargetConfig(
pkg,
pkg.srcPath.join('index.tsx').value,
{},
workerConfigs.map(config => config.name)
),
...workerConfigs,
];
}
case '@affine/electron-renderer': {
return [
createHTMLTargetConfig(
pkg,
{
index: pkg.srcPath.join('app/index.tsx').value,
shell: pkg.srcPath.join('shell/index.tsx').value,
popup: pkg.srcPath.join('popup/index.tsx').value,
backgroundWorker: pkg.srcPath.join('background-worker/index.ts')
.value,
},
{
additionalEntryForSelfhost: false,
injectGlobalErrorHandler: false,
emitAssetsManifest: false,
},
workerConfigs.map(config => config.name)
),
...workerConfigs,
];
}
}
throw new Error(`Unsupported package: ${pkg.name}`);
}
const IN_CI = !!process.env.CI;
const httpProxyMiddlewareLogLevel = IN_CI ? 'silent' : 'error';
export async function getConfig(pkg: Package, dev: boolean) {
let config = createWebpackConfig(pkg, {
mode: dev ? 'development' : 'production',
channel: getChannel(),
});
let configOverride: Configuration | undefined;
const overrideConfigPath = pkg.join('webpack.config.ts');
if (overrideConfigPath.isFile()) {
const override = await import(overrideConfigPath.toFileUrl().toString());
configOverride = override.config ?? override.default;
}
if (configOverride) {
config = merge(config, configOverride);
}
return config;
}
export async function start(
compiler: Compiler,
config: Configuration['devServer']
): Promise<WebpackDevServer> {
const devServer = new WebpackDevServer(config, compiler);
await devServer.start();
return devServer;
}
export async function build(compiler: Compiler) {
compiler.run((error, stats) => {
if (error) {
console.error(error);
process.exit(1);
}
if (stats) {
if (stats.hasErrors()) {
console.error(stats.toString('errors-only'));
process.exit(1);
} else {
console.log(stats.toString('minimal'));
}
}
});
}
const defaultDevServerConfig = {
host: '0.0.0.0',
allowedHosts: 'all',
hot: false,
liveReload: true,
client: {
overlay: process.env.DISABLE_DEV_OVERLAY === 'true' ? false : undefined,
logging: process.env.CI ? 'none' : 'error',
},
historyApiFallback: {
rewrites: [
{
from: /.*/,
to: () => {
return process.env.SELF_HOSTED === 'true'
? '/selfhost.html'
: '/index.html';
},
},
],
},
proxy: [
{
context: '/api',
target: 'http://localhost:3010',
logLevel: httpProxyMiddlewareLogLevel,
},
{
context: '/socket.io',
target: 'http://localhost:3010',
ws: true,
logLevel: httpProxyMiddlewareLogLevel,
},
{
context: '/graphql',
target: 'http://localhost:3010',
logLevel: httpProxyMiddlewareLogLevel,
},
],
} as DevServerConfiguration;
export class BundleCommand extends PackageCommand {
static override paths = [['bundle'], ['webpack'], ['pack'], ['bun']];
@@ -83,19 +138,60 @@ export class BundleCommand extends PackageCommand {
});
async execute() {
this.logger.info(`Packing package ${this.package}...`);
const pkg = this.workspace.getPackage(this.package);
const config = await getConfig(
this.workspace.getPackage(this.package),
this.dev
);
if (this.dev) {
await BundleCommand.dev(pkg);
} else {
await BundleCommand.build(pkg);
}
}
static async build(pkg: Package) {
process.env.NODE_ENV = 'production';
const logger = new Logger('bundle');
logger.info(`Packing package ${pkg.name}...`);
logger.info('Cleaning old output...');
rmSync(pkg.distPath.value, { recursive: true, force: true });
const config = getBundleConfigs(pkg);
// @ts-expect-error allow
config.parallelism = cpus().length;
const compiler = webpack(config);
if (this.dev) {
await start(compiler, config.devServer);
} else {
await build(compiler);
}
compiler.run((error, stats) => {
if (error) {
console.error(error);
process.exit(1);
}
if (stats) {
if (stats.hasErrors()) {
console.error(stats.toString('errors-only'));
process.exit(1);
} else {
console.log(stats.toString('minimal'));
}
}
});
}
static async dev(pkg: Package, devServerConfig?: DevServerConfiguration) {
process.env.NODE_ENV = 'development';
const logger = new Logger('bundle');
logger.info(`Starting dev server for ${pkg.name}...`);
const config = getBundleConfigs(pkg);
// @ts-expect-error allow
config.parallelism = cpus().length;
const compiler = webpack(config);
const devServer = new WebpackDevServer(
merge({}, defaultDevServerConfig, devServerConfig),
compiler
);
await devServer.start();
}
}

View File

@@ -8,19 +8,14 @@ import { once } from 'lodash-es';
import type { Compiler, WebpackPluginInstance } from 'webpack';
import webpack from 'webpack';
import type { BuildFlags } from './types.js';
export const getPublicPath = (
flags: BuildFlags,
BUILD_CONFIG: BUILD_CONFIG_TYPE
) => {
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 (
flags.mode === 'development' ||
BUILD_CONFIG.debug ||
BUILD_CONFIG.distribution === 'desktop' ||
BUILD_CONFIG.distribution === 'ios' ||
BUILD_CONFIG.distribution === 'android'
@@ -58,11 +53,15 @@ const gitShortHash = once(() => {
const currentDir = Path.dir(import.meta.url);
function getHTMLPluginOptions(
flags: BuildFlags,
BUILD_CONFIG: BUILD_CONFIG_TYPE
) {
const publicPath = getPublicPath(flags, BUILD_CONFIG);
export interface CreateHTMLPluginConfig {
filename?: string;
additionalEntryForSelfhost?: boolean;
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;
@@ -79,144 +78,159 @@ function getHTMLPluginOptions(
return {
template: currentDir.join('template.html').toString(),
inject: 'body',
filename: 'index.html',
minify: false,
templateParameters: templateParams,
chunks: ['app'],
} satisfies HTMLPlugin.Options;
}
export function createShellHTMLPlugin(
flags: BuildFlags,
BUILD_CONFIG: BUILD_CONFIG_TYPE
) {
const htmlPluginOptions = getHTMLPluginOptions(flags, BUILD_CONFIG);
return new HTMLPlugin({
...htmlPluginOptions,
chunks: ['shell'],
filename: `shell.html`,
});
}
export function createBackgroundWorkerHTMLPlugin(
flags: BuildFlags,
BUILD_CONFIG: BUILD_CONFIG_TYPE
) {
const htmlPluginOptions = getHTMLPluginOptions(flags, BUILD_CONFIG);
return new HTMLPlugin({
...htmlPluginOptions,
chunks: ['backgroundWorker'],
filename: `background-worker.html`,
});
}
export function createPopupHTMLPlugin(
flags: BuildFlags,
BUILD_CONFIG: BUILD_CONFIG_TYPE
) {
const htmlPluginOptions = getHTMLPluginOptions(flags, BUILD_CONFIG);
return new HTMLPlugin({
...htmlPluginOptions,
chunks: ['popup'],
filename: `popup.html`,
});
}
export function createHTMLPlugins(
flags: BuildFlags,
BUILD_CONFIG: BUILD_CONFIG_TYPE
): WebpackPluginInstance[] {
const publicPath = getPublicPath(flags, BUILD_CONFIG);
const globalErrorHandler = [
'js/global-error-handler.js',
readFileSync(currentDir.join('./error-handler.js').toString(), 'utf-8'),
];
const htmlPluginOptions = getHTMLPluginOptions(flags, BUILD_CONFIG);
return [
{
apply(compiler: Compiler) {
compiler.hooks.compilation.tap(
'assets-manifest-plugin',
compilation => {
HTMLPlugin.getHooks(compilation).beforeAssetTagGeneration.tap(
'assets-manifest-plugin',
arg => {
if (
!BUILD_CONFIG.isElectron &&
!compilation.getAsset(globalErrorHandler[0])
) {
compilation.emitAsset(
globalErrorHandler[0],
new webpack.sources.RawSource(globalErrorHandler[1])
);
arg.assets.js.unshift(
arg.assets.publicPath + globalErrorHandler[0]
);
}
if (!compilation.getAsset('assets-manifest.json')) {
compilation.emitAsset(
globalErrorHandler[0],
new webpack.sources.RawSource(globalErrorHandler[1])
);
compilation.emitAsset(
`assets-manifest.json`,
new webpack.sources.RawSource(
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:
htmlPluginOptions.templateParameters.GIT_SHORT_SHA,
description:
htmlPluginOptions.templateParameters.DESCRIPTION,
},
null,
2
)
const AssetsManifestPlugin = {
apply(compiler: Compiler) {
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`,
new webpack.sources.RawSource(
JSON.stringify(
{
...arg.assets,
js: arg.assets.js.map(file =>
file.substring(arg.assets.publicPath.length)
),
{
immutable: false,
}
);
}
return arg;
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: Compiler) {
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],
new webpack.sources.RawSource(globalErrorHandler[1])
);
arg.assets.js.unshift(
arg.assets.publicPath + globalErrorHandler[0]
);
}
return arg;
}
);
},
},
}
);
},
};
export function createHTMLPlugins(
BUILD_CONFIG: BUILD_CONFIG_TYPE,
config: CreateHTMLPluginConfig
): WebpackPluginInstance[] {
const publicPath = getPublicPath(BUILD_CONFIG);
const htmlPluginOptions = getHTMLPluginOptions(BUILD_CONFIG);
const plugins: WebpackPluginInstance[] = [];
plugins.push(
new HTMLPlugin({
...htmlPluginOptions,
chunks: ['index'],
filename: config.filename,
publicPath,
meta: {
'env:publicPath': publicPath,
},
}),
// selfhost html
new HTMLPlugin({
...htmlPluginOptions,
meta: {
'env:isSelfHosted': 'true',
'env:publicPath': '/',
},
filename: 'selfhost.html',
templateParameters: {
...htmlPluginOptions.templateParameters,
PRECONNECT: '',
},
}),
];
})
);
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 (config.emitAssetsManifest) {
plugins.push(AssetsManifestPlugin);
}
if (config.injectGlobalErrorHandler) {
plugins.push(GlobalErrorHandlerPlugin);
}
if (config.additionalEntryForSelfhost) {
plugins.push(
new HTMLPlugin({
...htmlPluginOptions,
chunks: ['index'],
meta: {
'env:isSelfHosted': 'true',
'env:publicPath': '/',
},
filename: 'selfhost.html',
templateParameters: {
...htmlPluginOptions.templateParameters,
PRECONNECT: '',
},
})
);
}
return plugins;
}

View File

@@ -1,127 +1,99 @@
import { createRequire } from 'node:module';
import path from 'node:path';
import { getBuildConfig } from '@affine-tools/utils/build-config';
import { ProjectRoot } from '@affine-tools/utils/path';
import type { Package } from '@affine-tools/utils/workspace';
import { Package } from '@affine-tools/utils/workspace';
import { PerfseePlugin } from '@perfsee/webpack';
import { sentryWebpackPlugin } from '@sentry/webpack-plugin';
import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin';
import CopyPlugin from 'copy-webpack-plugin';
import { compact } from 'lodash-es';
import { compact, merge } from 'lodash-es';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import TerserPlugin from 'terser-webpack-plugin';
import webpack from 'webpack';
import type { Configuration as DevServerConfiguration } from 'webpack-dev-server';
import { productionCacheGroups } from './cache-group.js';
import {
createBackgroundWorkerHTMLPlugin,
type CreateHTMLPluginConfig,
createHTMLPlugins,
createPopupHTMLPlugin,
createShellHTMLPlugin,
} from './html-plugin.js';
import { WebpackS3Plugin } from './s3-plugin.js';
import type { BuildFlags } from './types';
const require = createRequire(import.meta.url);
const cssnano = require('cssnano');
const IN_CI = !!process.env.CI;
const OptimizeOptionOptions: (
flags: BuildFlags
) => webpack.Configuration['optimization'] = flags => ({
minimize: flags.mode === 'production',
minimizer: [
new TerserPlugin({
minify: TerserPlugin.swcMinify,
exclude: [/plugins\/.+\/.+\.js$/, /plugins\/.+\/.+\.mjs$/],
parallel: true,
extractComments: true,
terserOptions: {
ecma: 2020,
compress: {
unused: true,
},
mangle: {
keep_classnames: true,
},
},
}),
],
removeEmptyChunks: true,
providedExports: true,
usedExports: true,
sideEffects: true,
removeAvailableModules: true,
runtimeChunk: {
name: 'runtime',
},
splitChunks: {
chunks: 'all',
minSize: 1,
minChunks: 1,
maxInitialRequests: Number.MAX_SAFE_INTEGER,
maxAsyncRequests: Number.MAX_SAFE_INTEGER,
cacheGroups: productionCacheGroups,
},
});
const availableChannels = ['canary', 'beta', 'stable', 'internal'];
function getBuildConfigFromEnv(pkg: Package) {
const channel = process.env.BUILD_TYPE ?? 'canary';
const dev = process.env.NODE_ENV === 'development';
if (!availableChannels.includes(channel)) {
throw new Error(
`BUILD_TYPE must be one of ${availableChannels.join(', ')}, received [${channel}]`
);
}
export function createWebpackConfig(
return getBuildConfig(pkg, {
// @ts-expect-error checked
channel,
mode: dev ? 'development' : 'production',
});
}
export function createHTMLTargetConfig(
pkg: Package,
flags: BuildFlags
entry: string | Record<string, string>,
htmlConfig: Partial<CreateHTMLPluginConfig> = {},
deps?: string[]
): webpack.Configuration {
const buildConfig = getBuildConfig(pkg, flags);
const httpProxyMiddlewareLogLevel = process.env.CI ? 'silent' : 'error';
entry = typeof entry === 'string' ? { index: entry } : entry;
const config = {
name: 'affine',
htmlConfig = merge(
{},
{
filename: 'index.html',
additionalEntryForSelfhost: true,
injectGlobalErrorHandler: true,
emitAssetsManifest: true,
},
htmlConfig
);
const buildConfig = getBuildConfigFromEnv(pkg);
const config: webpack.Configuration = {
//#region basic webpack config
name: entry['index'],
dependencies: deps,
context: pkg.path.value,
experiments: {
topLevelAwait: true,
outputModule: false,
syncWebAssembly: true,
},
entry: {
app: pkg.entry ?? './src/index.tsx',
},
entry,
output: {
environment: {
module: true,
dynamicImport: true,
},
filename:
flags.mode === 'production'
? 'js/[name].[contenthash:8].js'
: 'js/[name].js',
// In some cases webpack will emit files starts with "_" which is reserved in web extension.
chunkFilename: pathData =>
pathData.chunk?.name?.endsWith?.('worker')
? 'js/[name].[contenthash:8].js'
: flags.mode === 'production'
? 'js/chunk.[name].[contenthash:8].js'
: 'js/chunk.[name].js',
assetModuleFilename:
flags.mode === 'production'
? 'assets/[name].[contenthash:8][ext][query]'
: '[name].[contenthash:8][ext]',
devtoolModuleFilenameTemplate: 'webpack://[namespace]/[resource-path]',
hotUpdateChunkFilename: 'hot/[id].[fullhash].js',
hotUpdateMainFilename: 'hot/[runtime].[fullhash].json',
filename: buildConfig.debug
? 'js/[name].js'
: 'js/[name].[contenthash:8].js',
assetModuleFilename: buildConfig.debug
? '[name].[contenthash:8][ext]'
: 'assets/[name].[contenthash:8][ext][query]',
path: pkg.distPath.value,
clean: flags.mode === 'production',
clean: false,
globalObject: 'globalThis',
// NOTE(@forehalo): always keep it '/'
publicPath: '/',
workerPublicPath: '/',
},
target: ['web', 'es2022'],
mode: flags.mode,
devtool:
flags.mode === 'production' ? 'source-map' : 'cheap-module-source-map',
mode: buildConfig.debug ? 'development' : 'production',
devtool: buildConfig.debug ? 'cheap-module-source-map' : 'source-map',
resolve: {
symlinks: true,
extensionAlias: {
@@ -139,7 +111,9 @@ export function createWebpackConfig(
).value,
},
},
//#endregion
//#region module config
module: {
parser: {
javascript: {
@@ -151,6 +125,7 @@ export function createWebpackConfig(
strictExportPresence: true,
},
},
//#region rules
rules: [
{
test: /\.m?js?$/,
@@ -240,7 +215,7 @@ export function createWebpackConfig(
{
test: /\.css$/,
use: [
flags.mode === 'development'
buildConfig.debug
? 'style-loader'
: MiniCssExtractPlugin.loader,
{
@@ -283,28 +258,16 @@ export function createWebpackConfig(
],
},
],
//#endregion
},
//#endregion
//#region plugins
plugins: compact([
IN_CI ? null : new webpack.ProgressPlugin({ percentBy: 'entries' }),
flags.mode === 'development'
? null
: // todo: support multiple entry points
new MiniCssExtractPlugin({
filename: `[name].[contenthash:8].css`,
ignoreOrder: true,
}),
new VanillaExtractPlugin(),
!IN_CI && new webpack.ProgressPlugin({ percentBy: 'entries' }),
...createHTMLPlugins(buildConfig, htmlConfig),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(flags.mode),
'process.env.CAPTCHA_SITE_KEY': JSON.stringify(
process.env.CAPTCHA_SITE_KEY
),
'process.env.SENTRY_DSN': JSON.stringify(process.env.SENTRY_DSN),
'process.env.BUILD_TYPE': JSON.stringify(process.env.BUILD_TYPE),
'process.env.MIXPANEL_TOKEN': JSON.stringify(
process.env.MIXPANEL_TOKEN
),
'process.env.DEBUG_JOTAI': JSON.stringify(process.env.DEBUG_JOTAI),
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
...Object.entries(buildConfig).reduce(
(def, [k, v]) => {
def[`BUILD_CONFIG.${k}`] = JSON.stringify(v);
@@ -313,90 +276,100 @@ export function createWebpackConfig(
{} as Record<string, string>
),
}),
buildConfig.isAdmin && flags.mode !== 'production'
? null
: new CopyPlugin({
patterns: [
{
// copy the shared public assets into dist
from: pkg.workspace.getPackage('@affine/core').join('public')
.value,
},
],
}),
flags.mode === 'production' &&
(buildConfig.isWeb || buildConfig.isMobileWeb || buildConfig.isAdmin) &&
process.env.R2_SECRET_ACCESS_KEY
? new WebpackS3Plugin()
: null,
!buildConfig.debug &&
// todo: support multiple entry points
new MiniCssExtractPlugin({
filename: `[name].[contenthash:8].css`,
ignoreOrder: true,
}),
new VanillaExtractPlugin(),
!buildConfig.isAdmin &&
new CopyPlugin({
patterns: [
{
// copy the shared public assets into dist
from: new Package('@affine/core').join('public').value,
},
],
}),
!buildConfig.debug &&
(buildConfig.isWeb || buildConfig.isMobileWeb || buildConfig.isAdmin) &&
process.env.R2_SECRET_ACCESS_KEY &&
new WebpackS3Plugin(),
!buildConfig.debug &&
process.env.PERFSEE_TOKEN &&
new PerfseePlugin({
project: 'affine-toeverything',
}),
process.env.SENTRY_AUTH_TOKEN &&
process.env.SENTRY_ORG &&
process.env.SENTRY_PROJECT &&
sentryWebpackPlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
}),
// sourcemap url like # sourceMappingURL=76-6370cd185962bc89.js.map wont load in electron
// this is because the default file:// protocol will be ignored by Chromium
// so we need to replace the sourceMappingURL to assets:// protocol
// for example:
// replace # sourceMappingURL=76-6370cd185962bc89.js.map
// to # sourceMappingURL=assets://./{dir}/76-6370cd185962bc89.js.map
buildConfig.isElectron &&
new webpack.SourceMapDevToolPlugin({
append: pathData => {
return `\n//# sourceMappingURL=assets://./${pathData.filename}.map`;
},
filename: '[file].map',
}),
]),
//#endregion
stats: {
errorDetails: true,
},
optimization: OptimizeOptionOptions(flags),
devServer: {
host: '0.0.0.0',
allowedHosts: 'all',
hot: false,
liveReload: true,
client: {
overlay: process.env.DISABLE_DEV_OVERLAY === 'true' ? false : undefined,
logging: process.env.CI ? 'none' : 'error',
},
historyApiFallback: {
rewrites: [
{
from: /.*/,
to: () => {
return process.env.SELF_HOSTED === 'true'
? '/selfhost.html'
: '/index.html';
//#region optimization
optimization: {
minimize: !buildConfig.debug,
minimizer: [
new TerserPlugin({
minify: TerserPlugin.swcMinify,
exclude: [/plugins\/.+\/.+\.js$/, /plugins\/.+\/.+\.mjs$/],
parallel: true,
extractComments: true,
terserOptions: {
ecma: 2020,
compress: {
unused: true,
},
mangle: {
keep_classnames: true,
},
},
],
}),
],
removeEmptyChunks: true,
providedExports: true,
usedExports: true,
sideEffects: true,
removeAvailableModules: true,
runtimeChunk: {
name: 'runtime',
},
static: [
{
directory: pkg.workspace.getPackage('@affine/core').join('public')
.value,
publicPath: '/',
watch: !IN_CI,
staticOptions: {
immutable: IN_CI,
maxAge: '1d',
},
},
],
proxy: [
{
context: '/api',
target: 'http://localhost:3010',
logLevel: httpProxyMiddlewareLogLevel,
},
{
context: '/socket.io',
target: 'http://localhost:3010',
ws: true,
logLevel: httpProxyMiddlewareLogLevel,
},
{
context: '/graphql',
target: 'http://localhost:3010',
logLevel: httpProxyMiddlewareLogLevel,
},
],
} as DevServerConfiguration,
} satisfies webpack.Configuration;
splitChunks: {
chunks: 'all',
minSize: 1,
minChunks: 1,
maxInitialRequests: Number.MAX_SAFE_INTEGER,
maxAsyncRequests: Number.MAX_SAFE_INTEGER,
cacheGroups: productionCacheGroups,
},
},
//#endregion
};
if (flags.mode === 'production' && process.env.PERFSEE_TOKEN) {
config.plugins.push(
new PerfseePlugin({
project: 'affine-toeverything',
})
);
}
if (flags.mode === 'development' && !IN_CI) {
if (buildConfig.debug && !IN_CI) {
config.optimization = {
...config.optimization,
minimize: false,
@@ -426,42 +399,186 @@ export function createWebpackConfig(
};
}
if (
process.env.SENTRY_AUTH_TOKEN &&
process.env.SENTRY_ORG &&
process.env.SENTRY_PROJECT
) {
config.plugins.push(
sentryWebpackPlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
})
);
}
config.plugins = config.plugins.concat(createHTMLPlugins(flags, buildConfig));
if (buildConfig.isElectron) {
config.plugins.push(createShellHTMLPlugin(flags, buildConfig));
config.plugins.push(createBackgroundWorkerHTMLPlugin(flags, buildConfig));
config.plugins.push(createPopupHTMLPlugin(flags, buildConfig));
// sourcemap url like # sourceMappingURL=76-6370cd185962bc89.js.map wont load in electron
// this is because the default file:// protocol will be ignored by Chromium
// so we need to replace the sourceMappingURL to assets:// protocol
// for example:
// replace # sourceMappingURL=76-6370cd185962bc89.js.map
// to # sourceMappingURL=assets://./{dir}/76-6370cd185962bc89.js.map
config.plugins.push(
new webpack.SourceMapDevToolPlugin({
append: pathData => {
return `\n//# sourceMappingURL=assets://./${pathData.filename}.map`;
},
filename: '[file].map',
})
);
}
return config;
}
export function createWorkerTargetConfig(
pkg: Package,
entry: string
): Omit<webpack.Configuration, 'name'> & { name: string } {
const workerName = path.basename(entry).replace(/\.([^.]+)$/, '');
if (!workerName.endsWith('.worker')) {
throw new Error('Worker name must end with `.worker.[ext]`');
}
const buildConfig = getBuildConfigFromEnv(pkg);
return {
name: entry,
context: pkg.path.value,
experiments: {
topLevelAwait: true,
outputModule: false,
syncWebAssembly: true,
},
entry: {
[workerName]: entry,
},
output: {
filename: 'js/[name].js',
path: pkg.distPath.value,
clean: false,
globalObject: 'globalThis',
// NOTE(@forehalo): always keep it '/'
publicPath: '/',
},
target: ['webworker', 'es2022'],
mode: buildConfig.debug ? 'development' : 'production',
devtool: buildConfig.debug ? 'cheap-module-source-map' : 'source-map',
resolve: {
symlinks: true,
extensionAlias: {
'.js': ['.js', '.ts'],
'.mjs': ['.mjs', '.mts'],
},
extensions: ['.js', '.ts'],
alias: {
yjs: ProjectRoot.join('node_modules', 'yjs').value,
},
},
module: {
parser: {
javascript: {
// Do not mock Node.js globals
node: false,
requireJs: false,
import: true,
// Treat as missing export as error
strictExportPresence: true,
},
},
rules: [
{
test: /\.m?js?$/,
resolve: {
fullySpecified: false,
},
},
{
test: /\.js$/,
enforce: 'pre',
include: /@blocksuite/,
use: ['source-map-loader'],
},
{
oneOf: [
{
test: /\.ts$/,
exclude: /node_modules/,
loader: 'swc-loader',
options: {
// https://swc.rs/docs/configuring-swc/
jsc: {
preserveAllComments: true,
parser: {
syntax: 'typescript',
dynamicImport: true,
topLevelAwait: false,
tsx: false,
decorators: true,
},
target: 'es2022',
externalHelpers: false,
transform: {
useDefineForClassFields: false,
decoratorVersion: '2022-03',
},
},
sourceMaps: true,
inlineSourcesContent: true,
},
},
{
test: /\.tsx$/,
exclude: /node_modules/,
loader: 'swc-loader',
options: {
// https://swc.rs/docs/configuring-swc/
jsc: {
preserveAllComments: true,
parser: {
syntax: 'typescript',
dynamicImport: true,
topLevelAwait: false,
tsx: true,
decorators: true,
},
target: 'es2022',
externalHelpers: false,
transform: {
react: {
runtime: 'automatic',
},
useDefineForClassFields: false,
decoratorVersion: '2022-03',
},
},
sourceMaps: true,
inlineSourcesContent: true,
},
},
],
},
],
},
plugins: compact([
new webpack.DefinePlugin(
Object.entries(buildConfig).reduce(
(def, [k, v]) => {
def[`BUILD_CONFIG.${k}`] = JSON.stringify(v);
return def;
},
{} as Record<string, string>
)
),
process.env.SENTRY_AUTH_TOKEN &&
process.env.SENTRY_ORG &&
process.env.SENTRY_PROJECT &&
sentryWebpackPlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
}),
]),
stats: {
errorDetails: true,
},
optimization: {
minimize: !buildConfig.debug,
minimizer: [
new TerserPlugin({
minify: TerserPlugin.swcMinify,
exclude: [/plugins\/.+\/.+\.js$/, /plugins\/.+\/.+\.mjs$/],
parallel: true,
extractComments: true,
terserOptions: {
ecma: 2020,
compress: {
unused: true,
},
mangle: {
keep_classnames: true,
},
},
}),
],
removeEmptyChunks: true,
providedExports: true,
usedExports: true,
sideEffects: true,
removeAvailableModules: true,
runtimeChunk: false,
splitChunks: false,
},
};
}

View File

@@ -50,6 +50,10 @@ export function getBuildConfig(
discordUrl: 'https://affine.pro/redirect/discord',
imageProxyUrl: '/api/worker/image-proxy',
linkPreviewUrl: '/api/worker/link-preview',
CAPTCHA_SITE_KEY: process.env.CAPTCHA_SITE_KEY ?? '',
SENTRY_DSN: process.env.SENTRY_DSN ?? '',
MIXPANEL_TOKEN: process.env.MIXPANEL_TOKEN ?? '',
DEBUG_JOTAI: process.env.DEBUG_JOTAI ?? '',
};
},
get beta() {

View File

@@ -923,6 +923,7 @@ export const PackageList = [
workspaceDependencies: [
'packages/frontend/component',
'packages/frontend/core',
'packages/common/env',
'packages/frontend/i18n',
'packages/common/nbstore',
'blocksuite/affine/all',
@@ -961,6 +962,7 @@ export const PackageList = [
workspaceDependencies: [
'packages/frontend/component',
'packages/frontend/core',
'packages/common/env',
'packages/frontend/i18n',
'packages/common/nbstore',
'blocksuite/affine/all',
@@ -976,6 +978,7 @@ export const PackageList = [
workspaceDependencies: [
'packages/frontend/component',
'packages/frontend/core',
'packages/common/env',
'packages/frontend/i18n',
'packages/common/nbstore',
'blocksuite/affine/all',
@@ -988,6 +991,7 @@ export const PackageList = [
workspaceDependencies: [
'packages/frontend/component',
'packages/frontend/core',
'packages/common/env',
'packages/frontend/i18n',
'packages/common/nbstore',
'packages/frontend/track',

View File

@@ -243,6 +243,7 @@ __metadata:
dependencies:
"@affine/component": "workspace:*"
"@affine/core": "workspace:*"
"@affine/env": "workspace:*"
"@affine/i18n": "workspace:*"
"@affine/nbstore": "workspace:*"
"@blocksuite/affine": "workspace:*"
@@ -656,6 +657,7 @@ __metadata:
"@affine-tools/utils": "workspace:*"
"@affine/component": "workspace:*"
"@affine/core": "workspace:*"
"@affine/env": "workspace:*"
"@affine/i18n": "workspace:*"
"@affine/native": "workspace:*"
"@affine/nbstore": "workspace:*"
@@ -724,6 +726,7 @@ __metadata:
dependencies:
"@affine/component": "workspace:*"
"@affine/core": "workspace:*"
"@affine/env": "workspace:*"
"@affine/i18n": "workspace:*"
"@affine/nbstore": "workspace:*"
"@blocksuite/affine": "workspace:*"
@@ -1012,6 +1015,7 @@ __metadata:
dependencies:
"@affine/component": "workspace:*"
"@affine/core": "workspace:*"
"@affine/env": "workspace:*"
"@affine/i18n": "workspace:*"
"@affine/nbstore": "workspace:*"
"@affine/track": "workspace:*"