refactor: plugin loading logic (#3448)

This commit is contained in:
Alex Yang
2023-07-28 19:43:52 -07:00
committed by GitHub
parent 4cb1bf6a9f
commit 9f43c0ddc8
14 changed files with 332 additions and 138 deletions

View File

@@ -2,6 +2,7 @@
import 'ses';
import * as AFFiNEComponent from '@affine/component';
import { DebugLogger } from '@affine/debug';
import { FormatQuickBar } from '@blocksuite/blocks';
import * as BlockSuiteBlocksStd from '@blocksuite/blocks/std';
import { DisposableGroup } from '@blocksuite/global/utils';
@@ -40,6 +41,14 @@ if (!process.env.COVERAGE) {
});
}
const builtinPluginUrl = new Set([
'/plugins/bookmark',
'/plugins/copilot',
'/plugins/hello-world',
]);
const logger = new DebugLogger('register-plugins');
const PluginProvider = ({ children }: PropsWithChildren) =>
React.createElement(
Provider,
@@ -142,95 +151,127 @@ const createGlobalThis = () => {
};
const group = new DisposableGroup();
const pluginList = (await (
await fetch(new URL(`./plugins/plugin-list.json`, window.location.origin))
).json()) as { name: string; assets: string[]; release: boolean }[];
declare global {
// eslint-disable-next-line no-var
var __pluginPackageJson__: unknown[];
}
globalThis.__pluginPackageJson__ = [];
interface PluginLoadedEvent extends CustomEvent<{ plugins: unknown[] }> {}
// add to window
declare global {
interface WindowEventMap {
'plugin-loaded': PluginLoadedEvent;
}
}
await Promise.all(
pluginList.map(({ name: plugin, release, assets }) => {
if (!release && process.env.NODE_ENV !== 'development') {
return Promise.resolve();
}
const pluginCompartment = new Compartment(createGlobalThis(), {});
const pluginGlobalThis = pluginCompartment.globalThis;
const baseURL = new URL(`./plugins/${plugin}/`, window.location.origin);
const entryURL = new URL('index.js', baseURL);
rootStore.set(registeredPluginAtom, prev => [...prev, plugin]);
return fetch(entryURL).then(async res => {
if (assets.length > 0) {
await Promise.all(
assets.map(async asset => {
if (asset.endsWith('.css')) {
const res = await fetch(new URL(asset, baseURL));
if (res.ok) {
// todo: how to put css file into sandbox?
return res.text().then(text => {
const style = document.createElement('style');
style.setAttribute('plugin-id', plugin);
style.textContent = text;
document.head.appendChild(style);
});
}
return null;
} else {
return Promise.resolve();
}
})
);
}
const codeText = await res.text();
pluginCompartment.evaluate(codeText, {
__evadeHtmlCommentTest__: true,
});
pluginGlobalThis.__INTERNAL__ENTRY = {
register: (part, callback) => {
if (part === 'headerItem') {
rootStore.set(headerItemsAtom, items => ({
...items,
[plugin]: callback as CallbackMap['headerItem'],
}));
} else if (part === 'editor') {
rootStore.set(editorItemsAtom, items => ({
...items,
[plugin]: callback as CallbackMap['editor'],
}));
} else if (part === 'window') {
rootStore.set(windowItemsAtom, items => ({
...items,
[plugin]: callback as CallbackMap['window'],
}));
} else if (part === 'setting') {
console.log('setting');
rootStore.set(settingItemsAtom, items => ({
...items,
[plugin]: callback as CallbackMap['setting'],
}));
} else if (part === 'formatBar') {
console.log('1');
FormatQuickBar.customElements.push((page, getBlockRange) => {
console.log('2');
const div = document.createElement('div');
(callback as CallbackMap['formatBar'])(div, page, getBlockRange);
return div;
});
} else {
throw new Error(`Unknown part: ${part}`);
[...builtinPluginUrl].map(url => {
return fetch(`${url}/package.json`)
.then(async res => {
const packageJson = await res.json();
const {
name: pluginName,
affinePlugin: {
release,
entry: { core },
assets,
},
} = packageJson;
globalThis.__pluginPackageJson__.push(packageJson);
logger.debug(`registering plugin ${pluginName}`);
logger.debug(`package.json: ${packageJson}`);
if (!release) {
return Promise.resolve();
}
const pluginCompartment = new Compartment(createGlobalThis(), {});
const pluginGlobalThis = pluginCompartment.globalThis;
const baseURL = url;
const entryURL = `${baseURL}/${core}`;
rootStore.set(registeredPluginAtom, prev => [...prev, pluginName]);
await fetch(entryURL).then(async res => {
if (assets.length > 0) {
await Promise.all(
assets.map(async (asset: string) => {
if (asset.endsWith('.css')) {
const res = await fetch(`${baseURL}/${asset}`);
if (res.ok) {
// todo: how to put css file into sandbox?
return res.text().then(text => {
const style = document.createElement('style');
style.setAttribute('plugin-id', pluginName);
style.textContent = text;
document.head.appendChild(style);
});
}
return null;
} else {
return Promise.resolve();
}
})
);
}
},
utils: {
PluginProvider,
},
} satisfies PluginContext;
const dispose = pluginCompartment.evaluate(
'exports.entry(__INTERNAL__ENTRY)'
);
if (typeof dispose !== 'function') {
throw new Error('Plugin entry must return a function');
}
pluginGlobalThis.__INTERNAL__ENTRY = undefined;
group.add(dispose);
});
const codeText = await res.text();
pluginCompartment.evaluate(codeText, {
__evadeHtmlCommentTest__: true,
});
pluginGlobalThis.__INTERNAL__ENTRY = {
register: (part, callback) => {
logger.info(`Registering ${pluginName} to ${part}`);
if (part === 'headerItem') {
rootStore.set(headerItemsAtom, items => ({
...items,
[pluginName]: callback as CallbackMap['headerItem'],
}));
} else if (part === 'editor') {
rootStore.set(editorItemsAtom, items => ({
...items,
[pluginName]: callback as CallbackMap['editor'],
}));
} else if (part === 'window') {
rootStore.set(windowItemsAtom, items => ({
...items,
[pluginName]: callback as CallbackMap['window'],
}));
} else if (part === 'setting') {
rootStore.set(settingItemsAtom, items => ({
...items,
[pluginName]: callback as CallbackMap['setting'],
}));
} else if (part === 'formatBar') {
FormatQuickBar.customElements.push((page, getBlockRange) => {
const div = document.createElement('div');
(callback as CallbackMap['formatBar'])(
div,
page,
getBlockRange
);
return div;
});
} else {
throw new Error(`Unknown part: ${part}`);
}
},
utils: {
PluginProvider,
},
} satisfies PluginContext;
const dispose = pluginCompartment.evaluate(
'exports.entry(__INTERNAL__ENTRY)'
);
if (typeof dispose !== 'function') {
throw new Error('Plugin entry must return a function');
}
pluginGlobalThis.__INTERNAL__ENTRY = undefined;
group.add(dispose);
});
})
.catch(e => {
console.error(`error when fetch plugin from ${url}`, e);
});
})
);
console.log('register plugins finished');
).then(() => {
console.info('All plugins loaded');
});