diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index b3fd946c4b..a2623f0114 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -113,3 +113,6 @@ runs: shell: bash if: inputs.playwright-install == 'true' && steps.playwright-cache.outputs.cache-hit != 'true' run: yarn playwright install --with-deps + - name: Build Infra + shell: bash + run: yarn build:infra diff --git a/apps/electron/layers/main/src/index.ts b/apps/electron/layers/main/src/index.ts index e603c73d5c..d54c84d72a 100644 --- a/apps/electron/layers/main/src/index.ts +++ b/apps/electron/layers/main/src/index.ts @@ -7,6 +7,7 @@ import { registerEvents } from './events'; import { registerHandlers } from './handlers'; import { logger } from './logger'; import { restoreOrCreateWindow } from './main-window'; +import { registerPlugin } from './plugin'; import { registerProtocol } from './protocol'; import { registerUpdater } from './updater'; @@ -60,6 +61,7 @@ app .then(registerProtocol) .then(registerHandlers) .then(registerEvents) + .then(registerPlugin) .then(restoreOrCreateWindow) .then(createApplicationMenu) .then(registerUpdater) diff --git a/apps/electron/layers/main/src/plugin.ts b/apps/electron/layers/main/src/plugin.ts new file mode 100644 index 0000000000..e136a6e827 --- /dev/null +++ b/apps/electron/layers/main/src/plugin.ts @@ -0,0 +1,57 @@ +import { join } from 'node:path'; +import { Worker } from 'node:worker_threads'; + +import { AsyncCall } from 'async-call-rpc'; +import { ipcMain } from 'electron'; + +import { ThreadWorkerChannel } from './utils'; + +declare global { + // fixme(himself65): + // remove this when bookmark block plugin is migrated to plugin-infra + // eslint-disable-next-line no-var + var asyncCall: Record PromiseLike>; +} + +export async function registerPlugin() { + const pluginWorkerPath = join(__dirname, './workers/plugin.worker.js'); + const asyncCall = AsyncCall< + Record PromiseLike> + >( + {}, + { + channel: new ThreadWorkerChannel(new Worker(pluginWorkerPath)), + } + ); + globalThis.asyncCall = asyncCall; + await import('@toeverything/plugin-infra/manager').then( + ({ rootStore, affinePluginsAtom }) => { + const bookmarkPluginPath = join( + process.env.PLUGIN_DIR ?? '../../plugins', + './bookmark-block/index.mjs' + ); + import(bookmarkPluginPath); + let dispose: () => void = () => { + // noop + }; + rootStore.sub(affinePluginsAtom, () => { + dispose(); + const plugins = rootStore.get(affinePluginsAtom); + Object.values(plugins).forEach(plugin => { + plugin.definition.commands.forEach(command => { + ipcMain.handle(command, (event, ...args) => + asyncCall[command](...args) + ); + }); + }); + dispose = () => { + Object.values(plugins).forEach(plugin => { + plugin.definition.commands.forEach(command => { + ipcMain.removeHandler(command); + }); + }); + }; + }); + } + ); +} diff --git a/apps/electron/layers/main/src/ui/index.ts b/apps/electron/layers/main/src/ui/index.ts index a275c52320..fab6741872 100644 --- a/apps/electron/layers/main/src/ui/index.ts +++ b/apps/electron/layers/main/src/ui/index.ts @@ -1,17 +1,9 @@ -import { join } from 'node:path'; - import { app, BrowserWindow, nativeTheme } from 'electron'; import type { NamespaceHandlers } from '../type'; import { isMacOS } from '../utils'; import { getGoogleOauthCode } from './google-auth'; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const handlers = require(join( - process.env.PLUGIN_DIR ?? '../../plugins', - './bookmark-block/server' -)).default as NamespaceHandlers; - export const uiHandlers = { handleThemeChange: async (_, theme: (typeof nativeTheme)['themeSource']) => { nativeTheme.themeSource = theme; @@ -47,5 +39,12 @@ export const uiHandlers = { getGoogleOauthCode: async () => { return getGoogleOauthCode(); }, - ...handlers, + /** + * @deprecated Remove this when bookmark block plugin is migrated to plugin-infra + */ + getBookmarkDataByLink: async (_, link: string) => { + return globalThis.asyncCall[ + 'com.blocksuite.bookmark-block.get-bookmark-data-by-link' + ](link); + }, } satisfies NamespaceHandlers; diff --git a/apps/electron/layers/main/src/utils.ts b/apps/electron/layers/main/src/utils.ts index f2ac392fc4..2be415fc6c 100644 --- a/apps/electron/layers/main/src/utils.ts +++ b/apps/electron/layers/main/src/utils.ts @@ -1,3 +1,7 @@ +import type { MessagePort, Worker } from 'node:worker_threads'; + +import type { EventBasedChannel } from 'async-call-rpc'; + export function getTime() { return new Date().getTime(); } @@ -9,3 +13,33 @@ export const isMacOS = () => { export const isWindows = () => { return process.platform === 'win32'; }; + +export class ThreadWorkerChannel implements EventBasedChannel { + constructor(private worker: Worker) {} + + on(listener: (data: unknown) => void) { + this.worker.addListener('message', listener); + return () => { + this.worker.removeListener('message', listener); + }; + } + + send(data: unknown) { + this.worker.postMessage(data); + } +} + +export class MessagePortChannel implements EventBasedChannel { + constructor(private port: MessagePort) {} + + on(listener: (data: unknown) => void) { + this.port.addListener('message', listener); + return () => { + this.port.removeListener('message', listener); + }; + } + + send(data: unknown) { + this.port.postMessage(data); + } +} diff --git a/apps/electron/layers/main/src/workers/plugin.worker.ts b/apps/electron/layers/main/src/workers/plugin.worker.ts new file mode 100644 index 0000000000..be8f4fccb8 --- /dev/null +++ b/apps/electron/layers/main/src/workers/plugin.worker.ts @@ -0,0 +1,43 @@ +import { join } from 'node:path'; +import { parentPort } from 'node:worker_threads'; + +import { AsyncCall } from 'async-call-rpc'; + +import { MessagePortChannel } from '../utils'; + +const commandProxy: Record Promise> = {}; + +if (!parentPort) { + throw new Error('parentPort is undefined'); +} + +AsyncCall(commandProxy, { + channel: new MessagePortChannel(parentPort), +}); + +import('@toeverything/plugin-infra/manager').then( + ({ rootStore, affinePluginsAtom }) => { + const bookmarkPluginPath = join( + process.env.PLUGIN_DIR ?? '../../../plugins', + './bookmark-block/index.mjs' + ); + + import(bookmarkPluginPath); + rootStore.sub(affinePluginsAtom, () => { + const plugins = rootStore.get(affinePluginsAtom); + Object.values(plugins).forEach(plugin => { + if (plugin.serverAdapter) { + plugin.serverAdapter({ + registerCommand: (command, fn) => { + console.log('register command', command); + commandProxy[command] = fn; + }, + unregisterCommand: command => { + delete commandProxy[command]; + }, + }); + } + }); + }); + } +); diff --git a/apps/electron/layers/preload/src/bootstrap.ts b/apps/electron/layers/preload/src/bootstrap.ts index fc4e364db7..6eaabff6bd 100644 --- a/apps/electron/layers/preload/src/bootstrap.ts +++ b/apps/electron/layers/preload/src/bootstrap.ts @@ -7,36 +7,21 @@ import { contextBridge, ipcRenderer } from 'electron'; contextBridge.exposeInMainWorld('appInfo', affineApis.appInfo); // Credit to microsoft/vscode - function validateIPC(channel: string) { - if (!channel || !channel.startsWith('affine:')) { - throw new Error(`Unsupported event IPC channel '${channel}'`); - } - - return true; - } - const globals = { ipcRenderer: { send(channel: string, ...args: any[]) { - if (validateIPC(channel)) { - ipcRenderer.send(channel, ...args); - } + ipcRenderer.send(channel, ...args); }, invoke(channel: string, ...args: any[]) { - if (validateIPC(channel)) { - return ipcRenderer.invoke(channel, ...args); - } - return void 0; + return ipcRenderer.invoke(channel, ...args); }, on( channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void ) { - if (validateIPC(channel)) { - ipcRenderer.on(channel, listener); - } + ipcRenderer.on(channel, listener); return this; }, @@ -44,9 +29,7 @@ import { contextBridge, ipcRenderer } from 'electron'; channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void ) { - if (validateIPC(channel)) { - ipcRenderer.once(channel, listener); - } + ipcRenderer.once(channel, listener); return this; }, @@ -54,9 +37,7 @@ import { contextBridge, ipcRenderer } from 'electron'; channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void ) { - if (validateIPC(channel)) { - ipcRenderer.removeListener(channel, listener); - } + ipcRenderer.removeListener(channel, listener); return this; }, }, @@ -65,6 +46,6 @@ import { contextBridge, ipcRenderer } from 'electron'; try { contextBridge.exposeInMainWorld('affine', globals); } catch (error) { - console.error(error); + console.error('Failed to expose affine APIs to window object!', error); } })(); diff --git a/apps/electron/package.json b/apps/electron/package.json index 3cfcde53b2..8b5014cc62 100644 --- a/apps/electron/package.json +++ b/apps/electron/package.json @@ -55,6 +55,8 @@ "zx": "^7.2.2" }, "dependencies": { + "@toeverything/plugin-infra": "workspace:*", + "async-call-rpc": "^6.3.1", "cheerio": "^1.0.0-rc.12", "electron-updater": "^5.3.0", "lodash-es": "^4.17.21", diff --git a/apps/electron/scripts/build-layers.mjs b/apps/electron/scripts/build-layers.mjs index bbd40747a3..3ee618f7d4 100644 --- a/apps/electron/scripts/build-layers.mjs +++ b/apps/electron/scripts/build-layers.mjs @@ -1,9 +1,12 @@ #!/usr/bin/env zx import 'zx/globals'; +import { resolve } from 'node:path'; + +import { spawnSync } from 'child_process'; import * as esbuild from 'esbuild'; -import { config } from './common.mjs'; +import { config, rootDir } from './common.mjs'; const NODE_ENV = process.env.NODE_ENV === 'development' ? 'development' : 'production'; @@ -16,6 +19,11 @@ if (process.platform === 'win32') { async function buildLayers() { const common = config(); await esbuild.build(common.preload); + console.log('Build plugin infra'); + spawnSync('yarn', ['build'], { + stdio: 'inherit', + cwd: resolve(rootDir, './packages/plugin-infra'), + }); console.log('Build plugins'); await import('./plugins/build-plugins.mjs'); diff --git a/apps/electron/scripts/common.mjs b/apps/electron/scripts/common.mjs index ef71914392..66e357ab68 100644 --- a/apps/electron/scripts/common.mjs +++ b/apps/electron/scripts/common.mjs @@ -41,12 +41,20 @@ export const config = () => { electronDir, './layers/main/src/workers/merge-update.worker.ts' ), + resolve(electronDir, './layers/main/src/workers/plugin.worker.ts'), ], outdir: resolve(electronDir, './dist/layers/main'), bundle: true, target: `node${NODE_MAJOR_VERSION}`, platform: 'node', - external: ['electron', 'yjs', 'electron-updater', '@toeverything/infra'], + external: [ + 'electron', + 'yjs', + 'better-sqlite3', + 'electron-updater', + '@toeverything/plugin-infra', + 'async-call-rpc', + ], define: define, format: 'cjs', loader: { diff --git a/apps/electron/scripts/dev.mjs b/apps/electron/scripts/dev.mjs index d7971b9757..c92ce5f9c6 100644 --- a/apps/electron/scripts/dev.mjs +++ b/apps/electron/scripts/dev.mjs @@ -1,12 +1,12 @@ /* eslint-disable no-async-promise-executor */ import { spawn } from 'node:child_process'; import { readFileSync } from 'node:fs'; -import path from 'node:path'; +import path, { resolve } from 'node:path'; import electronPath from 'electron'; import * as esbuild from 'esbuild'; -import { config, electronDir } from './common.mjs'; +import { config, electronDir, rootDir } from './common.mjs'; // this means we don't spawn electron windows, mainly for testing const watchMode = process.argv.includes('--watch'); @@ -69,6 +69,10 @@ function spawnOrReloadElectron() { const common = config(); async function watchPlugins() { + spawn('yarn', ['dev'], { + stdio: 'inherit', + cwd: resolve(rootDir, './packages/plugin-infra'), + }); await import('./plugins/dev-plugins.mjs'); } diff --git a/apps/electron/scripts/plugins/build-plugins.mjs b/apps/electron/scripts/plugins/build-plugins.mjs index 60ea6138ee..4c73377a4e 100755 --- a/apps/electron/scripts/plugins/build-plugins.mjs +++ b/apps/electron/scripts/plugins/build-plugins.mjs @@ -5,5 +5,16 @@ import { definePluginServerConfig } from './utils.mjs'; await build({ ...definePluginServerConfig('bookmark-block'), - external: ['cheerio', 'electron', 'puppeteer', 'foxact'], + external: [ + // server.ts + 'link-preview-js', + // ui.ts + '@toeverything/plugin-infra', + '@affine/component', + '@blocksuite/store', + '@blocksuite/blocks', + 'react', + 'react-dom', + 'foxact', + ], }); diff --git a/apps/electron/scripts/plugins/dev-plugins.mjs b/apps/electron/scripts/plugins/dev-plugins.mjs index 6f4165d1dc..e8159757e8 100755 --- a/apps/electron/scripts/plugins/dev-plugins.mjs +++ b/apps/electron/scripts/plugins/dev-plugins.mjs @@ -5,7 +5,18 @@ import { definePluginServerConfig } from './utils.mjs'; const plugin = await context({ ...definePluginServerConfig('bookmark-block'), - external: ['cheerio', 'electron', 'puppeteer', 'foxact'], + external: [ + // server.ts + 'link-preview-js', + // ui.ts + '@toeverything/plugin-infra', + '@affine/component', + '@blocksuite/store', + '@blocksuite/blocks', + 'react', + 'react-dom', + 'foxact', + ], }); await plugin.watch(); diff --git a/apps/electron/scripts/plugins/utils.mjs b/apps/electron/scripts/plugins/utils.mjs index 7ab0ef6f20..258f8ccb57 100644 --- a/apps/electron/scripts/plugins/utils.mjs +++ b/apps/electron/scripts/plugins/utils.mjs @@ -18,12 +18,17 @@ export const pluginDir = resolve(rootDir, 'plugins'); */ export function definePluginServerConfig(pluginDirName) { const pluginRootDir = resolve(pluginDir, pluginDirName); - const serverEntryFile = resolve(pluginRootDir, 'src/server.ts'); + const mainEntryFile = resolve(pluginRootDir, 'src/index.ts'); const serverOutputDir = resolve(electronOutputDir, pluginDirName); return { - entryPoints: [serverEntryFile], - platform: 'node', + entryPoints: [mainEntryFile], + platform: 'neutral', + format: 'esm', + outExtension: { + '.js': '.mjs', + }, outdir: serverOutputDir, bundle: true, + splitting: true, }; } diff --git a/apps/electron/tsconfig.json b/apps/electron/tsconfig.json index 91ae6de0dc..b14dbe19bf 100644 --- a/apps/electron/tsconfig.json +++ b/apps/electron/tsconfig.json @@ -17,10 +17,7 @@ "exclude": ["node_modules", "out", "dist"], "references": [ { - "path": "./tsconfig.node.json" - }, - { - "path": "./tests/tsconfig.json" + "path": "../../packages/plugin-infra" }, { "path": "../../packages/native" @@ -28,6 +25,14 @@ { "path": "../../packages/infra" }, + + // Tests + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tests/tsconfig.json" + }, { "path": "../../tests/kit" } ], "ts-node": { diff --git a/apps/electron/tsconfig.node.json b/apps/electron/tsconfig.node.json index caf1aab7f6..2a4b820fbd 100644 --- a/apps/electron/tsconfig.node.json +++ b/apps/electron/tsconfig.node.json @@ -1,5 +1,5 @@ { - "extends": "./tsconfig.json", + "extends": "../../tsconfig.json", "compilerOptions": { "composite": true, "target": "ESNext", diff --git a/apps/web/src/adapters/affine/fetcher.ts b/apps/web/src/adapters/affine/fetcher.ts index 5db959f90e..afc9ee8615 100644 --- a/apps/web/src/adapters/affine/fetcher.ts +++ b/apps/web/src/adapters/affine/fetcher.ts @@ -2,10 +2,10 @@ import { Unreachable } from '@affine/env/constant'; import type { AffineLegacyCloudWorkspace } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { affineApis } from '@affine/workspace/affine/shared'; -import { rootStore } from '@affine/workspace/atom'; import { createAffineProviders } from '@affine/workspace/providers'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { assertExists } from '@blocksuite/store'; +import { rootStore } from '@toeverything/plugin-infra/manager'; import { workspacesAtom } from '../../atoms'; diff --git a/apps/web/src/adapters/affine/index.tsx b/apps/web/src/adapters/affine/index.tsx index 92d55c3611..f2d4aaa141 100644 --- a/apps/web/src/adapters/affine/index.tsx +++ b/apps/web/src/adapters/affine/index.tsx @@ -21,7 +21,7 @@ import { SignMethod, } from '@affine/workspace/affine/login'; import { affineApis, affineAuth } from '@affine/workspace/affine/shared'; -import { rootStore, rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; +import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { createAffineProviders, createIndexedDBBackgroundProvider, @@ -31,6 +31,7 @@ import { cleanupWorkspace, createEmptyBlockSuiteWorkspace, } from '@affine/workspace/utils'; +import { rootStore } from '@toeverything/plugin-infra/manager'; import { createJSONStorage } from 'jotai/utils'; import type { PropsWithChildren, ReactElement } from 'react'; import { Suspense, useEffect } from 'react'; diff --git a/apps/web/src/hooks/affine/use-toggle-workspace-publish.ts b/apps/web/src/hooks/affine/use-toggle-workspace-publish.ts index 3198b8b1dc..1c5951da4b 100644 --- a/apps/web/src/hooks/affine/use-toggle-workspace-publish.ts +++ b/apps/web/src/hooks/affine/use-toggle-workspace-publish.ts @@ -1,6 +1,7 @@ import type { AffineLegacyCloudWorkspace } from '@affine/env/workspace'; import { affineApis } from '@affine/workspace/affine/shared'; -import { rootStore, rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; +import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; +import { rootStore } from '@toeverything/plugin-infra/manager'; import { useCallback } from 'react'; import useSWR from 'swr'; diff --git a/apps/web/src/layouts/workspace-layout.tsx b/apps/web/src/layouts/workspace-layout.tsx index 03dff06b5e..2c7c52578c 100644 --- a/apps/web/src/layouts/workspace-layout.tsx +++ b/apps/web/src/layouts/workspace-layout.tsx @@ -20,7 +20,6 @@ import { createAffineGlobalChannel } from '@affine/workspace/affine/sync'; import { rootCurrentPageIdAtom, rootCurrentWorkspaceIdAtom, - rootStore, rootWorkspacesMetadataAtom, } from '@affine/workspace/atom'; import { assertEquals, assertExists, nanoid } from '@blocksuite/store'; @@ -35,6 +34,7 @@ import { useSensors, } from '@dnd-kit/core'; import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper'; +import { rootStore } from '@toeverything/plugin-infra/manager'; import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import Head from 'next/head'; import { useRouter } from 'next/router'; diff --git a/apps/web/src/pages/_app.tsx b/apps/web/src/pages/_app.tsx index 10cc84ca22..676eaf1dfb 100644 --- a/apps/web/src/pages/_app.tsx +++ b/apps/web/src/pages/_app.tsx @@ -3,12 +3,12 @@ import '@affine/component/theme/theme.css'; // bootstrap code before everything import '../bootstrap'; +import { AffineContext } from '@affine/component/context'; import { WorkspaceFallback } from '@affine/component/workspace'; import { config } from '@affine/env'; import { createI18n, I18nextProvider } from '@affine/i18n'; import type { EmotionCache } from '@emotion/cache'; import { CacheProvider } from '@emotion/react'; -import { AffinePluginContext } from '@toeverything/plugin-infra/react'; import type { AppProps } from 'next/app'; import Head from 'next/head'; import { useRouter } from 'next/router'; @@ -64,7 +64,7 @@ const App = function App({ }> - + AFFiNE {getLayout()} - + diff --git a/package.json b/package.json index 52998d4669..fdbf31d80e 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,9 @@ "dev:local": "PORT=8080 API_SERVER_PROFILE=local yarn workspace @affine/web dev", "dev:electron": "yarn workspace @affine/electron dev:app", "dev:plugins": "./apps/electron/scripts/plugins/dev-plugins.mjs", - "build": "yarn workspace @affine/web build", - "build:storybook": "yarn workspace @affine/storybook build-storybook", + "build": "yarn build:infra && yarn workspace @affine/web build", + "build:infra": "yarn workspace @toeverything/plugin-infra build && yarn workspace @toeverything/infra build", + "build:storybook": "yarn build:infra && yarn workspace @affine/storybook build-storybook", "build:plugins": "./apps/electron/scripts/plugins/build-plugins.mjs", "bump:nightly": "./scripts/bump-blocksuite.sh", "circular": "madge --circular --ts-config ./tsconfig.json ./apps/web/src/pages/**/*.tsx", diff --git a/packages/component/package.json b/packages/component/package.json index eb44117624..9553ee348a 100644 --- a/packages/component/package.json +++ b/packages/component/package.json @@ -35,6 +35,7 @@ "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-toast": "^1.1.4", "@toeverything/hooks": "workspace:*", + "@toeverything/plugin-infra": "workspace:*", "@toeverything/theme": "^0.6.1", "@vanilla-extract/dynamic": "^2.0.3", "clsx": "^1.2.1", diff --git a/packages/plugin-infra/src/react/context.tsx b/packages/component/src/components/context/index.tsx similarity index 82% rename from packages/plugin-infra/src/react/context.tsx rename to packages/component/src/components/context/index.tsx index a2806f1536..750a11517f 100644 --- a/packages/plugin-infra/src/react/context.tsx +++ b/packages/component/src/components/context/index.tsx @@ -1,11 +1,11 @@ import { ProviderComposer } from '@affine/component/provider-composer'; import { ThemeProvider } from '@affine/component/theme-provider'; -import { rootStore } from '@affine/workspace/atom'; +import { rootStore } from '@toeverything/plugin-infra/manager'; import { Provider } from 'jotai'; import type { PropsWithChildren } from 'react'; import { useMemo } from 'react'; -export function AffinePluginContext(props: PropsWithChildren) { +export function AffineContext(props: PropsWithChildren) { return ( >>({}); -const pluginLogger = new DebugLogger('affine:plugin'); - export function definePlugin( definition: Definition, uiAdapterLoader?: Loader>, - blockSuiteAdapter?: Loader> + blockSuiteAdapter?: Loader>, + serverAdapter?: Loader ) { const basePlugin = { definition, @@ -27,57 +30,70 @@ export function definePlugin( [definition.id]: basePlugin, })); - if (blockSuiteAdapter) { - const updateAdapter = (adapter: Partial) => { - rootStore.set(affinePluginsAtom, plugins => ({ - ...plugins, - [definition.id]: { - ...basePlugin, - blockSuiteAdapter: adapter, - }, - })); - }; - - blockSuiteAdapter - .load() - .then(({ default: adapter }) => updateAdapter(adapter)) - .catch(err => { - pluginLogger.error('[definePlugin] blockSuiteAdapter error', err); - }); - - if (import.meta.webpackHot) { - blockSuiteAdapter.hotModuleReload(async _ => { - const adapter = (await _).default; - updateAdapter(adapter); - pluginLogger.info('[HMR] Plugin', definition.id, 'hot reloaded.'); + if (isServer) { + if (serverAdapter) { + serverAdapter.load().then(({ default: adapter }) => { + rootStore.set(affinePluginsAtom, plugins => ({ + ...plugins, + [definition.id]: { + ...basePlugin, + serverAdapter: adapter, + }, + })); }); } - } + } else if (isClient) { + if (blockSuiteAdapter) { + const updateAdapter = (adapter: Partial) => { + rootStore.set(affinePluginsAtom, plugins => ({ + ...plugins, + [definition.id]: { + ...basePlugin, + blockSuiteAdapter: adapter, + }, + })); + }; - if (uiAdapterLoader) { - const updateAdapter = (adapter: Partial) => { - rootStore.set(affinePluginsAtom, plugins => ({ - ...plugins, - [definition.id]: { - ...basePlugin, - uiAdapter: adapter, - }, - })); - }; + blockSuiteAdapter + .load() + .then(({ default: adapter }) => updateAdapter(adapter)) + .catch(err => { + console.error('[definePlugin] blockSuiteAdapter error', err); + }); - uiAdapterLoader - .load() - .then(({ default: adapter }) => updateAdapter(adapter)) - .catch(err => { - pluginLogger.error('[definePlugin] blockSuiteAdapter error', err); - }); + if (import.meta.webpackHot) { + blockSuiteAdapter.hotModuleReload(async _ => { + const adapter = (await _).default; + updateAdapter(adapter); + console.info('[HMR] Plugin', definition.id, 'hot reloaded.'); + }); + } + } + if (uiAdapterLoader) { + const updateAdapter = (adapter: Partial) => { + rootStore.set(affinePluginsAtom, plugins => ({ + ...plugins, + [definition.id]: { + ...basePlugin, + uiAdapter: adapter, + }, + })); + }; - if (import.meta.webpackHot) { - uiAdapterLoader.hotModuleReload(async _ => { - const adapter = (await _).default; - updateAdapter(adapter); - pluginLogger.info('[HMR] Plugin', definition.id, 'hot reloaded.'); - }); + uiAdapterLoader + .load() + .then(({ default: adapter }) => updateAdapter(adapter)) + .catch(err => { + console.error('[definePlugin] blockSuiteAdapter error', err); + }); + + if (import.meta.webpackHot) { + uiAdapterLoader.hotModuleReload(async _ => { + const adapter = (await _).default; + updateAdapter(adapter); + console.info('[HMR] Plugin', definition.id, 'hot reloaded.'); + }); + } } } } diff --git a/packages/plugin-infra/src/react/index.ts b/packages/plugin-infra/src/react/index.ts deleted file mode 100644 index c38e8e8215..0000000000 --- a/packages/plugin-infra/src/react/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './context'; diff --git a/packages/plugin-infra/src/type.ts b/packages/plugin-infra/src/type.ts index 20a0524503..22daf1169e 100644 --- a/packages/plugin-infra/src/type.ts +++ b/packages/plugin-infra/src/type.ts @@ -141,6 +141,11 @@ export type Definition = { * @example ReleaseStage.PROD */ stage: ReleaseStage; + + /** + * Registered commands + */ + commands: string[]; }; // todo(himself65): support Vue.js @@ -171,12 +176,16 @@ export type PluginBlockSuiteAdapter = { uiDecorator: (root: EditorContainer) => Cleanup; }; -export type PluginAdapterCreator = ( - context: AffinePluginContext -) => PluginUIAdapter; +type AFFiNEServer = { + registerCommand: (command: string, fn: (...args: any[]) => any) => void; + unregisterCommand: (command: string) => void; +}; + +export type ServerAdapter = (affine: AFFiNEServer) => () => void; export type AffinePlugin = { definition: Definition; uiAdapter: Partial; blockSuiteAdapter: Partial; + serverAdapter?: ServerAdapter; }; diff --git a/packages/plugin-infra/tsconfig.json b/packages/plugin-infra/tsconfig.json index 3fcb503086..e9197b23ff 100644 --- a/packages/plugin-infra/tsconfig.json +++ b/packages/plugin-infra/tsconfig.json @@ -8,10 +8,7 @@ }, "references": [ { - "path": "../component" - }, - { - "path": "../workspace" + "path": "./tsconfig.node.json" } ] } diff --git a/packages/plugin-infra/tsconfig.node.json b/packages/plugin-infra/tsconfig.node.json new file mode 100644 index 0000000000..e66abbb359 --- /dev/null +++ b/packages/plugin-infra/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "outDir": "lib" + }, + "include": ["vite.config.ts"] +} diff --git a/packages/plugin-infra/vite.config.ts b/packages/plugin-infra/vite.config.ts new file mode 100644 index 0000000000..fc987684ba --- /dev/null +++ b/packages/plugin-infra/vite.config.ts @@ -0,0 +1,35 @@ +import { resolve } from 'node:path'; + +import { fileURLToPath } from 'url'; +import { defineConfig } from 'vite'; +import dts from 'vite-plugin-dts'; + +const root = fileURLToPath(new URL('.', import.meta.url)); + +export default defineConfig({ + build: { + minify: false, + lib: { + entry: { + type: resolve(root, 'src/type.ts'), + manager: resolve(root, 'src/manager.ts'), + }, + }, + rollupOptions: { + external: [ + 'jotai', + 'jotai/vanilla', + '@blocksuite/blocks', + '@blocksuite/store', + '@blocksuite/global', + '@blocksuite/editor', + '@blocksuite/lit', + ], + }, + }, + plugins: [ + dts({ + insertTypesEntry: true, + }), + ], +}); diff --git a/packages/workspace/package.json b/packages/workspace/package.json index e88332f1c2..fbf8739ac7 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -22,6 +22,7 @@ "@affine/debug": "workspace:*", "@affine/env": "workspace:*", "@toeverything/hooks": "workspace:*", + "@toeverything/plugin-infra": "workspace:*", "@toeverything/y-indexeddb": "workspace:*", "firebase": "^9.22.1", "jotai": "^2.1.1", diff --git a/packages/workspace/src/affine/shared.ts b/packages/workspace/src/affine/shared.ts index e90e2cc52e..0e47e9eb74 100644 --- a/packages/workspace/src/affine/shared.ts +++ b/packages/workspace/src/affine/shared.ts @@ -1,6 +1,6 @@ import { prefixUrl } from '@affine/env'; +import { rootStore } from '@toeverything/plugin-infra/manager'; -import { rootStore } from '../atom'; import { createUserApis, createWorkspaceApis } from './api/index'; import { currentAffineUserAtom } from './atom'; import type { LoginResponse } from './login'; diff --git a/packages/workspace/src/affine/sync.ts b/packages/workspace/src/affine/sync.ts index 35335c03c7..6a08165f6b 100644 --- a/packages/workspace/src/affine/sync.ts +++ b/packages/workspace/src/affine/sync.ts @@ -8,11 +8,12 @@ import { } from '@affine/env/workspace/legacy-cloud'; import { assertExists } from '@blocksuite/global/utils'; import type { Disposable } from '@blocksuite/store'; +import { rootStore } from '@toeverything/plugin-infra/manager'; import { z } from 'zod'; import { WebsocketClient } from '../affine/channel'; import { storageChangeSlot } from '../affine/login'; -import { rootStore, rootWorkspacesMetadataAtom } from '../atom'; +import { rootWorkspacesMetadataAtom } from '../atom'; const logger = new DebugLogger('affine-sync'); diff --git a/packages/workspace/src/atom.ts b/packages/workspace/src/atom.ts index 147aa67645..4fd20e5252 100644 --- a/packages/workspace/src/atom.ts +++ b/packages/workspace/src/atom.ts @@ -1,7 +1,7 @@ import type { WorkspaceFlavour } from '@affine/env/workspace'; import type { EditorContainer } from '@blocksuite/editor'; -import { atom, createStore } from 'jotai'; -import { atomWithStorage, createJSONStorage } from 'jotai/utils'; +import { atom } from 'jotai'; +import { atomWithStorage } from 'jotai/utils'; import Router from 'next/router'; export type RootWorkspaceMetadata = { @@ -77,13 +77,3 @@ export const rootCurrentEditorAtom = atom | null>( null ); //#endregion - -const getStorage = () => createJSONStorage(() => localStorage); - -export const getStoredWorkspaceMeta = () => { - const storage = getStorage(); - return storage.getItem('jotai-workspaces', []) as RootWorkspaceMetadata[]; -}; - -// global store -export const rootStore = createStore(); diff --git a/packages/workspace/src/utils.ts b/packages/workspace/src/utils.ts index 2ee0268101..38b4b66824 100644 --- a/packages/workspace/src/utils.ts +++ b/packages/workspace/src/utils.ts @@ -4,9 +4,10 @@ import { WorkspaceFlavour } from '@affine/env/workspace'; import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models'; import type { Generator, StoreOptions } from '@blocksuite/store'; import { createIndexeddbStorage, Workspace } from '@blocksuite/store'; +import { rootStore } from '@toeverything/plugin-infra/manager'; import type { createWorkspaceApis } from './affine/api'; -import { rootStore, rootWorkspacesMetadataAtom } from './atom'; +import { rootWorkspacesMetadataAtom } from './atom'; import { createAffineBlobStorage } from './blob'; import { createSQLiteStorage } from './blob/sqlite-blob-storage'; diff --git a/packages/workspace/tsconfig.json b/packages/workspace/tsconfig.json index c96773e131..57d5f932ad 100644 --- a/packages/workspace/tsconfig.json +++ b/packages/workspace/tsconfig.json @@ -10,6 +10,7 @@ { "path": "../y-indexeddb" }, { "path": "../env" }, { "path": "../debug" }, - { "path": "../hooks" } + { "path": "../hooks" }, + { "path": "../plugin-infra" } ] } diff --git a/plugins/bookmark-block/package.json b/plugins/bookmark-block/package.json index 2b0942608c..d701c5d2fb 100644 --- a/plugins/bookmark-block/package.json +++ b/plugins/bookmark-block/package.json @@ -8,6 +8,7 @@ "./server": "./src/server.ts" }, "dependencies": { + "@affine/component": "workspace:*", "@toeverything/plugin-infra": "workspace:*", "foxact": "^0.2.7", "link-preview-js": "^3.0.4" diff --git a/plugins/bookmark-block/src/index.ts b/plugins/bookmark-block/src/index.ts index 8d26bdb83c..3fd8ef053f 100644 --- a/plugins/bookmark-block/src/index.ts +++ b/plugins/bookmark-block/src/index.ts @@ -19,6 +19,7 @@ definePlugin( }, stage: ReleaseStage.NIGHTLY, version: '0.0.1', + commands: ['com.blocksuite.bookmark-block.get-bookmark-data-by-link'], }, undefined, { @@ -28,5 +29,19 @@ definePlugin( import.meta.webpackHot.accept('./blocksuite', () => onHot(import('./blocksuite/index')) ), + }, + { + load: () => + import( + /* webpackIgnore: true */ + './server' + ), + hotModuleReload: onHot => + onHot( + import( + /* webpackIgnore: true */ + './server' + ) + ), } ); diff --git a/plugins/bookmark-block/src/server.ts b/plugins/bookmark-block/src/server.ts index 3c1e6418af..6e95ea381a 100644 --- a/plugins/bookmark-block/src/server.ts +++ b/plugins/bookmark-block/src/server.ts @@ -1,3 +1,4 @@ +import type { ServerAdapter } from '@toeverything/plugin-infra/type'; import { getLinkPreview } from 'link-preview-js'; type MetaData = { @@ -26,31 +27,41 @@ export interface PreviewType { favicons: string[]; } -export default { - getBookmarkDataByLink: async (_: unknown, url: string): Promise => { - const previewData = (await getLinkPreview(url, { - timeout: 6000, - headers: { - 'user-agent': 'googlebot', - }, - followRedirects: 'follow', - }).catch(() => { - return { - title: '', - siteName: '', - description: '', - images: [], - videos: [], - contentType: `text/html`, - favicons: [], - }; - })) as PreviewType; +const adapter: ServerAdapter = affine => { + affine.registerCommand( + 'com.blocksuite.bookmark-block.get-bookmark-data-by-link', + async (url: string): Promise => { + const previewData = (await getLinkPreview(url, { + timeout: 6000, + headers: { + 'user-agent': 'googlebot', + }, + followRedirects: 'follow', + }).catch(() => { + return { + title: '', + siteName: '', + description: '', + images: [], + videos: [], + contentType: `text/html`, + favicons: [], + }; + })) as PreviewType; - return { - title: previewData.title, - description: previewData.description, - icon: previewData.favicons[0], - image: previewData.images[0], - }; - }, + return { + title: previewData.title, + description: previewData.description, + icon: previewData.favicons[0], + image: previewData.images[0], + }; + } + ); + return () => { + affine.unregisterCommand( + 'com.blocksuite.bookmark-block.get-bookmark-data-by-link' + ); + }; }; + +export default adapter; diff --git a/plugins/bookmark-block/tsconfig.json b/plugins/bookmark-block/tsconfig.json index 85a28c467d..1c21e13084 100644 --- a/plugins/bookmark-block/tsconfig.json +++ b/plugins/bookmark-block/tsconfig.json @@ -11,9 +11,6 @@ }, { "path": "../../packages/plugin-infra" - }, - { - "path": "../../packages/env" } ] } diff --git a/plugins/copilot/package.json b/plugins/copilot/package.json index a7eb4c9bfd..8140a0d9a3 100644 --- a/plugins/copilot/package.json +++ b/plugins/copilot/package.json @@ -7,6 +7,7 @@ ".": "./src/index.ts" }, "dependencies": { + "@affine/component": "workspace:*", "@toeverything/plugin-infra": "workspace:*" }, "devDependencies": { diff --git a/plugins/copilot/src/UI/detail-content.tsx b/plugins/copilot/src/UI/detail-content.tsx index 49b931474a..dcfcf95f84 100644 --- a/plugins/copilot/src/UI/detail-content.tsx +++ b/plugins/copilot/src/UI/detail-content.tsx @@ -1,5 +1,5 @@ import { Button, Input } from '@affine/component'; -import { rootStore } from '@affine/workspace/atom'; +import { rootStore } from '@toeverything/plugin-infra/manager'; import type { PluginUIAdapter } from '@toeverything/plugin-infra/type'; import { Provider, useAtom, useAtomValue, useSetAtom } from 'jotai'; import type { ReactElement } from 'react'; @@ -10,7 +10,7 @@ import { Conversation } from '../core/components/conversation'; import { Divider } from '../core/components/divider'; import { openAIApiKeyAtom, useChatAtoms } from '../core/hooks'; -if (!environment.isServer) { +if (typeof window === 'undefined') { import('@blocksuite/blocks').then(({ FormatQuickBar }) => { FormatQuickBar.customElements.push((_page, getSelection) => { const div = document.createElement('div'); diff --git a/plugins/copilot/src/index.ts b/plugins/copilot/src/index.ts index c7efcbc8db..e5ab4f9b41 100644 --- a/plugins/copilot/src/index.ts +++ b/plugins/copilot/src/index.ts @@ -1,5 +1,3 @@ -import '@affine/env/config'; - import { definePlugin } from '@toeverything/plugin-infra/manager'; import { ReleaseStage } from '@toeverything/plugin-infra/type'; @@ -22,6 +20,7 @@ definePlugin( }, stage: ReleaseStage.NIGHTLY, version: '0.0.1', + commands: [], }, { load: () => import('./UI/index'), diff --git a/plugins/copilot/tsconfig.json b/plugins/copilot/tsconfig.json index 990965669e..85a28c467d 100644 --- a/plugins/copilot/tsconfig.json +++ b/plugins/copilot/tsconfig.json @@ -3,7 +3,6 @@ "include": ["./src"], "compilerOptions": { "noEmit": false, - "composite": true, "outDir": "lib" }, "references": [ diff --git a/scripts/publish.sh b/scripts/publish.sh index 4a032cfe9e..655287902e 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -2,7 +2,8 @@ packages=( "y-indexeddb" - "infra" + "infra", + "plugin-infra" ) for package in "${packages[@]}"; do diff --git a/scripts/setup/build-plugins.ts b/scripts/setup/build-plugins.ts index 3f9abdbeeb..13b7cb8338 100644 --- a/scripts/setup/build-plugins.ts +++ b/scripts/setup/build-plugins.ts @@ -1,6 +1,18 @@ +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { spawnSync } from 'child_process'; import { beforeAll } from 'vitest'; +const rootDir = fileURLToPath(new URL('../../', import.meta.url)); + beforeAll(async () => { + console.log('Build plugin infra'); + spawnSync('yarn', ['build'], { + stdio: 'inherit', + cwd: resolve(rootDir, './packages/plugin-infra'), + }); + console.log('Build plugins'); // @ts-expect-error await import('../../apps/electron/scripts/plugins/build-plugins.mjs'); diff --git a/tsconfig.json b/tsconfig.json index 8bdb65e35b..2efbe3922d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -106,6 +106,17 @@ { "path": "./packages/debug" }, + // Plugins + { + "path": "./packages/plugin-infra" + }, + { + "path": "./plugins/bookmark-block" + }, + { + "path": "./plugins/copilot" + }, + // Tests { "path": "./tests" @@ -129,12 +140,7 @@ { "path": "./packages/y-indexeddb" }, - { - "path": "./packages/plugin-infra" - }, - { - "path": "./plugins/copilot" - }, + { "path": "./tests/fixtures" }, diff --git a/yarn.lock b/yarn.lock index 1d1f8a9584..3f35c2325f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,6 +34,7 @@ __metadata: version: 0.0.0-use.local resolution: "@affine/bookmark-block@workspace:plugins/bookmark-block" dependencies: + "@affine/component": "workspace:*" "@toeverything/plugin-infra": "workspace:*" foxact: ^0.2.7 link-preview-js: ^3.0.4 @@ -86,6 +87,7 @@ __metadata: "@radix-ui/react-radio-group": ^1.1.3 "@radix-ui/react-toast": ^1.1.4 "@toeverything/hooks": "workspace:*" + "@toeverything/plugin-infra": "workspace:*" "@toeverything/theme": ^0.6.1 "@types/react": ^18.2.6 "@types/react-datepicker": ^4.11.2 @@ -121,6 +123,7 @@ __metadata: version: 0.0.0-use.local resolution: "@affine/copilot@workspace:plugins/copilot" dependencies: + "@affine/component": "workspace:*" "@toeverything/plugin-infra": "workspace:*" "@types/marked": ^5.0.0 "@types/react": ^18.2.6 @@ -162,8 +165,10 @@ __metadata: "@electron-forge/shared-types": ^6.1.1 "@electron/remote": 2.0.9 "@toeverything/infra": "workspace:*" + "@toeverything/plugin-infra": "workspace:*" "@types/fs-extra": ^11.0.1 "@types/uuid": ^9.0.1 + async-call-rpc: ^6.3.1 cheerio: ^1.0.0-rc.12 cross-env: 7.0.3 electron: =25.0.1 @@ -441,6 +446,7 @@ __metadata: "@affine/debug": "workspace:*" "@affine/env": "workspace:*" "@toeverything/hooks": "workspace:*" + "@toeverything/plugin-infra": "workspace:*" "@toeverything/y-indexeddb": "workspace:*" "@types/ws": ^8.5.4 firebase: ^9.22.1 @@ -9080,15 +9086,14 @@ __metadata: version: 0.0.0-use.local resolution: "@toeverything/plugin-infra@workspace:packages/plugin-infra" dependencies: - "@affine/component": "workspace:*" - "@affine/env": "workspace:*" - "@affine/workspace": "workspace:*" "@blocksuite/blocks": 0.0.0-20230607055421-9b20fcaf-nightly "@blocksuite/editor": 0.0.0-20230607055421-9b20fcaf-nightly "@blocksuite/global": 0.0.0-20230607055421-9b20fcaf-nightly "@blocksuite/lit": 0.0.0-20230607055421-9b20fcaf-nightly "@blocksuite/store": 0.0.0-20230607055421-9b20fcaf-nightly jotai: ^2.1.1 + vite: ^4.3.9 + vite-plugin-dts: ^2.3.0 peerDependencies: "@blocksuite/blocks": "*" "@blocksuite/editor": "*" @@ -11332,6 +11337,13 @@ __metadata: languageName: node linkType: hard +"async-call-rpc@npm:^6.3.1": + version: 6.3.1 + resolution: "async-call-rpc@npm:6.3.1" + checksum: 024e1bab4752b3aa66145977d534e6dc4c316e1d746c11a6a5076b30b16d42fa0ce4793be5a330319644f1335b01cbe13ee4ac28454cded37b1c803608400934 + languageName: node + linkType: hard + "async-limiter@npm:~1.0.0": version: 1.0.1 resolution: "async-limiter@npm:1.0.1"