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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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
_next
storybook-static
web-static
public

View File

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

View File

@ -38,6 +38,7 @@
rel="shortcut icon"
href="https://affine.pro/favicon.ico"
/>
<link rel="stylesheet" href="/plugins/style.css" />
</head>
<body>
<div id="app"></div>

View File

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

View File

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

View File

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

View File

@ -48,8 +48,6 @@ currentWorkspaceIdAtom.onMount = set => {
if (value) {
set(value);
localStorage.setItem('last_workspace_id', value);
} else {
set(null);
}
};
callback(router.state);
@ -65,8 +63,6 @@ currentPageIdAtom.onMount = set => {
const value = state.location.pathname.split('/')[3];
if (value) {
set(value);
} else {
set(null);
}
};
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');
if (runtimeConfig.enablePlugin) {
import('@affine/copilot');
}
/// <reference types="@types/webpack-env" />
import 'ses';
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');

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 { affinePluginsAtom } from '@toeverything/plugin-infra/manager';
import { registeredPluginAtom } from '@toeverything/plugin-infra/manager';
import { useAtomValue } from 'jotai';
export const Plugins = () => {
const t = useAFFiNEI18N();
const plugins = useAtomValue(affinePluginsAtom);
const allowedPlugins = useAtomValue(registeredPluginAtom);
console.log('allowedPlugins', allowedPlugins);
return (
<>
<SettingHeader
title={'Plugins'}
subtitle={t['None yet']()}
subtitle={allowedPlugins.length === 0 && t['None yet']()}
data-testid="plugins-title"
/>
{Object.values(plugins).map(({ definition, uiAdapter }) => {
const Content = uiAdapter.debugContent;
return <div key={definition.id}>{Content && <Content />}</div>;
})}
{allowedPlugins.map(plugin => (
<SettingWrapper key={plugin} title={plugin}></SettingWrapper>
))}
</>
);
};

View File

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

View File

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

View File

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

View File

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

View File

@ -19,13 +19,14 @@
},
"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 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",
"start:web-static": "yarn workspace @affine/core static-server",
"start:storybook": "yarn exec serve apps/storybook/storybook-static -l 6006",
@ -76,7 +77,7 @@
"@typescript-eslint/parser": "^5.60.1",
"@vanilla-extract/vite-plugin": "^3.8.2",
"@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/ui": "^0.32.2",
"eslint": "^8.44.0",
@ -106,6 +107,7 @@
"typescript": "^5.1.6",
"vite": "^4.3.9",
"vite-plugin-istanbul": "^4.1.0",
"vite-plugin-static-copy": "^0.17.0",
"vite-tsconfig-paths": "^4.2.0",
"vitest": "^0.32.2",
"vitest-fetch-mock": "^0.2.2",

View File

@ -43,6 +43,12 @@ const flags = {
coverage: process.env.COVERAGE === 'true',
} satisfies BuildFlags;
spawn('vite', ['build'], {
cwd: projectRoot,
stdio: 'inherit',
shell: true,
});
spawn(
'node',
[

View File

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

View File

@ -9,6 +9,11 @@
"dist"
],
"exports": {
"./entry": {
"type": "./dist/entry.d.ts",
"import": "./dist/entry.js",
"require": "./dist/entry.cjs"
},
"./manager": {
"type": "./dist/manager.d.ts",
"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 { 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 { 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
// 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 currentWorkspaceIdAtom = 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;
});
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>(
definition: Definition<ID>,
uiAdapterLoader?: Loader<Partial<PluginUIAdapter>>,
@ -47,8 +92,8 @@ export function definePlugin<ID extends string>(
) {
const basePlugin = {
definition,
uiAdapter: {},
blockSuiteAdapter: {},
uiAdapter: undefined,
blockSuiteAdapter: undefined,
};
rootStore.set(affinePluginsAtom, plugins => ({
@ -56,7 +101,6 @@ export function definePlugin<ID extends string>(
[definition.id]: basePlugin,
}));
if (isServer) {
if (serverAdapter) {
console.log('register server adapter');
serverAdapter
@ -74,58 +118,4 @@ export function definePlugin<ID extends string>(
console.error(err);
});
}
} else if (isClient) {
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 => {
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> = {
definition: Definition<ID>;
uiAdapter: Partial<PluginUIAdapter>;
blockSuiteAdapter: Partial<PluginBlockSuiteAdapter>;
uiAdapter: undefined;
blockSuiteAdapter: undefined;
serverAdapter?: ServerAdapter;
};

View File

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

View File

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

View File

@ -12,18 +12,8 @@
"dev": "node ./scripts/dev.mjs"
},
"dependencies": {
"@affine/component": "workspace:*",
"@toeverything/plugin-infra": "workspace:*",
"foxact": "^0.2.11",
"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"
}

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'],
},
undefined,
undefined,
{
load: () => import('./blocksuite/index'),
hotModuleReload: onHot =>
import.meta.webpackHot &&
import.meta.webpackHot.accept('./blocksuite', () =>
onHot(import('./blocksuite/index'))
),
},
{
load: () =>
import(
/* webpackIgnore: true */
'./server'
),
hotModuleReload: onHot =>
onHot(
import(
/* webpackIgnore: true */
'./server'
)
),
load: () => import('./server'),
hotModuleReload: onHot => onHot(import('./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';
import { assertExists } from '@blocksuite/global/utils';
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';
export type BookMarkProps = {
@ -95,7 +96,8 @@ const shouldShowBookmarkMenu = (pastedBlocks: SerializedBlock[]) => {
}
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 [selectedOption, setSelectedOption] = useState<string>(
menuOptions[0].id
@ -218,3 +220,15 @@ export const BookMarkUI: FC<BookMarkProps> = ({ page }) => {
</MuiClickAwayListener>
) : 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",
"private": true,
"main": "./src/index.ts",
"module": "./src/index.ts",
"exports": {
".": "./src/index.ts"
"affinePlugin": {
"release": false,
"entry": {
"core": "./src/index.ts"
}
},
"dependencies": {
"@affine/component": "workspace:*",
"@toeverything/plugin-infra": "workspace:*",
"idb": "^7.1.1",
"langchain": "^0.0.107",
"marked": "^5.1.0",
"marked-gfm-heading-id": "^3.0.4",
"marked-mangle": "^1.1.0"
"marked-mangle": "^1.1.0",
"zod": "^3.21.4"
},
"devDependencies": {
"@types/marked": "^5.0.0",
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"idb": "^7.1.1",
"jotai": "^2.2.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"zod": "^3.21.4"
"react-dom": "18.2.0"
},
"peerDependencies": {
"react": "*",

View File

@ -1,11 +1,9 @@
import { IconButton } from '@affine/component';
import { SendIcon } from '@blocksuite/icons';
import { rootStore } from '@toeverything/plugin-infra/manager';
import type { PluginUIAdapter } from '@toeverything/plugin-infra/type';
import { Provider, useAtomValue, useSetAtom } from 'jotai';
import { contentLayoutAtom } from '@toeverything/plugin-infra/manager';
import { useAtomValue, useSetAtom } from 'jotai';
import type { ReactElement } from 'react';
import { StrictMode, Suspense, useCallback, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { Suspense, useCallback, useState } from 'react';
import { ConversationList } from '../core/components/conversation-list';
import { FollowingUp } from '../core/components/following-up';
@ -17,51 +15,6 @@ import {
textareaStyle,
} 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 { conversationAtom, followingUpAtoms } = useChatAtoms();
const call = useSetAtom(conversationAtom);
@ -108,12 +61,10 @@ const DetailContentImpl = () => {
);
};
export const DetailContent: PluginUIAdapter['detailContent'] = ({
contentLayoutAtom,
}): ReactElement => {
export const DetailContent = (): ReactElement => {
const layout = useAtomValue(contentLayoutAtom);
const key = useAtomValue(openAIApiKeyAtom);
if (layout === 'editor' || layout.second !== 'com.affine.copilot') {
if (layout === 'editor' || layout.second !== 'copilot') {
return <></>;
}
if (!key) {

View File

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

View File

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

View File

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

View File

@ -1,31 +1,33 @@
import { definePlugin } from '@toeverything/plugin-infra/manager';
import { ReleaseStage } from '@toeverything/plugin-infra/type';
import type { PluginContext } from '@toeverything/plugin-infra/entry';
import { createElement } from 'react';
import { createRoot } from 'react-dom/client';
definePlugin(
{
id: 'com.affine.copilot',
name: {
fallback: 'AFFiNE Copilot',
i18nKey: 'com.affine.copilot.name',
},
description: {
fallback:
'AFFiNE Copilot will help you with best writing experience on the World.',
},
publisher: {
name: {
fallback: 'AFFiNE',
},
link: 'https://affine.pro',
},
stage: ReleaseStage.NIGHTLY,
version: '0.0.1',
commands: [],
},
{
load: () => import('./UI/index'),
hotModuleReload: onHot =>
import.meta.webpackHot &&
import.meta.webpackHot.accept('./UI', () => onHot(import('./UI/index'))),
}
);
import { DetailContent } from './UI/detail-content';
import { HeaderItem } from './UI/header-item';
export const entry = (context: PluginContext) => {
context.register('headerItem', div => {
const root = createRoot(div);
root.render(
createElement(context.utils.PluginProvider, {}, createElement(HeaderItem))
);
return () => {
root.unmount();
};
});
context.register('window', div => {
const root = createRoot(div);
root.render(
createElement(
context.utils.PluginProvider,
{},
createElement(DetailContent)
)
);
return () => {
root.unmount();
};
});
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"
},
"include": [
"vite.config.ts",
"vitest.config.ts",
"scripts",
"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 { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import react from '@vitejs/plugin-react';
import react from '@vitejs/plugin-react-swc';
import { defineConfig } from 'vitest/config';
const rootDir = fileURLToPath(new URL('.', import.meta.url));

View File

@ -64,19 +64,23 @@ __metadata:
languageName: unknown
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
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:
"@affine/component": "workspace:*"
"@blocksuite/icons": ^2.1.25
"@toeverything/plugin-infra": "workspace:*"
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
linkType: soft
@ -165,8 +169,6 @@ __metadata:
"@affine/component": "workspace:*"
"@toeverything/plugin-infra": "workspace:*"
"@types/marked": ^5.0.0
"@types/react": ^18.2.14
"@types/react-dom": ^18.2.6
idb: ^7.1.1
jotai: ^2.2.2
langchain: ^0.0.107
@ -187,7 +189,6 @@ __metadata:
resolution: "@affine/core@workspace:apps/core"
dependencies:
"@affine-test/fixtures": "workspace:*"
"@affine/bookmark-block": "workspace:*"
"@affine/component": "workspace:*"
"@affine/copilot": "workspace:*"
"@affine/debug": "workspace:*"
@ -241,6 +242,7 @@ __metadata:
react-resizable-panels: ^0.0.53
react-router-dom: ^6.14.1
rxjs: ^7.8.1
ses: ^0.18.5
style-loader: ^3.3.3
swc-loader: ^0.2.3
swc-plugin-coverage-instrument: ^0.0.19
@ -373,6 +375,16 @@ __metadata:
languageName: unknown
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":
version: 0.0.0-use.local
resolution: "@affine/i18n@workspace:packages/i18n"
@ -434,7 +446,7 @@ __metadata:
"@typescript-eslint/parser": ^5.60.1
"@vanilla-extract/vite-plugin": ^3.8.2
"@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/ui": ^0.32.2
eslint: ^8.44.0
@ -464,6 +476,7 @@ __metadata:
typescript: ^5.1.6
vite: ^4.3.9
vite-plugin-istanbul: ^4.1.0
vite-plugin-static-copy: ^0.17.0
vite-tsconfig-paths: ^4.2.0
vitest: ^0.32.2
vitest-fetch-mock: ^0.2.2
@ -4657,6 +4670,13 @@ __metadata:
languageName: node
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":
version: 0.17.19
resolution: "@esbuild/android-arm64@npm:0.17.19"
@ -11385,7 +11405,7 @@ __metadata:
languageName: node
linkType: hard
"@swc/core@npm:^1.3.70":
"@swc/core@npm:^1.3.61, @swc/core@npm:^1.3.70":
version: 1.3.70
resolution: "@swc/core@npm:1.3.70"
dependencies:
@ -12841,6 +12861,17 @@ __metadata:
languageName: node
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":
version: 3.1.0
resolution: "@vitejs/plugin-react@npm:3.1.0"
@ -19460,8 +19491,8 @@ __metadata:
linkType: hard
"foxact@npm:^0.2.11":
version: 0.2.11
resolution: "foxact@npm:0.2.11"
version: 0.2.17
resolution: "foxact@npm:0.2.17"
dependencies:
client-only: ^0.0.1
server-only: ^0.0.1
@ -19470,7 +19501,7 @@ __metadata:
peerDependenciesMeta:
react:
optional: true
checksum: 711a5d1e287c45eb258a9003910d13fc2c82f3c6cd0e4e9c673dbe64c6a7d95da7cc983b5f143270bc6304b9ceb0de2d25c01588e066e01e8eb0dbb55e55eaa6
checksum: 9229ac42032dbce52c95f5583601ba5dbfe8e4bd40b6ec3b372d91793b7b621e01eb45bb953dfe077e65a2ecd4dcdaa42c8f1c60efbf43048a3cd0038a754b60
languageName: node
linkType: hard
@ -29218,6 +29249,15 @@ __metadata:
languageName: node
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":
version: 2.0.0
resolution: "set-blocking@npm:2.0.0"
@ -31924,6 +31964,20 @@ __metadata:
languageName: node
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":
version: 4.2.0
resolution: "vite-tsconfig-paths@npm:4.2.0"