diff --git a/packages/common/infra/package.json b/packages/common/infra/package.json index 5a6bff9a4..1b7d4f3d4 100644 --- a/packages/common/infra/package.json +++ b/packages/common/infra/package.json @@ -1,15 +1,7 @@ { "name": "@toeverything/infra", "type": "module", - "module": "./dist/index.js", - "main": "./dist/index.cjs", - "types": "./dist/src/index.d.ts", "exports": { - ".": { - "types": "./dist/src/index.d.ts", - "import": "./dist/index.js", - "require": "./dist/index.cjs" - }, "./blocksuite": { "types": "./dist/src/blocksuite/index.d.ts", "import": "./dist/blocksuite.js", @@ -20,26 +12,11 @@ "import": "./dist/command.js", "require": "./dist/command.cjs" }, - "./core/*": { - "types": "./dist/src/core/*.d.ts", - "import": "./dist/core/*.js", - "require": "./dist/core/*.cjs" - }, - "./preload/*": { - "types": "./dist/src/preload/*.d.ts", - "import": "./dist/preload/*.js", - "require": "./dist/preload/*.cjs" - }, "./atom": { "type": "./dist/src/atom.d.ts", "import": "./dist/atom.js", "require": "./dist/atom.cjs" }, - "./type": { - "type": "./dist/src/type.d.ts", - "import": "./dist/type.js", - "require": "./dist/type.cjs" - }, "./app-config-storage": { "type": "./dist/src/app-config-storage.d.ts", "import": "./dist/app-config-storage.js", diff --git a/packages/common/infra/preload/electron.d.ts b/packages/common/infra/preload/electron.d.ts deleted file mode 100644 index 2910e840a..000000000 --- a/packages/common/infra/preload/electron.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -/* eslint-disable */ -// @ts-ignore -export * from '../dist/src/preload/electron'; diff --git a/packages/common/infra/preload/electron.js b/packages/common/infra/preload/electron.js deleted file mode 100644 index a83b2722b..000000000 --- a/packages/common/infra/preload/electron.js +++ /dev/null @@ -1,3 +0,0 @@ -/* eslint-disable */ -/// -export * from '../dist/preload/electron.js'; diff --git a/packages/common/infra/src/atom/settings.ts b/packages/common/infra/src/atom/settings.ts index 229335ce4..ac66005dc 100644 --- a/packages/common/infra/src/atom/settings.ts +++ b/packages/common/infra/src/atom/settings.ts @@ -72,12 +72,13 @@ const appSettingEffect = atomEffect(get => { // some values in settings should be synced into electron side if (environment.isDesktop) { console.log('set config', settings); - window.apis?.updater + // this api type in @affine/electron-api, but it is circular dependency this package, use any here + (window as any).apis?.updater .setConfig({ autoCheckUpdate: settings.autoCheckUpdate, autoDownloadUpdate: settings.autoDownloadUpdate, }) - .catch(err => { + .catch((err: any) => { console.error(err); }); } diff --git a/packages/common/infra/src/core/event-emitter.ts b/packages/common/infra/src/core/event-emitter.ts deleted file mode 100644 index 2e019f631..000000000 --- a/packages/common/infra/src/core/event-emitter.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * The MIT License (MIT) - * - * Copyright (c) 2018 Andy Wermke - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -export type EventMap = { - [key: string]: (...args: any[]) => void; -}; - -/** - * Type-safe event emitter. - * - * Use it like this: - * - * ```typescript - * type MyEvents = { - * error: (error: Error) => void; - * message: (from: string, content: string) => void; - * } - * - * const myEmitter = new EventEmitter() as TypedEmitter; - * - * myEmitter.emit("error", "x") // <- Will catch this type error; - * ``` - * - * Lifecycle: - * invoke -> handle -> emit -> on/once - */ -export interface TypedEventEmitter { - addListener(event: E, listener: Events[E]): this; - on(event: E, listener: Events[E]): this; - once(event: E, listener: Events[E]): this; - - off(event: E, listener: Events[E]): this; - removeAllListeners(event?: E): this; - removeListener(event: E, listener: Events[E]): this; - - emit( - event: E, - ...args: Parameters - ): boolean; - // The sloppy `eventNames()` return type is to mitigate type incompatibilities - see #5 - eventNames(): (keyof Events | string | symbol)[]; - rawListeners(event: E): Events[E][]; - listeners(event: E): Events[E][]; - listenerCount(event: E): number; - - handle(event: E, handler: Events[E]): this; - invoke( - event: E, - ...args: Parameters - ): Promise>; - - getMaxListeners(): number; - setMaxListeners(maxListeners: number): this; -} diff --git a/packages/common/infra/src/handler.ts b/packages/common/infra/src/handler.ts deleted file mode 100644 index 722fc1389..000000000 --- a/packages/common/infra/src/handler.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { - ClipboardHandlers, - ConfigStorageHandlers, - DBHandlers, - DebugHandlers, - DialogHandlers, - ExportHandlers, - UIHandlers, - UpdaterHandlers, - WorkspaceHandlers, -} from './type.js'; -import { HandlerManager } from './type.js'; - -export abstract class DBHandlerManager extends HandlerManager< - 'db', - DBHandlers -> {} - -export abstract class DebugHandlerManager extends HandlerManager< - 'debug', - DebugHandlers -> {} - -export abstract class DialogHandlerManager extends HandlerManager< - 'dialog', - DialogHandlers -> {} - -export abstract class UIHandlerManager extends HandlerManager< - 'ui', - UIHandlers -> {} - -export abstract class ClipboardHandlerManager extends HandlerManager< - 'clipboard', - ClipboardHandlers -> {} - -export abstract class ExportHandlerManager extends HandlerManager< - 'export', - ExportHandlers -> {} - -export abstract class UpdaterHandlerManager extends HandlerManager< - 'updater', - UpdaterHandlers -> {} - -export abstract class WorkspaceHandlerManager extends HandlerManager< - 'workspace', - WorkspaceHandlers -> {} - -export abstract class ConfigStorageHandlerManager extends HandlerManager< - 'configStorage', - ConfigStorageHandlers -> {} diff --git a/packages/common/infra/src/index.ts b/packages/common/infra/src/index.ts deleted file mode 100644 index d0d18480a..000000000 --- a/packages/common/infra/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './handler.js'; -export * from './type.js'; diff --git a/packages/common/infra/src/type.ts b/packages/common/infra/src/type.ts deleted file mode 100644 index b08e8c2f0..000000000 --- a/packages/common/infra/src/type.ts +++ /dev/null @@ -1,285 +0,0 @@ -import type Buffer from 'buffer'; -import { z } from 'zod'; - -import type { AppConfigSchema } from './app-config-storage.js'; -import type { TypedEventEmitter } from './core/event-emitter.js'; - -type Buffer = Buffer.Buffer; - -export const packageJsonInputSchema = z.object({ - name: z.string(), - version: z.string(), - description: z.string(), - affinePlugin: z.object({ - release: z.union([z.boolean(), z.enum(['development'])]), - entry: z.object({ - core: z.string(), - }), - }), -}); - -export const packageJsonOutputSchema = z.object({ - name: z.string(), - version: z.string(), - description: z.string(), - affinePlugin: z.object({ - release: z.union([z.boolean(), z.enum(['development'])]), - entry: z.object({ - core: z.string(), - }), - assets: z.array(z.string()), - }), -}); - -export abstract class HandlerManager< - Namespace extends string, - Handlers extends Record, -> { - static instance: HandlerManager>; - private readonly _app: App; - private readonly _namespace: Namespace; - private _handlers: Handlers; - - constructor() { - throw new Error('Method not implemented.'); - } - - private _initialized = false; - - registerHandlers(handlers: Handlers) { - if (this._initialized) { - throw new Error('Already initialized'); - } - this._handlers = handlers; - for (const [name, handler] of Object.entries(this._handlers)) { - this._app.handle(`${this._namespace}:${name}`, (async (...args: any[]) => - handler(...args)) as any); - } - this._initialized = true; - } - - invokeHandler( - name: K, - ...args: Parameters - ): Promise> { - return this._handlers[name](...args); - } - - static getInstance(): HandlerManager< - string, - Record - > { - throw new Error('Method not implemented.'); - } -} - -export interface WorkspaceMeta { - id: string; - mainDBPath: string; - secondaryDBPath?: string; // assume there will be only one -} - -export type PrimitiveHandlers = (...args: any[]) => Promise; - -export type DBHandlers = { - getDocAsUpdates: ( - workspaceId: string, - subdocId?: string - ) => Promise; - applyDocUpdate: ( - id: string, - update: Uint8Array, - subdocId?: string - ) => Promise; - addBlob: ( - workspaceId: string, - key: string, - data: Uint8Array - ) => Promise; - getBlob: (workspaceId: string, key: string) => Promise; - deleteBlob: (workspaceId: string, key: string) => Promise; - getBlobKeys: (workspaceId: string) => Promise; - getDefaultStorageLocation: () => Promise; -}; - -export type DebugHandlers = { - revealLogFile: () => Promise; - logFilePath: () => Promise; -}; - -export type ErrorMessage = - | 'DB_FILE_ALREADY_LOADED' - | 'DB_FILE_PATH_INVALID' - | 'DB_FILE_INVALID' - | 'DB_FILE_MIGRATION_FAILED' - | 'FILE_ALREADY_EXISTS' - | 'UNKNOWN_ERROR'; - -export interface LoadDBFileResult { - workspaceId?: string; - error?: ErrorMessage; - canceled?: boolean; -} - -export interface SaveDBFileResult { - filePath?: string; - canceled?: boolean; - error?: ErrorMessage; -} - -export interface SelectDBFileLocationResult { - filePath?: string; - error?: ErrorMessage; - canceled?: boolean; -} - -export interface MoveDBFileResult { - filePath?: string; - error?: ErrorMessage; - canceled?: boolean; -} - -// provide a backdoor to set dialog path for testing in playwright -export interface FakeDialogResult { - canceled?: boolean; - filePath?: string; - filePaths?: string[]; -} - -export type DialogHandlers = { - revealDBFile: (workspaceId: string) => Promise; - loadDBFile: () => Promise; - saveDBFileAs: (workspaceId: string) => Promise; - moveDBFile: ( - workspaceId: string, - dbFileLocation?: string - ) => Promise; - selectDBFileLocation: () => Promise; - setFakeDialogResult: (result: any) => Promise; -}; - -export type UIHandlers = { - handleThemeChange: (theme: 'system' | 'light' | 'dark') => Promise; - handleSidebarVisibilityChange: (visible: boolean) => Promise; - handleMinimizeApp: () => Promise; - handleMaximizeApp: () => Promise; - handleCloseApp: () => Promise; - getGoogleOauthCode: () => Promise; - getChallengeResponse: (resource: string) => Promise; - handleOpenMainApp: () => Promise; -}; - -export type ClipboardHandlers = { - copyAsImageFromString: (dataURL: string) => Promise; -}; - -export type ExportHandlers = { - savePDFFileAs: (title: string) => Promise; -}; - -export interface UpdateMeta { - version: string; - allowAutoUpdate: boolean; -} - -export type UpdaterConfig = { - autoCheckUpdate: boolean; - autoDownloadUpdate: boolean; -}; - -export type UpdaterHandlers = { - currentVersion: () => Promise; - quitAndInstall: () => Promise; - downloadUpdate: () => Promise; - getConfig: () => Promise; - setConfig: (newConfig: Partial) => Promise; - checkForUpdates: () => Promise<{ version: string } | null>; -}; - -export type WorkspaceHandlers = { - list: () => Promise<[workspaceId: string, meta: WorkspaceMeta][]>; - delete: (id: string) => Promise; - getMeta: (id: string) => Promise; - clone: (id: string, newId: string) => Promise; -}; - -export type ConfigStorageHandlers = { - set: (config: AppConfigSchema | Partial) => Promise; - get: () => Promise; -}; - -export type UnwrapManagerHandlerToServerSide< - ElectronEvent extends { - frameId: number; - processId: number; - }, - Manager extends HandlerManager>, -> = Manager extends HandlerManager - ? { - [K in keyof Handlers]: Handlers[K] extends ( - ...args: infer Args - ) => Promise - ? (event: ElectronEvent, ...args: Args) => Promise - : never; - } - : never; - -export type UnwrapManagerHandlerToClientSide< - Manager extends HandlerManager>, -> = Manager extends HandlerManager - ? { - [K in keyof Handlers]: Handlers[K] extends ( - ...args: infer Args - ) => Promise - ? (...args: Args) => Promise - : never; - } - : never; - -/** - * @internal - */ -export type App< - Namespace extends string, - Handlers extends Record, -> = TypedEventEmitter<{ - [K in keyof Handlers as `${Namespace}:${K & string}`]: Handlers[K]; -}>; - -export interface UpdaterEvents { - onUpdateAvailable: (fn: (versionMeta: UpdateMeta) => void) => () => void; - onUpdateReady: (fn: (versionMeta: UpdateMeta) => void) => () => void; - onDownloadProgress: (fn: (progress: number) => void) => () => void; -} - -export interface ApplicationMenuEvents { - onNewPageAction: (fn: () => void) => () => void; -} - -export interface DBEvents { - onExternalUpdate: ( - fn: (update: { - workspaceId: string; - update: Uint8Array; - docId?: string; - }) => void - ) => () => void; -} - -export interface WorkspaceEvents { - onMetaChange: ( - fn: (workspaceId: string, meta: WorkspaceMeta) => void - ) => () => void; -} - -export interface UIEvents { - onMaximized: (fn: (maximized: boolean) => void) => () => void; -} - -export interface EventMap { - updater: UpdaterEvents; - applicationMenu: ApplicationMenuEvents; - db: DBEvents; - ui: UIEvents; - workspace: WorkspaceEvents; -} diff --git a/packages/common/infra/vite.config.ts b/packages/common/infra/vite.config.ts index 6055b068f..27a9c1832 100644 --- a/packages/common/infra/vite.config.ts +++ b/packages/common/infra/vite.config.ts @@ -12,12 +12,8 @@ export default defineConfig({ lib: { entry: { blocksuite: resolve(root, 'src/blocksuite/index.ts'), - index: resolve(root, 'src/index.ts'), atom: resolve(root, 'src/atom/index.ts'), command: resolve(root, 'src/command/index.ts'), - type: resolve(root, 'src/type.ts'), - 'core/event-emitter': resolve(root, 'src/core/event-emitter.ts'), - 'preload/electron': resolve(root, 'src/preload/electron.ts'), 'app-config-storage': resolve(root, 'src/app-config-storage.ts'), }, formats: ['es', 'cjs'], diff --git a/packages/frontend/component/package.json b/packages/frontend/component/package.json index 95f6d1b2a..08b5e52ff 100644 --- a/packages/frontend/component/package.json +++ b/packages/frontend/component/package.json @@ -20,6 +20,7 @@ }, "dependencies": { "@affine/debug": "workspace:*", + "@affine/electron-api": "workspace:*", "@affine/graphql": "workspace:*", "@affine/i18n": "workspace:*", "@affine/workspace": "workspace:*", diff --git a/packages/frontend/component/src/components/theme-provider/index.tsx b/packages/frontend/component/src/components/theme-provider/index.tsx index 097bafd2b..1a4c1d9f5 100644 --- a/packages/frontend/component/src/components/theme-provider/index.tsx +++ b/packages/frontend/component/src/components/theme-provider/index.tsx @@ -1,3 +1,4 @@ +import { apis } from '@affine/electron-api'; import { ThemeProvider as NextThemeProvider, useTheme } from 'next-themes'; import type { PropsWithChildren } from 'react'; import { memo, useRef } from 'react'; @@ -10,7 +11,7 @@ const DesktopThemeSync = memo(function DesktopThemeSync() { const onceRef = useRef(false); if (lastThemeRef.current !== theme || !onceRef.current) { if (environment.isDesktop && theme) { - window.apis?.ui + apis?.ui .handleThemeChange(theme as 'dark' | 'light' | 'system') .catch(err => { console.error(err); diff --git a/packages/frontend/component/tsconfig.json b/packages/frontend/component/tsconfig.json index 9e16b6cab..ab884ed08 100644 --- a/packages/frontend/component/tsconfig.json +++ b/packages/frontend/component/tsconfig.json @@ -14,6 +14,9 @@ { "path": "../../frontend/hooks" }, + { + "path": "../../frontend/electron-api" + }, { "path": "../../frontend/workspace" }, { "path": "../../common/debug" diff --git a/packages/frontend/core/package.json b/packages/frontend/core/package.json index 215f7f47e..580f8041d 100644 --- a/packages/frontend/core/package.json +++ b/packages/frontend/core/package.json @@ -20,6 +20,7 @@ "@affine/cmdk": "workspace:*", "@affine/component": "workspace:*", "@affine/debug": "workspace:*", + "@affine/electron-api": "workspace:*", "@affine/env": "workspace:*", "@affine/graphql": "workspace:*", "@affine/i18n": "workspace:*", diff --git a/packages/frontend/core/src/commands/affine-updates.tsx b/packages/frontend/core/src/commands/affine-updates.tsx index da4e16791..39a409147 100644 --- a/packages/frontend/core/src/commands/affine-updates.tsx +++ b/packages/frontend/core/src/commands/affine-updates.tsx @@ -1,3 +1,4 @@ +import { apis } from '@affine/electron-api'; import type { useAFFiNEI18N } from '@affine/i18n/hooks'; import { ResetIcon } from '@blocksuite/icons'; import { updateReadyAtom } from '@toeverything/hooks/use-app-updater'; @@ -21,7 +22,7 @@ export function registerAffineUpdatesCommands({ label: t['com.affine.cmdk.affine.restart-to-upgrade'](), preconditionStrategy: () => !!store.get(updateReadyAtom), run() { - window.apis?.updater.quitAndInstall().catch(err => { + apis?.updater.quitAndInstall().catch(err => { // TODO: add error toast here console.error(err); }); diff --git a/packages/frontend/core/src/components/affine/auth/use-captcha.tsx b/packages/frontend/core/src/components/affine/auth/use-captcha.tsx index 28a764901..69fac37f9 100644 --- a/packages/frontend/core/src/components/affine/auth/use-captcha.tsx +++ b/packages/frontend/core/src/components/affine/auth/use-captcha.tsx @@ -1,3 +1,4 @@ +import { apis } from '@affine/electron-api'; import { fetchWithTraceReport } from '@affine/graphql'; import { Turnstile } from '@marsidev/react-turnstile'; import { atom, useAtom, useSetAtom } from 'jotai'; @@ -32,7 +33,7 @@ const generateChallengeResponse = async (challenge: string) => { return undefined; } - return await window.apis?.ui?.getChallengeResponse(challenge); + return await apis?.ui?.getChallengeResponse(challenge); }; const captchaAtom = atom(undefined); diff --git a/packages/frontend/core/src/components/affine/create-workspace-modal/index.tsx b/packages/frontend/core/src/components/affine/create-workspace-modal/index.tsx index 265ba388c..801898f1a 100644 --- a/packages/frontend/core/src/components/affine/create-workspace-modal/index.tsx +++ b/packages/frontend/core/src/components/affine/create-workspace-modal/index.tsx @@ -5,6 +5,7 @@ import { Modal, } from '@affine/component/ui/modal'; import { DebugLogger } from '@affine/debug'; +import { apis } from '@affine/electron-api'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { workspaceManagerAtom } from '@affine/workspace/atom'; @@ -14,7 +15,6 @@ import { buildShowcaseWorkspace, initEmptyPage, } from '@toeverything/infra/blocksuite'; -import type { LoadDBFileResult } from '@toeverything/infra/type'; import { useAtomValue } from 'jotai'; import type { KeyboardEvent } from 'react'; import { useLayoutEffect } from 'react'; @@ -112,12 +112,12 @@ export const CreateWorkspaceModal = ({ // after it is done, it will effectively add a new workspace to app-data folder // so after that, we will be able to load it via importLocalWorkspace (async () => { - if (!window.apis) { + if (!apis) { return; } logger.info('load db file'); setStep(undefined); - const result: LoadDBFileResult = await window.apis.dialog.loadDBFile(); + const result = await apis.dialog.loadDBFile(); if (result.workspaceId && !canceled) { workspaceManager._addLocalWorkspace(result.workspaceId); onCreate(result.workspaceId); diff --git a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/export.tsx b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/export.tsx index 36ba065f2..cdf7d0c54 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/export.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/export.tsx @@ -1,10 +1,10 @@ import { pushNotificationAtom } from '@affine/component/notification-center'; import { SettingRow } from '@affine/component/setting-components'; import { Button } from '@affine/component/ui/button'; +import { apis } from '@affine/electron-api'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { Workspace, WorkspaceMetadata } from '@affine/workspace'; import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks'; -import type { SaveDBFileResult } from '@toeverything/infra/type'; import { useSetAtom } from 'jotai'; import { useState } from 'react'; @@ -30,8 +30,7 @@ export const ExportPanel = ({ try { await workspace.engine.sync.waitForSynced(); await workspace.engine.blob.sync(); - const result: SaveDBFileResult = - await window.apis?.dialog.saveDBFileAs(workspaceId); + const result = await apis?.dialog.saveDBFileAs(workspaceId); if (result?.error) { throw new Error(result.error); } else if (!result?.canceled) { diff --git a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/storage.tsx b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/storage.tsx index 5a6e45322..3548a9e85 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/storage.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/storage.tsx @@ -2,17 +2,17 @@ import { FlexWrapper, toast } from '@affine/component'; import { SettingRow } from '@affine/component/setting-components'; import { Button } from '@affine/component/ui/button'; import { Tooltip } from '@affine/component/ui/tooltip'; +import { apis, events } from '@affine/electron-api'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { WorkspaceMetadata } from '@affine/workspace/metadata'; -import type { MoveDBFileResult } from '@toeverything/infra/type'; import { useMemo } from 'react'; import { useCallback, useEffect, useState } from 'react'; const useDBFileSecondaryPath = (workspaceId: string) => { const [path, setPath] = useState(undefined); useEffect(() => { - if (window.apis && window.events && environment.isDesktop) { - window.apis?.workspace + if (apis && events && environment.isDesktop) { + apis?.workspace .getMeta(workspaceId) .then(meta => { setPath(meta.secondaryDBPath); @@ -20,7 +20,7 @@ const useDBFileSecondaryPath = (workspaceId: string) => { .catch(err => { console.error(err); }); - return window.events.workspace.onMetaChange((newMeta: any) => { + return events.workspace.onMetaChange((newMeta: any) => { if (newMeta.workspaceId === workspaceId) { const meta = newMeta.meta; setPath(meta.secondaryDBPath); @@ -43,7 +43,7 @@ export const StoragePanel = ({ workspaceMetadata }: StoragePanelProps) => { const [moveToInProgress, setMoveToInProgress] = useState(false); const onRevealDBFile = useCallback(() => { - window.apis?.dialog.revealDBFile(workspaceId).catch(err => { + apis?.dialog.revealDBFile(workspaceId).catch(err => { console.error(err); }); }, [workspaceId]); @@ -53,9 +53,9 @@ export const StoragePanel = ({ workspaceMetadata }: StoragePanelProps) => { return; } setMoveToInProgress(true); - window.apis?.dialog + apis?.dialog .moveDBFile(workspaceId) - .then((result: MoveDBFileResult) => { + .then(result => { if (!result?.error && !result?.canceled) { toast(t['Move folder success']()); } else if (result?.error) { diff --git a/packages/frontend/core/src/components/pure/header/windows-app-controls.tsx b/packages/frontend/core/src/components/pure/header/windows-app-controls.tsx index 3e6ecaa4d..b1dd04261 100644 --- a/packages/frontend/core/src/components/pure/header/windows-app-controls.tsx +++ b/packages/frontend/core/src/components/pure/header/windows-app-controls.tsx @@ -1,3 +1,4 @@ +import { apis, events } from '@affine/electron-api'; import { useAtomValue } from 'jotai'; import { atomWithObservable } from 'jotai/utils'; import { useCallback } from 'react'; @@ -8,7 +9,7 @@ import * as style from './style.css'; const maximizedAtom = atomWithObservable(() => { return new Observable(subscriber => { subscriber.next(false); - return window.events?.ui.onMaximized(maximized => { + return events?.ui.onMaximized(maximized => { return subscriber.next(maximized); }); }); @@ -76,17 +77,17 @@ const unmaximizedSVG = ( export const WindowsAppControls = () => { const handleMinimizeApp = useCallback(() => { - window.apis?.ui.handleMinimizeApp().catch(err => { + apis?.ui.handleMinimizeApp().catch(err => { console.error(err); }); }, []); const handleMaximizeApp = useCallback(() => { - window.apis?.ui.handleMaximizeApp().catch(err => { + apis?.ui.handleMaximizeApp().catch(err => { console.error(err); }); }, []); const handleCloseApp = useCallback(() => { - window.apis?.ui.handleCloseApp().catch(err => { + apis?.ui.handleCloseApp().catch(err => { console.error(err); }); }, []); diff --git a/packages/frontend/core/src/components/root-app-sidebar/index.tsx b/packages/frontend/core/src/components/root-app-sidebar/index.tsx index 151e3181c..101c289e3 100644 --- a/packages/frontend/core/src/components/root-app-sidebar/index.tsx +++ b/packages/frontend/core/src/components/root-app-sidebar/index.tsx @@ -20,6 +20,7 @@ import { } from '@affine/component/page-list'; import { Menu } from '@affine/component/ui/menu'; import { collectionsCRUDAtom } from '@affine/core/atoms/collections'; +import { apis, events } from '@affine/electron-api'; import { WorkspaceSubPath } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { Workspace } from '@affine/workspace'; @@ -141,7 +142,7 @@ export const RootAppSidebar = ({ // Listen to the "New Page" action from the menu useEffect(() => { if (environment.isDesktop) { - return window.events?.applicationMenu.onNewPageAction(onClickNewPage); + return events?.applicationMenu.onNewPageAction(onClickNewPage); } return; }, [onClickNewPage]); @@ -149,7 +150,7 @@ export const RootAppSidebar = ({ const sidebarOpen = useAtomValue(appSidebarOpenAtom); useEffect(() => { if (environment.isDesktop) { - window.apis?.ui.handleSidebarVisibilityChange(sidebarOpen).catch(err => { + apis?.ui.handleSidebarVisibilityChange(sidebarOpen).catch(err => { console.error(err); }); } diff --git a/packages/frontend/core/src/hooks/affine/use-export-page.ts b/packages/frontend/core/src/hooks/affine/use-export-page.ts index ecaa42937..7ad1714c7 100644 --- a/packages/frontend/core/src/hooks/affine/use-export-page.ts +++ b/packages/frontend/core/src/hooks/affine/use-export-page.ts @@ -3,6 +3,7 @@ import { resolveGlobalLoadingEventAtom, } from '@affine/component/global-loading'; import { pushNotificationAtom } from '@affine/component/notification-center'; +import { apis } from '@affine/electron-api'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { HtmlTransformer, @@ -49,7 +50,7 @@ async function exportHandler({ page, type }: ExportHandlerOptions) { break; case 'pdf': if (environment.isDesktop && page.meta.mode === 'page') { - await window.apis?.export.savePDFFileAs( + await apis?.export.savePDFFileAs( (page.root as PageBlockModel).title.toString() ); } else { diff --git a/packages/frontend/core/src/hooks/use-app-config-storage.ts b/packages/frontend/core/src/hooks/use-app-config-storage.ts index 35b94fc29..6430a9b6d 100644 --- a/packages/frontend/core/src/hooks/use-app-config-storage.ts +++ b/packages/frontend/core/src/hooks/use-app-config-storage.ts @@ -1,3 +1,5 @@ +import { apis } from '@affine/electron-api'; +import { assertExists } from '@blocksuite/global/utils'; import { type AppConfigSchema, AppConfigStorage, @@ -13,11 +15,13 @@ class AppConfigProxy { value: AppConfigSchema = defaultAppConfig; async getSync(): Promise { - return (this.value = await window.apis.configStorage.get()); + assertExists(apis); + return (this.value = await apis.configStorage.get()); } async setSync(): Promise { - await window.apis.configStorage.set(this.value); + assertExists(apis); + await apis.configStorage.set(this.value); } get(): AppConfigSchema { diff --git a/packages/frontend/core/src/pages/onboarding.tsx b/packages/frontend/core/src/pages/onboarding.tsx index 1cdb21007..dc3b1cf80 100644 --- a/packages/frontend/core/src/pages/onboarding.tsx +++ b/packages/frontend/core/src/pages/onboarding.tsx @@ -1,3 +1,5 @@ +import { apis } from '@affine/electron-api'; +import { assertExists } from '@blocksuite/global/utils'; import { useCallback } from 'react'; import { redirect } from 'react-router-dom'; @@ -23,7 +25,8 @@ export const Component = () => { const openApp = useCallback(() => { if (environment.isDesktop) { - window.apis.ui.handleOpenMainApp().catch(err => { + assertExists(apis); + apis.ui.handleOpenMainApp().catch(err => { console.log('failed to open main app', err); }); } else { diff --git a/packages/frontend/core/tsconfig.json b/packages/frontend/core/tsconfig.json index c250ef7c0..c60824a29 100644 --- a/packages/frontend/core/tsconfig.json +++ b/packages/frontend/core/tsconfig.json @@ -23,6 +23,9 @@ { "path": "../../frontend/workspace" }, + { + "path": "../../frontend/electron-api" + }, { "path": "../../common/debug" }, diff --git a/packages/frontend/electron-api/package.json b/packages/frontend/electron-api/package.json new file mode 100644 index 000000000..678d84f28 --- /dev/null +++ b/packages/frontend/electron-api/package.json @@ -0,0 +1,14 @@ +{ + "name": "@affine/electron-api", + "version": "0.10.3-canary.2", + "type": "module", + "private": true, + "main": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "devDependencies": { + "@toeverything/infra": "workspace:*", + "electron": "^27.1.0" + } +} diff --git a/packages/frontend/electron-api/src/index.ts b/packages/frontend/electron-api/src/index.ts new file mode 100644 index 000000000..66a23ef78 --- /dev/null +++ b/packages/frontend/electron-api/src/index.ts @@ -0,0 +1,37 @@ +import type { + events as helperEvents, + handlers as helperHandlers, +} from '@affine/electron/helper/exposed'; +import type { + events as mainEvents, + handlers as mainHandlers, +} from '@affine/electron/main/exposed'; +import type { + affine as exposedAffineGlobal, + appInfo as exposedAppInfo, +} from '@affine/electron/preload/electron-api'; + +type MainHandlers = typeof mainHandlers; +type HelperHandlers = typeof helperHandlers; +type HelperEvents = typeof helperEvents; +type MainEvents = typeof mainEvents; +type ClientHandler = { + [namespace in keyof MainHandlers]: { + [method in keyof MainHandlers[namespace]]: MainHandlers[namespace][method] extends ( + arg0: any, + ...rest: infer A + ) => any + ? (...args: A) => Promise> + : never; + }; +} & HelperHandlers; +type ClientEvents = MainEvents & HelperEvents; + +export const appInfo = (window as any).appInfo as typeof exposedAppInfo | null; +export const apis = (window as any).apis as ClientHandler | null; +export const events = (window as any).events as ClientEvents | null; +export const affine = (window as any).affine as + | typeof exposedAffineGlobal + | null; + +export type { UpdateMeta } from '@affine/electron/main/updater/event'; diff --git a/packages/frontend/electron-api/tsconfig.json b/packages/frontend/electron-api/tsconfig.json new file mode 100644 index 000000000..638bfe244 --- /dev/null +++ b/packages/frontend/electron-api/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["./src"], + "compilerOptions": { + "composite": true, + "noEmit": false, + "outDir": "lib" + }, + "references": [ + { + "path": "../../common/infra" + }, + { + "path": "../../frontend/electron" + } + ] +} diff --git a/packages/frontend/electron/src/helper/dialog/dialog.ts b/packages/frontend/electron/src/helper/dialog/dialog.ts index 3c92d4118..c532556d7 100644 --- a/packages/frontend/electron/src/helper/dialog/dialog.ts +++ b/packages/frontend/electron/src/helper/dialog/dialog.ts @@ -2,13 +2,6 @@ import path from 'node:path'; import { ValidationResult } from '@affine/native'; import { WorkspaceVersion } from '@toeverything/infra/blocksuite'; -import type { - FakeDialogResult, - LoadDBFileResult, - MoveDBFileResult, - SaveDBFileResult, - SelectDBFileLocationResult, -} from '@toeverything/infra/type'; import fs from 'fs-extra'; import { nanoid } from 'nanoid'; @@ -28,6 +21,45 @@ import { getWorkspacesBasePath, } from '../workspace/meta'; +export type ErrorMessage = + | 'DB_FILE_ALREADY_LOADED' + | 'DB_FILE_PATH_INVALID' + | 'DB_FILE_INVALID' + | 'DB_FILE_MIGRATION_FAILED' + | 'FILE_ALREADY_EXISTS' + | 'UNKNOWN_ERROR'; + +export interface LoadDBFileResult { + workspaceId?: string; + error?: ErrorMessage; + canceled?: boolean; +} + +export interface SaveDBFileResult { + filePath?: string; + canceled?: boolean; + error?: ErrorMessage; +} + +export interface SelectDBFileLocationResult { + filePath?: string; + error?: ErrorMessage; + canceled?: boolean; +} + +export interface MoveDBFileResult { + filePath?: string; + error?: ErrorMessage; + canceled?: boolean; +} + +// provide a backdoor to set dialog path for testing in playwright +export interface FakeDialogResult { + canceled?: boolean; + filePath?: string; + filePaths?: string[]; +} + // NOTE: // we are using native dialogs because HTML dialogs do not give full file paths diff --git a/packages/frontend/electron/src/helper/exposed.ts b/packages/frontend/electron/src/helper/exposed.ts index 63ac85e39..e3adb3d46 100644 --- a/packages/frontend/electron/src/helper/exposed.ts +++ b/packages/frontend/electron/src/helper/exposed.ts @@ -1,25 +1,13 @@ -import type { - DBHandlers, - DialogHandlers, - WorkspaceHandlers, -} from '@toeverything/infra/type'; - import { dbEvents, dbHandlers } from './db'; import { dialogHandlers } from './dialog'; import { provideExposed } from './provide'; import { workspaceEvents, workspaceHandlers } from './workspace'; -type AllHandlers = { - db: DBHandlers; - workspace: WorkspaceHandlers; - dialog: DialogHandlers; -}; - export const handlers = { db: dbHandlers, workspace: workspaceHandlers, dialog: dialogHandlers, -} satisfies AllHandlers; +}; export const events = { db: dbEvents, diff --git a/packages/frontend/electron/src/helper/index.ts b/packages/frontend/electron/src/helper/index.ts index bdc862659..7fe622259 100644 --- a/packages/frontend/electron/src/helper/index.ts +++ b/packages/frontend/electron/src/helper/index.ts @@ -1,6 +1,6 @@ -import type { RendererToHelper } from '@toeverything/infra/preload/electron'; import { AsyncCall } from 'async-call-rpc'; +import type { RendererToHelper } from '../shared/type'; import { events, handlers } from './exposed'; import { logger } from './logger'; diff --git a/packages/frontend/electron/src/helper/main-rpc.ts b/packages/frontend/electron/src/helper/main-rpc.ts index 12e362336..64862e44c 100644 --- a/packages/frontend/electron/src/helper/main-rpc.ts +++ b/packages/frontend/electron/src/helper/main-rpc.ts @@ -1,10 +1,7 @@ import { assertExists } from '@blocksuite/global/utils'; -import type { - HelperToMain, - MainToHelper, -} from '@toeverything/infra/preload/electron'; import { AsyncCall } from 'async-call-rpc'; +import type { HelperToMain, MainToHelper } from '../shared/type'; import { exposed } from './provide'; const helperToMainServer: HelperToMain = { diff --git a/packages/frontend/electron/src/helper/provide.ts b/packages/frontend/electron/src/helper/provide.ts index 6c7b4e9b3..d508654cf 100644 --- a/packages/frontend/electron/src/helper/provide.ts +++ b/packages/frontend/electron/src/helper/provide.ts @@ -1,4 +1,4 @@ -import type { ExposedMeta } from '@toeverything/infra/preload/electron'; +import type { ExposedMeta } from '../shared/type'; /** * A naive DI implementation to get rid of circular dependency. diff --git a/packages/frontend/electron/src/main/handlers.ts b/packages/frontend/electron/src/main/handlers.ts index 985ce0946..c97bdafb9 100644 --- a/packages/frontend/electron/src/main/handlers.ts +++ b/packages/frontend/electron/src/main/handlers.ts @@ -1,12 +1,3 @@ -import type { - ClipboardHandlerManager, - ConfigStorageHandlerManager, - DebugHandlerManager, - ExportHandlerManager, - UIHandlerManager, - UnwrapManagerHandlerToServerSide, - UpdaterHandlerManager, -} from '@toeverything/infra/index'; import { ipcMain } from 'electron'; import { clipboardHandlers } from './clipboard'; @@ -25,33 +16,6 @@ export const debugHandlers = { }, }; -type AllHandlers = { - debug: UnwrapManagerHandlerToServerSide< - Electron.IpcMainInvokeEvent, - DebugHandlerManager - >; - clipboard: UnwrapManagerHandlerToServerSide< - Electron.IpcMainInvokeEvent, - ClipboardHandlerManager - >; - export: UnwrapManagerHandlerToServerSide< - Electron.IpcMainInvokeEvent, - ExportHandlerManager - >; - ui: UnwrapManagerHandlerToServerSide< - Electron.IpcMainInvokeEvent, - UIHandlerManager - >; - updater: UnwrapManagerHandlerToServerSide< - Electron.IpcMainInvokeEvent, - UpdaterHandlerManager - >; - configStorage: UnwrapManagerHandlerToServerSide< - Electron.IpcMainInvokeEvent, - ConfigStorageHandlerManager - >; -}; - // Note: all of these handlers will be the single-source-of-truth for the apis exposed to the renderer process export const allHandlers = { debug: debugHandlers, @@ -60,7 +24,7 @@ export const allHandlers = { export: exportHandlers, updater: updaterHandlers, configStorage: configStorageHandlers, -} satisfies AllHandlers; +}; export const registerHandlers = () => { // TODO: listen to namespace instead of individual event types diff --git a/packages/frontend/electron/src/main/helper-process.ts b/packages/frontend/electron/src/main/helper-process.ts index d2d3d05fc..2af2c3ef0 100644 --- a/packages/frontend/electron/src/main/helper-process.ts +++ b/packages/frontend/electron/src/main/helper-process.ts @@ -1,9 +1,5 @@ import path from 'node:path'; -import type { - HelperToMain, - MainToHelper, -} from '@toeverything/infra/preload/electron'; import { type _AsyncVersionOf, AsyncCall } from 'async-call-rpc'; import { app, @@ -15,6 +11,7 @@ import { type WebContents, } from 'electron'; +import type { HelperToMain, MainToHelper } from '../shared/type'; import { MessageEventChannel } from '../shared/utils'; import { logger } from './logger'; diff --git a/packages/frontend/electron/src/preload/bootstrap.ts b/packages/frontend/electron/src/preload/bootstrap.ts index 444d0deb4..d1c113751 100644 --- a/packages/frontend/electron/src/preload/bootstrap.ts +++ b/packages/frontend/electron/src/preload/bootstrap.ts @@ -1,57 +1,15 @@ -import { contextBridge, ipcRenderer } from 'electron'; +import { contextBridge } from 'electron'; -(async () => { - const { appInfo, getElectronAPIs } = await import( - '@toeverything/infra/preload/electron' - ); - const { apis, events } = getElectronAPIs(); +import { affine, appInfo, getElectronAPIs } from './electron-api'; - contextBridge.exposeInMainWorld('appInfo', appInfo); - contextBridge.exposeInMainWorld('apis', apis); - contextBridge.exposeInMainWorld('events', events); +const { apis, events } = getElectronAPIs(); - // Credit to microsoft/vscode - const globals = { - ipcRenderer: { - send(channel: string, ...args: any[]) { - ipcRenderer.send(channel, ...args); - }, +contextBridge.exposeInMainWorld('appInfo', appInfo); +contextBridge.exposeInMainWorld('apis', apis); +contextBridge.exposeInMainWorld('events', events); - invoke(channel: string, ...args: any[]) { - return ipcRenderer.invoke(channel, ...args); - }, - - on( - channel: string, - listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void - ) { - ipcRenderer.on(channel, listener); - return this; - }, - - once( - channel: string, - listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void - ) { - ipcRenderer.once(channel, listener); - return this; - }, - - removeListener( - channel: string, - listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void - ) { - ipcRenderer.removeListener(channel, listener); - return this; - }, - }, - }; - - try { - contextBridge.exposeInMainWorld('affine', globals); - } catch (error) { - console.error('Failed to expose affine APIs to window object!', error); - } -})().catch(err => { - console.error('Failed to bootstrap preload script!', err); -}); +try { + contextBridge.exposeInMainWorld('affine', affine); +} catch (error) { + console.error('Failed to expose affine APIs to window object!', error); +} diff --git a/packages/common/infra/src/preload/electron.ts b/packages/frontend/electron/src/preload/electron-api.ts similarity index 85% rename from packages/common/infra/src/preload/electron.ts rename to packages/frontend/electron/src/preload/electron-api.ts index 087141aaa..aaec001f6 100644 --- a/packages/common/infra/src/preload/electron.ts +++ b/packages/frontend/electron/src/preload/electron-api.ts @@ -1,37 +1,50 @@ // Please add modules to `external` in `rollupOptions` to avoid wrong bundling. import { AsyncCall, type EventBasedChannel } from 'async-call-rpc'; -import type { app, dialog, shell } from 'electron'; import { ipcRenderer } from 'electron'; import { Subject } from 'rxjs'; import { z } from 'zod'; -export interface ExposedMeta { - handlers: [string, string[]][]; - events: [string, string[]][]; -} +import type { + ExposedMeta, + HelperToRenderer, + RendererToHelper, +} from '../shared/type'; -// render <-> helper -export interface RendererToHelper { - postEvent: (channel: string, ...args: any[]) => void; -} +export const affine = { + ipcRenderer: { + send(channel: string, ...args: any[]) { + ipcRenderer.send(channel, ...args); + }, -export interface HelperToRenderer { - [key: string]: (...args: any[]) => Promise; -} + invoke(channel: string, ...args: any[]) { + return ipcRenderer.invoke(channel, ...args); + }, -// helper <-> main -export interface HelperToMain { - getMeta: () => ExposedMeta; -} + on( + channel: string, + listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void + ) { + ipcRenderer.on(channel, listener); + return this; + }, -export type MainToHelper = Pick< - typeof dialog & typeof shell & typeof app, - | 'showOpenDialog' - | 'showSaveDialog' - | 'openExternal' - | 'showItemInFolder' - | 'getPath' ->; + once( + channel: string, + listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void + ) { + ipcRenderer.once(channel, listener); + return this; + }, + + removeListener( + channel: string, + listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void + ) { + ipcRenderer.removeListener(channel, listener); + return this; + }, + }, +}; export function getElectronAPIs() { const mainAPIs = getMainAPIs(); diff --git a/packages/frontend/electron/src/shared/type.ts b/packages/frontend/electron/src/shared/type.ts new file mode 100644 index 000000000..0cf8db77e --- /dev/null +++ b/packages/frontend/electron/src/shared/type.ts @@ -0,0 +1,29 @@ +import type { app, dialog, shell } from 'electron'; + +export interface ExposedMeta { + handlers: [string, string[]][]; + events: [string, string[]][]; +} + +// render <-> helper +export interface RendererToHelper { + postEvent: (channel: string, ...args: any[]) => void; +} + +export interface HelperToRenderer { + [key: string]: (...args: any[]) => Promise; +} + +// helper <-> main +export interface HelperToMain { + getMeta: () => ExposedMeta; +} + +export type MainToHelper = Pick< + typeof dialog & typeof shell & typeof app, + | 'showOpenDialog' + | 'showSaveDialog' + | 'openExternal' + | 'showItemInFolder' + | 'getPath' +>; diff --git a/packages/frontend/hooks/package.json b/packages/frontend/hooks/package.json index d83466ab0..753e78187 100644 --- a/packages/frontend/hooks/package.json +++ b/packages/frontend/hooks/package.json @@ -18,6 +18,7 @@ }, "devDependencies": { "@affine/debug": "workspace:*", + "@affine/electron-api": "workspace:*", "@affine/env": "workspace:*", "@affine/workspace": "workspace:*", "@blocksuite/block-std": "0.11.0-nightly-202312220916-e3abcbb", diff --git a/packages/frontend/hooks/src/use-app-updater.ts b/packages/frontend/hooks/src/use-app-updater.ts index d4e28c1dc..f86758ee9 100644 --- a/packages/frontend/hooks/src/use-app-updater.ts +++ b/packages/frontend/hooks/src/use-app-updater.ts @@ -1,6 +1,6 @@ +import { apis, events, type UpdateMeta } from '@affine/electron-api'; import { isBrowser } from '@affine/env/constant'; import { appSettingAtom } from '@toeverything/infra/atom'; -import type { UpdateMeta } from '@toeverything/infra/type'; import { atom, useAtom, useAtomValue } from 'jotai'; import { atomWithObservable, atomWithStorage } from 'jotai/utils'; import { useCallback, useState } from 'react'; @@ -47,21 +47,21 @@ function rpcToObservable< // download complete, ready to install export const updateReadyAtom = atomWithObservable(() => { return rpcToObservable(null as UpdateMeta | null, { - event: window.events?.updater.onUpdateReady, + event: events?.updater.onUpdateReady, }); }); // update available, but not downloaded yet export const updateAvailableAtom = atomWithObservable(() => { return rpcToObservable(null as UpdateMeta | null, { - event: window.events?.updater.onUpdateAvailable, + event: events?.updater.onUpdateAvailable, }); }); // downloading new update export const downloadProgressAtom = atomWithObservable(() => { return rpcToObservable(null as number | null, { - event: window.events?.updater.onDownloadProgress, + event: events?.updater.onDownloadProgress, }); }); @@ -76,7 +76,7 @@ export const currentVersionAtom = atom(async () => { if (!isBrowser) { return null; } - const currentVersion = await window.apis?.updater.currentVersion(); + const currentVersion = await apis?.updater.currentVersion(); return currentVersion; }); @@ -121,7 +121,7 @@ export const useAppUpdater = () => { const quitAndInstall = useCallback(() => { if (updateReady) { setAppQuitting(true); - window.apis?.updater.quitAndInstall().catch(err => { + apis?.updater.quitAndInstall().catch(err => { // TODO: add error toast here console.error(err); }); @@ -134,7 +134,7 @@ export const useAppUpdater = () => { } setCheckingForUpdates(true); try { - const updateInfo = await window.apis?.updater.checkForUpdates(); + const updateInfo = await apis?.updater.checkForUpdates(); return updateInfo?.version ?? false; } catch (err) { console.error('Error checking for updates:', err); @@ -145,7 +145,7 @@ export const useAppUpdater = () => { }, [checkingForUpdates, setCheckingForUpdates]); const downloadUpdate = useCallback(() => { - window.apis?.updater.downloadUpdate().catch(err => { + apis?.updater.downloadUpdate().catch(err => { console.error('Error downloading update:', err); }); }, []); diff --git a/packages/frontend/hooks/tsconfig.json b/packages/frontend/hooks/tsconfig.json index 6351cba59..4f7367530 100644 --- a/packages/frontend/hooks/tsconfig.json +++ b/packages/frontend/hooks/tsconfig.json @@ -11,6 +11,7 @@ { "path": "../../common/y-indexeddb" }, { "path": "../../common/debug" }, { "path": "../../common/infra" }, + { "path": "../electron-api" }, { "path": "../workspace" } ] } diff --git a/packages/frontend/workspace/package.json b/packages/frontend/workspace/package.json index 51266ac30..81f4af2e4 100644 --- a/packages/frontend/workspace/package.json +++ b/packages/frontend/workspace/package.json @@ -14,6 +14,7 @@ "dependencies": { "@affine-test/fixtures": "workspace:*", "@affine/debug": "workspace:*", + "@affine/electron-api": "workspace:*", "@affine/env": "workspace:*", "@affine/graphql": "workspace:*", "@toeverything/infra": "workspace:*", diff --git a/packages/frontend/workspace/src/impl/local/blob-sqlite.ts b/packages/frontend/workspace/src/impl/local/blob-sqlite.ts index 118893613..07bb5ac82 100644 --- a/packages/frontend/workspace/src/impl/local/blob-sqlite.ts +++ b/packages/frontend/workspace/src/impl/local/blob-sqlite.ts @@ -1,15 +1,16 @@ +import { apis } from '@affine/electron-api'; import { assertExists } from '@blocksuite/global/utils'; import type { BlobStorage } from '../../engine/blob'; import { bufferToBlob } from '../../utils/buffer-to-blob'; export const createSQLiteBlobStorage = (workspaceId: string): BlobStorage => { - const apis = window.apis; assertExists(apis); return { name: 'sqlite', readonly: false, get: async (key: string) => { + assertExists(apis); const buffer = await apis.db.getBlob(workspaceId, key); if (buffer) { return bufferToBlob(buffer); @@ -17,6 +18,7 @@ export const createSQLiteBlobStorage = (workspaceId: string): BlobStorage => { return null; }, set: async (key: string, value: Blob) => { + assertExists(apis); await apis.db.addBlob( workspaceId, key, @@ -25,9 +27,11 @@ export const createSQLiteBlobStorage = (workspaceId: string): BlobStorage => { return key; }, delete: async (key: string) => { + assertExists(apis); return apis.db.deleteBlob(workspaceId, key); }, list: async () => { + assertExists(apis); return apis.db.getBlobKeys(workspaceId); }, }; diff --git a/packages/frontend/workspace/src/impl/local/list.ts b/packages/frontend/workspace/src/impl/local/list.ts index afe3088bc..5d7fa6943 100644 --- a/packages/frontend/workspace/src/impl/local/list.ts +++ b/packages/frontend/workspace/src/impl/local/list.ts @@ -1,3 +1,4 @@ +import { apis } from '@affine/electron-api'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; import { difference } from 'lodash-es'; @@ -96,8 +97,8 @@ export function createLocalWorkspaceListProvider(): WorkspaceListProvider { JSON.stringify(allWorkspaceIDs.filter(x => x !== workspaceId)) ); - if (window.apis && environment.isDesktop) { - await window.apis.workspace.delete(workspaceId); + if (apis && environment.isDesktop) { + await apis.workspace.delete(workspaceId); } // notify all browser tabs, so they can update their workspace list diff --git a/packages/frontend/workspace/src/impl/local/sync-sqlite.ts b/packages/frontend/workspace/src/impl/local/sync-sqlite.ts index 1f103e577..8472e4b9f 100644 --- a/packages/frontend/workspace/src/impl/local/sync-sqlite.ts +++ b/packages/frontend/workspace/src/impl/local/sync-sqlite.ts @@ -1,16 +1,20 @@ +import { apis } from '@affine/electron-api'; import { encodeStateVectorFromUpdate } from 'yjs'; import type { SyncStorage } from '../../engine/sync'; export function createSQLiteStorage(workspaceId: string): SyncStorage { - if (!window.apis?.db) { + if (!apis?.db) { throw new Error('sqlite datasource is not available'); } return { name: 'sqlite', async pull(docId, _state) { - const update = await window.apis.db.getDocAsUpdates( + if (!apis?.db) { + throw new Error('sqlite datasource is not available'); + } + const update = await apis.db.getDocAsUpdates( workspaceId, workspaceId === docId ? undefined : docId ); @@ -25,7 +29,10 @@ export function createSQLiteStorage(workspaceId: string): SyncStorage { return null; }, async push(docId, data) { - return window.apis.db.applyDocUpdate( + if (!apis?.db) { + throw new Error('sqlite datasource is not available'); + } + return apis.db.applyDocUpdate( workspaceId, data, workspaceId === docId ? undefined : docId diff --git a/packages/frontend/workspace/tsconfig.json b/packages/frontend/workspace/tsconfig.json index dc07b1050..15af0a098 100644 --- a/packages/frontend/workspace/tsconfig.json +++ b/packages/frontend/workspace/tsconfig.json @@ -10,6 +10,7 @@ { "path": "../../common/env" }, { "path": "../../common/debug" }, { "path": "../../common/infra" }, - { "path": "../../frontend/graphql" } + { "path": "../../frontend/graphql" }, + { "path": "../../frontend/electron-api" } ] } diff --git a/tests/affine-desktop/e2e/workspace.spec.ts b/tests/affine-desktop/e2e/workspace.spec.ts index 920e600b7..8e4ba69fa 100644 --- a/tests/affine-desktop/e2e/workspace.spec.ts +++ b/tests/affine-desktop/e2e/workspace.spec.ts @@ -1,10 +1,17 @@ import path from 'node:path'; +import type { apis } from '@affine/electron-api'; import { test } from '@affine-test/kit/electron'; import { clickSideBarCurrentWorkspaceBanner } from '@affine-test/kit/utils/sidebar'; import { expect } from '@playwright/test'; import fs from 'fs-extra'; +declare global { + interface Window { + apis: typeof apis; + } +} + test('check workspace has a DB file', async ({ appInfo, workspace }) => { const w = await workspace.current(); const dbPath = path.join( diff --git a/tests/affine-desktop/package.json b/tests/affine-desktop/package.json index 4d07a23ae..8b8cd1f7b 100644 --- a/tests/affine-desktop/package.json +++ b/tests/affine-desktop/package.json @@ -7,6 +7,7 @@ "devDependencies": { "@affine-test/fixtures": "workspace:*", "@affine-test/kit": "workspace:*", + "@affine/electron-api": "workspace:*", "@playwright/test": "^1.39.0", "@types/fs-extra": "^11.0.2", "fs-extra": "^11.1.1", diff --git a/tests/affine-desktop/tsconfig.json b/tests/affine-desktop/tsconfig.json index 828052ad7..4eb24f6b2 100644 --- a/tests/affine-desktop/tsconfig.json +++ b/tests/affine-desktop/tsconfig.json @@ -11,6 +11,9 @@ }, { "path": "../../tests/fixtures" + }, + { + "path": "../../packages/frontend/electron-api" } ] } diff --git a/tests/kit/package.json b/tests/kit/package.json index 39125bcb5..fd570c505 100644 --- a/tests/kit/package.json +++ b/tests/kit/package.json @@ -10,6 +10,7 @@ "./e2e-enhance/*": "./e2e-enhance/*.ts" }, "devDependencies": { + "@affine/electron-api": "workspace:*", "@node-rs/argon2": "^1.5.2", "@playwright/test": "^1.39.0", "express": "^4.18.2", diff --git a/tests/kit/utils/ipc.ts b/tests/kit/utils/ipc.ts index 1c03168c8..27a1f0961 100644 --- a/tests/kit/utils/ipc.ts +++ b/tests/kit/utils/ipc.ts @@ -1,11 +1,18 @@ +import type { affine } from '@affine/electron-api'; // Credit: https://github.com/spaceagetv/electron-playwright-helpers/blob/main/src/ipc_helpers.ts import type { Page } from '@playwright/test'; import type { ElectronApplication } from 'playwright'; +declare global { + interface Window { + affine: typeof affine; + } +} + export function ipcRendererInvoke(page: Page, channel: string, ...args: any[]) { return page.evaluate( ({ channel, args }) => { - return window.affine.ipcRenderer.invoke(channel, ...args); + return window.affine?.ipcRenderer.invoke(channel, ...args); }, { channel, args } ); @@ -14,7 +21,7 @@ export function ipcRendererInvoke(page: Page, channel: string, ...args: any[]) { export function ipcRendererSend(page: Page, channel: string, ...args: any[]) { return page.evaluate( ({ channel, args }) => { - window.affine.ipcRenderer.send(channel, ...args); + window.affine?.ipcRenderer.send(channel, ...args); }, { channel, args } ); diff --git a/tools/@types/env/__all.d.ts b/tools/@types/env/__all.d.ts index 7e75bf080..5722295cd 100644 --- a/tools/@types/env/__all.d.ts +++ b/tools/@types/env/__all.d.ts @@ -1,53 +1,6 @@ import type { Environment, RuntimeConfig } from '@affine/env/global'; -import type { - ConfigStorageHandlerManager, - DBHandlerManager, - DebugHandlerManager, - DialogHandlerManager, - EventMap, - ExportHandlerManager, - UIHandlerManager, - UnwrapManagerHandlerToClientSide, - UpdaterHandlerManager, - WorkspaceHandlerManager, -} from '@toeverything/infra/index'; declare global { - interface Window { - appInfo: { - electron: boolean; - }; - apis: { - db: UnwrapManagerHandlerToClientSide; - debug: UnwrapManagerHandlerToClientSide; - dialog: UnwrapManagerHandlerToClientSide; - export: UnwrapManagerHandlerToClientSide; - ui: UnwrapManagerHandlerToClientSide; - updater: UnwrapManagerHandlerToClientSide; - workspace: UnwrapManagerHandlerToClientSide; - configStorage: UnwrapManagerHandlerToClientSide; - }; - events: EventMap; - affine: { - ipcRenderer: { - send(channel: string, ...args: any[]): void; - invoke(channel: string, ...args: any[]): Promise; - on( - channel: string, - listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void - ): this; - once( - channel: string, - listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void - ): this; - removeListener( - channel: string, - listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void - ): this; - }; - }; - } - // eslint-disable-next-line no-var var process: { env: Record; diff --git a/tools/@types/env/package.json b/tools/@types/env/package.json index 0fdd4c9e2..0c1a7cdaa 100644 --- a/tools/@types/env/package.json +++ b/tools/@types/env/package.json @@ -4,8 +4,7 @@ "types": "./__all.d.ts", "type": "module", "dependencies": { - "@affine/env": "workspace:*", - "@toeverything/infra": "workspace:*" + "@affine/env": "workspace:*" }, "version": "0.11.0" } diff --git a/yarn.lock b/yarn.lock index 671af0459..e2bd2cd02 100644 --- a/yarn.lock +++ b/yarn.lock @@ -115,6 +115,7 @@ __metadata: dependencies: "@affine-test/fixtures": "workspace:*" "@affine-test/kit": "workspace:*" + "@affine/electron-api": "workspace:*" "@playwright/test": "npm:^1.39.0" "@types/fs-extra": "npm:^11.0.2" fs-extra: "npm:^11.1.1" @@ -156,6 +157,7 @@ __metadata: version: 0.0.0-use.local resolution: "@affine-test/kit@workspace:tests/kit" dependencies: + "@affine/electron-api": "workspace:*" "@node-rs/argon2": "npm:^1.5.2" "@playwright/test": "npm:^1.39.0" express: "npm:^4.18.2" @@ -209,6 +211,7 @@ __metadata: resolution: "@affine/component@workspace:packages/frontend/component" dependencies: "@affine/debug": "workspace:*" + "@affine/electron-api": "workspace:*" "@affine/graphql": "workspace:*" "@affine/i18n": "workspace:*" "@affine/workspace": "workspace:*" @@ -309,6 +312,7 @@ __metadata: "@affine/cmdk": "workspace:*" "@affine/component": "workspace:*" "@affine/debug": "workspace:*" + "@affine/electron-api": "workspace:*" "@affine/env": "workspace:*" "@affine/graphql": "workspace:*" "@affine/i18n": "workspace:*" @@ -418,6 +422,15 @@ __metadata: languageName: unknown linkType: soft +"@affine/electron-api@workspace:*, @affine/electron-api@workspace:packages/frontend/electron-api": + version: 0.0.0-use.local + resolution: "@affine/electron-api@workspace:packages/frontend/electron-api" + dependencies: + "@toeverything/infra": "workspace:*" + electron: "npm:^27.1.0" + languageName: unknown + linkType: soft + "@affine/electron@workspace:packages/frontend/electron": version: 0.0.0-use.local resolution: "@affine/electron@workspace:packages/frontend/electron" @@ -788,6 +801,7 @@ __metadata: dependencies: "@affine-test/fixtures": "workspace:*" "@affine/debug": "workspace:*" + "@affine/electron-api": "workspace:*" "@affine/env": "workspace:*" "@affine/graphql": "workspace:*" "@testing-library/react": "npm:^14.0.0" @@ -13438,6 +13452,7 @@ __metadata: resolution: "@toeverything/hooks@workspace:packages/frontend/hooks" dependencies: "@affine/debug": "workspace:*" + "@affine/electron-api": "workspace:*" "@affine/env": "workspace:*" "@affine/workspace": "workspace:*" "@blocksuite/block-std": "npm:0.11.0-nightly-202312220916-e3abcbb" @@ -13646,7 +13661,6 @@ __metadata: resolution: "@types/affine__env@workspace:tools/@types/env" dependencies: "@affine/env": "workspace:*" - "@toeverything/infra": "workspace:*" languageName: unknown linkType: soft