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('*'); debug.enable('*');
console.warn('Debug logs enabled'); console.warn('Debug logs enabled');
} }
if (process.env.NODE_ENV === 'development') { if (BUILD_CONFIG.debug) {
debug.enable('*,-micromark'); debug.enable('*,-micromark');
console.warn('Debug logs enabled'); console.warn('Debug logs enabled');
} }

View File

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

View File

@@ -17,6 +17,7 @@ export function setupGlobal() {
isMobile: false, isMobile: false,
isSelfHosted: false, isSelfHosted: false,
publicPath: '/', publicPath: '/',
workerPath: '/js/',
}; };
if (globalThis.navigator) { 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", "./utils": "./src/utils/index.ts",
"./app-config-storage": "./src/app-config-storage.ts", "./app-config-storage": "./src/app-config-storage.ts",
"./op": "./src/op/index.ts", "./op": "./src/op/index.ts",
"./atom": "./src/atom/index.ts",
".": "./src/index.ts" ".": "./src/index.ts"
}, },
"dependencies": { "dependencies": {

View File

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

View File

@@ -9,6 +9,7 @@
"references": [ "references": [
{ "path": "../../component" }, { "path": "../../component" },
{ "path": "../../core" }, { "path": "../../core" },
{ "path": "../../../common/env" },
{ "path": "../../i18n" }, { "path": "../../i18n" },
{ "path": "../../../common/nbstore" }, { "path": "../../../common/nbstore" },
{ "path": "../../../../blocksuite/affine/all" }, { "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 type { _AsyncVersionOf } from 'async-call-rpc';
import { AsyncCall } 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 { import {
app, app,
dialog, dialog,
@@ -57,7 +63,6 @@ class HelperProcessManager {
this.ready = new Promise((resolve, reject) => { this.ready = new Promise((resolve, reject) => {
helperProcess.once('spawn', () => { helperProcess.once('spawn', () => {
try { try {
this.#connectMain();
logger.info('[helper] forked', helperProcess.pid); logger.info('[helper] forked', helperProcess.pid);
resolve(); resolve();
} catch (err) { } catch (err) {
@@ -91,11 +96,15 @@ class HelperProcessManager {
// bridge main <-> helper process // bridge main <-> helper process
// also set up the RPC to the helper process // also set up the RPC to the helper process
#connectMain() { connectMain(window: BaseWindow) {
const dialogMethods = pickAndBind(dialog, [ const dialogMethods = {
'showOpenDialog', showOpenDialog: async (opts: OpenDialogOptions) => {
'showSaveDialog', return dialog.showOpenDialog(window, opts);
]); },
showSaveDialog: async (opts: SaveDialogOptions) => {
return dialog.showSaveDialog(window, opts);
},
};
const shellMethods = pickAndBind(shell, [ const shellMethods = pickAndBind(shell, [
'openExternal', 'openExternal',
'showItemInFolder', 'showItemInFolder',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -54,18 +54,15 @@ export default {
inlineSourcesContent: true, inlineSourcesContent: true,
}), }),
], ],
define: { define: Object.entries(
'process.env.CAPTCHA_SITE_KEY': `"${process.env.CAPTCHA_SITE_KEY}"`, getBuildConfig(new Package('@affine/web'), {
...Object.entries( mode: 'development',
getBuildConfig(new Package('@affine/web'), { channel: 'canary',
mode: 'development', })
channel: 'canary', ).reduce((envs, [key, value]) => {
}) envs[`BUILD_CONFIG.${key}`] = JSON.stringify(value);
).reduce((envs, [key, value]) => { return envs;
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 */ /* eslint-disable react-hooks/exhaustive-deps */
// the `process.env.NODE_ENV !== 'production'` condition is resolved by the build tool // 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 = () => {}; const noop: (...args: any[]) => any = () => {};
@@ -67,7 +66,7 @@ export const useRefEffect = <T>(
// Show the current ref value in development // Show the current ref value in development
// in react dev tools // in react dev tools
if (process.env.NODE_ENV !== 'production') { if (BUILD_CONFIG.debug) {
useDebugValue(internalRef.ref_.current); useDebugValue(internalRef.ref_.current);
} }

View File

@@ -7,8 +7,6 @@
import type { PropertyDeclaration } from 'lit'; import type { PropertyDeclaration } from 'lit';
import type React from 'react'; import type React from 'react';
const DEV_MODE = process.env.NODE_ENV !== 'production';
type DistributiveOmit<T, K extends string | number | symbol> = T extends any type DistributiveOmit<T, K extends string | number | symbol> = T extends any
? K extends keyof T ? K extends keyof T
? Omit<T, K> ? Omit<T, K>
@@ -245,7 +243,7 @@ export const createComponent = <
}: Options<I, E>): ReactWebComponent<I, E> => { }: Options<I, E>): ReactWebComponent<I, E> => {
const eventProps = new Set(Object.keys(events ?? {})); const eventProps = new Set(Object.keys(events ?? {}));
if (DEV_MODE) { if (BUILD_CONFIG.debug) {
for (const p of reservedReactProperties) { for (const p of reservedReactProperties) {
if (p in elementClass.prototype && !(p in HTMLElement.prototype)) { if (p in elementClass.prototype && !(p in HTMLElement.prototype)) {
// Note, this effectively warns only for `ref` since the other // 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 { ListLayoutHandlerExtension } from '@blocksuite/affine/blocks/list';
import { ParagraphLayoutHandlerExtension } from '@blocksuite/affine/blocks/paragraph'; import { ParagraphLayoutHandlerExtension } from '@blocksuite/affine/blocks/paragraph';
import { import {
@@ -6,15 +7,7 @@ import {
} from '@blocksuite/affine/gfx/turbo-renderer'; } from '@blocksuite/affine/gfx/turbo-renderer';
function createPainterWorker() { function createPainterWorker() {
const worker = new Worker( const worker = new Worker(getWorkerUrl('turbo-painter-entry.worker.js'));
/* webpackChunkName: "turbo-painter-entry" */ new URL(
'./turbo-painter-entry.worker.ts',
import.meta.url
),
{
type: 'module',
}
);
return worker; return worker;
} }

View File

@@ -1,5 +1,5 @@
import { mixpanel, sentry } from '@affine/track'; 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(); mixpanel.init();
sentry.init(); sentry.init();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ export const createIsland = () => {
Provider: ({ children }: React.PropsWithChildren) => { Provider: ({ children }: React.PropsWithChildren) => {
const target = useLiveData(targetLiveData$); const target = useLiveData(targetLiveData$);
useEffect(() => { 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'); throw new Error('Island should not be provided more than once');
} }
provided = true; provided = true;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,75 +1,130 @@
import type { Package } from '@affine-tools/utils/workspace'; import { rmSync } from 'node:fs';
import webpack, { type Compiler, type Configuration } from 'webpack'; import { cpus } from 'node:os';
import WebpackDevServer from 'webpack-dev-server';
import { merge } from 'webpack-merge'; 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 { Option, PackageCommand } from './command';
import { createWebpackConfig } from './webpack'; import { createHTMLTargetConfig, createWorkerTargetConfig } from './webpack';
function getChannel() { function getBundleConfigs(pkg: Package) {
const channel = process.env.BUILD_TYPE ?? 'canary'; const core = new Package('@affine/core');
switch (channel) {
case 'canary': const workerConfigs = [
case 'beta': createWorkerTargetConfig(
case 'stable': pkg,
case 'internal': core.srcPath.join(
return channel; 'modules/workspace-engine/impls/workspace-profile.worker.ts'
default: { ).value
throw new Error( ),
`BUILD_TYPE must be one of canary, beta, stable, internal, received [${channel}]` 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) { const defaultDevServerConfig = {
let config = createWebpackConfig(pkg, { host: '0.0.0.0',
mode: dev ? 'development' : 'production', allowedHosts: 'all',
channel: getChannel(), hot: false,
}); liveReload: true,
client: {
let configOverride: Configuration | undefined; overlay: process.env.DISABLE_DEV_OVERLAY === 'true' ? false : undefined,
const overrideConfigPath = pkg.join('webpack.config.ts'); logging: process.env.CI ? 'none' : 'error',
},
if (overrideConfigPath.isFile()) { historyApiFallback: {
const override = await import(overrideConfigPath.toFileUrl().toString()); rewrites: [
configOverride = override.config ?? override.default; {
} from: /.*/,
to: () => {
if (configOverride) { return process.env.SELF_HOSTED === 'true'
config = merge(config, configOverride); ? '/selfhost.html'
} : '/index.html';
},
return config; },
} ],
},
export async function start( proxy: [
compiler: Compiler, {
config: Configuration['devServer'] context: '/api',
): Promise<WebpackDevServer> { target: 'http://localhost:3010',
const devServer = new WebpackDevServer(config, compiler); logLevel: httpProxyMiddlewareLogLevel,
},
await devServer.start(); {
context: '/socket.io',
return devServer; target: 'http://localhost:3010',
} ws: true,
logLevel: httpProxyMiddlewareLogLevel,
export async function build(compiler: Compiler) { },
compiler.run((error, stats) => { {
if (error) { context: '/graphql',
console.error(error); target: 'http://localhost:3010',
process.exit(1); logLevel: httpProxyMiddlewareLogLevel,
} },
if (stats) { ],
if (stats.hasErrors()) { } as DevServerConfiguration;
console.error(stats.toString('errors-only'));
process.exit(1);
} else {
console.log(stats.toString('minimal'));
}
}
});
}
export class BundleCommand extends PackageCommand { export class BundleCommand extends PackageCommand {
static override paths = [['bundle'], ['webpack'], ['pack'], ['bun']]; static override paths = [['bundle'], ['webpack'], ['pack'], ['bun']];
@@ -83,19 +138,60 @@ export class BundleCommand extends PackageCommand {
}); });
async execute() { async execute() {
this.logger.info(`Packing package ${this.package}...`); const pkg = this.workspace.getPackage(this.package);
const config = await getConfig( if (this.dev) {
this.workspace.getPackage(this.package), await BundleCommand.dev(pkg);
this.dev } 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); const compiler = webpack(config);
if (this.dev) { compiler.run((error, stats) => {
await start(compiler, config.devServer); if (error) {
} else { console.error(error);
await build(compiler); 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 type { Compiler, WebpackPluginInstance } from 'webpack';
import webpack from 'webpack'; import webpack from 'webpack';
import type { BuildFlags } from './types.js'; export const getPublicPath = (BUILD_CONFIG: BUILD_CONFIG_TYPE) => {
export const getPublicPath = (
flags: BuildFlags,
BUILD_CONFIG: BUILD_CONFIG_TYPE
) => {
const { BUILD_TYPE } = process.env; const { BUILD_TYPE } = process.env;
if (typeof process.env.PUBLIC_PATH === 'string') { if (typeof process.env.PUBLIC_PATH === 'string') {
return process.env.PUBLIC_PATH; return process.env.PUBLIC_PATH;
} }
if ( if (
flags.mode === 'development' || BUILD_CONFIG.debug ||
BUILD_CONFIG.distribution === 'desktop' || BUILD_CONFIG.distribution === 'desktop' ||
BUILD_CONFIG.distribution === 'ios' || BUILD_CONFIG.distribution === 'ios' ||
BUILD_CONFIG.distribution === 'android' BUILD_CONFIG.distribution === 'android'
@@ -58,11 +53,15 @@ const gitShortHash = once(() => {
const currentDir = Path.dir(import.meta.url); const currentDir = Path.dir(import.meta.url);
function getHTMLPluginOptions( export interface CreateHTMLPluginConfig {
flags: BuildFlags, filename?: string;
BUILD_CONFIG: BUILD_CONFIG_TYPE additionalEntryForSelfhost?: boolean;
) { injectGlobalErrorHandler?: boolean;
const publicPath = getPublicPath(flags, BUILD_CONFIG); emitAssetsManifest?: boolean;
}
function getHTMLPluginOptions(BUILD_CONFIG: BUILD_CONFIG_TYPE) {
const publicPath = getPublicPath(BUILD_CONFIG);
const cdnOrigin = publicPath.startsWith('/') const cdnOrigin = publicPath.startsWith('/')
? undefined ? undefined
: new URL(publicPath).origin; : new URL(publicPath).origin;
@@ -79,144 +78,159 @@ function getHTMLPluginOptions(
return { return {
template: currentDir.join('template.html').toString(), template: currentDir.join('template.html').toString(),
inject: 'body', inject: 'body',
filename: 'index.html',
minify: false, minify: false,
templateParameters: templateParams, templateParameters: templateParams,
chunks: ['app'], chunks: ['app'],
} satisfies HTMLPlugin.Options; } satisfies HTMLPlugin.Options;
} }
export function createShellHTMLPlugin( const AssetsManifestPlugin = {
flags: BuildFlags, apply(compiler: Compiler) {
BUILD_CONFIG: BUILD_CONFIG_TYPE compiler.hooks.compilation.tap('assets-manifest-plugin', compilation => {
) { HTMLPlugin.getHooks(compilation).beforeAssetTagGeneration.tap(
const htmlPluginOptions = getHTMLPluginOptions(flags, BUILD_CONFIG); 'assets-manifest-plugin',
arg => {
return new HTMLPlugin({ if (!compilation.getAsset('assets-manifest.json')) {
...htmlPluginOptions, compilation.emitAsset(
chunks: ['shell'], `assets-manifest.json`,
filename: `shell.html`, new webpack.sources.RawSource(
}); JSON.stringify(
} {
...arg.assets,
export function createBackgroundWorkerHTMLPlugin( js: arg.assets.js.map(file =>
flags: BuildFlags, file.substring(arg.assets.publicPath.length)
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
)
), ),
{ css: arg.assets.css.map(file =>
immutable: false, file.substring(arg.assets.publicPath.length)
} ),
); gitHash: gitShortHash(),
} description: DESCRIPTION,
},
return arg; 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({ new HTMLPlugin({
...htmlPluginOptions, ...htmlPluginOptions,
chunks: ['index'],
filename: config.filename,
publicPath, publicPath,
meta: { meta: {
'env:publicPath': publicPath, 'env:publicPath': publicPath,
}, },
}), })
// selfhost html );
new HTMLPlugin({
...htmlPluginOptions, if (BUILD_CONFIG.isElectron) {
meta: { plugins.push(
'env:isSelfHosted': 'true', new HTMLPlugin({
'env:publicPath': '/', ...htmlPluginOptions,
}, chunks: ['shell'],
filename: 'selfhost.html', filename: 'shell.html',
templateParameters: { publicPath,
...htmlPluginOptions.templateParameters, meta: {
PRECONNECT: '', '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 { createRequire } from 'node:module';
import path from 'node:path';
import { getBuildConfig } from '@affine-tools/utils/build-config'; import { getBuildConfig } from '@affine-tools/utils/build-config';
import { ProjectRoot } from '@affine-tools/utils/path'; 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 { PerfseePlugin } from '@perfsee/webpack';
import { sentryWebpackPlugin } from '@sentry/webpack-plugin'; import { sentryWebpackPlugin } from '@sentry/webpack-plugin';
import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin'; import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin';
import CopyPlugin from 'copy-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 MiniCssExtractPlugin from 'mini-css-extract-plugin';
import TerserPlugin from 'terser-webpack-plugin'; import TerserPlugin from 'terser-webpack-plugin';
import webpack from 'webpack'; import webpack from 'webpack';
import type { Configuration as DevServerConfiguration } from 'webpack-dev-server';
import { productionCacheGroups } from './cache-group.js'; import { productionCacheGroups } from './cache-group.js';
import { import {
createBackgroundWorkerHTMLPlugin, type CreateHTMLPluginConfig,
createHTMLPlugins, createHTMLPlugins,
createPopupHTMLPlugin,
createShellHTMLPlugin,
} from './html-plugin.js'; } from './html-plugin.js';
import { WebpackS3Plugin } from './s3-plugin.js'; import { WebpackS3Plugin } from './s3-plugin.js';
import type { BuildFlags } from './types';
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const cssnano = require('cssnano'); const cssnano = require('cssnano');
const IN_CI = !!process.env.CI; const IN_CI = !!process.env.CI;
const OptimizeOptionOptions: ( const availableChannels = ['canary', 'beta', 'stable', 'internal'];
flags: BuildFlags function getBuildConfigFromEnv(pkg: Package) {
) => webpack.Configuration['optimization'] = flags => ({ const channel = process.env.BUILD_TYPE ?? 'canary';
minimize: flags.mode === 'production', const dev = process.env.NODE_ENV === 'development';
minimizer: [ if (!availableChannels.includes(channel)) {
new TerserPlugin({ throw new Error(
minify: TerserPlugin.swcMinify, `BUILD_TYPE must be one of ${availableChannels.join(', ')}, received [${channel}]`
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,
},
});
export function createWebpackConfig( return getBuildConfig(pkg, {
// @ts-expect-error checked
channel,
mode: dev ? 'development' : 'production',
});
}
export function createHTMLTargetConfig(
pkg: Package, pkg: Package,
flags: BuildFlags entry: string | Record<string, string>,
htmlConfig: Partial<CreateHTMLPluginConfig> = {},
deps?: string[]
): webpack.Configuration { ): webpack.Configuration {
const buildConfig = getBuildConfig(pkg, flags); entry = typeof entry === 'string' ? { index: entry } : entry;
const httpProxyMiddlewareLogLevel = process.env.CI ? 'silent' : 'error';
const config = { htmlConfig = merge(
name: 'affine', {},
{
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, context: pkg.path.value,
experiments: { experiments: {
topLevelAwait: true, topLevelAwait: true,
outputModule: false, outputModule: false,
syncWebAssembly: true, syncWebAssembly: true,
}, },
entry: { entry,
app: pkg.entry ?? './src/index.tsx',
},
output: { output: {
environment: { environment: {
module: true, module: true,
dynamicImport: true, dynamicImport: true,
}, },
filename: filename: buildConfig.debug
flags.mode === 'production' ? 'js/[name].js'
? 'js/[name].[contenthash:8].js' : 'js/[name].[contenthash:8].js',
: 'js/[name].js', assetModuleFilename: buildConfig.debug
// In some cases webpack will emit files starts with "_" which is reserved in web extension. ? '[name].[contenthash:8][ext]'
chunkFilename: pathData => : 'assets/[name].[contenthash:8][ext][query]',
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',
path: pkg.distPath.value, path: pkg.distPath.value,
clean: flags.mode === 'production', clean: false,
globalObject: 'globalThis', globalObject: 'globalThis',
// NOTE(@forehalo): always keep it '/' // NOTE(@forehalo): always keep it '/'
publicPath: '/', publicPath: '/',
workerPublicPath: '/',
}, },
target: ['web', 'es2022'], target: ['web', 'es2022'],
mode: buildConfig.debug ? 'development' : 'production',
mode: flags.mode, devtool: buildConfig.debug ? 'cheap-module-source-map' : 'source-map',
devtool:
flags.mode === 'production' ? 'source-map' : 'cheap-module-source-map',
resolve: { resolve: {
symlinks: true, symlinks: true,
extensionAlias: { extensionAlias: {
@@ -139,7 +111,9 @@ export function createWebpackConfig(
).value, ).value,
}, },
}, },
//#endregion
//#region module config
module: { module: {
parser: { parser: {
javascript: { javascript: {
@@ -151,6 +125,7 @@ export function createWebpackConfig(
strictExportPresence: true, strictExportPresence: true,
}, },
}, },
//#region rules
rules: [ rules: [
{ {
test: /\.m?js?$/, test: /\.m?js?$/,
@@ -240,7 +215,7 @@ export function createWebpackConfig(
{ {
test: /\.css$/, test: /\.css$/,
use: [ use: [
flags.mode === 'development' buildConfig.debug
? 'style-loader' ? 'style-loader'
: MiniCssExtractPlugin.loader, : MiniCssExtractPlugin.loader,
{ {
@@ -283,28 +258,16 @@ export function createWebpackConfig(
], ],
}, },
], ],
//#endregion
}, },
//#endregion
//#region plugins
plugins: compact([ plugins: compact([
IN_CI ? null : new webpack.ProgressPlugin({ percentBy: 'entries' }), !IN_CI && new webpack.ProgressPlugin({ percentBy: 'entries' }),
flags.mode === 'development' ...createHTMLPlugins(buildConfig, htmlConfig),
? null
: // todo: support multiple entry points
new MiniCssExtractPlugin({
filename: `[name].[contenthash:8].css`,
ignoreOrder: true,
}),
new VanillaExtractPlugin(),
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(flags.mode), 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'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),
...Object.entries(buildConfig).reduce( ...Object.entries(buildConfig).reduce(
(def, [k, v]) => { (def, [k, v]) => {
def[`BUILD_CONFIG.${k}`] = JSON.stringify(v); def[`BUILD_CONFIG.${k}`] = JSON.stringify(v);
@@ -313,90 +276,100 @@ export function createWebpackConfig(
{} as Record<string, string> {} as Record<string, string>
), ),
}), }),
buildConfig.isAdmin && flags.mode !== 'production' !buildConfig.debug &&
? null // todo: support multiple entry points
: new CopyPlugin({ new MiniCssExtractPlugin({
patterns: [ filename: `[name].[contenthash:8].css`,
{ ignoreOrder: true,
// copy the shared public assets into dist }),
from: pkg.workspace.getPackage('@affine/core').join('public') new VanillaExtractPlugin(),
.value, !buildConfig.isAdmin &&
}, new CopyPlugin({
], patterns: [
}), {
flags.mode === 'production' && // copy the shared public assets into dist
(buildConfig.isWeb || buildConfig.isMobileWeb || buildConfig.isAdmin) && from: new Package('@affine/core').join('public').value,
process.env.R2_SECRET_ACCESS_KEY },
? new WebpackS3Plugin() ],
: null, }),
!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: { stats: {
errorDetails: true, errorDetails: true,
}, },
optimization: OptimizeOptionOptions(flags),
devServer: { //#region optimization
host: '0.0.0.0', optimization: {
allowedHosts: 'all', minimize: !buildConfig.debug,
hot: false, minimizer: [
liveReload: true, new TerserPlugin({
client: { minify: TerserPlugin.swcMinify,
overlay: process.env.DISABLE_DEV_OVERLAY === 'true' ? false : undefined, exclude: [/plugins\/.+\/.+\.js$/, /plugins\/.+\/.+\.mjs$/],
logging: process.env.CI ? 'none' : 'error', parallel: true,
}, extractComments: true,
historyApiFallback: { terserOptions: {
rewrites: [ ecma: 2020,
{ compress: {
from: /.*/, unused: true,
to: () => { },
return process.env.SELF_HOSTED === 'true' mangle: {
? '/selfhost.html' keep_classnames: true,
: '/index.html';
}, },
}, },
], }),
],
removeEmptyChunks: true,
providedExports: true,
usedExports: true,
sideEffects: true,
removeAvailableModules: true,
runtimeChunk: {
name: 'runtime',
}, },
static: [ splitChunks: {
{ chunks: 'all',
directory: pkg.workspace.getPackage('@affine/core').join('public') minSize: 1,
.value, minChunks: 1,
publicPath: '/', maxInitialRequests: Number.MAX_SAFE_INTEGER,
watch: !IN_CI, maxAsyncRequests: Number.MAX_SAFE_INTEGER,
staticOptions: { cacheGroups: productionCacheGroups,
immutable: IN_CI, },
maxAge: '1d', },
}, //#endregion
}, };
],
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;
if (flags.mode === 'production' && process.env.PERFSEE_TOKEN) { if (buildConfig.debug && !IN_CI) {
config.plugins.push(
new PerfseePlugin({
project: 'affine-toeverything',
})
);
}
if (flags.mode === 'development' && !IN_CI) {
config.optimization = { config.optimization = {
...config.optimization, ...config.optimization,
minimize: false, 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; 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', discordUrl: 'https://affine.pro/redirect/discord',
imageProxyUrl: '/api/worker/image-proxy', imageProxyUrl: '/api/worker/image-proxy',
linkPreviewUrl: '/api/worker/link-preview', 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() { get beta() {

View File

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

View File

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