diff --git a/packages/common/debug/src/index.ts b/packages/common/debug/src/index.ts index b69441ac1a..ad564f9a7c 100644 --- a/packages/common/debug/src/index.ts +++ b/packages/common/debug/src/index.ts @@ -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'); } diff --git a/packages/common/env/package.json b/packages/common/env/package.json index 4af43d4582..bbf70e3313 100644 --- a/packages/common/env/package.json +++ b/packages/common/env/package.json @@ -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:*", diff --git a/packages/common/env/src/global.ts b/packages/common/env/src/global.ts index 10ef177ce4..2de2fd7f5d 100644 --- a/packages/common/env/src/global.ts +++ b/packages/common/env/src/global.ts @@ -17,6 +17,7 @@ export function setupGlobal() { isMobile: false, isSelfHosted: false, publicPath: '/', + workerPath: '/js/', }; if (globalThis.navigator) { diff --git a/packages/common/env/src/worker.ts b/packages/common/env/src/worker.ts new file mode 100644 index 0000000000..e14724be79 --- /dev/null +++ b/packages/common/env/src/worker.ts @@ -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; +} diff --git a/packages/common/infra/package.json b/packages/common/infra/package.json index 3763e94a6a..9829b8624f 100644 --- a/packages/common/infra/package.json +++ b/packages/common/infra/package.json @@ -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": { diff --git a/packages/frontend/apps/android/package.json b/packages/frontend/apps/android/package.json index 28945db478..d463215ab5 100644 --- a/packages/frontend/apps/android/package.json +++ b/packages/frontend/apps/android/package.json @@ -10,6 +10,7 @@ "dependencies": { "@affine/component": "workspace:*", "@affine/core": "workspace:*", + "@affine/env": "workspace:*", "@affine/i18n": "workspace:*", "@affine/nbstore": "workspace:*", "@blocksuite/affine": "workspace:*", diff --git a/packages/frontend/apps/android/src/app.tsx b/packages/frontend/apps/android/src/app.tsx index 94d0a0ada3..14585ae706 100644 --- a/packages/frontend/apps/android/src/app.tsx +++ b/packages/frontend/apps/android/src/app.tsx @@ -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(); diff --git a/packages/frontend/apps/android/src/nbstore.ts b/packages/frontend/apps/android/src/nbstore.worker.ts similarity index 100% rename from packages/frontend/apps/android/src/nbstore.ts rename to packages/frontend/apps/android/src/nbstore.worker.ts diff --git a/packages/frontend/apps/android/tsconfig.json b/packages/frontend/apps/android/tsconfig.json index 7bdf03aa09..a94ca0f32d 100644 --- a/packages/frontend/apps/android/tsconfig.json +++ b/packages/frontend/apps/android/tsconfig.json @@ -9,6 +9,7 @@ "references": [ { "path": "../../component" }, { "path": "../../core" }, + { "path": "../../../common/env" }, { "path": "../../i18n" }, { "path": "../../../common/nbstore" }, { "path": "../../../../blocksuite/affine/all" }, diff --git a/packages/frontend/apps/electron-renderer/webpack.config.ts b/packages/frontend/apps/electron-renderer/webpack.config.ts deleted file mode 100644 index 4a1b947a98..0000000000 --- a/packages/frontend/apps/electron-renderer/webpack.config.ts +++ /dev/null @@ -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', - }, -}; diff --git a/packages/frontend/apps/electron/src/main/helper-process.ts b/packages/frontend/apps/electron/src/main/helper-process.ts index 0918b0fa88..13a00e2d01 100644 --- a/packages/frontend/apps/electron/src/main/helper-process.ts +++ b/packages/frontend/apps/electron/src/main/helper-process.ts @@ -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', diff --git a/packages/frontend/apps/electron/src/main/windows-manager/main-window.ts b/packages/frontend/apps/electron/src/main/windows-manager/main-window.ts index fe903aff46..b67547e006 100644 --- a/packages/frontend/apps/electron/src/main/windows-manager/main-window.ts +++ b/packages/frontend/apps/electron/src/main/windows-manager/main-window.ts @@ -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( diff --git a/packages/frontend/apps/ios/package.json b/packages/frontend/apps/ios/package.json index 76ed7feacf..4f4a22acdf 100644 --- a/packages/frontend/apps/ios/package.json +++ b/packages/frontend/apps/ios/package.json @@ -16,6 +16,7 @@ "dependencies": { "@affine/component": "workspace:*", "@affine/core": "workspace:*", + "@affine/env": "workspace:*", "@affine/i18n": "workspace:*", "@affine/nbstore": "workspace:*", "@blocksuite/affine": "workspace:*", diff --git a/packages/frontend/apps/ios/src/app.tsx b/packages/frontend/apps/ios/src/app.tsx index 4c0709f05b..5abfbac735 100644 --- a/packages/frontend/apps/ios/src/app.tsx +++ b/packages/frontend/apps/ios/src/app.tsx @@ -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(NbStoreNativeDBApis, { diff --git a/packages/frontend/apps/ios/src/worker.ts b/packages/frontend/apps/ios/src/nbstore.worker.ts similarity index 100% rename from packages/frontend/apps/ios/src/worker.ts rename to packages/frontend/apps/ios/src/nbstore.worker.ts diff --git a/packages/frontend/apps/ios/tsconfig.json b/packages/frontend/apps/ios/tsconfig.json index 8e6e11d4b8..240cd51ff3 100644 --- a/packages/frontend/apps/ios/tsconfig.json +++ b/packages/frontend/apps/ios/tsconfig.json @@ -9,6 +9,7 @@ "references": [ { "path": "../../component" }, { "path": "../../core" }, + { "path": "../../../common/env" }, { "path": "../../i18n" }, { "path": "../../../common/nbstore" }, { "path": "../../../../blocksuite/affine/all" }, diff --git a/packages/frontend/apps/mobile/package.json b/packages/frontend/apps/mobile/package.json index a7a9289e4b..262586f16e 100644 --- a/packages/frontend/apps/mobile/package.json +++ b/packages/frontend/apps/mobile/package.json @@ -11,6 +11,7 @@ "dependencies": { "@affine/component": "workspace:*", "@affine/core": "workspace:*", + "@affine/env": "workspace:*", "@affine/i18n": "workspace:*", "@affine/nbstore": "workspace:*", "@blocksuite/affine": "workspace:*", diff --git a/packages/frontend/apps/mobile/src/app.tsx b/packages/frontend/apps/mobile/src/app.tsx index eb8d56facf..6ccbbc85d1 100644 --- a/packages/frontend/apps/mobile/src/app.tsx +++ b/packages/frontend/apps/mobile/src/app.tsx @@ -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', () => { diff --git a/packages/frontend/apps/mobile/src/nbstore.ts b/packages/frontend/apps/mobile/src/nbstore.worker.ts similarity index 100% rename from packages/frontend/apps/mobile/src/nbstore.ts rename to packages/frontend/apps/mobile/src/nbstore.worker.ts diff --git a/packages/frontend/apps/mobile/tsconfig.json b/packages/frontend/apps/mobile/tsconfig.json index 7bdf03aa09..a94ca0f32d 100644 --- a/packages/frontend/apps/mobile/tsconfig.json +++ b/packages/frontend/apps/mobile/tsconfig.json @@ -9,6 +9,7 @@ "references": [ { "path": "../../component" }, { "path": "../../core" }, + { "path": "../../../common/env" }, { "path": "../../i18n" }, { "path": "../../../common/nbstore" }, { "path": "../../../../blocksuite/affine/all" }, diff --git a/packages/frontend/apps/web/package.json b/packages/frontend/apps/web/package.json index fd407816c0..f708ecd6a7 100644 --- a/packages/frontend/apps/web/package.json +++ b/packages/frontend/apps/web/package.json @@ -11,6 +11,7 @@ "dependencies": { "@affine/component": "workspace:*", "@affine/core": "workspace:*", + "@affine/env": "workspace:*", "@affine/i18n": "workspace:*", "@affine/nbstore": "workspace:*", "@affine/track": "workspace:*", diff --git a/packages/frontend/apps/web/src/app.tsx b/packages/frontend/apps/web/src/app.tsx index 71f21804b3..40d08c27c4 100644 --- a/packages/frontend/apps/web/src/app.tsx +++ b/packages/frontend/apps/web/src/app.tsx @@ -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', () => { diff --git a/packages/frontend/apps/web/src/nbstore.ts b/packages/frontend/apps/web/src/nbstore.worker.ts similarity index 100% rename from packages/frontend/apps/web/src/nbstore.ts rename to packages/frontend/apps/web/src/nbstore.worker.ts diff --git a/packages/frontend/apps/web/tsconfig.json b/packages/frontend/apps/web/tsconfig.json index cf76a48c22..d0d86fdeb6 100644 --- a/packages/frontend/apps/web/tsconfig.json +++ b/packages/frontend/apps/web/tsconfig.json @@ -9,6 +9,7 @@ "references": [ { "path": "../../component" }, { "path": "../../core" }, + { "path": "../../../common/env" }, { "path": "../../i18n" }, { "path": "../../../common/nbstore" }, { "path": "../../track" }, diff --git a/packages/frontend/component/.storybook/main.ts b/packages/frontend/component/.storybook/main.ts index 4d1f4b1620..b81b0e3046 100644 --- a/packages/frontend/component/.storybook/main.ts +++ b/packages/frontend/component/.storybook/main.ts @@ -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; + }, {}), }); }, diff --git a/packages/frontend/component/src/hooks/use-ref-effect.ts b/packages/frontend/component/src/hooks/use-ref-effect.ts index 4530a83c28..447c2f2b6d 100644 --- a/packages/frontend/component/src/hooks/use-ref-effect.ts +++ b/packages/frontend/component/src/hooks/use-ref-effect.ts @@ -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 = ( // 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); } diff --git a/packages/frontend/component/src/lit-react/create-component.ts b/packages/frontend/component/src/lit-react/create-component.ts index 8d14d4e79b..435ff37830 100644 --- a/packages/frontend/component/src/lit-react/create-component.ts +++ b/packages/frontend/component/src/lit-react/create-component.ts @@ -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 extends any ? K extends keyof T ? Omit @@ -245,7 +243,7 @@ export const createComponent = < }: Options): ReactWebComponent => { 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 diff --git a/packages/frontend/core/src/blocksuite/extensions/turbo-painter-entry.worker.ts b/packages/frontend/core/src/blocksuite/extensions/turbo-painter.worker.ts similarity index 100% rename from packages/frontend/core/src/blocksuite/extensions/turbo-painter-entry.worker.ts rename to packages/frontend/core/src/blocksuite/extensions/turbo-painter.worker.ts diff --git a/packages/frontend/core/src/blocksuite/extensions/turbo-renderer.ts b/packages/frontend/core/src/blocksuite/extensions/turbo-renderer.ts index 7632034873..79223e8008 100644 --- a/packages/frontend/core/src/blocksuite/extensions/turbo-renderer.ts +++ b/packages/frontend/core/src/blocksuite/extensions/turbo-renderer.ts @@ -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; } diff --git a/packages/frontend/core/src/bootstrap/telemetry.ts b/packages/frontend/core/src/bootstrap/telemetry.ts index 141ed572bc..e9950d3b70 100644 --- a/packages/frontend/core/src/bootstrap/telemetry.ts +++ b/packages/frontend/core/src/bootstrap/telemetry.ts @@ -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(); diff --git a/packages/frontend/core/src/components/sign-in/captcha.tsx b/packages/frontend/core/src/components/sign-in/captcha.tsx index 8546386dcc..cf2180693b 100644 --- a/packages/frontend/core/src/components/sign-in/captcha.tsx +++ b/packages/frontend/core/src/components/sign-in/captcha.tsx @@ -36,7 +36,7 @@ export const Captcha = () => { return ( ); diff --git a/packages/frontend/core/src/desktop/dialogs/import-workspace/index.tsx b/packages/frontend/core/src/desktop/dialogs/import-workspace/index.tsx index 80e227835e..2810065678 100644 --- a/packages/frontend/core/src/desktop/dialogs/import-workspace/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/import-workspace/index.tsx @@ -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) => { + 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; diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/backup/index.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/backup/index.tsx index 9934ad1014..79a46e829c 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/backup/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/backup/index.tsx @@ -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$); diff --git a/packages/frontend/core/src/modules/pdf/renderer/worker.ts b/packages/frontend/core/src/modules/pdf/renderer/pdf.worker.ts similarity index 100% rename from packages/frontend/core/src/modules/pdf/renderer/worker.ts rename to packages/frontend/core/src/modules/pdf/renderer/pdf.worker.ts diff --git a/packages/frontend/core/src/modules/pdf/renderer/renderer.ts b/packages/frontend/core/src/modules/pdf/renderer/renderer.ts index 6ca502772a..f25c418428 100644 --- a/packages/frontend/core/src/modules/pdf/renderer/renderer.ts +++ b/packages/frontend/core/src/modules/pdf/renderer/renderer.ts @@ -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 { 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; diff --git a/packages/frontend/core/src/modules/workspace-engine/impls/out-worker.ts b/packages/frontend/core/src/modules/workspace-engine/impls/out-worker.ts index 9a1f1c3b30..19f531c81c 100644 --- a/packages/frontend/core/src/modules/workspace-engine/impls/out-worker.ts +++ b/packages/frontend/core/src/modules/workspace-engine/impls/out-worker.ts @@ -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(rawWorker); return worker; diff --git a/packages/frontend/core/src/modules/workspace-engine/impls/in-worker.ts b/packages/frontend/core/src/modules/workspace-engine/impls/workspace-profile.worker.ts similarity index 100% rename from packages/frontend/core/src/modules/workspace-engine/impls/in-worker.ts rename to packages/frontend/core/src/modules/workspace-engine/impls/workspace-profile.worker.ts diff --git a/packages/frontend/core/src/utils/channel.ts b/packages/frontend/core/src/utils/channel.ts index d1a16ae639..8c1a001451 100644 --- a/packages/frontend/core/src/utils/channel.ts +++ b/packages/frontend/core/src/utils/channel.ts @@ -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; diff --git a/packages/frontend/core/src/utils/island.tsx b/packages/frontend/core/src/utils/island.tsx index 5e993a927d..11e65254d7 100644 --- a/packages/frontend/core/src/utils/island.tsx +++ b/packages/frontend/core/src/utils/island.tsx @@ -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; diff --git a/packages/frontend/track/src/mixpanel.ts b/packages/frontend/track/src/mixpanel.ts index 5c8f6d65cf..507d0e885c 100644 --- a/packages/frontend/track/src/mixpanel.ts +++ b/packages/frontend/track/src/mixpanel.ts @@ -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', diff --git a/packages/frontend/track/src/sentry.ts b/packages/frontend/track/src/sentry.ts index 5f88418fd4..555e0e969e 100644 --- a/packages/frontend/track/src/sentry.ts +++ b/packages/frontend/track/src/sentry.ts @@ -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, diff --git a/tests/affine-local/dev-server.ts b/tests/affine-local/dev-server.ts index cf8a8ff846..401116e140 100644 --- a/tests/affine-local/dev-server.ts +++ b/tests/affine-local/dev-server.ts @@ -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((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'); }; diff --git a/tests/kit/src/electron.ts b/tests/kit/src/electron.ts index 02bce1683a..9f81c42cff 100644 --- a/tests/kit/src/electron.ts +++ b/tests/kit/src/electron.ts @@ -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 diff --git a/tools/@types/build-config/__all.d.ts b/tools/@types/build-config/__all.d.ts index 56b016e004..725a72f6ea 100644 --- a/tools/@types/build-config/__all.d.ts +++ b/tools/@types/build-config/__all.d.ts @@ -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; diff --git a/tools/@types/env/__all.d.ts b/tools/@types/env/__all.d.ts index 3e7e84ed87..61150f9870 100644 --- a/tools/@types/env/__all.d.ts +++ b/tools/@types/env/__all.d.ts @@ -37,6 +37,7 @@ declare type Environment = { // runtime configs publicPath: string; + workerPath: string }; var process: { diff --git a/tools/cli/src/bundle.ts b/tools/cli/src/bundle.ts index 34078d974a..7f4d7c5dac 100644 --- a/tools/cli/src/bundle.ts +++ b/tools/cli/src/bundle.ts @@ -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 { - 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(); } } diff --git a/tools/cli/src/webpack/html-plugin.ts b/tools/cli/src/webpack/html-plugin.ts index 6b804ee51e..b710cae85e 100644 --- a/tools/cli/src/webpack/html-plugin.ts +++ b/tools/cli/src/webpack/html-plugin.ts @@ -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; } diff --git a/tools/cli/src/webpack/index.ts b/tools/cli/src/webpack/index.ts index 172ebca570..e46d09c096 100644 --- a/tools/cli/src/webpack/index.ts +++ b/tools/cli/src/webpack/index.ts @@ -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, + htmlConfig: Partial = {}, + 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 ), }), - 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 & { 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 + ) + ), + 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, + }, + }; +} diff --git a/tools/utils/src/build-config.ts b/tools/utils/src/build-config.ts index 755e9a112b..52cfd23347 100644 --- a/tools/utils/src/build-config.ts +++ b/tools/utils/src/build-config.ts @@ -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() { diff --git a/tools/utils/src/workspace.gen.ts b/tools/utils/src/workspace.gen.ts index f825c86558..e33ed8a4c4 100644 --- a/tools/utils/src/workspace.gen.ts +++ b/tools/utils/src/workspace.gen.ts @@ -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', diff --git a/yarn.lock b/yarn.lock index 3d83a14f89..45d5feca4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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:*"