mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-12 23:56:36 +08:00
feat(plugin-infra): support worker thread in server side (#3462)
This commit is contained in:
@@ -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'),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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<string, (...args: any) => PromiseLike<any>>;
|
||||
}
|
||||
|
||||
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<string, (...args: any) => PromiseLike<any>>
|
||||
>(
|
||||
{
|
||||
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];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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<string, (...args: any[]) => Promise<any>> = {};
|
||||
|
||||
parentPort.start();
|
||||
|
||||
const mainThread = AsyncCall<{
|
||||
log: (...args: any[]) => Promise<void>;
|
||||
}>(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);
|
||||
@@ -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<typeof packageJsonOutputSchema>;
|
||||
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],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
Reference in New Issue
Block a user