feat(plugin-infra): add plugin cli (#3344)

This commit is contained in:
Alex Yang
2023-07-22 10:17:40 -07:00
committed by GitHub
parent a494bad543
commit dd31d1e8c6
12 changed files with 244 additions and 135 deletions
-1
View File
@@ -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>
+36 -26
View File
@@ -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
View File
@@ -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",
+2 -1
View File
@@ -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"
-6
View File
@@ -43,12 +43,6 @@ const flags = {
coverage: process.env.COVERAGE === 'true',
} satisfies BuildFlags;
spawn('vite', ['build'], {
cwd: projectRoot,
stdio: 'inherit',
shell: true,
});
spawn(
'node',
[
-6
View File
@@ -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',
[
+16
View File
@@ -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);
+172
View File
@@ -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);
}
},
},
],
});
+16
View File
@@ -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,
});
-92
View File
@@ -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()],
});
+1
View File
@@ -97,6 +97,7 @@ __metadata:
bin:
build-core: ./src/bin/build-core.mjs
dev-core: ./src/bin/dev-core.mjs
dev-plugin: ./src/bin/dev-plugin.mjs
languageName: unknown
linkType: soft