feat: support enable/disable plugin (#3605)

This commit is contained in:
Alex Yang
2023-08-07 20:58:31 -04:00
committed by GitHub
parent ec05bd3f53
commit b147624f1c
14 changed files with 434 additions and 209 deletions
+125 -18
View File
@@ -1,18 +1,26 @@
import { DebugLogger } from '@affine/debug';
import type { CallbackMap, PluginContext } from '@affine/sdk/entry';
import type {
CallbackMap,
ExpectedLayout,
LayoutNode,
PluginContext,
} from '@affine/sdk/entry';
import { FormatQuickBar } from '@blocksuite/blocks';
import { assertExists } from '@blocksuite/global/utils';
import { DisposableGroup } from '@blocksuite/global/utils';
import {
addCleanup,
pluginEditorAtom,
pluginHeaderItemAtom,
pluginSettingAtom,
pluginWindowAtom,
} from '@toeverything/infra/__internal__/plugin';
import {
contentLayoutAtom,
currentPageAtom,
currentWorkspaceAtom,
editorItemsAtom,
headerItemsAtom,
rootStore,
settingItemsAtom,
windowItemsAtom,
} from '@toeverything/infra/atom';
import { atom } from 'jotai';
import { Provider } from 'jotai/react';
import { createElement, type PropsWithChildren } from 'react';
@@ -25,6 +33,84 @@ const dynamicImportKey = '$h_import';
const permissionLogger = new DebugLogger('plugins:permission');
const importLogger = new DebugLogger('plugins:import');
const pushLayoutAtom = atom<
null,
// fixme: check plugin name here
[pluginName: string, create: (root: HTMLElement) => () => void],
void
>(null, (_, set, pluginName, callback) => {
set(pluginWindowAtom, items => ({
...items,
[pluginName]: callback,
}));
set(contentLayoutAtom, layout => {
if (layout === 'editor') {
return {
direction: 'horizontal',
first: 'editor',
second: pluginName,
splitPercentage: 70,
};
} else {
return {
...layout,
direction: 'horizontal',
first: 'editor',
second: {
direction: 'horizontal',
// fixme: incorrect type here
first: layout.second,
second: pluginName,
splitPercentage: 70,
},
} as ExpectedLayout;
}
});
addCleanup(pluginName, () => {
set(deleteLayoutAtom, pluginName);
});
});
const deleteLayoutAtom = atom<null, [string], void>(null, (_, set, id) => {
set(pluginWindowAtom, items => {
const newItems = { ...items };
delete newItems[id];
return newItems;
});
const removeLayout = (layout: LayoutNode): LayoutNode => {
if (layout === 'editor') {
return 'editor';
} else {
if (typeof layout === 'string') {
return layout as ExpectedLayout;
}
if (layout.first === id) {
return layout.second;
} else if (layout.second === id) {
return layout.first;
} else {
return removeLayout(layout.second);
}
}
};
set(contentLayoutAtom, layout => {
if (layout === 'editor') {
return 'editor';
} else {
if (typeof layout === 'string') {
return layout as ExpectedLayout;
}
if (layout.first === id) {
return layout.second as ExpectedLayout;
} else if (layout.second === id) {
return layout.first as ExpectedLayout;
} else {
return removeLayout(layout.second) as ExpectedLayout;
}
}
});
});
// module -> importName -> updater[]
export const _rootImportsMap = new Map<string, Map<string, any>>();
const rootImportsMapSetupPromise = setupImportsMap(_rootImportsMap, {
@@ -41,7 +127,8 @@ const rootImportsMapSetupPromise = setupImportsMap(_rootImportsMap, {
rootStore: rootStore,
currentWorkspaceAtom: currentWorkspaceAtom,
currentPageAtom: currentPageAtom,
contentLayoutAtom: contentLayoutAtom,
pushLayoutAtom: pushLayoutAtom,
deleteLayoutAtom: deleteLayoutAtom,
},
'@blocksuite/blocks/std': import('@blocksuite/blocks/std'),
'@blocksuite/global/utils': import('@blocksuite/global/utils'),
@@ -379,7 +466,6 @@ const PluginProvider = ({ children }: PropsWithChildren) =>
children
);
const group = new DisposableGroup();
const entryLogger = new DebugLogger('plugin:entry');
export const evaluatePluginEntry = (pluginName: string) => {
@@ -392,29 +478,50 @@ export const evaluatePluginEntry = (pluginName: string) => {
register: (part, callback) => {
entryLogger.info(`Registering ${pluginName} to ${part}`);
if (part === 'headerItem') {
rootStore.set(headerItemsAtom, items => ({
rootStore.set(pluginHeaderItemAtom, items => ({
...items,
[pluginName]: callback as CallbackMap['headerItem'],
}));
addCleanup(pluginName, () => {
rootStore.set(pluginHeaderItemAtom, items => {
const newItems = { ...items };
delete newItems[pluginName];
return newItems;
});
});
} else if (part === 'editor') {
rootStore.set(editorItemsAtom, items => ({
rootStore.set(pluginEditorAtom, items => ({
...items,
[pluginName]: callback as CallbackMap['editor'],
}));
} else if (part === 'window') {
rootStore.set(windowItemsAtom, items => ({
...items,
[pluginName]: callback as CallbackMap['window'],
}));
addCleanup(pluginName, () => {
rootStore.set(pluginEditorAtom, items => {
const newItems = { ...items };
delete newItems[pluginName];
return newItems;
});
});
} else if (part === 'setting') {
rootStore.set(settingItemsAtom, items => ({
rootStore.set(pluginSettingAtom, items => ({
...items,
[pluginName]: callback as CallbackMap['setting'],
}));
addCleanup(pluginName, () => {
rootStore.set(pluginSettingAtom, items => {
const newItems = { ...items };
delete newItems[pluginName];
return newItems;
});
});
} else if (part === 'formatBar') {
FormatQuickBar.customElements.push((page, getBlockRange) => {
const div = document.createElement('div');
(callback as CallbackMap['formatBar'])(div, page, getBlockRange);
const cleanup = (callback as CallbackMap['formatBar'])(
div,
page,
getBlockRange
);
addCleanup(pluginName, cleanup);
return div;
});
} else {
@@ -428,5 +535,5 @@ export const evaluatePluginEntry = (pluginName: string) => {
if (typeof cleanup !== 'function') {
throw new Error('Plugin entry must return a function');
}
group.add(cleanup);
addCleanup(pluginName, cleanup);
};
+56 -20
View File
@@ -1,18 +1,16 @@
import { DebugLogger } from '@affine/debug';
import { registeredPluginAtom, rootStore } from '@toeverything/infra/atom';
import {
builtinPluginPaths,
enabledPluginAtom,
invokeCleanup,
pluginPackageJson,
} from '@toeverything/infra/__internal__/plugin';
import { loadedPluginNameAtom, rootStore } from '@toeverything/infra/atom';
import { packageJsonOutputSchema } from '@toeverything/infra/type';
import type { z } from 'zod';
import { evaluatePluginEntry, setupPluginCode } from './plugins/setup';
const builtinPluginUrl = new Set([
'/plugins/bookmark',
'/plugins/copilot',
'/plugins/hello-world',
'/plugins/image-preview',
'/plugins/vue-hello-world',
]);
const logger = new DebugLogger('register-plugins');
declare global {
@@ -20,10 +18,44 @@ declare global {
var __pluginPackageJson__: unknown[];
}
globalThis.__pluginPackageJson__ = [];
Object.defineProperty(globalThis, '__pluginPackageJson__', {
get() {
return rootStore.get(pluginPackageJson);
},
});
rootStore.sub(enabledPluginAtom, () => {
const added = new Set<string>();
const removed = new Set<string>();
const enabledPlugin = new Set(rootStore.get(enabledPluginAtom));
enabledPlugin.forEach(pluginName => {
if (!enabledPluginSet.has(pluginName)) {
added.add(pluginName);
}
});
enabledPluginSet.forEach(pluginName => {
if (!enabledPlugin.has(pluginName)) {
removed.add(pluginName);
}
});
// update plugins
enabledPluginSet.clear();
enabledPlugin.forEach(pluginName => {
enabledPluginSet.add(pluginName);
});
added.forEach(pluginName => {
evaluatePluginEntry(pluginName);
});
removed.forEach(pluginName => {
invokeCleanup(pluginName);
});
});
const enabledPluginSet = new Set(rootStore.get(enabledPluginAtom));
const loadedAssets = new Set<string>();
// we will load all plugins in parallel from builtinPlugins
export const pluginRegisterPromise = Promise.all(
[...builtinPluginUrl].map(url => {
[...builtinPluginPaths].map(url => {
return fetch(`${url}/package.json`)
.then(async res => {
const packageJson = (await res.json()) as z.infer<
@@ -38,28 +70,27 @@ export const pluginRegisterPromise = Promise.all(
assets,
},
} = packageJson;
globalThis.__pluginPackageJson__.push(packageJson);
rootStore.set(pluginPackageJson, json => [...json, packageJson]);
logger.debug(`registering plugin ${pluginName}`);
logger.debug(`package.json: ${packageJson}`);
if (!release && !runtimeConfig.enablePlugin) {
return Promise.resolve();
}
if (
release === 'development' &&
process.env.NODE_ENV !== 'development'
) {
return Promise.resolve();
}
const baseURL = url;
const entryURL = `${baseURL}/${core}`;
rootStore.set(registeredPluginAtom, prev => [...prev, pluginName]);
rootStore.set(loadedPluginNameAtom, prev => [...prev, pluginName]);
await setupPluginCode(baseURL, pluginName, core);
console.log(`prepareImports for ${pluginName} done`);
await fetch(entryURL).then(async () => {
if (assets.length > 0) {
await Promise.all(
assets.map(async (asset: string) => {
// todo(himself65): add assets into shadow dom
if (loadedAssets.has(asset)) {
return Promise.resolve();
}
if (asset.endsWith('.css')) {
loadedAssets.add(asset);
const res = await fetch(`${baseURL}/${asset}`);
if (res.ok) {
// todo: how to put css file into sandbox?
@@ -77,7 +108,12 @@ export const pluginRegisterPromise = Promise.all(
})
);
}
evaluatePluginEntry(pluginName);
if (!enabledPluginSet.has(pluginName)) {
logger.debug(`plugin ${pluginName} is not enabled`);
} else {
logger.debug(`plugin ${pluginName} is enabled`);
evaluatePluginEntry(pluginName);
}
});
})
.catch(e => {