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