refactor: merge plugin-infra into infra (#3540)
This commit is contained in:
@@ -19,6 +19,26 @@
|
||||
"types": "./dist/src/preload/*.d.ts",
|
||||
"import": "./dist/preload/*.js",
|
||||
"require": "./dist/preload/*.cjs"
|
||||
},
|
||||
"./atom": {
|
||||
"type": "./dist/src/atom.d.ts",
|
||||
"import": "./dist/atom.js",
|
||||
"require": "./dist/atom.cjs"
|
||||
},
|
||||
"./type": {
|
||||
"type": "./dist/src/type.d.ts",
|
||||
"import": "./dist/type.js",
|
||||
"require": "./dist/type.cjs"
|
||||
},
|
||||
"./__internal__/workspace": {
|
||||
"type": "./dist/src/__internal__/workspace.d.ts",
|
||||
"import": "./dist/__internal__/workspace.js",
|
||||
"require": "./dist/__internal__/workspace.cjs"
|
||||
},
|
||||
"./__internal__/react": {
|
||||
"type": "./dist/src/__internal__/react.d.ts",
|
||||
"import": "./dist/__internal__/react.js",
|
||||
"require": "./dist/__internal__/react.cjs"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
@@ -28,22 +48,43 @@
|
||||
"build": "vite build",
|
||||
"dev": "vite build --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/sdk": "workspace:*",
|
||||
"@blocksuite/blocks": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"jotai": "^2.2.2",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocksuite/editor": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20230802200139-381599c0-nightly",
|
||||
"async-call-rpc": "^6.3.1",
|
||||
"electron": "link:../../apps/electron/node_modules/electron",
|
||||
"react": "^18.2.0",
|
||||
"vite": "^4.4.7",
|
||||
"vite-plugin-dts": "3.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"async-call-rpc": "*",
|
||||
"electron": "*"
|
||||
"electron": "*",
|
||||
"react": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@blocksuite/editor": {
|
||||
"optional": true
|
||||
},
|
||||
"@blocksuite/lit": {
|
||||
"optional": true
|
||||
},
|
||||
"async-call-rpc": {
|
||||
"optional": true
|
||||
},
|
||||
"electron": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"version": "0.8.0-canary.8"
|
||||
|
||||
35
packages/infra/src/__internal__/react.ts
Normal file
35
packages/infra/src/__internal__/react.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { type PassiveDocProvider } from '@blocksuite/store';
|
||||
import { useAtomValue } from 'jotai/react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import {
|
||||
getActiveBlockSuiteWorkspaceAtom,
|
||||
workspacePassiveEffectWeakMap,
|
||||
} from './workspace.js';
|
||||
|
||||
export function useStaticBlockSuiteWorkspace(id: string): Workspace {
|
||||
return useAtomValue(getActiveBlockSuiteWorkspaceAtom(id));
|
||||
}
|
||||
|
||||
export function usePassiveWorkspaceEffect(workspace: Workspace) {
|
||||
useEffect(() => {
|
||||
if (workspacePassiveEffectWeakMap.get(workspace) === true) {
|
||||
return;
|
||||
}
|
||||
const providers = workspace.providers.filter(
|
||||
(provider): provider is PassiveDocProvider =>
|
||||
'passive' in provider && provider.passive === true
|
||||
);
|
||||
providers.forEach(provider => {
|
||||
provider.connect();
|
||||
});
|
||||
workspacePassiveEffectWeakMap.set(workspace, true);
|
||||
return () => {
|
||||
providers.forEach(provider => {
|
||||
provider.disconnect();
|
||||
});
|
||||
workspacePassiveEffectWeakMap.delete(workspace);
|
||||
};
|
||||
}, [workspace]);
|
||||
}
|
||||
59
packages/infra/src/__internal__/workspace.ts
Normal file
59
packages/infra/src/__internal__/workspace.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { ActiveDocProvider, Workspace } from '@blocksuite/store';
|
||||
import type { Atom } from 'jotai/vanilla';
|
||||
import { atom } from 'jotai/vanilla';
|
||||
|
||||
/**
|
||||
* DO NOT ACCESS THIS MAP IN PRODUCTION, OR YOU WILL BE FIRED
|
||||
* Map: guid -> Workspace
|
||||
*/
|
||||
export const INTERNAL_BLOCKSUITE_HASH_MAP = new Map<string, Workspace>([]);
|
||||
|
||||
const workspacePassiveAtomWeakMap = new WeakMap<
|
||||
Workspace,
|
||||
Atom<Promise<Workspace>>
|
||||
>();
|
||||
|
||||
// Whether the workspace is active to use
|
||||
export const workspaceActiveWeakMap = new WeakMap<Workspace, boolean>();
|
||||
|
||||
// Whether the workspace has been enabled the passive effect (background)
|
||||
export const workspacePassiveEffectWeakMap = new WeakMap<Workspace, boolean>();
|
||||
|
||||
export async function waitForWorkspace(workspace: Workspace) {
|
||||
if (workspaceActiveWeakMap.get(workspace) !== true) {
|
||||
const providers = workspace.providers.filter(
|
||||
(provider): provider is ActiveDocProvider =>
|
||||
'active' in provider && provider.active === true
|
||||
);
|
||||
for (const provider of providers) {
|
||||
provider.sync();
|
||||
// we will wait for the necessary providers to be ready
|
||||
await provider.whenReady;
|
||||
}
|
||||
workspaceActiveWeakMap.set(workspace, true);
|
||||
}
|
||||
}
|
||||
|
||||
export function getWorkspace(id: string) {
|
||||
if (!INTERNAL_BLOCKSUITE_HASH_MAP.has(id)) {
|
||||
throw new Error('Workspace not found');
|
||||
}
|
||||
return INTERNAL_BLOCKSUITE_HASH_MAP.get(id) as Workspace;
|
||||
}
|
||||
|
||||
export function getActiveBlockSuiteWorkspaceAtom(
|
||||
id: string
|
||||
): Atom<Promise<Workspace>> {
|
||||
if (!INTERNAL_BLOCKSUITE_HASH_MAP.has(id)) {
|
||||
throw new Error('Workspace not found');
|
||||
}
|
||||
const workspace = INTERNAL_BLOCKSUITE_HASH_MAP.get(id) as Workspace;
|
||||
if (!workspacePassiveAtomWeakMap.has(workspace)) {
|
||||
const baseAtom = atom(async () => {
|
||||
await waitForWorkspace(workspace);
|
||||
return workspace;
|
||||
});
|
||||
workspacePassiveAtomWeakMap.set(workspace, baseAtom);
|
||||
}
|
||||
return workspacePassiveAtomWeakMap.get(workspace) as Atom<Promise<Workspace>>;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import { Workspace } from '@blocksuite/store';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { getDefaultStore } from 'jotai/vanilla';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
usePassiveWorkspaceEffect,
|
||||
useStaticBlockSuiteWorkspace,
|
||||
} from '../__internal__/react.js';
|
||||
import {
|
||||
getActiveBlockSuiteWorkspaceAtom,
|
||||
INTERNAL_BLOCKSUITE_HASH_MAP,
|
||||
} from '../__internal__/workspace.js';
|
||||
|
||||
test('useStaticBlockSuiteWorkspace', async () => {
|
||||
const sync = vi.fn();
|
||||
let connected = false;
|
||||
const connect = vi.fn(() => (connected = true));
|
||||
const workspace = new Workspace({
|
||||
id: '1',
|
||||
providerCreators: [
|
||||
() => ({
|
||||
flavour: 'fake',
|
||||
active: true,
|
||||
sync,
|
||||
get whenReady(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
},
|
||||
}),
|
||||
() => ({
|
||||
flavour: 'fake-2',
|
||||
passive: true,
|
||||
get connected() {
|
||||
return connected;
|
||||
},
|
||||
connect,
|
||||
disconnect: vi.fn(),
|
||||
}),
|
||||
],
|
||||
});
|
||||
INTERNAL_BLOCKSUITE_HASH_MAP.set('1', workspace);
|
||||
|
||||
{
|
||||
const workspaceHook = renderHook(() => useStaticBlockSuiteWorkspace('1'));
|
||||
// wait for suspense to resolve
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
expect(workspaceHook.result.current).toBe(workspace);
|
||||
expect(sync).toBeCalledTimes(1);
|
||||
expect(connect).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
{
|
||||
const atom = getActiveBlockSuiteWorkspaceAtom('1');
|
||||
const store = getDefaultStore();
|
||||
const result = await store.get(atom);
|
||||
expect(result).toBe(workspace);
|
||||
expect(sync).toBeCalledTimes(1);
|
||||
expect(connect).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
{
|
||||
renderHook(() => usePassiveWorkspaceEffect(workspace));
|
||||
expect(sync).toBeCalledTimes(1);
|
||||
expect(connect).toBeCalledTimes(1);
|
||||
expect(connected).toBe(true);
|
||||
}
|
||||
});
|
||||
79
packages/infra/src/atom.ts
Normal file
79
packages/infra/src/atom.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { CallbackMap, ExpectedLayout } from '@affine/sdk/entry';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Page, Workspace } from '@blocksuite/store';
|
||||
import { atom, createStore } from 'jotai/vanilla';
|
||||
|
||||
import { getWorkspace, waitForWorkspace } from './__internal__/workspace.js';
|
||||
|
||||
// global store
|
||||
export const rootStore = createStore();
|
||||
|
||||
// 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']>>({});
|
||||
export const settingItemsAtom = atom<Record<string, CallbackMap['setting']>>(
|
||||
{}
|
||||
);
|
||||
export const formatBarItemsAtom = atom<
|
||||
Record<string, CallbackMap['formatBar']>
|
||||
>({});
|
||||
|
||||
export const currentWorkspaceIdAtom = atom<string | null>(null);
|
||||
export const currentPageIdAtom = atom<string | null>(null);
|
||||
export const currentWorkspaceAtom = atom<Promise<Workspace>>(async get => {
|
||||
const currentWorkspaceId = get(currentWorkspaceIdAtom);
|
||||
assertExists(currentWorkspaceId, 'current workspace id');
|
||||
const workspace = getWorkspace(currentWorkspaceId);
|
||||
await waitForWorkspace(workspace);
|
||||
return workspace;
|
||||
});
|
||||
export const currentPageAtom = atom<Promise<Page>>(async get => {
|
||||
const currentWorkspaceId = get(currentWorkspaceIdAtom);
|
||||
assertExists(currentWorkspaceId, 'current workspace id');
|
||||
const currentPageId = get(currentPageIdAtom);
|
||||
assertExists(currentPageId, 'current page id');
|
||||
const workspace = getWorkspace(currentWorkspaceId);
|
||||
await waitForWorkspace(workspace);
|
||||
const page = workspace.getPage(currentPageId);
|
||||
assertExists(page);
|
||||
if (!page.loaded) {
|
||||
await page.waitForLoaded();
|
||||
}
|
||||
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),
|
||||
(_, 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;
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,5 +1,45 @@
|
||||
import type { ExpectedLayout } from '@affine/sdk/entry';
|
||||
import type { WritableAtom } from 'jotai';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { TypedEventEmitter } from './core/event-emitter.js';
|
||||
|
||||
export const packageJsonInputSchema = z.object({
|
||||
name: z.string(),
|
||||
version: z.string(),
|
||||
description: z.string(),
|
||||
affinePlugin: z.object({
|
||||
release: z.boolean(),
|
||||
entry: z.object({
|
||||
core: z.string(),
|
||||
server: z.string().optional(),
|
||||
}),
|
||||
serverCommand: z.array(z.string()).optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const packageJsonOutputSchema = z.object({
|
||||
name: z.string(),
|
||||
version: z.string(),
|
||||
description: z.string(),
|
||||
affinePlugin: z.object({
|
||||
release: z.boolean(),
|
||||
entry: z.object({
|
||||
core: z.string(),
|
||||
}),
|
||||
assets: z.array(z.string()),
|
||||
serverCommand: z.array(z.string()).optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
type SetStateAction<Value> = Value | ((prev: Value) => Value);
|
||||
|
||||
export type ContentLayoutAtom = WritableAtom<
|
||||
ExpectedLayout,
|
||||
[SetStateAction<ExpectedLayout>],
|
||||
void
|
||||
>;
|
||||
|
||||
export abstract class HandlerManager<
|
||||
Namespace extends string,
|
||||
Handlers extends Record<string, PrimitiveHandlers>,
|
||||
|
||||
@@ -12,14 +12,29 @@ export default defineConfig({
|
||||
lib: {
|
||||
entry: {
|
||||
index: resolve(root, 'src/index.ts'),
|
||||
atom: resolve(root, 'src/atom.ts'),
|
||||
type: resolve(root, 'src/type.ts'),
|
||||
'core/event-emitter': resolve(root, 'src/core/event-emitter.ts'),
|
||||
'preload/electron': resolve(root, 'src/preload/electron.ts'),
|
||||
'__internal__/workspace': resolve(
|
||||
root,
|
||||
'src/__internal__/workspace.ts'
|
||||
),
|
||||
'__internal__/react': resolve(root, 'src/__internal__/react.ts'),
|
||||
},
|
||||
formats: ['es', 'cjs'],
|
||||
name: 'AffineInfra',
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['electron', 'async-call-rpc', 'rxjs'],
|
||||
external: [
|
||||
'electron',
|
||||
'async-call-rpc',
|
||||
'rxjs',
|
||||
'zod',
|
||||
'react',
|
||||
/^jotai/,
|
||||
/^@blocksuite/,
|
||||
],
|
||||
},
|
||||
},
|
||||
plugins: [dts()],
|
||||
|
||||
Reference in New Issue
Block a user