diff --git a/apps/electron/scripts/common.mjs b/apps/electron/scripts/common.mjs index 7e411b9a06..25de256427 100644 --- a/apps/electron/scripts/common.mjs +++ b/apps/electron/scripts/common.mjs @@ -32,6 +32,7 @@ export const config = () => { resolve(electronDir, './src/main/index.ts'), resolve(electronDir, './src/preload/index.ts'), resolve(electronDir, './src/helper/index.ts'), + resolve(electronDir, './src/worker/plugin.ts'), ], entryNames: '[dir]', outdir: resolve(electronDir, './dist'), diff --git a/apps/electron/src/main/application-menu/create.ts b/apps/electron/src/main/application-menu/create.ts index 1da3de92b3..c57ee266f9 100644 --- a/apps/electron/src/main/application-menu/create.ts +++ b/apps/electron/src/main/application-menu/create.ts @@ -1,8 +1,8 @@ import { app, Menu } from 'electron'; +import { isMacOS } from '../../shared/utils'; import { revealLogFile } from '../logger'; import { checkForUpdates } from '../updater'; -import { isMacOS } from '../utils'; import { applicationMenuSubjects } from './subject'; // Unique id for menuitems diff --git a/apps/electron/src/main/helper-process.ts b/apps/electron/src/main/helper-process.ts index 8cfeb13412..4a1fa27b9c 100644 --- a/apps/electron/src/main/helper-process.ts +++ b/apps/electron/src/main/helper-process.ts @@ -15,8 +15,8 @@ import { type WebContents, } from 'electron'; +import { MessageEventChannel } from '../shared/utils'; import { logger } from './logger'; -import { MessageEventChannel } from './utils'; const HELPER_PROCESS_PATH = path.join(__dirname, './helper.js'); diff --git a/apps/electron/src/main/logger.ts b/apps/electron/src/main/logger.ts index 9f6faef8df..f7089a9b3d 100644 --- a/apps/electron/src/main/logger.ts +++ b/apps/electron/src/main/logger.ts @@ -2,6 +2,7 @@ import { shell } from 'electron'; import log from 'electron-log'; export const logger = log.scope('main'); +export const pluginLogger = log.scope('plugin'); log.initialize(); export function getLogFilePath() { diff --git a/apps/electron/src/main/main-window.ts b/apps/electron/src/main/main-window.ts index ca9c3b5fad..93a6adc5ca 100644 --- a/apps/electron/src/main/main-window.ts +++ b/apps/electron/src/main/main-window.ts @@ -4,10 +4,10 @@ import { BrowserWindow, nativeTheme } from 'electron'; import electronWindowState from 'electron-window-state'; import { join } from 'path'; +import { isMacOS, isWindows } from '../shared/utils'; import { getExposedMeta } from './exposed'; import { ensureHelperProcess } from './helper-process'; import { logger } from './logger'; -import { isMacOS, isWindows } from './utils'; const IS_DEV: boolean = process.env.NODE_ENV === 'development' && !process.env.CI; @@ -114,6 +114,7 @@ async function createWindow() { // singleton let browserWindow: Electron.BrowserWindow | undefined; + /** * Restore existing BrowserWindow or Create new BrowserWindow */ diff --git a/apps/electron/src/main/plugin.ts b/apps/electron/src/main/plugin.ts index 4cc6cb1ce3..4f12c160b0 100644 --- a/apps/electron/src/main/plugin.ts +++ b/apps/electron/src/main/plugin.ts @@ -1,7 +1,14 @@ import { join, resolve } from 'node:path'; +import { Worker } from 'node:worker_threads'; -import { logger } from '@affine/electron/main/logger'; +import { logger, pluginLogger } from '@affine/electron/main/logger'; +import { AsyncCall } from 'async-call-rpc'; import { ipcMain } from 'electron'; +import { readFile } from 'fs/promises'; + +import { MessageEventChannel } from '../shared/utils'; + +const builtInPlugins = ['bookmark']; declare global { // fixme(himself65): @@ -10,26 +17,41 @@ declare global { var asyncCall: Record PromiseLike>; } -export function registerPlugin() { +export async function registerPlugin() { logger.info('import plugin manager'); - globalThis.asyncCall = {}; - const bookmarkPluginPath = join( - process.env.PLUGIN_DIR ?? resolve(__dirname, './plugins'), - './bookmark/index.js' + const asyncCall = AsyncCall< + Record PromiseLike> + >( + { + log: (...args: any[]) => { + pluginLogger.log(...args); + }, + }, + { + channel: new MessageEventChannel( + new Worker(resolve(__dirname, './worker.js'), {}) + ), + } + ); + globalThis.asyncCall = asyncCall; + await Promise.all( + builtInPlugins.map(async plugin => { + const pluginPackageJsonPath = join( + process.env.PLUGIN_DIR ?? resolve(__dirname, './plugins'), + `./${plugin}/package.json` + ); + logger.info(`${plugin} plugin path:`, pluginPackageJsonPath); + const packageJson = JSON.parse( + await readFile(pluginPackageJsonPath, 'utf-8') + ); + console.log('packageJson', packageJson); + const serverCommand: string[] = packageJson.affinePlugin.serverCommand; + serverCommand.forEach(command => { + ipcMain.handle(command, async (_, ...args) => { + logger.info(`plugin ${plugin} called`); + return asyncCall[command](...args); + }); + }); + }) ); - logger.info('bookmark plugin path:', bookmarkPluginPath); - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { entry } = require(bookmarkPluginPath); - - entry({ - registerCommand: (command: string, handler: (...args: any[]) => any) => { - logger.info('register plugin command', command); - ipcMain.handle(command, (event, ...args) => handler(...args)); - globalThis.asyncCall[command] = handler; - }, - registerCommands: (command: string) => { - ipcMain.removeHandler(command); - delete globalThis.asyncCall[command]; - }, - }); } diff --git a/apps/electron/src/main/ui/index.ts b/apps/electron/src/main/ui/index.ts index fab6741872..d84a927d70 100644 --- a/apps/electron/src/main/ui/index.ts +++ b/apps/electron/src/main/ui/index.ts @@ -1,7 +1,7 @@ import { app, BrowserWindow, nativeTheme } from 'electron'; +import { isMacOS } from '../../shared/utils'; import type { NamespaceHandlers } from '../type'; -import { isMacOS } from '../utils'; import { getGoogleOauthCode } from './google-auth'; export const uiHandlers = { diff --git a/apps/electron/src/main/updater/electron-updater.ts b/apps/electron/src/main/updater/electron-updater.ts index d009b42e9c..975be9e8c1 100644 --- a/apps/electron/src/main/updater/electron-updater.ts +++ b/apps/electron/src/main/updater/electron-updater.ts @@ -2,8 +2,8 @@ import { app } from 'electron'; import { autoUpdater } from 'electron-updater'; import { z } from 'zod'; +import { isMacOS } from '../../shared/utils'; import { logger } from '../logger'; -import { isMacOS } from '../utils'; import { updaterSubjects } from './event'; export const ReleaseTypeSchema = z.enum([ diff --git a/apps/electron/src/main/utils.ts b/apps/electron/src/shared/utils.ts similarity index 100% rename from apps/electron/src/main/utils.ts rename to apps/electron/src/shared/utils.ts diff --git a/apps/electron/src/worker/plugin.ts b/apps/electron/src/worker/plugin.ts new file mode 100644 index 0000000000..7234ac86bc --- /dev/null +++ b/apps/electron/src/worker/plugin.ts @@ -0,0 +1,45 @@ +import { join, resolve } from 'node:path'; +import { parentPort } from 'node:worker_threads'; + +import type { ServerContext } from '@toeverything/plugin-infra/server'; +import { AsyncCall } from 'async-call-rpc'; + +import { MessageEventChannel } from '../shared/utils'; + +if (!parentPort) { + throw new Error('parentPort is null'); +} +const commandProxy: Record Promise> = {}; + +parentPort.start(); + +const mainThread = AsyncCall<{ + log: (...args: any[]) => Promise; +}>(commandProxy, { + channel: new MessageEventChannel(parentPort), +}); + +globalThis.console.log = mainThread.log; +globalThis.console.error = mainThread.log; +globalThis.console.info = mainThread.log; +globalThis.console.debug = mainThread.log; +globalThis.console.warn = mainThread.log; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const bookmarkPluginModule = require(join( + process.env.PLUGIN_DIR ?? resolve(__dirname, './plugins'), + './bookmark/index.js' +)); + +const serverContext: ServerContext = { + registerCommand: (command, fn) => { + console.log('register command', command); + commandProxy[command] = fn; + }, + unregisterCommand: command => { + console.log('unregister command', command); + delete commandProxy[command]; + }, +}; + +bookmarkPluginModule.entry(serverContext); diff --git a/packages/cli/src/bin/dev-plugin.ts b/packages/cli/src/bin/dev-plugin.ts index 702dc04435..e0a4d7351b 100644 --- a/packages/cli/src/bin/dev-plugin.ts +++ b/packages/cli/src/bin/dev-plugin.ts @@ -10,7 +10,7 @@ import { } from '@toeverything/plugin-infra/type'; import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'; import react from '@vitejs/plugin-react-swc'; -import { build } from 'vite'; +import { build, type PluginOption } from 'vite'; import type { z } from 'zod'; import { projectRoot } from '../config/index.js'; @@ -107,26 +107,33 @@ const serverOutDir = path.resolve( ); const coreEntry = path.resolve(pluginDir, json.affinePlugin.entry.core); -if (json.affinePlugin.entry.server) { - const serverEntry = path.resolve(pluginDir, json.affinePlugin.entry.server); - await build({ - build: { - watch: isWatch ? {} : undefined, - minify: false, - outDir: serverOutDir, - emptyOutDir: true, - lib: { - entry: serverEntry, - fileName: 'index', - formats: ['cjs'], - }, - rollupOptions: { - external, - }, - }, - }); -} +const generatePackageJson: PluginOption = { + name: 'generate-package.json', + async generateBundle() { + const packageJson = { + name: json.name, + version: json.version, + description: json.description, + affinePlugin: { + release: json.affinePlugin.release, + entry: { + core: 'index.mjs', + }, + assets: [...metadata.assets], + serverCommand: json.affinePlugin.serverCommand, + }, + } satisfies z.infer; + packageJsonOutputSchema.parse(packageJson); + this.emitFile({ + type: 'asset', + fileName: 'package.json', + source: JSON.stringify(packageJson, null, 2), + }); + }, +}; + +// step 1: generate core bundle await build({ build: { watch: isWatch ? {} : undefined, @@ -178,28 +185,28 @@ await build({ return code; }, }, - { - name: 'generate-package.json', - async generateBundle() { - const packageJson = { - name: json.name, - version: json.version, - description: json.description, - affinePlugin: { - release: json.affinePlugin.release, - entry: { - core: 'index.mjs', - }, - assets: [...metadata.assets], - }, - }; - packageJsonOutputSchema.parse(packageJson); - this.emitFile({ - type: 'asset', - fileName: 'package.json', - source: JSON.stringify(packageJson, null, 2), - }); - }, - }, + generatePackageJson, ], }); + +// step 2: generate server bundle +if (json.affinePlugin.entry.server) { + const serverEntry = path.resolve(pluginDir, json.affinePlugin.entry.server); + await build({ + build: { + watch: isWatch ? {} : undefined, + minify: false, + outDir: serverOutDir, + emptyOutDir: true, + lib: { + entry: serverEntry, + fileName: 'index', + formats: ['cjs'], + }, + rollupOptions: { + external, + }, + }, + plugins: [generatePackageJson], + }); +} diff --git a/packages/plugin-infra/src/type.ts b/packages/plugin-infra/src/type.ts index e0cc82424c..6896fe7672 100644 --- a/packages/plugin-infra/src/type.ts +++ b/packages/plugin-infra/src/type.ts @@ -11,6 +11,7 @@ export const packageJsonInputSchema = z.object({ core: z.string(), server: z.string().optional(), }), + serverCommand: z.array(z.string()).optional(), }), }); @@ -24,6 +25,7 @@ export const packageJsonOutputSchema = z.object({ core: z.string(), }), assets: z.array(z.string()), + serverCommand: z.array(z.string()).optional(), }), }); diff --git a/plugins/bookmark/package.json b/plugins/bookmark/package.json index 2a3ed99fb1..2e45faeb6f 100644 --- a/plugins/bookmark/package.json +++ b/plugins/bookmark/package.json @@ -8,7 +8,10 @@ "entry": { "core": "./src/index.ts", "server": "./src/server.ts" - } + }, + "serverCommand": [ + "com.blocksuite.bookmark-block.get-bookmark-data-by-link" + ] }, "dependencies": { "@affine/component": "workspace:*",