mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-02 02:00:49 +08:00
feat(plugin-infra): add plugin cli (#3344)
This commit is contained in:
@@ -38,7 +38,6 @@
|
||||
rel="shortcut icon"
|
||||
href="https://affine.pro/favicon.ico"
|
||||
/>
|
||||
<link rel="stylesheet" href="/plugins/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -81,9 +81,6 @@ const customRequire = (id: string) => {
|
||||
if (id === 'jotai/utils') {
|
||||
return JotaiUtils;
|
||||
}
|
||||
if (id === '../plugin.js') {
|
||||
return entryCompartment.evaluate('exports');
|
||||
}
|
||||
throw new Error(`Cannot find module '${id}'`);
|
||||
};
|
||||
|
||||
@@ -121,35 +118,48 @@ const createGlobalThis = () => {
|
||||
};
|
||||
|
||||
const group = new DisposableGroup();
|
||||
const pluginList = await (
|
||||
const pluginList = (await (
|
||||
await fetch(new URL(`./plugins/plugin-list.json`, window.location.origin))
|
||||
).json();
|
||||
const builtInPlugins: string[] = pluginList.map((plugin: any) => plugin.name);
|
||||
const pluginGlobalThis = createGlobalThis();
|
||||
const pluginEntry = await fetch('/plugins/plugin.js').then(res => res.text());
|
||||
const entryCompartment = new Compartment(pluginGlobalThis, {});
|
||||
entryCompartment.evaluate(pluginEntry, {
|
||||
__evadeHtmlCommentTest__: true,
|
||||
});
|
||||
).json()) as { name: string; assets: string[]; release: boolean }[];
|
||||
|
||||
await Promise.all(
|
||||
builtInPlugins.map(plugin => {
|
||||
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 packageJsonURL = new URL('package.json', baseURL);
|
||||
return fetch(packageJsonURL).then(async res => {
|
||||
const packageJson = await res.json();
|
||||
const pluginConfig = packageJson['affinePlugin'];
|
||||
if (
|
||||
pluginConfig.release === false &&
|
||||
process.env.NODE_ENV !== 'development'
|
||||
) {
|
||||
return;
|
||||
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(asset => {
|
||||
if (asset.endsWith('.css')) {
|
||||
return fetch(new URL(asset, baseURL)).then(res => {
|
||||
if (res.ok) {
|
||||
// todo: how to put css file into sandbox?
|
||||
return res.text().then(text => {
|
||||
console.log('text', text);
|
||||
const style = document.createElement('style');
|
||||
style.setAttribute('plugin-id', plugin);
|
||||
style.textContent = text;
|
||||
document.head.appendChild(style);
|
||||
});
|
||||
}
|
||||
return null;
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
rootStore.set(registeredPluginAtom, prev => [...prev, plugin]);
|
||||
const coreEntry = new URL(pluginConfig.entry.core, baseURL.toString());
|
||||
const codeText = await fetch(coreEntry).then(res => res.text());
|
||||
pluginCompartment.evaluate(codeText);
|
||||
const codeText = await res.text();
|
||||
pluginCompartment.evaluate(codeText, {
|
||||
__evadeHtmlCommentTest__: true,
|
||||
});
|
||||
pluginGlobalThis.__INTERNAL__ENTRY = {
|
||||
register: (part, callback) => {
|
||||
if (part === 'headerItem') {
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useAtomValue } from 'jotai';
|
||||
export const Plugins = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const allowedPlugins = useAtomValue(registeredPluginAtom);
|
||||
console.log('allowedPlugins', allowedPlugins);
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
|
||||
+1
-2
@@ -19,14 +19,13 @@
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "dev-core",
|
||||
"dev:plugin": "vite build --watch",
|
||||
"dev:electron": "yarn workspace @affine/electron dev:app",
|
||||
"dev:plugins": "./apps/electron/scripts/plugins/dev-plugins.mjs",
|
||||
"build": "yarn nx build @affine/core",
|
||||
"build:electron": "yarn nx build @affine/electron",
|
||||
"build:storage": "yarn nx run-many -t build -p @affine/storage",
|
||||
"build:infra": "yarn nx run-many -t build -p plugin-infra infra",
|
||||
"build:plugins": "yarn workspace @affine/bookmark-block build && yarn vite build",
|
||||
"build:plugins": "yarn workspace @affine/bookmark-block build && node ./scripts/build-plugins.mjs",
|
||||
"build:storybook": "yarn nx build @affine/storybook",
|
||||
"start:web-static": "yarn workspace @affine/core static-server",
|
||||
"start:storybook": "yarn exec serve apps/storybook/storybook-static -l 6006",
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"private": true,
|
||||
"bin": {
|
||||
"build-core": "./src/bin/build-core.mjs",
|
||||
"dev-core": "./src/bin/dev-core.mjs"
|
||||
"dev-core": "./src/bin/dev-core.mjs",
|
||||
"dev-plugin": "./src/bin/dev-plugin.mjs"
|
||||
},
|
||||
"exports": {
|
||||
"./config": "./src/config/index.ts"
|
||||
|
||||
@@ -43,12 +43,6 @@ const flags = {
|
||||
coverage: process.env.COVERAGE === 'true',
|
||||
} satisfies BuildFlags;
|
||||
|
||||
spawn('vite', ['build'], {
|
||||
cwd: projectRoot,
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
});
|
||||
|
||||
spawn(
|
||||
'node',
|
||||
[
|
||||
|
||||
@@ -91,12 +91,6 @@ flags.mode = buildFlags.mode as any;
|
||||
flags.channel = buildFlags.channel as any;
|
||||
flags.coverage = buildFlags.coverage;
|
||||
|
||||
spawn('vite', ['build', '--watch'], {
|
||||
cwd: projectRoot,
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
});
|
||||
|
||||
spawn(
|
||||
'node',
|
||||
[
|
||||
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const child = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
'--loader',
|
||||
'ts-node/esm/transpile-only',
|
||||
fileURLToPath(new URL('./dev-plugin.ts', import.meta.url)),
|
||||
...process.argv.slice(2),
|
||||
],
|
||||
{ stdio: 'inherit' }
|
||||
);
|
||||
|
||||
if (child.status) process.exit(child.status);
|
||||
@@ -0,0 +1,172 @@
|
||||
import { ok } from 'node:assert';
|
||||
import { open, readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { parseArgs } from 'node:util';
|
||||
|
||||
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
|
||||
import react from '@vitejs/plugin-react-swc';
|
||||
import { build } from 'vite';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { projectRoot } from '../config/index.js';
|
||||
|
||||
const args = process.argv.splice(2);
|
||||
|
||||
const result = parseArgs({
|
||||
args,
|
||||
options: {
|
||||
watch: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
plugin: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const plugin = result.values.plugin;
|
||||
|
||||
if (typeof plugin !== 'string') {
|
||||
throw new Error('plugin is required');
|
||||
}
|
||||
|
||||
const isWatch = result.values.watch;
|
||||
ok(typeof isWatch === 'boolean');
|
||||
|
||||
const packageJsonSchema = z.object({
|
||||
name: z.string(),
|
||||
affinePlugin: z.object({
|
||||
release: z.boolean(),
|
||||
entry: z.object({
|
||||
core: z.string(),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
const external = [
|
||||
// built-in packages
|
||||
/^@affine/,
|
||||
/^@blocksuite/,
|
||||
/^@toeverything/,
|
||||
|
||||
// react
|
||||
/^react/,
|
||||
/^react-dom/,
|
||||
|
||||
// store
|
||||
/^jotai/,
|
||||
|
||||
// css
|
||||
/^@vanilla-extract/,
|
||||
];
|
||||
|
||||
const allPluginDir = path.resolve(projectRoot, 'plugins');
|
||||
|
||||
const getPluginDir = (plugin: string) => path.resolve(allPluginDir, plugin);
|
||||
const pluginDir = getPluginDir(plugin);
|
||||
const packageJsonFile = path.resolve(pluginDir, 'package.json');
|
||||
|
||||
const json: z.infer<typeof packageJsonSchema> = await readFile(
|
||||
packageJsonFile,
|
||||
{
|
||||
encoding: 'utf-8',
|
||||
}
|
||||
)
|
||||
.then(text => JSON.parse(text))
|
||||
.then(async json => {
|
||||
const { success } = await packageJsonSchema.safeParseAsync(json);
|
||||
if (success) {
|
||||
return json;
|
||||
} else {
|
||||
throw new Error('invalid package.json');
|
||||
}
|
||||
});
|
||||
|
||||
type Metadata = {
|
||||
assets: Set<string>;
|
||||
};
|
||||
|
||||
const metadata: Metadata = {
|
||||
assets: new Set(),
|
||||
};
|
||||
|
||||
const outDir = path.resolve(
|
||||
projectRoot,
|
||||
'apps',
|
||||
'core',
|
||||
'public',
|
||||
'plugins',
|
||||
plugin
|
||||
);
|
||||
|
||||
const pluginListJsonPath = path.resolve(outDir, '..', 'plugin-list.json');
|
||||
|
||||
const coreEntry = path.resolve(pluginDir, json.affinePlugin.entry.core);
|
||||
|
||||
await build({
|
||||
build: {
|
||||
watch: isWatch ? {} : undefined,
|
||||
minify: false,
|
||||
outDir,
|
||||
emptyOutDir: true,
|
||||
lib: {
|
||||
entry: coreEntry,
|
||||
fileName: 'index',
|
||||
formats: ['cjs'],
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
assetFileNames: chunkInfo => {
|
||||
if (chunkInfo.name) {
|
||||
metadata.assets.add(chunkInfo.name);
|
||||
return chunkInfo.name;
|
||||
} else {
|
||||
throw new Error('no name');
|
||||
}
|
||||
},
|
||||
manualChunks: () => 'plugin',
|
||||
},
|
||||
external,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
vanillaExtractPlugin(),
|
||||
react(),
|
||||
{
|
||||
name: 'generate-list-json',
|
||||
async generateBundle() {
|
||||
const file = await open(pluginListJsonPath, 'as+');
|
||||
const txt = await file.readFile({
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
if (!txt) {
|
||||
console.log('generate plugin-list.json');
|
||||
await file.write(
|
||||
JSON.stringify([
|
||||
{
|
||||
release: json.affinePlugin.release,
|
||||
name: plugin,
|
||||
assets: [...metadata.assets],
|
||||
},
|
||||
])
|
||||
);
|
||||
} else {
|
||||
console.log('modify plugin-list.json');
|
||||
const list = JSON.parse(txt);
|
||||
const index = list.findIndex((item: any) => item.name === plugin);
|
||||
if (index === -1) {
|
||||
list.push({
|
||||
release: json.affinePlugin.release,
|
||||
name: plugin,
|
||||
assets: [...metadata.assets],
|
||||
});
|
||||
} else {
|
||||
list[index].assets = [...metadata.assets];
|
||||
}
|
||||
await file.write(JSON.stringify(list), 0);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
spawn('yarn', ['-T', 'run', 'dev-plugin', '--plugin', 'bookmark'], {
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
});
|
||||
|
||||
spawn('yarn', ['-T', 'run', 'dev-plugin', '--plugin', 'hello-world'], {
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
});
|
||||
|
||||
spawn('yarn', ['-T', 'run', 'dev-plugin', '--plugin', 'copilot'], {
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
import { createRequire } from 'node:module';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
|
||||
import react from '@vitejs/plugin-react-swc';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
const root = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const builtInPlugins = ['hello-world', 'bookmark', 'copilot'];
|
||||
|
||||
const outputJson: [pluginName: string, output: string][] = [];
|
||||
|
||||
const entry = builtInPlugins.reduce(
|
||||
(acc, plugin) => {
|
||||
const packageJson = require(resolve(
|
||||
root,
|
||||
'plugins',
|
||||
plugin,
|
||||
'package.json'
|
||||
));
|
||||
const entry = packageJson.affinePlugin.entry.core;
|
||||
acc[`${plugin}/index`] = resolve(root, 'plugins', plugin, entry);
|
||||
packageJson.affinePlugin.entry.core = './index.js';
|
||||
outputJson.push([plugin, JSON.stringify(packageJson, null, 2)]);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: resolve(root, 'apps', 'core', 'public', 'plugins'),
|
||||
emptyOutDir: true,
|
||||
minify: false,
|
||||
lib: {
|
||||
entry,
|
||||
formats: ['cjs'],
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: () => 'plugin',
|
||||
entryFileNames: '[name].js',
|
||||
chunkFileNames: '[name].js',
|
||||
},
|
||||
external: [
|
||||
// built-in packages
|
||||
/^@affine/,
|
||||
/^@blocksuite/,
|
||||
/^@toeverything/,
|
||||
|
||||
// react
|
||||
/^react/,
|
||||
/^react-dom/,
|
||||
|
||||
// store
|
||||
/^jotai/,
|
||||
|
||||
// css
|
||||
/^@vanilla-extract/,
|
||||
],
|
||||
plugins: [
|
||||
vanillaExtractPlugin(),
|
||||
{
|
||||
name: 'generate-manifest',
|
||||
generateBundle() {
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: `plugin-list.json`,
|
||||
source: JSON.stringify(
|
||||
builtInPlugins.map(plugin => ({
|
||||
name: plugin,
|
||||
}))
|
||||
),
|
||||
});
|
||||
outputJson.forEach(([name, json]) => {
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: `${name}/package.json`,
|
||||
source: json,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
plugins: [react()],
|
||||
});
|
||||
Reference in New Issue
Block a user