mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 01:09:54 +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/main/index.ts'),
|
||||||
resolve(electronDir, './src/preload/index.ts'),
|
resolve(electronDir, './src/preload/index.ts'),
|
||||||
resolve(electronDir, './src/helper/index.ts'),
|
resolve(electronDir, './src/helper/index.ts'),
|
||||||
|
resolve(electronDir, './src/worker/plugin.ts'),
|
||||||
],
|
],
|
||||||
entryNames: '[dir]',
|
entryNames: '[dir]',
|
||||||
outdir: resolve(electronDir, './dist'),
|
outdir: resolve(electronDir, './dist'),
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { app, Menu } from 'electron';
|
import { app, Menu } from 'electron';
|
||||||
|
|
||||||
|
import { isMacOS } from '../../shared/utils';
|
||||||
import { revealLogFile } from '../logger';
|
import { revealLogFile } from '../logger';
|
||||||
import { checkForUpdates } from '../updater';
|
import { checkForUpdates } from '../updater';
|
||||||
import { isMacOS } from '../utils';
|
|
||||||
import { applicationMenuSubjects } from './subject';
|
import { applicationMenuSubjects } from './subject';
|
||||||
|
|
||||||
// Unique id for menuitems
|
// Unique id for menuitems
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import {
|
|||||||
type WebContents,
|
type WebContents,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
|
|
||||||
|
import { MessageEventChannel } from '../shared/utils';
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
import { MessageEventChannel } from './utils';
|
|
||||||
|
|
||||||
const HELPER_PROCESS_PATH = path.join(__dirname, './helper.js');
|
const HELPER_PROCESS_PATH = path.join(__dirname, './helper.js');
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { shell } from 'electron';
|
|||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
export const logger = log.scope('main');
|
export const logger = log.scope('main');
|
||||||
|
export const pluginLogger = log.scope('plugin');
|
||||||
log.initialize();
|
log.initialize();
|
||||||
|
|
||||||
export function getLogFilePath() {
|
export function getLogFilePath() {
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { BrowserWindow, nativeTheme } from 'electron';
|
|||||||
import electronWindowState from 'electron-window-state';
|
import electronWindowState from 'electron-window-state';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import { isMacOS, isWindows } from '../shared/utils';
|
||||||
import { getExposedMeta } from './exposed';
|
import { getExposedMeta } from './exposed';
|
||||||
import { ensureHelperProcess } from './helper-process';
|
import { ensureHelperProcess } from './helper-process';
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
import { isMacOS, isWindows } from './utils';
|
|
||||||
|
|
||||||
const IS_DEV: boolean =
|
const IS_DEV: boolean =
|
||||||
process.env.NODE_ENV === 'development' && !process.env.CI;
|
process.env.NODE_ENV === 'development' && !process.env.CI;
|
||||||
@@ -114,6 +114,7 @@ async function createWindow() {
|
|||||||
|
|
||||||
// singleton
|
// singleton
|
||||||
let browserWindow: Electron.BrowserWindow | undefined;
|
let browserWindow: Electron.BrowserWindow | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore existing BrowserWindow or Create new BrowserWindow
|
* Restore existing BrowserWindow or Create new BrowserWindow
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { join, resolve } from 'node:path';
|
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 { ipcMain } from 'electron';
|
||||||
|
import { readFile } from 'fs/promises';
|
||||||
|
|
||||||
|
import { MessageEventChannel } from '../shared/utils';
|
||||||
|
|
||||||
|
const builtInPlugins = ['bookmark'];
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// fixme(himself65):
|
// fixme(himself65):
|
||||||
@@ -10,26 +17,41 @@ declare global {
|
|||||||
var asyncCall: Record<string, (...args: any) => PromiseLike<any>>;
|
var asyncCall: Record<string, (...args: any) => PromiseLike<any>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerPlugin() {
|
export async function registerPlugin() {
|
||||||
logger.info('import plugin manager');
|
logger.info('import plugin manager');
|
||||||
globalThis.asyncCall = {};
|
const asyncCall = AsyncCall<
|
||||||
const bookmarkPluginPath = join(
|
Record<string, (...args: any) => PromiseLike<any>>
|
||||||
process.env.PLUGIN_DIR ?? resolve(__dirname, './plugins'),
|
>(
|
||||||
'./bookmark/index.js'
|
{
|
||||||
|
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 { app, BrowserWindow, nativeTheme } from 'electron';
|
||||||
|
|
||||||
|
import { isMacOS } from '../../shared/utils';
|
||||||
import type { NamespaceHandlers } from '../type';
|
import type { NamespaceHandlers } from '../type';
|
||||||
import { isMacOS } from '../utils';
|
|
||||||
import { getGoogleOauthCode } from './google-auth';
|
import { getGoogleOauthCode } from './google-auth';
|
||||||
|
|
||||||
export const uiHandlers = {
|
export const uiHandlers = {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { app } from 'electron';
|
|||||||
import { autoUpdater } from 'electron-updater';
|
import { autoUpdater } from 'electron-updater';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { isMacOS } from '../../shared/utils';
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import { isMacOS } from '../utils';
|
|
||||||
import { updaterSubjects } from './event';
|
import { updaterSubjects } from './event';
|
||||||
|
|
||||||
export const ReleaseTypeSchema = z.enum([
|
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';
|
} from '@toeverything/plugin-infra/type';
|
||||||
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
|
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
|
||||||
import react from '@vitejs/plugin-react-swc';
|
import react from '@vitejs/plugin-react-swc';
|
||||||
import { build } from 'vite';
|
import { build, type PluginOption } from 'vite';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import { projectRoot } from '../config/index.js';
|
import { projectRoot } from '../config/index.js';
|
||||||
@@ -107,26 +107,33 @@ const serverOutDir = path.resolve(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const coreEntry = path.resolve(pluginDir, json.affinePlugin.entry.core);
|
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({
|
await build({
|
||||||
build: {
|
build: {
|
||||||
watch: isWatch ? {} : undefined,
|
watch: isWatch ? {} : undefined,
|
||||||
@@ -178,28 +185,28 @@ await build({
|
|||||||
return code;
|
return code;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
generatePackageJson,
|
||||||
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),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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(),
|
core: z.string(),
|
||||||
server: z.string().optional(),
|
server: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
|
serverCommand: z.array(z.string()).optional(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ export const packageJsonOutputSchema = z.object({
|
|||||||
core: z.string(),
|
core: z.string(),
|
||||||
}),
|
}),
|
||||||
assets: z.array(z.string()),
|
assets: z.array(z.string()),
|
||||||
|
serverCommand: z.array(z.string()).optional(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,10 @@
|
|||||||
"entry": {
|
"entry": {
|
||||||
"core": "./src/index.ts",
|
"core": "./src/index.ts",
|
||||||
"server": "./src/server.ts"
|
"server": "./src/server.ts"
|
||||||
}
|
},
|
||||||
|
"serverCommand": [
|
||||||
|
"com.blocksuite.bookmark-block.get-bookmark-data-by-link"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@affine/component": "workspace:*",
|
"@affine/component": "workspace:*",
|
||||||
|
|||||||
Reference in New Issue
Block a user