refactor(electron): server side plugin (#3360)

This commit is contained in:
Alex Yang
2023-07-25 14:32:34 -07:00
committed by GitHub
parent 521e505a01
commit 10f879f29a
28 changed files with 79 additions and 840 deletions
@@ -9,13 +9,12 @@ import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
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 type { LayoutNode } from '@toeverything/plugin-infra/type';
import clsx from 'clsx';
import { useAtomValue, useSetAtom } from 'jotai';
import type { CSSProperties, FC, ReactElement } from 'react';
@@ -164,7 +163,6 @@ const PluginContentAdapter = memo<{
type LayoutPanelProps = {
node: LayoutNode;
editorProps: PageDetailEditorProps;
plugins: AffinePlugin<string>[];
};
const LayoutPanel = memo(function LayoutPanel(
@@ -189,21 +187,13 @@ const LayoutPanel = memo(function LayoutPanel(
>
<Panel defaultSize={node.splitPercentage}>
<Suspense>
<LayoutPanel
node={node.first}
editorProps={props.editorProps}
plugins={props.plugins}
/>
<LayoutPanel node={node.first} editorProps={props.editorProps} />
</Suspense>
</Panel>
<PanelResizeHandle />
<Panel defaultSize={100 - node.splitPercentage}>
<Suspense>
<LayoutPanel
node={node.second}
editorProps={props.editorProps}
plugins={props.plugins}
/>
<LayoutPanel node={node.second} editorProps={props.editorProps} />
</Suspense>
</Panel>
</PanelGroup>
@@ -219,16 +209,11 @@ export const PageDetailEditor: FC<PageDetailEditorProps> = props => {
}
const layout = useAtomValue(contentLayoutAtom);
const affinePluginsMap = useAtomValue(affinePluginsAtom);
const plugins = useMemo(
() => Object.values(affinePluginsMap),
[affinePluginsMap]
);
return (
<>
<Suspense>
<LayoutPanel node={layout} editorProps={props} plugins={plugins} />
<LayoutPanel node={layout} editorProps={props} />
</Suspense>
</>
);
-1
View File
@@ -15,7 +15,6 @@ if (process.platform === 'win32') {
async function buildLayers() {
const common = config();
await esbuild.build(common.workers);
await esbuild.build({
...common.layers,
define: {
+1 -19
View File
@@ -15,7 +15,7 @@ const DEV_SERVER_URL = process.env.DEV_SERVER_URL;
/** @type 'production' | 'development'' */
const mode = (process.env.NODE_ENV = process.env.NODE_ENV || 'development');
/** @return {{layers: import('esbuild').BuildOptions, workers: import('esbuild').BuildOptions}} */
/** @return {{layers: import('esbuild').BuildOptions}} */
export const config = () => {
const define = Object.fromEntries([
['process.env.NODE_ENV', `"${mode}"`],
@@ -52,23 +52,5 @@ export const config = () => {
assetNames: '[name]',
treeShaking: true,
},
workers: {
entryPoints: [
resolve(electronDir, './src/main/workers/plugin.worker.ts'),
],
entryNames: '[dir]/[name]',
outdir: resolve(electronDir, './dist/workers'),
bundle: true,
target: `node${NODE_MAJOR_VERSION}`,
platform: 'node',
external: ['@toeverything/plugin-infra', 'async-call-rpc'],
define: define,
format: 'cjs',
loader: {
'.node': 'copy',
},
assetNames: '[name]',
treeShaking: true,
},
};
};
-29
View File
@@ -96,37 +96,8 @@ async function watchLayers() {
});
}
async function watchWorkers() {
return new Promise(async resolve => {
let initialBuild = false;
const buildContext = await esbuild.context({
...common.workers,
plugins: [
...(common.workers.plugins ?? []),
{
name: 'electron-dev:reload-app-on-workers-change',
setup(build) {
build.onEnd(() => {
if (initialBuild) {
console.log(`[workers] has changed, [re]launching electron...`);
spawnOrReloadElectron();
} else {
resolve();
initialBuild = true;
}
});
},
},
],
});
await buildContext.watch();
});
}
async function main() {
await watchLayers();
await watchWorkers();
if (watchMode) {
console.log(`Watching for changes...`);
@@ -16,18 +16,10 @@ const outputList = [
'preload.js',
'affine.darwin-arm64.node',
'plugins',
'workers',
],
],
['dist/plugins', ['bookmark-block']],
['dist/plugins/bookmark-block', ['index.mjs']],
['dist/workers', ['plugin.worker.js']],
[
'node_modules/@toeverything/plugin-infra/dist',
['manager.js', 'manager.cjs'],
],
['node_modules/@blocksuite/global/dist', ['utils.js']],
['node_modules/jotai', ['vanilla.js']],
['dist/plugins', ['bookmark']],
['dist/plugins/bookmark', ['index.js']],
] as [entry: string, expected: string[]][];
await Promise.all(
+20 -55
View File
@@ -1,12 +1,8 @@
import { join, resolve } from 'node:path';
import { Worker } from 'node:worker_threads';
import { logger } from '@affine/electron/main/logger';
import { AsyncCall } from 'async-call-rpc';
import { ipcMain } from 'electron';
import { MessageEventChannel } from './utils';
declare global {
// fixme(himself65):
// remove this when bookmark block plugin is migrated to plugin-infra
@@ -15,56 +11,25 @@ declare global {
}
export function registerPlugin() {
const pluginWorkerPath = join(__dirname, './workers/plugin.worker.js');
const asyncCall = AsyncCall<
Record<string, (...args: any) => PromiseLike<any>>
>(
{
log: (...args: any[]) => {
logger.log('Plugin Worker', ...args);
},
},
{
channel: new MessageEventChannel(new Worker(pluginWorkerPath)),
}
);
globalThis.asyncCall = asyncCall;
logger.info('import plugin manager');
import('@toeverything/plugin-infra/manager')
.then(({ rootStore, affinePluginsAtom }) => {
logger.info('import plugin manager');
const bookmarkPluginPath = join(
process.env.PLUGIN_DIR ?? resolve(__dirname, './plugins'),
'./bookmark-block/index.mjs'
);
logger.info('bookmark plugin path:', bookmarkPluginPath);
import('file://' + bookmarkPluginPath);
let dispose: () => void = () => {
// noop
};
rootStore.sub(affinePluginsAtom, () => {
dispose();
const plugins = rootStore.get(affinePluginsAtom);
Object.values(plugins).forEach(plugin => {
logger.info('register plugin', plugin.definition.id);
plugin.definition.commands.forEach(command => {
logger.info('register plugin command', command);
ipcMain.handle(command, (event, ...args) =>
asyncCall[command](...args)
);
});
});
dispose = () => {
Object.values(plugins).forEach(plugin => {
plugin.definition.commands.forEach(command => {
logger.info('unregister plugin command', command);
ipcMain.removeHandler(command);
});
});
};
});
})
.catch(error => {
logger.error('import plugin manager error', error);
});
globalThis.asyncCall = {};
const bookmarkPluginPath = join(
process.env.PLUGIN_DIR ?? resolve(__dirname, './plugins'),
'./bookmark/index.js'
);
logger.info('bookmark plugin path:', bookmarkPluginPath);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { entry } = require(bookmarkPluginPath);
entry({
registerCommand: (command: string, handler: (...args: any[]) => any) => {
logger.info('register plugin command', command);
ipcMain.handle(command, (event, ...args) => handler(...args));
globalThis.asyncCall[command] = handler;
},
registerCommands: (command: string) => {
ipcMain.removeHandler(command);
delete globalThis.asyncCall[command];
},
});
}
@@ -1,69 +0,0 @@
import { join, resolve } from 'node:path';
import { parentPort } from 'node:worker_threads';
import { AsyncCall } from 'async-call-rpc';
import { MessageEventChannel } from '../utils';
const commandProxy: Record<string, (...args: any[]) => Promise<any>> = {};
if (!parentPort) {
throw new Error('parentPort is undefined');
}
const mainThread = AsyncCall<{
log: (...args: any[]) => Promise<void>;
}>(commandProxy, {
channel: new MessageEventChannel(parentPort),
});
globalThis.console.log = mainThread.log;
globalThis.console.error = mainThread.log;
globalThis.console.info = mainThread.log;
globalThis.console.debug = mainThread.log;
globalThis.console.warn = mainThread.log;
console.log('import plugin infra');
import('@toeverything/plugin-infra/manager')
.then(({ rootStore, affinePluginsAtom }) => {
const bookmarkPluginPath = join(
process.env.PLUGIN_DIR ?? resolve(__dirname, '../plugins'),
'./bookmark-block/index.mjs'
);
console.log('import bookmark plugin', bookmarkPluginPath);
import('file://' + bookmarkPluginPath).catch(console.log);
rootStore.sub(affinePluginsAtom, () => {
const plugins = rootStore.get(affinePluginsAtom);
Object.values(plugins).forEach(plugin => {
console.log('handle plugin', plugin.definition.id);
if (plugin.serverAdapter) {
try {
plugin.serverAdapter({
registerCommand: (command, fn) => {
console.log('register command', command);
commandProxy[command] = fn;
},
unregisterCommand: command => {
console.log('unregister command', command);
delete commandProxy[command];
},
});
} catch (e) {
console.log(
'error when handle plugin',
plugin.definition.id,
`${e}`
);
}
} else {
console.log('no server adapter, skipping.');
}
});
});
})
.catch(err => {
console.error(err);
});
+1 -1
View File
@@ -25,7 +25,7 @@
"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 workspace @affine/bookmark-block build && node ./scripts/build-plugins.mjs",
"build:plugins": "node ./scripts/build-plugins.mjs",
"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",
+35 -3
View File
@@ -40,6 +40,7 @@ const packageJsonSchema = z.object({
release: z.boolean(),
entry: z.object({
core: z.string(),
server: z.string().optional(),
}),
}),
});
@@ -59,6 +60,9 @@ const external = [
// css
/^@vanilla-extract/,
// remove this when bookmark plugin is ready
'link-preview-js',
];
const allPluginDir = path.resolve(projectRoot, 'plugins');
@@ -91,7 +95,7 @@ const metadata: Metadata = {
assets: new Set(),
};
const outDir = path.resolve(
const coreOutDir = path.resolve(
projectRoot,
'apps',
'core',
@@ -100,15 +104,43 @@ const outDir = path.resolve(
plugin
);
const pluginListJsonPath = path.resolve(outDir, '..', 'plugin-list.json');
const serverOutDir = path.resolve(
projectRoot,
'apps',
'electron',
'dist',
'plugins',
plugin
);
const pluginListJsonPath = path.resolve(coreOutDir, '..', 'plugin-list.json');
const coreEntry = path.resolve(pluginDir, json.affinePlugin.entry.core);
if (json.affinePlugin.entry.server) {
const serverEntry = path.resolve(pluginDir, json.affinePlugin.entry.server);
await build({
build: {
watch: isWatch ? {} : undefined,
minify: false,
outDir: serverOutDir,
emptyOutDir: true,
lib: {
entry: serverEntry,
fileName: 'index',
formats: ['cjs'],
},
rollupOptions: {
external,
},
},
});
}
await build({
build: {
watch: isWatch ? {} : undefined,
minify: false,
outDir,
outDir: coreOutDir,
emptyOutDir: true,
lib: {
entry: coreEntry,
+1 -48
View File
@@ -4,14 +4,7 @@ import { atom, createStore } from 'jotai/vanilla';
import { getWorkspace, waitForWorkspace } from './__internal__/workspace';
import type { CallbackMap } from './entry';
import type {
AffinePlugin,
Definition,
ExpectedLayout,
ServerAdapter,
} from './type';
import type { Loader, PluginUIAdapter } from './type';
import type { PluginBlockSuiteAdapter } from './type';
import type { ExpectedLayout } from './type';
// global store
export const rootStore = createStore();
@@ -24,10 +17,6 @@ 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);
export const currentWorkspaceAtom = atom<Promise<Workspace>>(async get => {
@@ -83,39 +72,3 @@ export const contentLayoutAtom = atom<
});
}
);
export function definePlugin<ID extends string>(
definition: Definition<ID>,
uiAdapterLoader?: Loader<Partial<PluginUIAdapter>>,
blockSuiteAdapter?: Loader<Partial<PluginBlockSuiteAdapter>>,
serverAdapter?: Loader<ServerAdapter>
) {
const basePlugin = {
definition,
uiAdapter: undefined,
blockSuiteAdapter: undefined,
};
rootStore.set(affinePluginsAtom, plugins => ({
...plugins,
[definition.id]: basePlugin,
}));
if (serverAdapter) {
console.log('register server adapter');
serverAdapter
.load()
.then(({ default: adapter }) => {
rootStore.set(affinePluginsAtom, plugins => ({
...plugins,
[definition.id]: {
...basePlugin,
serverAdapter: adapter,
},
}));
})
.catch(err => {
console.error(err);
});
}
}
+4
View File
@@ -0,0 +1,4 @@
export interface ServerContext {
registerCommand: (command: string, fn: (...args: any[]) => any) => void;
unregisterCommand: (command: string) => void;
}
-162
View File
@@ -1,84 +1,4 @@
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="./webpack-hmr.d.ts" />
/**
* AFFiNE Plugin System Types
*/
import type { EditorContainer } from '@blocksuite/editor';
import type { Workspace } from '@blocksuite/store';
import type { Page } from '@playwright/test';
import type { WritableAtom } from 'jotai';
import type { ReactElement } from 'react';
/**
* A code loader interface of the plugin API.
*
* Plugin should be lazy-loaded. If a plugin is not enabled, it will not be loaded into the Mask.
*
* @example
* ```ts
* const loader = {
* load: () => import("./code"),
* hotModuleReload: hot => import.meta.webpackHot && import.meta.webpackHot.accept('./code', () => hot(import("./code")))
* }
* ```
*
* The `./code` should use `export default` to export what loader expects.
*/
export interface Loader<DeferredModule> {
/**
* The `load()` function will be called on demand.
*
* It should not have side effects (e.g. start some daemon, start a new HTTP request or WebSocket client),
* those work should be in the `.init()` function.
* @returns the actual definition of this plugin
* @example load: () => import('./path')
*/
load(): Promise<{
default: DeferredModule;
}>;
/**
* This provides the functionality for hot module reload on the plugin.
* When the callback is called, the old instance of the plugin will be unloaded, then the new instance will be init.
* @example hotModuleReload: hot => import.meta.webpackHot && import.meta.webpackHot.accept('./path', () => hot(import('./path')))
*/
hotModuleReload(
onHot: (
hot: Promise<{
default: DeferredModule;
}>
) => void
): void;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-unused-vars
interface AFFiNEPlugin {
// todo: add more fields
}
export interface I18NStringField {
/** The i18n key of the string content. */
i18nKey?: string;
/** The fallback content to display if there is no i18n string found. */
fallback: string;
}
/** The publisher of the plugin */
export interface Publisher {
/** The name of the publisher */
name: I18NStringField;
/** URL of the publisher */
link: string;
}
/** For what stage the plugin */
export enum ReleaseStage {
NIGHTLY = 'nightly',
PROD = 'prod',
DEV = 'dev',
}
export type LayoutDirection = 'horizontal' | 'vertical';
export type LayoutNode = LayoutParentNode | string;
@@ -107,85 +27,3 @@ export type ContentLayoutAtom = WritableAtom<
[SetStateAction<ExpectedLayout>],
void
>;
export type Definition<ID extends string> = {
/**
* ID of the plugin. It should be unique.
* @example "com.affine.pro"
*/
id: ID;
/**
* The human-readable name of the plugin.
* @example { i18nKey: "name", fallback: "Never gonna give you up" }
*/
name: I18NStringField;
/**
* A brief description of this plugin.
* @example { i18nKey: "description", fallback: "This plugin is going to replace every link in the page to https://www.youtube.com/watch?v=dQw4w9WgXcQ" }
*/
description?: I18NStringField;
/**
* Publisher of this plugin.
* @example { link: "https://affine.pro", name: { fallback: "AFFiNE", i18nKey: "org_name" } }
*/
publisher?: Publisher;
/**
* The version of this plugin.
* @example "1.0.0"
*/
version: string;
/**
* The loader of this plugin.
* @example ReleaseStage.PROD
*/
stage: ReleaseStage;
/**
* Registered commands
*/
commands: string[];
};
// todo(himself65): support Vue.js
export type Adapter<Props extends Record<string, unknown>> = (
props: Props
) => ReactElement;
export type AffinePluginContext = {
toast: (text: string) => void;
};
export type BaseProps = {
contentLayoutAtom: ContentLayoutAtom;
};
export type PluginUIAdapter = {
sidebarItem: Adapter<BaseProps>;
headerItem: Adapter<BaseProps>;
detailContent: Adapter<BaseProps>;
debugContent: Adapter<Record<string, unknown>>;
};
type Cleanup = () => void;
export type PluginBlockSuiteAdapter = {
storeDecorator: (currentWorkspace: Workspace) => Promise<void>;
pageDecorator: (currentPage: Page) => Cleanup;
uiDecorator: (root: EditorContainer) => Cleanup;
};
type AFFiNEServer = {
registerCommand: (command: string, fn: (...args: any[]) => any) => void;
unregisterCommand: (command: string) => void;
};
export type ServerAdapter = (affine: AFFiNEServer) => () => void;
export type AffinePlugin<ID extends string> = {
definition: Definition<ID>;
uiAdapter: undefined;
blockSuiteAdapter: undefined;
serverAdapter?: ServerAdapter;
};
-224
View File
@@ -1,224 +0,0 @@
// Copied from @types/webpack-env
/**
* Webpack module API - variables and global functions available inside modules
*/
declare namespace __WebpackModuleApi {
type ModuleId = any;
interface HotNotifierInfo {
type:
| 'self-declined'
| 'declined'
| 'unaccepted'
| 'accepted'
| 'disposed'
| 'accept-errored'
| 'self-accept-errored'
| 'self-accept-error-handler-errored';
/**
* The module in question.
*/
moduleId: number;
/**
* For errors: the module id owning the accept handler.
*/
dependencyId?: number | undefined;
/**
* For declined/accepted/unaccepted: the chain from where the update was propagated.
*/
chain?: number[] | undefined;
/**
* For declined: the module id of the declining parent
*/
parentId?: number | undefined;
/**
* For accepted: the modules that are outdated and will be disposed
*/
outdatedModules?: number[] | undefined;
/**
* For accepted: The location of accept handlers that will handle the update
*/
outdatedDependencies?:
| {
[dependencyId: number]: number[];
}
| undefined;
/**
* For errors: the thrown error
*/
error?: Error | undefined;
/**
* For self-accept-error-handler-errored: the error thrown by the module
* before the error handler tried to handle it.
*/
originalError?: Error | undefined;
}
interface Hot {
/**
* Accept code updates for the specified dependencies. The callback is called when dependencies were replaced.
* @param dependencies
* @param callback
* @param errorHandler
*/
accept(
dependencies: string[],
callback?: (updatedDependencies: ModuleId[]) => void,
errorHandler?: (err: Error) => void
): void;
/**
* Accept code updates for the specified dependencies. The callback is called when dependencies were replaced.
* @param dependency
* @param callback
* @param errorHandler
*/
accept(
dependency: string,
callback?: () => void,
errorHandler?: (err: Error) => void
): void;
/**
* Accept code updates for this module without notification of parents.
* This should only be used if the module doesnt export anything.
* The errHandler can be used to handle errors that occur while loading the updated module.
* @param errHandler
*/
accept(errHandler?: (err: Error) => void): void;
/**
* Do not accept updates for the specified dependencies. If any dependencies is updated, the code update fails with code "decline".
*/
decline(dependencies: string[]): void;
/**
* Do not accept updates for the specified dependencies. If any dependencies is updated, the code update fails with code "decline".
*/
decline(dependency: string): void;
/**
* Flag the current module as not update-able. If updated the update code would fail with code "decline".
*/
decline(): void;
/**
* Add a one time handler, which is executed when the current module code is replaced.
* Here you should destroy/remove any persistent resource you have claimed/created.
* If you want to transfer state to the new module, add it to data object.
* The data will be available at module.hot.data on the new module.
* @param callback
*/
dispose(callback: (data: any) => void): void;
dispose(callback: <T>(data: T) => void): void;
/**
* Add a one time handler, which is executed when the current module code is replaced.
* Here you should destroy/remove any persistent resource you have claimed/created.
* If you want to transfer state to the new module, add it to data object.
* The data will be available at module.hot.data on the new module.
* @param callback
*/
addDisposeHandler(callback: (data: any) => void): void;
addDisposeHandler<T>(callback: (data: T) => void): void;
/**
* Remove a handler.
* This can useful to add a temporary dispose handler. You could i. e. replace code while in the middle of a multi-step async function.
* @param callback
*/
removeDisposeHandler(callback: (data: any) => void): void;
removeDisposeHandler<T>(callback: (data: T) => void): void;
/**
* Throws an exceptions if status() is not idle.
* Check all currently loaded modules for updates and apply updates if found.
* If no update was found, the callback is called with null.
* If autoApply is truthy the callback will be called with all modules that were disposed.
* apply() is automatically called with autoApply as options parameter.
* If autoApply is not set the callback will be called with all modules that will be disposed on apply().
* @param autoApply
* @param callback
*/
check(
autoApply: boolean,
callback: (err: Error, outdatedModules: ModuleId[]) => void
): void;
/**
* Throws an exceptions if status() is not idle.
* Check all currently loaded modules for updates and apply updates if found.
* If no update was found, the callback is called with null.
* The callback will be called with all modules that will be disposed on apply().
* @param callback
*/
check(callback: (err: Error, outdatedModules: ModuleId[]) => void): void;
/**
* If status() != "ready" it throws an error.
* Continue the update process.
* @param options
* @param callback
*/
apply(
options: AcceptOptions,
callback: (err: Error, outdatedModules: ModuleId[]) => void
): void;
/**
* If status() != "ready" it throws an error.
* Continue the update process.
* @param callback
*/
apply(callback: (err: Error, outdatedModules: ModuleId[]) => void): void;
/**
* Return one of idle, check, watch, watch-delay, prepare, ready, dispose, apply, abort or fail.
*/
status(): string;
/** Register a callback on status change. */
status(callback: (status: string) => void): void;
/** Register a callback on status change. */
addStatusHandler(callback: (status: string) => void): void;
/**
* Remove a registered status change handler.
* @param callback
*/
removeStatusHandler(callback: (status: string) => void): void;
active: boolean;
data: any;
}
interface AcceptOptions {
/**
* If true the update process continues even if some modules are not accepted (and would bubble to the entry point).
*/
ignoreUnaccepted?: boolean | undefined;
/**
* Ignore changes made to declined modules.
*/
ignoreDeclined?: boolean | undefined;
/**
* Ignore errors throw in accept handlers, error handlers and while reevaluating module.
*/
ignoreErrored?: boolean | undefined;
/**
* Notifier for declined modules.
*/
onDeclined?: ((info: HotNotifierInfo) => void) | undefined;
/**
* Notifier for unaccepted modules.
*/
onUnaccepted?: ((info: HotNotifierInfo) => void) | undefined;
/**
* Notifier for accepted modules.
*/
onAccepted?: ((info: HotNotifierInfo) => void) | undefined;
/**
* Notifier for disposed modules.
*/
onDisposed?: ((info: HotNotifierInfo) => void) | undefined;
/**
* Notifier for errors.
*/
onErrored?: ((info: HotNotifierInfo) => void) | undefined;
/**
* Indicates that apply() is automatically called by check function
*/
autoApply?: boolean | undefined;
}
}
interface ImportMeta {
/**
* `import.meta.webpackHot` is an alias for` module.hot` which is also available in strict ESM
*/
webpackHot?: __WebpackModuleApi.Hot | undefined;
}
-7
View File
@@ -1,7 +0,0 @@
# `@affine/bookmark-block`
> Moved to [@affine/bookmark-plugin](../bookmark)
>
> A block for bookmarking a website
![preview](assets/preview.png)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

-19
View File
@@ -1,19 +0,0 @@
{
"name": "@affine/bookmark-block",
"private": true,
"main": "./src/index.ts",
"module": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./server": "./src/server.ts"
},
"scripts": {
"build": "node ./scripts/build.mjs",
"dev": "node ./scripts/dev.mjs"
},
"dependencies": {
"@toeverything/plugin-infra": "workspace:*",
"link-preview-js": "^3.0.4"
},
"version": "0.7.0-canary.52"
}
-17
View File
@@ -1,17 +0,0 @@
{
"name": "@affine/bookmark-block",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"root": "plugins/bookmark-block",
"sourceRoot": "plugins/bookmark-block/src",
"targets": {
"build": {
"executor": "nx:run-script",
"dependsOn": ["^build"],
"options": {
"script": "build"
},
"outputs": ["{workspaceRoot}/apps/electron/dist/plugins/bookmark-block"]
}
}
}
-20
View File
@@ -1,20 +0,0 @@
#!/usr/bin/env node
import { build } from 'esbuild';
import { definePluginServerConfig } from './utils.mjs';
await build({
...definePluginServerConfig('bookmark-block'),
external: [
// server.ts
'link-preview-js',
// ui.ts
'@toeverything/plugin-infra',
'@affine/component',
'@blocksuite/store',
'@blocksuite/blocks',
'react',
'react-dom',
'foxact',
],
});
-22
View File
@@ -1,22 +0,0 @@
#!/usr/bin/env node
import { context } from 'esbuild';
import { definePluginServerConfig } from './utils.mjs';
const plugin = await context({
...definePluginServerConfig('bookmark-block'),
external: [
// server.ts
'link-preview-js',
// ui.ts
'@toeverything/plugin-infra',
'@affine/component',
'@blocksuite/store',
'@blocksuite/blocks',
'react',
'react-dom',
'foxact',
],
});
await plugin.watch();
-34
View File
@@ -1,34 +0,0 @@
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
export const rootDir = fileURLToPath(new URL('../../..', import.meta.url));
export const electronOutputDir = resolve(
rootDir,
'apps',
'electron',
'dist',
'plugins'
);
export const pluginDir = resolve(rootDir, 'plugins');
/**
*
* @param pluginDirName {string}
* @return {import('esbuild').BuildOptions}
*/
export function definePluginServerConfig(pluginDirName) {
const pluginRootDir = resolve(pluginDir, pluginDirName);
const mainEntryFile = resolve(pluginRootDir, 'src/index.ts');
const serverOutputDir = resolve(electronOutputDir, pluginDirName);
return {
entryPoints: [mainEntryFile],
platform: 'neutral',
format: 'esm',
outExtension: {
'.js': '.mjs',
},
outdir: serverOutputDir,
bundle: true,
splitting: true,
};
}
-30
View File
@@ -1,30 +0,0 @@
import { definePlugin } from '@toeverything/plugin-infra/manager';
import { ReleaseStage } from '@toeverything/plugin-infra/type';
definePlugin(
{
id: 'com.blocksuite.bookmark-block',
name: {
fallback: 'BlockSuite Bookmark Block',
i18nKey: 'com.blocksuite.bookmark.name',
},
description: {
fallback: 'Bookmark block',
},
publisher: {
name: {
fallback: 'AFFiNE',
},
link: 'https://affine.pro',
},
stage: ReleaseStage.NIGHTLY,
version: '0.0.1',
commands: ['com.blocksuite.bookmark-block.get-bookmark-data-by-link'],
},
undefined,
undefined,
{
load: () => import('./server'),
hotModuleReload: onHot => onHot(import('./server')),
}
);
-16
View File
@@ -1,16 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["./src"],
"compilerOptions": {
"noEmit": false,
"outDir": "lib"
},
"references": [
{
"path": "../../packages/component"
},
{
"path": "../../packages/plugin-infra"
}
]
}
+4 -2
View File
@@ -4,13 +4,15 @@
"affinePlugin": {
"release": true,
"entry": {
"core": "./src/index.ts"
"core": "./src/index.ts",
"server": "./src/server.ts"
}
},
"dependencies": {
"@affine/component": "workspace:*",
"@blocksuite/icons": "^2.1.27",
"@toeverything/plugin-infra": "workspace:*",
"foxact": "^0.2.17"
"foxact": "^0.2.17",
"link-preview-js": "^3.0.4"
}
}
@@ -1,4 +1,4 @@
import type { ServerAdapter } from '@toeverything/plugin-infra/type';
import type { ServerContext } from '@toeverything/plugin-infra/server';
import { getLinkPreview } from 'link-preview-js';
type MetaData = {
@@ -27,8 +27,8 @@ export interface PreviewType {
favicons: string[];
}
const adapter: ServerAdapter = affine => {
affine.registerCommand(
export const entry = (context: ServerContext) => {
context.registerCommand(
'com.blocksuite.bookmark-block.get-bookmark-data-by-link',
async (url: string): Promise<MetaData> => {
const previewData = (await getLinkPreview(url, {
@@ -58,10 +58,8 @@ const adapter: ServerAdapter = affine => {
}
);
return () => {
affine.unregisterCommand(
context.unregisterCommand(
'com.blocksuite.bookmark-block.get-bookmark-data-by-link'
);
};
};
export default adapter;
+2 -3
View File
@@ -1,14 +1,13 @@
import { Button, FlexWrapper, Input } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { SettingWrapper } from '@affine/component/setting-components';
import type { PluginUIAdapter } from '@toeverything/plugin-infra/type';
import { useAtom } from 'jotai';
import { useCallback } from 'react';
import { type ReactElement, useCallback } from 'react';
import { openAIApiKeyAtom } from '../core/hooks';
import { conversationHistoryDBName } from '../core/langchain/message-history';
export const DebugContent: PluginUIAdapter['debugContent'] = () => {
export const DebugContent = (): ReactElement => {
const [key, setKey] = useAtom(openAIApiKeyAtom);
const desc = (
<>
-12
View File
@@ -1,12 +0,0 @@
import type { PluginUIAdapter } from '@toeverything/plugin-infra/type';
import { createElement } from 'react';
import { DebugContent } from './debug-content';
import { DetailContent } from './detail-content';
import { HeaderItem } from './header-item';
export default {
headerItem: props => createElement(HeaderItem, props),
detailContent: props => createElement(DetailContent, props),
debugContent: props => createElement(DebugContent, props),
} satisfies Partial<PluginUIAdapter>;
-3
View File
@@ -114,9 +114,6 @@
{
"path": "./packages/plugin-infra"
},
{
"path": "./plugins/bookmark-block"
},
{
"path": "./plugins/bookmark"
},
+1 -9
View File
@@ -64,15 +64,6 @@ __metadata:
languageName: unknown
linkType: soft
"@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"
@@ -81,6 +72,7 @@ __metadata:
"@blocksuite/icons": ^2.1.27
"@toeverything/plugin-infra": "workspace:*"
foxact: ^0.2.17
link-preview-js: ^3.0.4
languageName: unknown
linkType: soft