feat: init new plugin system (#3323)

This commit is contained in:
Alex Yang
2023-07-20 18:52:29 +08:00
committed by GitHub
parent 604b53d9a4
commit 19055baa49
45 changed files with 768 additions and 389 deletions

View File

@@ -12,3 +12,5 @@ tests/affine-legacy/0.7.0-canary.18/static
.github/helm .github/helm
_next _next
storybook-static storybook-static
web-static
public

View File

@@ -276,12 +276,13 @@ export const createConfiguration: (
devServer: { devServer: {
hot: 'only', hot: 'only',
liveReload: false, liveReload: true,
client: undefined, client: undefined,
historyApiFallback: true, historyApiFallback: true,
static: { static: {
directory: resolve(rootPath, 'public'), directory: resolve(rootPath, 'public'),
publicPath: '/', publicPath: '/',
watch: true,
}, },
} as DevServerConfiguration, } as DevServerConfiguration,
} satisfies webpack.Configuration; } satisfies webpack.Configuration;

View File

@@ -38,6 +38,7 @@
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>

View File

@@ -10,7 +10,6 @@
}, },
"dependencies": { "dependencies": {
"@affine-test/fixtures": "workspace:*", "@affine-test/fixtures": "workspace:*",
"@affine/bookmark-block": "workspace:*",
"@affine/component": "workspace:*", "@affine/component": "workspace:*",
"@affine/copilot": "workspace:*", "@affine/copilot": "workspace:*",
"@affine/debug": "workspace:*", "@affine/debug": "workspace:*",
@@ -53,6 +52,7 @@
"react-resizable-panels": "^0.0.53", "react-resizable-panels": "^0.0.53",
"react-router-dom": "^6.14.1", "react-router-dom": "^6.14.1",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"ses": "^0.18.5",
"swr": "^2.1.5", "swr": "^2.1.5",
"y-protocols": "^1.0.5", "y-protocols": "^1.0.5",
"yjs": "^13.6.6", "yjs": "^13.6.6",

View File

@@ -45,7 +45,7 @@
"options": { "options": {
"script": "build" "script": "build"
}, },
"outputs": ["{projectRoot}/dist"] "outputs": ["{projectRoot}/dist", "{projectRoot}/public/plugins"]
} }
} }
} }

View File

@@ -1,2 +1,4 @@
*.js *.js
*.map *.map
plugins

View File

@@ -48,8 +48,6 @@ currentWorkspaceIdAtom.onMount = set => {
if (value) { if (value) {
set(value); set(value);
localStorage.setItem('last_workspace_id', value); localStorage.setItem('last_workspace_id', value);
} else {
set(null);
} }
}; };
callback(router.state); callback(router.state);
@@ -65,8 +63,6 @@ currentPageIdAtom.onMount = set => {
const value = state.location.pathname.split('/')[3]; const value = state.location.pathname.split('/')[3];
if (value) { if (value) {
set(value); set(value);
} else {
set(null);
} }
}; };
callback(router.state); callback(router.state);

View File

@@ -1,34 +0,0 @@
import type { ExpectedLayout } from '@toeverything/plugin-infra/type';
import { atom } from 'jotai';
export const contentLayoutBaseAtom = atom<ExpectedLayout>('editor');
type SetStateAction<Value> = Value | ((prev: Value) => Value);
export const contentLayoutAtom = atom<
ExpectedLayout,
[SetStateAction<ExpectedLayout>],
void
>(
get => get(contentLayoutBaseAtom),
(get, set, layout) => {
set(contentLayoutBaseAtom, prev => {
let setV: (prev: ExpectedLayout) => ExpectedLayout;
if (typeof layout !== 'function') {
setV = () => layout;
} else {
setV = layout;
}
const nextValue = setV(prev);
if (nextValue === 'editor') {
return nextValue;
}
if (nextValue.first !== 'editor') {
throw new Error('The first element of the layout should be editor.');
}
if (nextValue.splitPercentage && nextValue.splitPercentage < 70) {
throw new Error('The split percentage should be greater than 70.');
}
return nextValue;
});
}
);

View File

@@ -1,6 +1,181 @@
import('@affine/bookmark-block'); /// <reference types="@types/webpack-env" />
if (runtimeConfig.enablePlugin) { import 'ses';
import('@affine/copilot');
} import * as AFFiNEComponent from '@affine/component';
import * as BlockSuiteBlocksStd from '@blocksuite/blocks/std';
import { DisposableGroup } from '@blocksuite/global/utils';
import * as BlockSuiteGlobalUtils from '@blocksuite/global/utils';
import * as Icons from '@blocksuite/icons';
import type {
CallbackMap,
PluginContext,
} from '@toeverything/plugin-infra/entry';
import * as Manager from '@toeverything/plugin-infra/manager';
import {
editorItemsAtom,
headerItemsAtom,
registeredPluginAtom,
rootStore,
windowItemsAtom,
} from '@toeverything/plugin-infra/manager';
import * as Jotai from 'jotai';
import { Provider } from 'jotai/react';
import * as JotaiUtils from 'jotai/utils';
import type { PropsWithChildren } from 'react';
import * as React from 'react';
import * as ReactJSXRuntime from 'react/jsx-runtime';
import * as ReactDom from 'react-dom';
import * as ReactDomClient from 'react-dom/client';
const PluginProvider = ({ children }: PropsWithChildren) =>
React.createElement(
Provider,
{
store: rootStore,
},
children
);
console.log('JotaiUtils', JotaiUtils);
const customRequire = (id: string) => {
if (id === '@toeverything/plugin-infra/manager') {
return harden(Manager);
}
if (id === 'react') {
return React;
}
if (id === 'react/jsx-runtime') {
return ReactJSXRuntime;
}
if (id === 'react-dom') {
return ReactDom;
}
if (id === 'react-dom/client') {
return ReactDomClient;
}
if (id === '@blocksuite/icons') {
return harden(Icons);
}
if (id === '@affine/component') {
return harden(AFFiNEComponent);
}
if (id === '@blocksuite/blocks/std') {
return harden(BlockSuiteBlocksStd);
}
if (id === '@blocksuite/global/utils') {
return harden(BlockSuiteGlobalUtils);
}
if (id === 'jotai') {
return harden(Jotai);
}
if (id === 'jotai/utils') {
return harden(JotaiUtils);
}
if (id === '../plugin.js') {
return entryCompartment.evaluate('exports');
}
throw new Error(`Cannot find module '${id}'`);
};
const createGlobalThis = () => {
return {
process: harden({
env: {
NODE_ENV: process.env.NODE_ENV,
},
}),
// UNSAFE: React will read `window` and `document`
window,
document,
navigator,
userAgent: navigator.userAgent,
// fixme: use our own db api
indexedDB: globalThis.indexedDB,
IDBRequest: globalThis.IDBRequest,
IDBDatabase: globalThis.IDBDatabase,
IDBCursorWithValue: globalThis.IDBCursorWithValue,
IDBFactory: globalThis.IDBFactory,
IDBKeyRange: globalThis.IDBKeyRange,
IDBOpenDBRequest: globalThis.IDBOpenDBRequest,
IDBTransaction: globalThis.IDBTransaction,
IDBObjectStore: globalThis.IDBObjectStore,
IDBIndex: globalThis.IDBIndex,
IDBCursor: globalThis.IDBCursor,
IDBVersionChangeEvent: globalThis.IDBVersionChangeEvent,
exports: {},
console: globalThis.console,
require: customRequire,
};
};
const group = new DisposableGroup();
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,
});
await Promise.all(
builtInPlugins.map(plugin => {
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;
}
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);
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 {
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);
});
})
);
console.log('register plugins finished'); console.log('register plugins finished');

View File

@@ -1,22 +1,25 @@
import { SettingHeader } from '@affine/component/setting-components'; import {
SettingHeader,
SettingWrapper,
} from '@affine/component/setting-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { affinePluginsAtom } from '@toeverything/plugin-infra/manager'; import { registeredPluginAtom } from '@toeverything/plugin-infra/manager';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
export const Plugins = () => { export const Plugins = () => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const plugins = useAtomValue(affinePluginsAtom); const allowedPlugins = useAtomValue(registeredPluginAtom);
console.log('allowedPlugins', allowedPlugins);
return ( return (
<> <>
<SettingHeader <SettingHeader
title={'Plugins'} title={'Plugins'}
subtitle={t['None yet']()} subtitle={allowedPlugins.length === 0 && t['None yet']()}
data-testid="plugins-title" data-testid="plugins-title"
/> />
{Object.values(plugins).map(({ definition, uiAdapter }) => { {allowedPlugins.map(plugin => (
const Content = uiAdapter.debugContent; <SettingWrapper key={plugin} title={plugin}></SettingWrapper>
return <div key={definition.id}>{Content && <Content />}</div>; ))}
})}
</> </>
); );
}; };

View File

@@ -7,20 +7,18 @@ import { SidebarSwitch } from '@affine/component/app-sidebar/sidebar-header';
import { isDesktop } from '@affine/env/constant'; import { isDesktop } from '@affine/env/constant';
import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons'; import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
import { affinePluginsAtom } from '@toeverything/plugin-infra/manager'; import { headerItemsAtom } from '@toeverything/plugin-infra/manager';
import type { PluginUIAdapter } from '@toeverything/plugin-infra/type';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import type { FC, HTMLAttributes, PropsWithChildren, ReactNode } from 'react'; import type { FC, HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
import { import {
forwardRef, forwardRef,
memo,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
useRef,
useState, useState,
} from 'react'; } from 'react';
import { contentLayoutAtom } from '../../../atoms/layout';
import { currentModeAtom } from '../../../atoms/mode'; import { currentModeAtom } from '../../../atoms/mode';
import type { AffineOfficialWorkspace } from '../../../shared'; import type { AffineOfficialWorkspace } from '../../../shared';
import DownloadClientTip from './download-tips'; import DownloadClientTip from './download-tips';
@@ -124,41 +122,37 @@ const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
export type HeaderProps = BaseHeaderProps; export type HeaderProps = BaseHeaderProps;
const PluginHeaderItemAdapter = memo<{
headerItem: PluginUIAdapter['headerItem'];
}>(function PluginHeaderItemAdapter({ headerItem }) {
return (
<div>
{headerItem({
contentLayoutAtom,
})}
</div>
);
});
const PluginHeader = () => { const PluginHeader = () => {
const affinePluginsMap = useAtomValue(affinePluginsAtom); const rootRef = useRef<HTMLDivElement>(null);
const plugins = useMemo( const headerItems = useAtomValue(headerItemsAtom);
() => Object.values(affinePluginsMap), useEffect(() => {
[affinePluginsMap] const root = rootRef.current;
); if (!root) {
return;
}
let disposes: (() => void)[] = [];
const renderTimeout = setTimeout(() => {
disposes = Object.entries(headerItems).map(([id, headerItem]) => {
const div = document.createElement('div');
div.setAttribute('plugin-id', id);
const cleanup = headerItem(div);
root.appendChild(div);
return () => {
cleanup();
root.removeChild(div);
};
});
});
return ( return () => {
<div> clearTimeout(renderTimeout);
{plugins setTimeout(() => {
.filter(plugin => plugin.uiAdapter.headerItem != null) disposes.forEach(dispose => dispose());
.map(plugin => { });
const headerItem = plugin.uiAdapter };
.headerItem as PluginUIAdapter['headerItem']; }, [headerItems]);
return (
<PluginHeaderItemAdapter return <div className={styles.pluginHeaderItems} ref={rootRef} />;
key={plugin.definition.id}
headerItem={headerItem}
/>
);
})}
</div>
);
}; };
export const Header = forwardRef< export const Header = forwardRef<

View File

@@ -238,3 +238,10 @@ export const windowAppControl = style({
}, },
}, },
} as ComplexStyleRule); } as ComplexStyleRule);
export const pluginHeaderItems = style({
display: 'flex',
gap: '12px',
alignItems: 'center',
height: '100%',
});

View File

@@ -7,21 +7,22 @@ import { assertExists } from '@blocksuite/global/utils';
import type { Page, Workspace } from '@blocksuite/store'; import type { Page, Workspace } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta'; import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page'; import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
import { affinePluginsAtom } from '@toeverything/plugin-infra/manager'; import type { CallbackMap } from '@toeverything/plugin-infra/entry';
import type { import {
AffinePlugin, affinePluginsAtom,
LayoutNode, contentLayoutAtom,
PluginUIAdapter, editorItemsAtom,
} from '@toeverything/plugin-infra/type'; rootStore,
import type { PluginBlockSuiteAdapter } from '@toeverything/plugin-infra/type'; windowItemsAtom,
} from '@toeverything/plugin-infra/manager';
import type { AffinePlugin, LayoutNode } from '@toeverything/plugin-infra/type';
import clsx from 'clsx'; import clsx from 'clsx';
import { useAtomValue, useSetAtom } from 'jotai'; import { useAtomValue, useSetAtom } from 'jotai';
import type { CSSProperties, FC, ReactElement } from 'react'; import type { CSSProperties, FC, ReactElement } from 'react';
import { memo, Suspense, useCallback, useMemo } from 'react'; import { memo, Suspense, useCallback, useEffect, useMemo, useRef } from 'react';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import { pageSettingFamily } from '../atoms'; import { pageSettingFamily } from '../atoms';
import { contentLayoutAtom } from '../atoms/layout';
import { fontStyleOptions, useAppSetting } from '../atoms/settings'; import { fontStyleOptions, useAppSetting } from '../atoms/settings';
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor'; import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
import * as styles from './page-detail-editor.css'; import * as styles from './page-detail-editor.css';
@@ -42,11 +43,6 @@ const EditorWrapper = memo(function EditorWrapper({
onLoad, onLoad,
isPublic, isPublic,
}: PageDetailEditorProps) { }: PageDetailEditorProps) {
const affinePluginsMap = useAtomValue(affinePluginsAtom);
const plugins = useMemo(
() => Object.values(affinePluginsMap),
[affinePluginsMap]
);
const page = useBlockSuiteWorkspacePage(workspace, pageId); const page = useBlockSuiteWorkspacePage(workspace, pageId);
if (!page) { if (!page) {
throw new PageNotFoundError(workspace, pageId); throw new PageNotFoundError(workspace, pageId);
@@ -100,33 +96,65 @@ const EditorWrapper = memo(function EditorWrapper({
if (onLoad) { if (onLoad) {
dispose = onLoad(page, editor); dispose = onLoad(page, editor);
} }
const uiDecorators = plugins const editorItems = rootStore.get(editorItemsAtom);
.map(plugin => plugin.blockSuiteAdapter.uiDecorator) let disposes: (() => void)[] = [];
.filter((ui): ui is PluginBlockSuiteAdapter['uiDecorator'] => const renderTimeout = setTimeout(() => {
Boolean(ui) disposes = Object.entries(editorItems).map(([id, editorItem]) => {
); const div = document.createElement('div');
const disposes = uiDecorators.map(ui => ui(editor)); div.setAttribute('plugin-id', id);
const cleanup = editorItem(div, editor);
assertExists(parent);
document.body.appendChild(div);
return () => {
cleanup();
document.body.removeChild(div);
};
});
});
return () => { return () => {
disposes.forEach(fn => fn());
dispose(); dispose();
clearTimeout(renderTimeout);
setTimeout(() => {
disposes.forEach(dispose => dispose());
});
}; };
}, },
[plugins, onLoad] [onLoad]
)} )}
/> />
); );
}); });
const PluginContentAdapter = memo<{ const PluginContentAdapter = memo<{
detailContent: PluginUIAdapter['detailContent']; windowItem: CallbackMap['window'];
}>(function PluginContentAdapter({ detailContent }) { }>(function PluginContentAdapter({ windowItem }) {
return ( const ref = useRef<HTMLDivElement>(null);
<div className={pluginContainer}> useEffect(() => {
{detailContent({ const root = ref.current;
contentLayoutAtom, if (!root) {
})} return;
</div> }
); let cleanup: () => void = () => {};
let childDiv: HTMLDivElement | null = null;
const renderTimeout = setTimeout(() => {
const div = document.createElement('div');
cleanup = windowItem(div);
root.appendChild(div);
childDiv = div;
});
return () => {
clearTimeout(renderTimeout);
setTimeout(() => {
cleanup();
if (childDiv) {
root.removeChild(childDiv);
}
});
};
}, [windowItem]);
return <div className={pluginContainer} ref={ref} />;
}); });
type LayoutPanelProps = { type LayoutPanelProps = {
@@ -139,16 +167,13 @@ const LayoutPanel = memo(function LayoutPanel(
props: LayoutPanelProps props: LayoutPanelProps
): ReactElement { ): ReactElement {
const node = props.node; const node = props.node;
const windowItems = useAtomValue(windowItemsAtom);
if (typeof node === 'string') { if (typeof node === 'string') {
if (node === 'editor') { if (node === 'editor') {
return <EditorWrapper {...props.editorProps} />; return <EditorWrapper {...props.editorProps} />;
} else { } else {
const plugin = props.plugins.find( const windowItem = windowItems[node];
plugin => plugin.definition.id === node return <PluginContentAdapter windowItem={windowItem} />;
);
const Content = plugin?.uiAdapter.detailContent;
assertExists(Content);
return <PluginContentAdapter detailContent={Content} />;
} }
} else { } else {
return ( return (

View File

@@ -8,7 +8,7 @@ import type { EditorContainer } from '@blocksuite/editor';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
import { currentPageIdAtom } from '@toeverything/plugin-infra/manager'; import { currentPageIdAtom } from '@toeverything/plugin-infra/manager';
import { useAtomValue } from 'jotai/index'; import { useAtomValue } from 'jotai';
import { useAtom } from 'jotai/react'; import { useAtom } from 'jotai/react';
import { type ReactElement, useCallback, useEffect } from 'react'; import { type ReactElement, useCallback, useEffect } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { useLocation, useNavigate, useParams } from 'react-router-dom';

View File

@@ -46,9 +46,6 @@
"path": "../../packages/workspace" "path": "../../packages/workspace"
}, },
{
"path": "../../plugins/bookmark-block"
},
{ {
"path": "../../plugins/copilot" "path": "../../plugins/copilot"
}, },

View File

@@ -19,13 +19,14 @@
}, },
"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 nx run-many -t build -p @affine/bookmark-block", "build:plugins": "yarn workspace @affine/bookmark-block build && yarn vite build",
"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",
@@ -76,7 +77,7 @@
"@typescript-eslint/parser": "^5.60.1", "@typescript-eslint/parser": "^5.60.1",
"@vanilla-extract/vite-plugin": "^3.8.2", "@vanilla-extract/vite-plugin": "^3.8.2",
"@vanilla-extract/webpack-plugin": "^2.2.0", "@vanilla-extract/webpack-plugin": "^2.2.0",
"@vitejs/plugin-react": "^4.0.1", "@vitejs/plugin-react-swc": "^3.3.2",
"@vitest/coverage-istanbul": "^0.32.2", "@vitest/coverage-istanbul": "^0.32.2",
"@vitest/ui": "^0.32.2", "@vitest/ui": "^0.32.2",
"eslint": "^8.44.0", "eslint": "^8.44.0",
@@ -106,6 +107,7 @@
"typescript": "^5.1.6", "typescript": "^5.1.6",
"vite": "^4.3.9", "vite": "^4.3.9",
"vite-plugin-istanbul": "^4.1.0", "vite-plugin-istanbul": "^4.1.0",
"vite-plugin-static-copy": "^0.17.0",
"vite-tsconfig-paths": "^4.2.0", "vite-tsconfig-paths": "^4.2.0",
"vitest": "^0.32.2", "vitest": "^0.32.2",
"vitest-fetch-mock": "^0.2.2", "vitest-fetch-mock": "^0.2.2",

View File

@@ -43,6 +43,12 @@ 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',
[ [

View File

@@ -91,6 +91,12 @@ 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',
[ [

View File

@@ -9,6 +9,11 @@
"dist" "dist"
], ],
"exports": { "exports": {
"./entry": {
"type": "./dist/entry.d.ts",
"import": "./dist/entry.js",
"require": "./dist/entry.cjs"
},
"./manager": { "./manager": {
"type": "./dist/manager.d.ts", "type": "./dist/manager.d.ts",
"import": "./dist/manager.js", "import": "./dist/manager.js",

View File

@@ -0,0 +1,17 @@
import type { EditorContainer } from '@blocksuite/editor';
import type { FC } from 'react';
export type Part = 'headerItem' | 'editor' | 'window';
export type CallbackMap = {
headerItem: (root: HTMLElement) => () => void;
window: (root: HTMLElement) => () => void;
editor: (root: HTMLElement, editor: EditorContainer) => () => void;
};
export interface PluginContext {
register: <T extends Part>(part: T, callback: CallbackMap[T]) => void;
utils: {
PluginProvider: FC;
};
}

View File

@@ -3,17 +3,30 @@ import type { Page, Workspace } from '@blocksuite/store';
import { atom, createStore } from 'jotai/vanilla'; import { atom, createStore } from 'jotai/vanilla';
import { getWorkspace, waitForWorkspace } from './__internal__/workspace'; import { getWorkspace, waitForWorkspace } from './__internal__/workspace';
import type { AffinePlugin, Definition, ServerAdapter } from './type'; import type { CallbackMap } from './entry';
import type {
AffinePlugin,
Definition,
ExpectedLayout,
ServerAdapter,
} from './type';
import type { Loader, PluginUIAdapter } from './type'; import type { Loader, PluginUIAdapter } from './type';
import type { PluginBlockSuiteAdapter } from './type'; import type { PluginBlockSuiteAdapter } from './type';
const isServer = typeof window === 'undefined';
const isClient = typeof window !== 'undefined';
// global store // global store
export const rootStore = createStore(); export const rootStore = createStore();
// todo: for now every plugin is enabled by default // id -> HTML element
export const headerItemsAtom = atom<Record<string, CallbackMap['headerItem']>>(
{}
);
export const editorItemsAtom = atom<Record<string, CallbackMap['editor']>>({});
export const registeredPluginAtom = atom<string[]>([]);
export const windowItemsAtom = atom<Record<string, CallbackMap['window']>>({});
/**
* @deprecated
*/
export const affinePluginsAtom = atom<Record<string, AffinePlugin<string>>>({}); export const affinePluginsAtom = atom<Record<string, AffinePlugin<string>>>({});
export const currentWorkspaceIdAtom = atom<string | null>(null); export const currentWorkspaceIdAtom = atom<string | null>(null);
export const currentPageIdAtom = atom<string | null>(null); export const currentPageIdAtom = atom<string | null>(null);
@@ -39,6 +52,38 @@ export const currentPageAtom = atom<Promise<Page>>(async get => {
return page; return page;
}); });
const contentLayoutBaseAtom = atom<ExpectedLayout>('editor');
type SetStateAction<Value> = Value | ((prev: Value) => Value);
export const contentLayoutAtom = atom<
ExpectedLayout,
[SetStateAction<ExpectedLayout>],
void
>(
get => get(contentLayoutBaseAtom),
(get, set, layout) => {
set(contentLayoutBaseAtom, prev => {
let setV: (prev: ExpectedLayout) => ExpectedLayout;
if (typeof layout !== 'function') {
setV = () => layout;
} else {
setV = layout;
}
const nextValue = setV(prev);
if (nextValue === 'editor') {
return nextValue;
}
if (nextValue.first !== 'editor') {
throw new Error('The first element of the layout should be editor.');
}
if (nextValue.splitPercentage && nextValue.splitPercentage < 70) {
throw new Error('The split percentage should be greater than 70.');
}
return nextValue;
});
}
);
export function definePlugin<ID extends string>( export function definePlugin<ID extends string>(
definition: Definition<ID>, definition: Definition<ID>,
uiAdapterLoader?: Loader<Partial<PluginUIAdapter>>, uiAdapterLoader?: Loader<Partial<PluginUIAdapter>>,
@@ -47,8 +92,8 @@ export function definePlugin<ID extends string>(
) { ) {
const basePlugin = { const basePlugin = {
definition, definition,
uiAdapter: {}, uiAdapter: undefined,
blockSuiteAdapter: {}, blockSuiteAdapter: undefined,
}; };
rootStore.set(affinePluginsAtom, plugins => ({ rootStore.set(affinePluginsAtom, plugins => ({
@@ -56,76 +101,21 @@ export function definePlugin<ID extends string>(
[definition.id]: basePlugin, [definition.id]: basePlugin,
})); }));
if (isServer) { if (serverAdapter) {
if (serverAdapter) { console.log('register server adapter');
console.log('register server adapter'); serverAdapter
serverAdapter .load()
.load() .then(({ default: adapter }) => {
.then(({ default: adapter }) => {
rootStore.set(affinePluginsAtom, plugins => ({
...plugins,
[definition.id]: {
...basePlugin,
serverAdapter: adapter,
},
}));
})
.catch(err => {
console.error(err);
});
}
} else if (isClient) {
if (blockSuiteAdapter) {
const updateAdapter = (adapter: Partial<PluginBlockSuiteAdapter>) => {
rootStore.set(affinePluginsAtom, plugins => ({ rootStore.set(affinePluginsAtom, plugins => ({
...plugins, ...plugins,
[definition.id]: { [definition.id]: {
...basePlugin, ...basePlugin,
blockSuiteAdapter: adapter, serverAdapter: adapter,
}, },
})); }));
}; })
.catch(err => {
blockSuiteAdapter console.error(err);
.load() });
.then(({ default: adapter }) => updateAdapter(adapter))
.catch(err => {
console.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,
},
}));
};
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.');
});
}
}
} }
} }

View File

@@ -185,7 +185,7 @@ export type ServerAdapter = (affine: AFFiNEServer) => () => void;
export type AffinePlugin<ID extends string> = { export type AffinePlugin<ID extends string> = {
definition: Definition<ID>; definition: Definition<ID>;
uiAdapter: Partial<PluginUIAdapter>; uiAdapter: undefined;
blockSuiteAdapter: Partial<PluginBlockSuiteAdapter>; blockSuiteAdapter: undefined;
serverAdapter?: ServerAdapter; serverAdapter?: ServerAdapter;
}; };

View File

@@ -11,6 +11,7 @@ export default defineConfig({
minify: false, minify: false,
lib: { lib: {
entry: { entry: {
entry: resolve(root, 'src/entry.ts'),
type: resolve(root, 'src/type.ts'), type: resolve(root, 'src/type.ts'),
manager: resolve(root, 'src/manager.ts'), manager: resolve(root, 'src/manager.ts'),
'__internal__/workspace': resolve( '__internal__/workspace': resolve(

View File

@@ -1,5 +1,7 @@
# `@affine/bookmark-block` # `@affine/bookmark-block`
> Moved to [@affine/bookmark-plugin](../bookmark)
>
> A block for bookmarking a website > A block for bookmarking a website
![preview](assets/preview.png) ![preview](assets/preview.png)

View File

@@ -12,18 +12,8 @@
"dev": "node ./scripts/dev.mjs" "dev": "node ./scripts/dev.mjs"
}, },
"dependencies": { "dependencies": {
"@affine/component": "workspace:*",
"@toeverything/plugin-infra": "workspace:*", "@toeverything/plugin-infra": "workspace:*",
"foxact": "^0.2.11",
"link-preview-js": "^3.0.4" "link-preview-js": "^3.0.4"
}, },
"devDependencies": {
"react": "18.2.0",
"react-dom": "18.2.0"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
},
"version": "0.7.0-canary.47" "version": "0.7.0-canary.47"
} }

View File

@@ -1,30 +0,0 @@
import type { PluginBlockSuiteAdapter } from '@toeverything/plugin-infra/type';
import { noop } from 'foxact/noop';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BookMarkUI } from './ui';
export default {
uiDecorator: editor => {
if (
editor.parentElement &&
editor.page.awarenessStore.getFlag('enable_bookmark_operation')
) {
const div = document.createElement('div');
editor.parentElement.appendChild(div);
const root = createRoot(div);
root.render(
<StrictMode>
<BookMarkUI page={editor.page} />
</StrictMode>
);
return () => {
root.unmount();
div.remove();
};
} else {
return noop;
}
},
} satisfies Partial<PluginBlockSuiteAdapter>;

View File

@@ -22,26 +22,9 @@ definePlugin(
commands: ['com.blocksuite.bookmark-block.get-bookmark-data-by-link'], commands: ['com.blocksuite.bookmark-block.get-bookmark-data-by-link'],
}, },
undefined, undefined,
undefined,
{ {
load: () => import('./blocksuite/index'), load: () => import('./server'),
hotModuleReload: onHot => hotModuleReload: onHot => onHot(import('./server')),
import.meta.webpackHot &&
import.meta.webpackHot.accept('./blocksuite', () =>
onHot(import('./blocksuite/index'))
),
},
{
load: () =>
import(
/* webpackIgnore: true */
'./server'
),
hotModuleReload: onHot =>
onHot(
import(
/* webpackIgnore: true */
'./server'
)
),
} }
); );

View File

@@ -0,0 +1,16 @@
{
"name": "@affine/bookmark-plugin",
"version": "0.1.0",
"affinePlugin": {
"release": true,
"entry": {
"core": "./src/index.ts"
}
},
"dependencies": {
"@affine/component": "workspace:*",
"@blocksuite/icons": "^2.1.25",
"@toeverything/plugin-infra": "workspace:*",
"foxact": "^0.2.11"
}
}

View File

@@ -8,7 +8,8 @@ import {
} from '@blocksuite/blocks/std'; } from '@blocksuite/blocks/std';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
import type { FC } from 'react'; import type { FC, ReactElement } from 'react';
import { StrictMode } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
export type BookMarkProps = { export type BookMarkProps = {
@@ -95,7 +96,8 @@ const shouldShowBookmarkMenu = (pastedBlocks: SerializedBlock[]) => {
} }
return !!firstBlock.text[0].attributes?.link; return !!firstBlock.text[0].attributes?.link;
}; };
export const BookMarkUI: FC<BookMarkProps> = ({ page }) => {
const BookMarkUI: FC<BookMarkProps> = ({ page }) => {
const [anchor, setAnchor] = useState<Range | null>(null); const [anchor, setAnchor] = useState<Range | null>(null);
const [selectedOption, setSelectedOption] = useState<string>( const [selectedOption, setSelectedOption] = useState<string>(
menuOptions[0].id menuOptions[0].id
@@ -218,3 +220,15 @@ export const BookMarkUI: FC<BookMarkProps> = ({ page }) => {
</MuiClickAwayListener> </MuiClickAwayListener>
) : null; ) : null;
}; };
type AppProps = {
page: Page;
};
export const App = (props: AppProps): ReactElement => {
return (
<StrictMode>
<BookMarkUI page={props.page} />
</StrictMode>
);
};

View File

@@ -0,0 +1,21 @@
import type { PluginContext } from '@toeverything/plugin-infra/entry';
import { createElement } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './app';
export const entry = (context: PluginContext) => {
console.log('register');
context.register('editor', (div, editor) => {
const root = createRoot(div);
root.render(createElement(App, { page: editor.page }));
return () => {
root.unmount();
};
});
return () => {
console.log('unregister');
};
};

View File

@@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.json",
"include": ["./src"],
"compilerOptions": {
"noEmit": false,
"outDir": "lib",
"jsx": "preserve"
},
"references": [
{
"path": "../../packages/plugin-infra"
},
{
"path": "../../packages/component"
}
]
}

View File

@@ -1,28 +1,27 @@
{ {
"name": "@affine/copilot", "name": "@affine/copilot",
"private": true, "private": true,
"main": "./src/index.ts", "affinePlugin": {
"module": "./src/index.ts", "release": false,
"exports": { "entry": {
".": "./src/index.ts" "core": "./src/index.ts"
}
}, },
"dependencies": { "dependencies": {
"@affine/component": "workspace:*", "@affine/component": "workspace:*",
"@toeverything/plugin-infra": "workspace:*", "@toeverything/plugin-infra": "workspace:*",
"idb": "^7.1.1",
"langchain": "^0.0.107", "langchain": "^0.0.107",
"marked": "^5.1.0", "marked": "^5.1.0",
"marked-gfm-heading-id": "^3.0.4", "marked-gfm-heading-id": "^3.0.4",
"marked-mangle": "^1.1.0" "marked-mangle": "^1.1.0",
"zod": "^3.21.4"
}, },
"devDependencies": { "devDependencies": {
"@types/marked": "^5.0.0", "@types/marked": "^5.0.0",
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"idb": "^7.1.1",
"jotai": "^2.2.2", "jotai": "^2.2.2",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0"
"zod": "^3.21.4"
}, },
"peerDependencies": { "peerDependencies": {
"react": "*", "react": "*",

View File

@@ -1,11 +1,9 @@
import { IconButton } from '@affine/component'; import { IconButton } from '@affine/component';
import { SendIcon } from '@blocksuite/icons'; import { SendIcon } from '@blocksuite/icons';
import { rootStore } from '@toeverything/plugin-infra/manager'; import { contentLayoutAtom } from '@toeverything/plugin-infra/manager';
import type { PluginUIAdapter } from '@toeverything/plugin-infra/type'; import { useAtomValue, useSetAtom } from 'jotai';
import { Provider, useAtomValue, useSetAtom } from 'jotai';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import { StrictMode, Suspense, useCallback, useState } from 'react'; import { Suspense, useCallback, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { ConversationList } from '../core/components/conversation-list'; import { ConversationList } from '../core/components/conversation-list';
import { FollowingUp } from '../core/components/following-up'; import { FollowingUp } from '../core/components/following-up';
@@ -17,51 +15,6 @@ import {
textareaStyle, textareaStyle,
} from './index.css'; } from './index.css';
if (typeof window === 'undefined') {
import('@blocksuite/blocks')
.then(({ FormatQuickBar }) => {
FormatQuickBar.customElements.push((_page, getSelection) => {
const div = document.createElement('div');
const root = createRoot(div);
const AskAI = (): ReactElement => {
const { conversationAtom } = useChatAtoms();
const call = useSetAtom(conversationAtom);
const onClickAskAI = useCallback(() => {
const selection = getSelection();
if (selection != null) {
const text = selection.models
.map(model => {
return model.text?.toString();
})
.filter((v): v is string => Boolean(v))
.join('\n');
console.log('selected text:', text);
call(
`I selected some text from the document: \n"${text}."`
).catch(err => {
console.error(err);
});
}
}, [call]);
return <div onClick={onClickAskAI}>Ask AI</div>;
};
root.render(
<StrictMode>
<Provider store={rootStore}>
<AskAI />
</Provider>
</StrictMode>
);
return div;
});
})
.catch(error => {
console.error(error);
});
}
const Actions = () => { const Actions = () => {
const { conversationAtom, followingUpAtoms } = useChatAtoms(); const { conversationAtom, followingUpAtoms } = useChatAtoms();
const call = useSetAtom(conversationAtom); const call = useSetAtom(conversationAtom);
@@ -108,12 +61,10 @@ const DetailContentImpl = () => {
); );
}; };
export const DetailContent: PluginUIAdapter['detailContent'] = ({ export const DetailContent = (): ReactElement => {
contentLayoutAtom,
}): ReactElement => {
const layout = useAtomValue(contentLayoutAtom); const layout = useAtomValue(contentLayoutAtom);
const key = useAtomValue(openAIApiKeyAtom); const key = useAtomValue(openAIApiKeyAtom);
if (layout === 'editor' || layout.second !== 'com.affine.copilot') { if (layout === 'editor' || layout.second !== 'copilot') {
return <></>; return <></>;
} }
if (!key) { if (!key) {

View File

@@ -1,12 +1,10 @@
import { IconButton, Tooltip } from '@affine/component'; import { IconButton, Tooltip } from '@affine/component';
import type { PluginUIAdapter } from '@toeverything/plugin-infra/type'; import { contentLayoutAtom } from '@toeverything/plugin-infra/manager';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import { useCallback } from 'react'; import { useCallback } from 'react';
export const HeaderItem: PluginUIAdapter['headerItem'] = ({ export const HeaderItem = (): ReactElement => {
contentLayoutAtom,
}): ReactElement => {
const setLayout = useSetAtom(contentLayoutAtom); const setLayout = useSetAtom(contentLayoutAtom);
return ( return (
<Tooltip content="Chat with AI" placement="bottom-end"> <Tooltip content="Chat with AI" placement="bottom-end">
@@ -18,7 +16,7 @@ export const HeaderItem: PluginUIAdapter['headerItem'] = ({
return { return {
direction: 'horizontal', direction: 'horizontal',
first: 'editor', first: 'editor',
second: 'com.affine.copilot', second: 'copilot',
splitPercentage: 70, splitPercentage: 70,
}; };
} else { } else {

View File

@@ -1,5 +1,4 @@
import { Button } from '@affine/component'; import { Button } from '@affine/component';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import { PlusIcon, ResetIcon } from '@blocksuite/icons'; import { PlusIcon, ResetIcon } from '@blocksuite/icons';
import { clsx } from 'clsx'; import { clsx } from 'clsx';
import type { MessageType } from 'langchain/schema'; import type { MessageType } from 'langchain/schema';
@@ -31,7 +30,6 @@ export const Conversation = (props: ConversationProps): ReactElement => {
[styles.avatarRightStyle]: props.type === 'human', [styles.avatarRightStyle]: props.type === 'human',
})} })}
> >
<WorkspaceAvatar workspace={null} />
<div className={styles.conversationContainerStyle}> <div className={styles.conversationContainerStyle}>
<div <div
className={clsx(styles.conversationStyle, { className={clsx(styles.conversationStyle, {

View File

@@ -1,7 +1,6 @@
import type { IndexedDBChatMessageHistory } from '@affine/copilot/core/langchain/message-history'; import type { IndexedDBChatMessageHistory } from '@affine/copilot/core/langchain/message-history';
import { atom, useAtomValue } from 'jotai'; import { atom, useAtomValue } from 'jotai';
import { atomWithDefault } from 'jotai/utils'; import { atomWithDefault, atomWithStorage } from 'jotai/utils';
import { atomWithStorage } from 'jotai/utils';
import type { WritableAtom } from 'jotai/vanilla'; import type { WritableAtom } from 'jotai/vanilla';
import type { LLMChain } from 'langchain/chains'; import type { LLMChain } from 'langchain/chains';
import { type ConversationChain } from 'langchain/chains'; import { type ConversationChain } from 'langchain/chains';

View File

@@ -1,31 +1,33 @@
import { definePlugin } from '@toeverything/plugin-infra/manager'; import type { PluginContext } from '@toeverything/plugin-infra/entry';
import { ReleaseStage } from '@toeverything/plugin-infra/type'; import { createElement } from 'react';
import { createRoot } from 'react-dom/client';
definePlugin( import { DetailContent } from './UI/detail-content';
{ import { HeaderItem } from './UI/header-item';
id: 'com.affine.copilot',
name: { export const entry = (context: PluginContext) => {
fallback: 'AFFiNE Copilot', context.register('headerItem', div => {
i18nKey: 'com.affine.copilot.name', const root = createRoot(div);
}, root.render(
description: { createElement(context.utils.PluginProvider, {}, createElement(HeaderItem))
fallback: );
'AFFiNE Copilot will help you with best writing experience on the World.', return () => {
}, root.unmount();
publisher: { };
name: { });
fallback: 'AFFiNE',
}, context.register('window', div => {
link: 'https://affine.pro', const root = createRoot(div);
}, root.render(
stage: ReleaseStage.NIGHTLY, createElement(
version: '0.0.1', context.utils.PluginProvider,
commands: [], {},
}, createElement(DetailContent)
{ )
load: () => import('./UI/index'), );
hotModuleReload: onHot => return () => {
import.meta.webpackHot && root.unmount();
import.meta.webpackHot.accept('./UI', () => onHot(import('./UI/index'))), };
} });
); return () => {};
};

View File

@@ -0,0 +1,16 @@
{
"name": "@affine/hello-world-plugin",
"private": true,
"version": "0.0.1",
"affinePlugin": {
"release": false,
"entry": {
"core": "./src/index.ts"
}
},
"dependencies": {
"@affine/component": "workspace:*",
"@blocksuite/icons": "^2.1.25",
"@toeverything/plugin-infra": "workspace:*"
}
}

View File

@@ -0,0 +1,17 @@
import { IconButton, Tooltip } from '@affine/component';
import { AffineLogoSBlue2_1Icon } from '@blocksuite/icons';
import { useCallback } from 'react';
export const HeaderItem = () => {
return (
<Tooltip content="Plugin Enabled">
<IconButton
onClick={useCallback(() => {
console.log('clicked hello world!');
}, [])}
>
<AffineLogoSBlue2_1Icon />
</IconButton>
</Tooltip>
);
};

View File

@@ -0,0 +1,26 @@
import type { PluginContext } from '@toeverything/plugin-infra/entry';
import {
currentWorkspaceIdAtom,
rootStore,
} from '@toeverything/plugin-infra/manager';
import { createElement } from 'react';
import { createRoot } from 'react-dom/client';
import { HeaderItem } from './app';
export const entry = (context: PluginContext) => {
console.log('register');
console.log('hello, world!');
console.log(rootStore.get(currentWorkspaceIdAtom));
context.register('headerItem', div => {
const root = createRoot(div);
root.render(createElement(HeaderItem));
return () => {
root.unmount();
};
});
return () => {
console.log('unregister');
};
};

View File

@@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.json",
"include": ["./src"],
"compilerOptions": {
"noEmit": false,
"outDir": "lib",
"jsx": "preserve"
},
"references": [
{
"path": "../../packages/plugin-infra"
},
{
"path": "../../packages/component"
}
]
}

View File

@@ -8,6 +8,7 @@
"outDir": "lib" "outDir": "lib"
}, },
"include": [ "include": [
"vite.config.ts",
"vitest.config.ts", "vitest.config.ts",
"scripts", "scripts",
"apps/core/.webpack/runtime-config.ts" "apps/core/.webpack/runtime-config.ts"

92
vite.config.ts Normal file
View File

@@ -0,0 +1,92 @@
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()],
});

View File

@@ -2,7 +2,7 @@ import path, { resolve } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'; import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react-swc';
import { defineConfig } from 'vitest/config'; import { defineConfig } from 'vitest/config';
const rootDir = fileURLToPath(new URL('.', import.meta.url)); const rootDir = fileURLToPath(new URL('.', import.meta.url));

View File

@@ -64,19 +64,23 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@affine/bookmark-block@workspace:*, @affine/bookmark-block@workspace:plugins/bookmark-block": "@affine/bookmark-block@workspace:plugins/bookmark-block":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@affine/bookmark-block@workspace:plugins/bookmark-block" resolution: "@affine/bookmark-block@workspace:plugins/bookmark-block"
dependencies:
"@toeverything/plugin-infra": "workspace:*"
link-preview-js: ^3.0.4
languageName: unknown
linkType: soft
"@affine/bookmark-plugin@workspace:plugins/bookmark":
version: 0.0.0-use.local
resolution: "@affine/bookmark-plugin@workspace:plugins/bookmark"
dependencies: dependencies:
"@affine/component": "workspace:*" "@affine/component": "workspace:*"
"@blocksuite/icons": ^2.1.25
"@toeverything/plugin-infra": "workspace:*" "@toeverything/plugin-infra": "workspace:*"
foxact: ^0.2.11 foxact: ^0.2.11
link-preview-js: ^3.0.4
react: 18.2.0
react-dom: 18.2.0
peerDependencies:
react: "*"
react-dom: "*"
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@@ -165,8 +169,6 @@ __metadata:
"@affine/component": "workspace:*" "@affine/component": "workspace:*"
"@toeverything/plugin-infra": "workspace:*" "@toeverything/plugin-infra": "workspace:*"
"@types/marked": ^5.0.0 "@types/marked": ^5.0.0
"@types/react": ^18.2.14
"@types/react-dom": ^18.2.6
idb: ^7.1.1 idb: ^7.1.1
jotai: ^2.2.2 jotai: ^2.2.2
langchain: ^0.0.107 langchain: ^0.0.107
@@ -187,7 +189,6 @@ __metadata:
resolution: "@affine/core@workspace:apps/core" resolution: "@affine/core@workspace:apps/core"
dependencies: dependencies:
"@affine-test/fixtures": "workspace:*" "@affine-test/fixtures": "workspace:*"
"@affine/bookmark-block": "workspace:*"
"@affine/component": "workspace:*" "@affine/component": "workspace:*"
"@affine/copilot": "workspace:*" "@affine/copilot": "workspace:*"
"@affine/debug": "workspace:*" "@affine/debug": "workspace:*"
@@ -241,6 +242,7 @@ __metadata:
react-resizable-panels: ^0.0.53 react-resizable-panels: ^0.0.53
react-router-dom: ^6.14.1 react-router-dom: ^6.14.1
rxjs: ^7.8.1 rxjs: ^7.8.1
ses: ^0.18.5
style-loader: ^3.3.3 style-loader: ^3.3.3
swc-loader: ^0.2.3 swc-loader: ^0.2.3
swc-plugin-coverage-instrument: ^0.0.19 swc-plugin-coverage-instrument: ^0.0.19
@@ -373,6 +375,16 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@affine/hello-world-plugin@workspace:plugins/hello-world":
version: 0.0.0-use.local
resolution: "@affine/hello-world-plugin@workspace:plugins/hello-world"
dependencies:
"@affine/component": "workspace:*"
"@blocksuite/icons": ^2.1.25
"@toeverything/plugin-infra": "workspace:*"
languageName: unknown
linkType: soft
"@affine/i18n@workspace:*, @affine/i18n@workspace:packages/i18n": "@affine/i18n@workspace:*, @affine/i18n@workspace:packages/i18n":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@affine/i18n@workspace:packages/i18n" resolution: "@affine/i18n@workspace:packages/i18n"
@@ -434,7 +446,7 @@ __metadata:
"@typescript-eslint/parser": ^5.60.1 "@typescript-eslint/parser": ^5.60.1
"@vanilla-extract/vite-plugin": ^3.8.2 "@vanilla-extract/vite-plugin": ^3.8.2
"@vanilla-extract/webpack-plugin": ^2.2.0 "@vanilla-extract/webpack-plugin": ^2.2.0
"@vitejs/plugin-react": ^4.0.1 "@vitejs/plugin-react-swc": ^3.3.2
"@vitest/coverage-istanbul": ^0.32.2 "@vitest/coverage-istanbul": ^0.32.2
"@vitest/ui": ^0.32.2 "@vitest/ui": ^0.32.2
eslint: ^8.44.0 eslint: ^8.44.0
@@ -464,6 +476,7 @@ __metadata:
typescript: ^5.1.6 typescript: ^5.1.6
vite: ^4.3.9 vite: ^4.3.9
vite-plugin-istanbul: ^4.1.0 vite-plugin-istanbul: ^4.1.0
vite-plugin-static-copy: ^0.17.0
vite-tsconfig-paths: ^4.2.0 vite-tsconfig-paths: ^4.2.0
vitest: ^0.32.2 vitest: ^0.32.2
vitest-fetch-mock: ^0.2.2 vitest-fetch-mock: ^0.2.2
@@ -4657,6 +4670,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@endo/env-options@npm:^0.1.1":
version: 0.1.1
resolution: "@endo/env-options@npm:0.1.1"
checksum: c721db3e2d3f9a52a5c391dc8e6d473ffc44cf2bb252fad3048551e35aac52484d612753211116ff9a7157a0768d7c61352c494f491e3463e78fef438beb1d42
languageName: node
linkType: hard
"@esbuild/android-arm64@npm:0.17.19": "@esbuild/android-arm64@npm:0.17.19":
version: 0.17.19 version: 0.17.19
resolution: "@esbuild/android-arm64@npm:0.17.19" resolution: "@esbuild/android-arm64@npm:0.17.19"
@@ -11385,7 +11405,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@swc/core@npm:^1.3.70": "@swc/core@npm:^1.3.61, @swc/core@npm:^1.3.70":
version: 1.3.70 version: 1.3.70
resolution: "@swc/core@npm:1.3.70" resolution: "@swc/core@npm:1.3.70"
dependencies: dependencies:
@@ -12841,6 +12861,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@vitejs/plugin-react-swc@npm:^3.3.2":
version: 3.3.2
resolution: "@vitejs/plugin-react-swc@npm:3.3.2"
dependencies:
"@swc/core": ^1.3.61
peerDependencies:
vite: ^4
checksum: 1e44b1b7c50b6ae381e2ded9a7971af42286144bf257d7ed1a9dbf8acec0c4af78fe46982941566d31b80b7886649d9b1c06a873571d228e0b8fce68742b9ff4
languageName: node
linkType: hard
"@vitejs/plugin-react@npm:^3.0.1": "@vitejs/plugin-react@npm:^3.0.1":
version: 3.1.0 version: 3.1.0
resolution: "@vitejs/plugin-react@npm:3.1.0" resolution: "@vitejs/plugin-react@npm:3.1.0"
@@ -19460,8 +19491,8 @@ __metadata:
linkType: hard linkType: hard
"foxact@npm:^0.2.11": "foxact@npm:^0.2.11":
version: 0.2.11 version: 0.2.17
resolution: "foxact@npm:0.2.11" resolution: "foxact@npm:0.2.17"
dependencies: dependencies:
client-only: ^0.0.1 client-only: ^0.0.1
server-only: ^0.0.1 server-only: ^0.0.1
@@ -19470,7 +19501,7 @@ __metadata:
peerDependenciesMeta: peerDependenciesMeta:
react: react:
optional: true optional: true
checksum: 711a5d1e287c45eb258a9003910d13fc2c82f3c6cd0e4e9c673dbe64c6a7d95da7cc983b5f143270bc6304b9ceb0de2d25c01588e066e01e8eb0dbb55e55eaa6 checksum: 9229ac42032dbce52c95f5583601ba5dbfe8e4bd40b6ec3b372d91793b7b621e01eb45bb953dfe077e65a2ecd4dcdaa42c8f1c60efbf43048a3cd0038a754b60
languageName: node languageName: node
linkType: hard linkType: hard
@@ -29218,6 +29249,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ses@npm:^0.18.5":
version: 0.18.5
resolution: "ses@npm:0.18.5"
dependencies:
"@endo/env-options": ^0.1.1
checksum: b41faaf28efc97c09b0322de5c12603e015238e4b2cf5195c8fc3f5fb9bc66169dd394c63bcdb30d8fa36a55d03eeda3df64857ab724b2297fb6b60c4f5dc0e2
languageName: node
linkType: hard
"set-blocking@npm:^2.0.0": "set-blocking@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "set-blocking@npm:2.0.0" resolution: "set-blocking@npm:2.0.0"
@@ -31924,6 +31964,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"vite-plugin-static-copy@npm:^0.17.0":
version: 0.17.0
resolution: "vite-plugin-static-copy@npm:0.17.0"
dependencies:
chokidar: ^3.5.3
fast-glob: ^3.2.11
fs-extra: ^11.1.0
picocolors: ^1.0.0
peerDependencies:
vite: ^3.0.0 || ^4.0.0
checksum: b73f001f080517d7c2342ad5b0779c1e5ec8b5820e85a57280e8f979d095946a32ab717a59eaae60e3f715784a138785208619aa6d936cd9a80e6a03e364e4fc
languageName: node
linkType: hard
"vite-tsconfig-paths@npm:^4.2.0": "vite-tsconfig-paths@npm:^4.2.0":
version: 4.2.0 version: 4.2.0
resolution: "vite-tsconfig-paths@npm:4.2.0" resolution: "vite-tsconfig-paths@npm:4.2.0"