mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 14:56:59 +08:00
feat: isolated plugin system (#2742)
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"@toeverything/hooks": "workspace:*",
|
||||
"@toeverything/plugin-infra": "workspace:*",
|
||||
"@toeverything/theme": "^0.6.1",
|
||||
"@vanilla-extract/dynamic": "^2.0.3",
|
||||
"clsx": "^1.2.1",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ProviderComposer } from '@affine/component/provider-composer';
|
||||
import { ThemeProvider } from '@affine/component/theme-provider';
|
||||
import { rootStore } from '@affine/workspace/atom';
|
||||
import { rootStore } from '@toeverything/plugin-infra/manager';
|
||||
import { Provider } from 'jotai';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function AffinePluginContext(props: PropsWithChildren) {
|
||||
export function AffineContext(props: PropsWithChildren) {
|
||||
return (
|
||||
<ProviderComposer
|
||||
contexts={useMemo(
|
||||
@@ -20,10 +20,10 @@
|
||||
{
|
||||
"path": "../hooks"
|
||||
},
|
||||
{ "path": "../workspace" },
|
||||
{
|
||||
"path": "../../apps/electron"
|
||||
"path": "../plugin-infra"
|
||||
},
|
||||
{ "path": "../workspace" },
|
||||
{ "path": "../../tests/fixtures" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// to prevent the `@affine/components` contains circular references with `@affine/workspace`
|
||||
// the include files should be excluded in `./tsconfig.json`
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": false,
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"./src/components/page-list/filter/shared-types.tsx",
|
||||
"./src/components/page-list/filter/logical/custom-type.ts",
|
||||
"./src/components/page-list/filter/logical/matcher.ts",
|
||||
"./src/components/page-list/filter/logical/typesystem.ts"
|
||||
],
|
||||
"references": [{ "path": "../env" }],
|
||||
"exclude": ["lib"]
|
||||
}
|
||||
@@ -1,18 +1,23 @@
|
||||
{
|
||||
"name": "@toeverything/plugin-infra",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build"
|
||||
"build": "vite build",
|
||||
"dev": "vite build --watch"
|
||||
},
|
||||
"exports": {
|
||||
"./manager": "./src/manager.ts",
|
||||
"./type": "./src/type.ts",
|
||||
"./react": "./src/react/index.ts"
|
||||
"./manager": {
|
||||
"type": "./dist/manager.d.ts",
|
||||
"import": "./dist/manager.js",
|
||||
"require": "./dist/manager.cjs"
|
||||
},
|
||||
"./type": {
|
||||
"type": "./dist/type.d.ts",
|
||||
"import": "./dist/type.js",
|
||||
"require": "./dist/type.cjs"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/component": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
"@affine/workspace": "workspace:*",
|
||||
"@blocksuite/blocks": "0.0.0-20230607055421-9b20fcaf-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230607055421-9b20fcaf-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230607055421-9b20fcaf-nightly",
|
||||
@@ -20,7 +25,9 @@
|
||||
"@blocksuite/store": "0.0.0-20230607055421-9b20fcaf-nightly"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jotai": "^2.1.1"
|
||||
"jotai": "^2.1.1",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-dts": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@blocksuite/blocks": "*",
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { rootStore } from '@affine/workspace/atom';
|
||||
import { atom } from 'jotai';
|
||||
import { atom, createStore } from 'jotai/vanilla';
|
||||
|
||||
import type { AffinePlugin, Definition } from './type';
|
||||
import type { AffinePlugin, Definition, ServerAdapter } from './type';
|
||||
import type { Loader, PluginUIAdapter } from './type';
|
||||
import type { PluginBlockSuiteAdapter } from './type';
|
||||
|
||||
const isServer = typeof window === 'undefined';
|
||||
const isClient = typeof window !== 'undefined';
|
||||
|
||||
// global store
|
||||
export const rootStore = createStore();
|
||||
|
||||
// todo: for now every plugin is enabled by default
|
||||
export const affinePluginsAtom = atom<Record<string, AffinePlugin<string>>>({});
|
||||
|
||||
const pluginLogger = new DebugLogger('affine:plugin');
|
||||
|
||||
export function definePlugin<ID extends string>(
|
||||
definition: Definition<ID>,
|
||||
uiAdapterLoader?: Loader<Partial<PluginUIAdapter>>,
|
||||
blockSuiteAdapter?: Loader<Partial<PluginBlockSuiteAdapter>>
|
||||
blockSuiteAdapter?: Loader<Partial<PluginBlockSuiteAdapter>>,
|
||||
serverAdapter?: Loader<ServerAdapter>
|
||||
) {
|
||||
const basePlugin = {
|
||||
definition,
|
||||
@@ -27,57 +30,70 @@ export function definePlugin<ID extends string>(
|
||||
[definition.id]: basePlugin,
|
||||
}));
|
||||
|
||||
if (blockSuiteAdapter) {
|
||||
const updateAdapter = (adapter: Partial<PluginBlockSuiteAdapter>) => {
|
||||
rootStore.set(affinePluginsAtom, plugins => ({
|
||||
...plugins,
|
||||
[definition.id]: {
|
||||
...basePlugin,
|
||||
blockSuiteAdapter: adapter,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
blockSuiteAdapter
|
||||
.load()
|
||||
.then(({ default: adapter }) => updateAdapter(adapter))
|
||||
.catch(err => {
|
||||
pluginLogger.error('[definePlugin] blockSuiteAdapter error', err);
|
||||
});
|
||||
|
||||
if (import.meta.webpackHot) {
|
||||
blockSuiteAdapter.hotModuleReload(async _ => {
|
||||
const adapter = (await _).default;
|
||||
updateAdapter(adapter);
|
||||
pluginLogger.info('[HMR] Plugin', definition.id, 'hot reloaded.');
|
||||
if (isServer) {
|
||||
if (serverAdapter) {
|
||||
serverAdapter.load().then(({ default: adapter }) => {
|
||||
rootStore.set(affinePluginsAtom, plugins => ({
|
||||
...plugins,
|
||||
[definition.id]: {
|
||||
...basePlugin,
|
||||
serverAdapter: adapter,
|
||||
},
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (isClient) {
|
||||
if (blockSuiteAdapter) {
|
||||
const updateAdapter = (adapter: Partial<PluginBlockSuiteAdapter>) => {
|
||||
rootStore.set(affinePluginsAtom, plugins => ({
|
||||
...plugins,
|
||||
[definition.id]: {
|
||||
...basePlugin,
|
||||
blockSuiteAdapter: adapter,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
if (uiAdapterLoader) {
|
||||
const updateAdapter = (adapter: Partial<PluginUIAdapter>) => {
|
||||
rootStore.set(affinePluginsAtom, plugins => ({
|
||||
...plugins,
|
||||
[definition.id]: {
|
||||
...basePlugin,
|
||||
uiAdapter: adapter,
|
||||
},
|
||||
}));
|
||||
};
|
||||
blockSuiteAdapter
|
||||
.load()
|
||||
.then(({ default: adapter }) => updateAdapter(adapter))
|
||||
.catch(err => {
|
||||
console.error('[definePlugin] blockSuiteAdapter error', err);
|
||||
});
|
||||
|
||||
uiAdapterLoader
|
||||
.load()
|
||||
.then(({ default: adapter }) => updateAdapter(adapter))
|
||||
.catch(err => {
|
||||
pluginLogger.error('[definePlugin] blockSuiteAdapter error', err);
|
||||
});
|
||||
if (import.meta.webpackHot) {
|
||||
blockSuiteAdapter.hotModuleReload(async _ => {
|
||||
const adapter = (await _).default;
|
||||
updateAdapter(adapter);
|
||||
console.info('[HMR] Plugin', definition.id, 'hot reloaded.');
|
||||
});
|
||||
}
|
||||
}
|
||||
if (uiAdapterLoader) {
|
||||
const updateAdapter = (adapter: Partial<PluginUIAdapter>) => {
|
||||
rootStore.set(affinePluginsAtom, plugins => ({
|
||||
...plugins,
|
||||
[definition.id]: {
|
||||
...basePlugin,
|
||||
uiAdapter: adapter,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
if (import.meta.webpackHot) {
|
||||
uiAdapterLoader.hotModuleReload(async _ => {
|
||||
const adapter = (await _).default;
|
||||
updateAdapter(adapter);
|
||||
pluginLogger.info('[HMR] Plugin', definition.id, 'hot reloaded.');
|
||||
});
|
||||
uiAdapterLoader
|
||||
.load()
|
||||
.then(({ default: adapter }) => updateAdapter(adapter))
|
||||
.catch(err => {
|
||||
console.error('[definePlugin] blockSuiteAdapter error', err);
|
||||
});
|
||||
|
||||
if (import.meta.webpackHot) {
|
||||
uiAdapterLoader.hotModuleReload(async _ => {
|
||||
const adapter = (await _).default;
|
||||
updateAdapter(adapter);
|
||||
console.info('[HMR] Plugin', definition.id, 'hot reloaded.');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './context';
|
||||
@@ -141,6 +141,11 @@ export type Definition<ID extends string> = {
|
||||
* @example ReleaseStage.PROD
|
||||
*/
|
||||
stage: ReleaseStage;
|
||||
|
||||
/**
|
||||
* Registered commands
|
||||
*/
|
||||
commands: string[];
|
||||
};
|
||||
|
||||
// todo(himself65): support Vue.js
|
||||
@@ -171,12 +176,16 @@ export type PluginBlockSuiteAdapter = {
|
||||
uiDecorator: (root: EditorContainer) => Cleanup;
|
||||
};
|
||||
|
||||
export type PluginAdapterCreator = (
|
||||
context: AffinePluginContext
|
||||
) => PluginUIAdapter;
|
||||
type AFFiNEServer = {
|
||||
registerCommand: (command: string, fn: (...args: any[]) => any) => void;
|
||||
unregisterCommand: (command: string) => void;
|
||||
};
|
||||
|
||||
export type ServerAdapter = (affine: AFFiNEServer) => () => void;
|
||||
|
||||
export type AffinePlugin<ID extends string> = {
|
||||
definition: Definition<ID>;
|
||||
uiAdapter: Partial<PluginUIAdapter>;
|
||||
blockSuiteAdapter: Partial<PluginBlockSuiteAdapter>;
|
||||
serverAdapter?: ServerAdapter;
|
||||
};
|
||||
|
||||
@@ -8,10 +8,7 @@
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../component"
|
||||
},
|
||||
{
|
||||
"path": "../workspace"
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
10
packages/plugin-infra/tsconfig.node.json
Normal file
10
packages/plugin-infra/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
35
packages/plugin-infra/vite.config.ts
Normal file
35
packages/plugin-infra/vite.config.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
import { defineConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
|
||||
const root = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
minify: false,
|
||||
lib: {
|
||||
entry: {
|
||||
type: resolve(root, 'src/type.ts'),
|
||||
manager: resolve(root, 'src/manager.ts'),
|
||||
},
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [
|
||||
'jotai',
|
||||
'jotai/vanilla',
|
||||
'@blocksuite/blocks',
|
||||
'@blocksuite/store',
|
||||
'@blocksuite/global',
|
||||
'@blocksuite/editor',
|
||||
'@blocksuite/lit',
|
||||
],
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
dts({
|
||||
insertTypesEntry: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -22,6 +22,7 @@
|
||||
"@affine/debug": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
"@toeverything/hooks": "workspace:*",
|
||||
"@toeverything/plugin-infra": "workspace:*",
|
||||
"@toeverything/y-indexeddb": "workspace:*",
|
||||
"firebase": "^9.22.1",
|
||||
"jotai": "^2.1.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { prefixUrl } from '@affine/env';
|
||||
import { rootStore } from '@toeverything/plugin-infra/manager';
|
||||
|
||||
import { rootStore } from '../atom';
|
||||
import { createUserApis, createWorkspaceApis } from './api/index';
|
||||
import { currentAffineUserAtom } from './atom';
|
||||
import type { LoginResponse } from './login';
|
||||
|
||||
@@ -8,11 +8,12 @@ import {
|
||||
} from '@affine/env/workspace/legacy-cloud';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Disposable } from '@blocksuite/store';
|
||||
import { rootStore } from '@toeverything/plugin-infra/manager';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { WebsocketClient } from '../affine/channel';
|
||||
import { storageChangeSlot } from '../affine/login';
|
||||
import { rootStore, rootWorkspacesMetadataAtom } from '../atom';
|
||||
import { rootWorkspacesMetadataAtom } from '../atom';
|
||||
|
||||
const logger = new DebugLogger('affine-sync');
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import { atom, createStore } from 'jotai';
|
||||
import { atomWithStorage, createJSONStorage } from 'jotai/utils';
|
||||
import { atom } from 'jotai';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
import Router from 'next/router';
|
||||
|
||||
export type RootWorkspaceMetadata = {
|
||||
@@ -77,13 +77,3 @@ export const rootCurrentEditorAtom = atom<Readonly<EditorContainer> | null>(
|
||||
null
|
||||
);
|
||||
//#endregion
|
||||
|
||||
const getStorage = () => createJSONStorage(() => localStorage);
|
||||
|
||||
export const getStoredWorkspaceMeta = () => {
|
||||
const storage = getStorage();
|
||||
return storage.getItem('jotai-workspaces', []) as RootWorkspaceMetadata[];
|
||||
};
|
||||
|
||||
// global store
|
||||
export const rootStore = createStore();
|
||||
|
||||
@@ -4,9 +4,10 @@ import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
||||
import type { Generator, StoreOptions } from '@blocksuite/store';
|
||||
import { createIndexeddbStorage, Workspace } from '@blocksuite/store';
|
||||
import { rootStore } from '@toeverything/plugin-infra/manager';
|
||||
|
||||
import type { createWorkspaceApis } from './affine/api';
|
||||
import { rootStore, rootWorkspacesMetadataAtom } from './atom';
|
||||
import { rootWorkspacesMetadataAtom } from './atom';
|
||||
import { createAffineBlobStorage } from './blob';
|
||||
import { createSQLiteStorage } from './blob/sqlite-blob-storage';
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
{ "path": "../y-indexeddb" },
|
||||
{ "path": "../env" },
|
||||
{ "path": "../debug" },
|
||||
{ "path": "../hooks" }
|
||||
{ "path": "../hooks" },
|
||||
{ "path": "../plugin-infra" }
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user