feat(plugin-infra): add plugin cli (#3344)
This commit is contained in:
@@ -38,7 +38,6 @@
|
|||||||
rel="shortcut icon"
|
rel="shortcut icon"
|
||||||
href="https://affine.pro/favicon.ico"
|
href="https://affine.pro/favicon.ico"
|
||||||
/>
|
/>
|
||||||
<link rel="stylesheet" href="/plugins/style.css" />
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -81,9 +81,6 @@ const customRequire = (id: string) => {
|
|||||||
if (id === 'jotai/utils') {
|
if (id === 'jotai/utils') {
|
||||||
return JotaiUtils;
|
return JotaiUtils;
|
||||||
}
|
}
|
||||||
if (id === '../plugin.js') {
|
|
||||||
return entryCompartment.evaluate('exports');
|
|
||||||
}
|
|
||||||
throw new Error(`Cannot find module '${id}'`);
|
throw new Error(`Cannot find module '${id}'`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -121,35 +118,48 @@ const createGlobalThis = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const group = new DisposableGroup();
|
const group = new DisposableGroup();
|
||||||
const pluginList = await (
|
const pluginList = (await (
|
||||||
await fetch(new URL(`./plugins/plugin-list.json`, window.location.origin))
|
await fetch(new URL(`./plugins/plugin-list.json`, window.location.origin))
|
||||||
).json();
|
).json()) as { name: string; assets: string[]; release: boolean }[];
|
||||||
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,
|
|
||||||
});
|
|
||||||
await Promise.all(
|
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 pluginCompartment = new Compartment(createGlobalThis(), {});
|
||||||
const pluginGlobalThis = pluginCompartment.globalThis;
|
const pluginGlobalThis = pluginCompartment.globalThis;
|
||||||
const baseURL = new URL(`./plugins/${plugin}/`, window.location.origin);
|
const baseURL = new URL(`./plugins/${plugin}/`, window.location.origin);
|
||||||
const packageJsonURL = new URL('package.json', baseURL);
|
const entryURL = new URL('index.js', baseURL);
|
||||||
return fetch(packageJsonURL).then(async res => {
|
rootStore.set(registeredPluginAtom, prev => [...prev, plugin]);
|
||||||
const packageJson = await res.json();
|
return fetch(entryURL).then(async res => {
|
||||||
const pluginConfig = packageJson['affinePlugin'];
|
if (assets.length > 0) {
|
||||||
if (
|
await Promise.all(
|
||||||
pluginConfig.release === false &&
|
assets.map(asset => {
|
||||||
process.env.NODE_ENV !== 'development'
|
if (asset.endsWith('.css')) {
|
||||||
) {
|
return fetch(new URL(asset, baseURL)).then(res => {
|
||||||
return;
|
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 codeText = await res.text();
|
||||||
const coreEntry = new URL(pluginConfig.entry.core, baseURL.toString());
|
pluginCompartment.evaluate(codeText, {
|
||||||
const codeText = await fetch(coreEntry).then(res => res.text());
|
__evadeHtmlCommentTest__: true,
|
||||||
pluginCompartment.evaluate(codeText);
|
});
|
||||||
pluginGlobalThis.__INTERNAL__ENTRY = {
|
pluginGlobalThis.__INTERNAL__ENTRY = {
|
||||||
register: (part, callback) => {
|
register: (part, callback) => {
|
||||||
if (part === 'headerItem') {
|
if (part === 'headerItem') {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { useAtomValue } from 'jotai';
|
|||||||
export const Plugins = () => {
|
export const Plugins = () => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const allowedPlugins = useAtomValue(registeredPluginAtom);
|
const allowedPlugins = useAtomValue(registeredPluginAtom);
|
||||||
console.log('allowedPlugins', allowedPlugins);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingHeader
|
<SettingHeader
|
||||||
|
|||||||
@@ -19,14 +19,13 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "dev-core",
|
"dev": "dev-core",
|
||||||
"dev:plugin": "vite build --watch",
|
|
||||||
"dev:electron": "yarn workspace @affine/electron dev:app",
|
"dev:electron": "yarn workspace @affine/electron dev:app",
|
||||||
"dev:plugins": "./apps/electron/scripts/plugins/dev-plugins.mjs",
|
"dev:plugins": "./apps/electron/scripts/plugins/dev-plugins.mjs",
|
||||||
"build": "yarn nx build @affine/core",
|
"build": "yarn nx build @affine/core",
|
||||||
"build:electron": "yarn nx build @affine/electron",
|
"build:electron": "yarn nx build @affine/electron",
|
||||||
"build:storage": "yarn nx run-many -t build -p @affine/storage",
|
"build:storage": "yarn nx run-many -t build -p @affine/storage",
|
||||||
"build:infra": "yarn nx run-many -t build -p plugin-infra infra",
|
"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",
|
"build:storybook": "yarn nx build @affine/storybook",
|
||||||
"start:web-static": "yarn workspace @affine/core static-server",
|
"start:web-static": "yarn workspace @affine/core static-server",
|
||||||
"start:storybook": "yarn exec serve apps/storybook/storybook-static -l 6006",
|
"start:storybook": "yarn exec serve apps/storybook/storybook-static -l 6006",
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"build-core": "./src/bin/build-core.mjs",
|
"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": {
|
"exports": {
|
||||||
"./config": "./src/config/index.ts"
|
"./config": "./src/config/index.ts"
|
||||||
|
|||||||
@@ -43,12 +43,6 @@ const flags = {
|
|||||||
coverage: process.env.COVERAGE === 'true',
|
coverage: process.env.COVERAGE === 'true',
|
||||||
} satisfies BuildFlags;
|
} satisfies BuildFlags;
|
||||||
|
|
||||||
spawn('vite', ['build'], {
|
|
||||||
cwd: projectRoot,
|
|
||||||
stdio: 'inherit',
|
|
||||||
shell: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
spawn(
|
spawn(
|
||||||
'node',
|
'node',
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -91,12 +91,6 @@ flags.mode = buildFlags.mode as any;
|
|||||||
flags.channel = buildFlags.channel as any;
|
flags.channel = buildFlags.channel as any;
|
||||||
flags.coverage = buildFlags.coverage;
|
flags.coverage = buildFlags.coverage;
|
||||||
|
|
||||||
spawn('vite', ['build', '--watch'], {
|
|
||||||
cwd: projectRoot,
|
|
||||||
stdio: 'inherit',
|
|
||||||
shell: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
spawn(
|
spawn(
|
||||||
'node',
|
'node',
|
||||||
[
|
[
|
||||||
|
|||||||
16
packages/cli/src/bin/dev-plugin.mjs
Executable file
16
packages/cli/src/bin/dev-plugin.mjs
Executable 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
packages/cli/src/bin/dev-plugin.ts
Normal file
172
packages/cli/src/bin/dev-plugin.ts
Normal 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
scripts/build-plugins.mjs
Normal file
16
scripts/build-plugins.mjs
Normal 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,
|
||||||
|
});
|
||||||
@@ -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()],
|
|
||||||
});
|
|
||||||
@@ -97,6 +97,7 @@ __metadata:
|
|||||||
bin:
|
bin:
|
||||||
build-core: ./src/bin/build-core.mjs
|
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
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user