diff --git a/blocksuite/affine/all/package.json b/blocksuite/affine/all/package.json
index 42956c040d..acc45d5f44 100644
--- a/blocksuite/affine/all/package.json
+++ b/blocksuite/affine/all/package.json
@@ -208,7 +208,9 @@
"./model": "./src/model/index.ts",
"./sync": "./src/sync/index.ts",
"./adapters": "./src/adapters/index.ts",
- "./extensions": "./src/extensions/index.ts"
+ "./extensions": "./src/extensions/index.ts",
+ "./extensions/store": "./src/extensions/store.ts",
+ "./extensions/view": "./src/extensions/view.ts"
},
"files": [
"src",
diff --git a/blocksuite/affine/all/src/extensions/migrating.ts b/blocksuite/affine/all/src/extensions/migrating.ts
new file mode 100644
index 0000000000..aa6411a9c7
--- /dev/null
+++ b/blocksuite/affine/all/src/extensions/migrating.ts
@@ -0,0 +1,153 @@
+import { AttachmentBlockSpec } from '@blocksuite/affine-block-attachment';
+import { BookmarkBlockSpec } from '@blocksuite/affine-block-bookmark';
+import { CalloutBlockSpec } from '@blocksuite/affine-block-callout';
+import { CodeBlockSpec } from '@blocksuite/affine-block-code';
+import { DataViewBlockSpec } from '@blocksuite/affine-block-data-view';
+import { DatabaseBlockSpec } from '@blocksuite/affine-block-database';
+import { DividerBlockSpec } from '@blocksuite/affine-block-divider';
+import { EdgelessTextBlockSpec } from '@blocksuite/affine-block-edgeless-text';
+import { EmbedExtensions } from '@blocksuite/affine-block-embed';
+import { FrameBlockSpec } from '@blocksuite/affine-block-frame';
+import { ImageBlockSpec } from '@blocksuite/affine-block-image';
+import { LatexBlockSpec } from '@blocksuite/affine-block-latex';
+import { ListBlockSpec } from '@blocksuite/affine-block-list';
+import {
+ EdgelessNoteBlockSpec,
+ NoteBlockSpec,
+} from '@blocksuite/affine-block-note';
+import { ParagraphBlockSpec } from '@blocksuite/affine-block-paragraph';
+import {
+ EdgelessBuiltInSpecs,
+ PageRootBlockSpec,
+ PreviewEdgelessRootBlockSpec,
+ PreviewPageRootBlockSpec,
+ ReadOnlyClipboard,
+} from '@blocksuite/affine-block-root';
+import {
+ EdgelessSurfaceBlockAdapterExtensions,
+ EdgelessSurfaceBlockSpec,
+ PageSurfaceBlockSpec,
+ SurfaceBlockAdapterExtensions,
+} from '@blocksuite/affine-block-surface';
+import {
+ EdgelessSurfaceRefBlockSpec,
+ PageSurfaceRefBlockSpec,
+} from '@blocksuite/affine-block-surface-ref';
+import { TableBlockSpec } from '@blocksuite/affine-block-table';
+import {
+ brushToMarkdownAdapterMatcher,
+ brushToPlainTextAdapterMatcher,
+} from '@blocksuite/affine-gfx-brush';
+import {
+ connectorToMarkdownAdapterMatcher,
+ connectorToPlainTextAdapterMatcher,
+} from '@blocksuite/affine-gfx-connector';
+import {
+ groupToMarkdownAdapterMatcher,
+ groupToPlainTextAdapterMatcher,
+} from '@blocksuite/affine-gfx-group';
+import {
+ mindmapToMarkdownAdapterMatcher,
+ mindmapToPlainTextAdapterMatcher,
+} from '@blocksuite/affine-gfx-mindmap';
+import {
+ shapeToMarkdownAdapterMatcher,
+ shapeToPlainTextAdapterMatcher,
+} from '@blocksuite/affine-gfx-shape';
+import {
+ textToMarkdownAdapterMatcher,
+ textToPlainTextAdapterMatcher,
+} from '@blocksuite/affine-gfx-text';
+import { inlinePresetExtensions } from '@blocksuite/affine-inline-preset';
+import {
+ DefaultOpenDocExtension,
+ DocDisplayMetaService,
+ EditPropsStore,
+ FontLoaderService,
+} from '@blocksuite/affine-shared/services';
+import type { ExtensionType } from '@blocksuite/store';
+
+const elementToPlainTextAdapterMatchers = [
+ groupToPlainTextAdapterMatcher,
+ shapeToPlainTextAdapterMatcher,
+ connectorToPlainTextAdapterMatcher,
+ brushToPlainTextAdapterMatcher,
+ textToPlainTextAdapterMatcher,
+ mindmapToPlainTextAdapterMatcher,
+];
+
+const elementToMarkdownAdapterMatchers = [
+ groupToMarkdownAdapterMatcher,
+ shapeToMarkdownAdapterMatcher,
+ connectorToMarkdownAdapterMatcher,
+ brushToMarkdownAdapterMatcher,
+ textToMarkdownAdapterMatcher,
+ mindmapToMarkdownAdapterMatcher,
+];
+
+const CommonBlockSpecs: ExtensionType[] = [
+ inlinePresetExtensions,
+ DocDisplayMetaService,
+ EditPropsStore,
+ LatexBlockSpec,
+ ListBlockSpec,
+ DatabaseBlockSpec,
+ TableBlockSpec,
+ DataViewBlockSpec,
+ DividerBlockSpec,
+ BookmarkBlockSpec,
+ EmbedExtensions,
+ AttachmentBlockSpec,
+ CodeBlockSpec,
+ ImageBlockSpec,
+ ParagraphBlockSpec,
+ DefaultOpenDocExtension,
+ FontLoaderService,
+ CalloutBlockSpec,
+ FrameBlockSpec,
+
+ elementToPlainTextAdapterMatchers,
+ elementToMarkdownAdapterMatchers,
+].flat();
+
+const PageFirstPartyBlockSpecs: ExtensionType[] = [
+ CommonBlockSpecs,
+ NoteBlockSpec,
+ PageSurfaceBlockSpec,
+ PageSurfaceRefBlockSpec,
+
+ ...SurfaceBlockAdapterExtensions,
+].flat();
+
+const EdgelessFirstPartyBlockSpecs: ExtensionType[] = [
+ CommonBlockSpecs,
+
+ EdgelessNoteBlockSpec,
+ EdgelessSurfaceBlockSpec,
+ EdgelessSurfaceRefBlockSpec,
+ EdgelessTextBlockSpec,
+
+ ...EdgelessSurfaceBlockAdapterExtensions,
+].flat();
+
+export const MigratingEdgelessEditorBlockSpecs: ExtensionType[] = [
+ EdgelessBuiltInSpecs,
+ EdgelessFirstPartyBlockSpecs,
+].flat();
+
+export const MigratingPageEditorBlockSpecs: ExtensionType[] = [
+ PageRootBlockSpec,
+ PageFirstPartyBlockSpecs,
+].flat();
+
+export const MigratingPreviewEdgelessEditorBlockSpecs: ExtensionType[] = [
+ PreviewEdgelessRootBlockSpec,
+ EdgelessFirstPartyBlockSpecs,
+ ReadOnlyClipboard,
+].flat();
+
+export const MigratingPreviewPageEditorBlockSpecs: ExtensionType[] = [
+ PreviewPageRootBlockSpec,
+ PageFirstPartyBlockSpecs,
+ ReadOnlyClipboard,
+].flat();
diff --git a/blocksuite/affine/all/src/extensions/store.ts b/blocksuite/affine/all/src/extensions/store.ts
index 48085e9c11..1cdcb8ec5a 100644
--- a/blocksuite/affine/all/src/extensions/store.ts
+++ b/blocksuite/affine/all/src/extensions/store.ts
@@ -4,6 +4,10 @@ import { EmbedIframeConfigExtensions } from '@blocksuite/affine-block-embed';
import { ImageStoreSpec } from '@blocksuite/affine-block-image';
import { SurfaceBlockSchemaExtension } from '@blocksuite/affine-block-surface';
import { TableSelectionExtension } from '@blocksuite/affine-block-table';
+import {
+ type StoreExtensionContext,
+ StoreExtensionProvider,
+} from '@blocksuite/affine-ext-loader';
import {
AttachmentBlockSchemaExtension,
BookmarkBlockSchemaExtension,
@@ -110,3 +114,12 @@ export const StoreExtensions: ExtensionType[] = [
EmbedIframeConfigExtensions,
EmbedIframeService,
].flat();
+
+export class MigratingStoreExtension extends StoreExtensionProvider {
+ override name = 'migrating';
+
+ override setup(context: StoreExtensionContext) {
+ super.setup(context);
+ context.register(StoreExtensions);
+ }
+}
diff --git a/blocksuite/affine/all/src/extensions/view.ts b/blocksuite/affine/all/src/extensions/view.ts
new file mode 100644
index 0000000000..e440078357
--- /dev/null
+++ b/blocksuite/affine/all/src/extensions/view.ts
@@ -0,0 +1,42 @@
+import {
+ type ViewExtensionContext,
+ ViewExtensionProvider,
+} from '@blocksuite/affine-ext-loader';
+
+import { effects } from '../effects';
+import {
+ MigratingEdgelessEditorBlockSpecs,
+ MigratingPageEditorBlockSpecs,
+ MigratingPreviewEdgelessEditorBlockSpecs,
+ MigratingPreviewPageEditorBlockSpecs,
+} from './migrating';
+
+export class MigratingViewExtension extends ViewExtensionProvider {
+ override name = 'migrating';
+
+ override effect() {
+ super.effect();
+ effects();
+ }
+
+ override setup(context: ViewExtensionContext) {
+ super.setup(context);
+ const scope = context.scope;
+ if (scope === 'preview-page') {
+ context.register(MigratingPreviewPageEditorBlockSpecs);
+ return;
+ }
+ if (scope === 'preview-edgeless') {
+ context.register(MigratingPreviewEdgelessEditorBlockSpecs);
+ return;
+ }
+ if (scope === 'page' || scope === 'mobile-page') {
+ context.register(MigratingPageEditorBlockSpecs);
+ return;
+ }
+ if (scope === 'edgeless' || scope === 'mobile-edgeless') {
+ context.register(MigratingEdgelessEditorBlockSpecs);
+ return;
+ }
+ }
+}
diff --git a/blocksuite/affine/blocks/embed/src/common/render-linked-doc.ts b/blocksuite/affine/blocks/embed/src/common/render-linked-doc.ts
index 270dfc96c2..3601db8c52 100644
--- a/blocksuite/affine/blocks/embed/src/common/render-linked-doc.ts
+++ b/blocksuite/affine/blocks/embed/src/common/render-linked-doc.ts
@@ -1,4 +1,5 @@
import { getSurfaceBlock } from '@blocksuite/affine-block-surface';
+import { ViewExtensionManagerIdentifier } from '@blocksuite/affine-ext-loader';
import {
type DocMode,
ImageBlockModel,
@@ -9,7 +10,7 @@ import {
} from '@blocksuite/affine-model';
import { EMBED_CARD_HEIGHT } from '@blocksuite/affine-shared/consts';
import { NotificationProvider } from '@blocksuite/affine-shared/services';
-import { matchModels, SpecProvider } from '@blocksuite/affine-shared/utils';
+import { matchModels } from '@blocksuite/affine-shared/utils';
import { BlockStdScope, EditorLifeCycleExtension } from '@blocksuite/std';
import {
type BlockModel,
@@ -202,10 +203,13 @@ async function renderNoteContent(
match: ids.map(id => ({ id, viewType: 'display' })),
};
const previewDoc = doc.doc.getStore({ query });
- const previewSpec = SpecProvider._.getSpec('preview:page');
+ const std = card.host.std;
+ const previewSpec = std
+ .get(ViewExtensionManagerIdentifier)
+ .get('preview-page');
const previewStd = new BlockStdScope({
store: previewDoc,
- extensions: previewSpec.value,
+ extensions: previewSpec,
});
const previewTemplate = previewStd.render();
const fragment = document.createDocumentFragment();
diff --git a/blocksuite/affine/blocks/embed/src/embed-synced-doc-block/embed-edgeless-synced-doc-block.ts b/blocksuite/affine/blocks/embed/src/embed-synced-doc-block/embed-edgeless-synced-doc-block.ts
index 00446dd284..0a92d398a6 100644
--- a/blocksuite/affine/blocks/embed/src/embed-synced-doc-block/embed-edgeless-synced-doc-block.ts
+++ b/blocksuite/affine/blocks/embed/src/embed-synced-doc-block/embed-edgeless-synced-doc-block.ts
@@ -70,7 +70,7 @@ export class EmbedEdgelessSyncedDocBlockComponent extends toEdgelessEmbedBlock(
${new BlockStdScope({
store: syncedDoc,
- extensions: this._buildPreviewSpec('preview:page'),
+ extensions: this._buildPreviewSpec('preview-page'),
}).render()}
`,
@@ -81,7 +81,7 @@ export class EmbedEdgelessSyncedDocBlockComponent extends toEdgelessEmbedBlock(
${new BlockStdScope({
store: syncedDoc,
- extensions: this._buildPreviewSpec('preview:edgeless'),
+ extensions: this._buildPreviewSpec('preview-edgeless'),
}).render()}
`,
diff --git a/blocksuite/affine/blocks/embed/src/embed-synced-doc-block/embed-synced-doc-block.ts b/blocksuite/affine/blocks/embed/src/embed-synced-doc-block/embed-synced-doc-block.ts
index c31d6e4dad..467f78f532 100644
--- a/blocksuite/affine/blocks/embed/src/embed-synced-doc-block/embed-synced-doc-block.ts
+++ b/blocksuite/affine/blocks/embed/src/embed-synced-doc-block/embed-synced-doc-block.ts
@@ -1,4 +1,5 @@
import { Peekable } from '@blocksuite/affine-components/peek';
+import { ViewExtensionManagerIdentifier } from '@blocksuite/affine-ext-loader';
import {
type DocLinkClickedEvent,
RefNodeSlotsProvider,
@@ -20,10 +21,7 @@ import {
ThemeExtensionIdentifier,
ThemeProvider,
} from '@blocksuite/affine-shared/services';
-import {
- cloneReferenceInfo,
- SpecProvider,
-} from '@blocksuite/affine-shared/utils';
+import { cloneReferenceInfo } from '@blocksuite/affine-shared/utils';
import { Bound, getCommonBound } from '@blocksuite/global/gfx';
import {
BlockSelection,
@@ -113,9 +111,10 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent {
+ protected _buildPreviewSpec = (name: 'preview-page' | 'preview-edgeless') => {
const nextDepth = this.depth + 1;
- const previewSpecBuilder = SpecProvider._.getSpec(name);
+ const viewExtensionManager = this.std.get(ViewExtensionManagerIdentifier);
+ const previewSpec = viewExtensionManager.get(name);
const currentDisposables = this.disposables;
const editorSetting = this.std.getOptional(EditorSettingProvider) ?? {
setting$: signal(GeneralSettingSchema.parse({})),
@@ -157,13 +156,11 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent {
@@ -204,7 +201,7 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent
${new BlockStdScope({
store: syncedDoc,
- extensions: this._buildPreviewSpec('preview:page'),
+ extensions: this._buildPreviewSpec('preview-page'),
}).render()}
`,
@@ -215,7 +212,7 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent
${new BlockStdScope({
store: syncedDoc,
- extensions: this._buildPreviewSpec('preview:edgeless'),
+ extensions: this._buildPreviewSpec('preview-edgeless'),
}).render()}
`,
diff --git a/blocksuite/affine/blocks/surface-ref/package.json b/blocksuite/affine/blocks/surface-ref/package.json
index d130cd07c1..b57d48fac2 100644
--- a/blocksuite/affine/blocks/surface-ref/package.json
+++ b/blocksuite/affine/blocks/surface-ref/package.json
@@ -13,6 +13,7 @@
"@blocksuite/affine-block-frame": "workspace:*",
"@blocksuite/affine-block-surface": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
+ "@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-inline-reference": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
diff --git a/blocksuite/affine/blocks/surface-ref/src/portal/note.ts b/blocksuite/affine/blocks/surface-ref/src/portal/note.ts
index a80637743a..5089bfc072 100644
--- a/blocksuite/affine/blocks/surface-ref/src/portal/note.ts
+++ b/blocksuite/affine/blocks/surface-ref/src/portal/note.ts
@@ -1,4 +1,5 @@
import type { CanvasRenderer } from '@blocksuite/affine-block-surface';
+import { ViewExtensionManagerIdentifier } from '@blocksuite/affine-ext-loader';
import type { NoteBlockModel } from '@blocksuite/affine-model';
import {
DefaultTheme,
@@ -10,7 +11,6 @@ import {
EDGELESS_BLOCK_CHILD_PADDING,
} from '@blocksuite/affine-shared/consts';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
-import { SpecProvider } from '@blocksuite/affine-shared/utils';
import { deserializeXYWH } from '@blocksuite/global/gfx';
import { WithDisposable } from '@blocksuite/global/lit';
import {
@@ -122,10 +122,12 @@ export class SurfaceRefNotePortal extends WithDisposable(ShadowlessElement) {
query: this.query,
readonly: true,
});
- const previewSpec = SpecProvider._.getSpec('preview:page');
+ const previewSpec = this.host.std
+ .get(ViewExtensionManagerIdentifier)
+ .get('preview-page');
return new BlockStdScope({
store: doc,
- extensions: previewSpec.value.slice(),
+ extensions: previewSpec,
}).render();
}
diff --git a/blocksuite/affine/blocks/surface-ref/src/surface-ref-block.ts b/blocksuite/affine/blocks/surface-ref/src/surface-ref-block.ts
index f93c8b5478..4556971f78 100644
--- a/blocksuite/affine/blocks/surface-ref/src/surface-ref-block.ts
+++ b/blocksuite/affine/blocks/surface-ref/src/surface-ref-block.ts
@@ -6,6 +6,7 @@ import {
import type { BlockCaptionEditor } from '@blocksuite/affine-components/caption';
import { whenHover } from '@blocksuite/affine-components/hover';
import { Peekable } from '@blocksuite/affine-components/peek';
+import { ViewExtensionManagerIdentifier } from '@blocksuite/affine-ext-loader';
import { RefNodeSlotsProvider } from '@blocksuite/affine-inline-reference';
import {
FrameBlockModel,
@@ -20,10 +21,7 @@ import {
ViewportElementExtension,
} from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
-import {
- requestConnectedFrame,
- SpecProvider,
-} from '@blocksuite/affine-shared/utils';
+import { requestConnectedFrame } from '@blocksuite/affine-shared/utils';
import { DisposableGroup } from '@blocksuite/global/disposable';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import {
@@ -46,7 +44,7 @@ import {
type GfxModel,
GfxPrimitiveElementModel,
} from '@blocksuite/std/gfx';
-import type { BaseSelection, Store } from '@blocksuite/store';
+import type { BaseSelection, ExtensionType, Store } from '@blocksuite/store';
import { effect, signal } from '@preact/signals-core';
import { css, html, nothing } from 'lit';
import { query } from 'lit/decorators.js';
@@ -114,9 +112,18 @@ export class SurfaceRefBlockComponent extends BlockComponent
{
class ViewExt1 extends ViewExtensionProvider {
override name = 'ViewExt1';
- override setup(context: ViewExtensionContext) {
- super.setup(context);
+ constructor() {
+ super();
+ setup1();
+ }
+
+ override setup(context: ViewExtensionContext, option?: { foo: number }) {
+ super.setup(context, option);
if (context.scope === 'page') {
- setup1();
context.register(Ext2);
}
if (context.scope === 'edgeless') {
- setup2();
context.register(Ext3);
}
}
@@ -69,6 +72,11 @@ describe('multiple scopes', () => {
class ViewExt2 extends ViewExtensionProvider {
override name = 'ViewExt2';
+ constructor() {
+ super();
+ setup2();
+ }
+
override setup(context: ViewExtensionContext) {
super.setup(context);
if (context.scope === 'page') {
@@ -87,7 +95,7 @@ describe('multiple scopes', () => {
expect(edgelessExtensions).toEqual([Ext3, Ext5]);
});
- it('should setup be cached', () => {
+ it('should cache provider instances', () => {
manager.get('page');
manager.get('edgeless');
expect(setup1).toHaveBeenCalledTimes(1);
diff --git a/blocksuite/affine/ext-loader/src/manager.ts b/blocksuite/affine/ext-loader/src/manager.ts
index 661e2d20db..da7857e039 100644
--- a/blocksuite/affine/ext-loader/src/manager.ts
+++ b/blocksuite/affine/ext-loader/src/manager.ts
@@ -84,16 +84,17 @@ export class ExtensionManager
{
/**
* Retrieves all extensions registered for a specific scope.
- * If the scope hasn't been built yet, it triggers the build process.
+ * It triggers the build process.
*
* @param scope - The scope to retrieve extensions for
* @returns An array of extensions registered for the specified scope
* @throws {BlockSuiteError} If the scope is not found
*/
get(scope: Scope) {
- if (!this._extensions.has(scope)) {
- this._build(scope);
+ if (this._extensions.has(scope)) {
+ this._extensions.delete(scope);
}
+ this._build(scope);
const extensionSet = this._extensions.get(scope);
if (!extensionSet) {
throw new BlockSuiteError(
@@ -117,14 +118,19 @@ export class ExtensionManager {
provider: typeof BaseExtensionProvider,
options: ((prev: T | undefined) => T | undefined) | T | undefined
) {
+ const prev = this._providerOptions.get(provider);
+
let config: T | undefined;
if (typeof options === 'function') {
- const prev = this._providerOptions.get(provider);
config = (options as (prev: unknown) => T)(prev);
} else {
config = options;
}
+ if (prev === config) {
+ return;
+ }
+
if (config === undefined) {
this._providerOptions.delete(provider);
} else {
diff --git a/blocksuite/affine/fragments/frame-panel/package.json b/blocksuite/affine/fragments/frame-panel/package.json
index c5db24a79f..051485adc0 100644
--- a/blocksuite/affine/fragments/frame-panel/package.json
+++ b/blocksuite/affine/fragments/frame-panel/package.json
@@ -13,6 +13,7 @@
"@blocksuite/affine-block-frame": "workspace:*",
"@blocksuite/affine-block-surface": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
+ "@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
diff --git a/blocksuite/affine/fragments/frame-panel/src/card/frame-preview.ts b/blocksuite/affine/fragments/frame-panel/src/card/frame-preview.ts
index 3db44beac0..8399a19bca 100644
--- a/blocksuite/affine/fragments/frame-panel/src/card/frame-preview.ts
+++ b/blocksuite/affine/fragments/frame-panel/src/card/frame-preview.ts
@@ -1,10 +1,10 @@
+import { ViewExtensionManagerIdentifier } from '@blocksuite/affine-ext-loader';
import type { FrameBlockModel } from '@blocksuite/affine-model';
import {
DocModeExtension,
DocModeProvider,
ViewportElementExtension,
} from '@blocksuite/affine-shared/services';
-import { SpecProvider } from '@blocksuite/affine-shared/utils';
import { DisposableGroup } from '@blocksuite/global/disposable';
import { Bound, deserializeXYWH } from '@blocksuite/global/gfx';
import { WithDisposable } from '@blocksuite/global/lit';
@@ -15,13 +15,12 @@ import {
ShadowlessElement,
} from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
-import { type Query, type Store } from '@blocksuite/store';
+import { type ExtensionType, type Query, type Store } from '@blocksuite/store';
import { css, html, nothing, type PropertyValues } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { guard } from 'lit/directives/guard.js';
import { styleMap } from 'lit/directives/style-map.js';
import debounce from 'lodash-es/debounce';
-
const DEFAULT_PREVIEW_CONTAINER_WIDTH = 280;
const DEFAULT_PREVIEW_CONTAINER_HEIGHT = 166;
@@ -86,7 +85,15 @@ export class FramePreview extends WithDisposable(ShadowlessElement) {
private _previewDoc: Store | null = null;
- private readonly _previewSpec = SpecProvider._.getSpec('preview:edgeless');
+ private _runtimePreviewExt: ExtensionType[] = [];
+
+ private get _viewExtensionManager() {
+ return this.std.get(ViewExtensionManagerIdentifier);
+ }
+
+ private get _previewSpec() {
+ return this._viewExtensionManager.get('preview-edgeless');
+ }
private readonly _updateFrameViewportWH = () => {
const [, , w, h] = deserializeXYWH(this.frame.xywh);
@@ -143,11 +150,11 @@ export class FramePreview extends WithDisposable(ShadowlessElement) {
}
const docModeService = this.std.get(DocModeProvider);
- this._previewSpec.extend([
+ this._runtimePreviewExt = [
ViewportElementExtension('.frame-preview-viewport'),
FramePreviewWatcher,
DocModeExtension(docModeService),
- ]);
+ ];
}
private _refreshViewport() {
@@ -163,7 +170,7 @@ export class FramePreview extends WithDisposable(ShadowlessElement) {
private _renderSurfaceContent() {
const { width, height } = this.frameViewportWH;
- const _previewSpec = this._previewSpec.value;
+ const _previewSpec = this._previewSpec.concat(this._runtimePreviewExt);
return html` {
- di.addImpl(EditorSettingProvider, () => service);
+ di.override(EditorSettingProvider, () => service);
},
};
}
diff --git a/blocksuite/affine/widgets/drag-handle/package.json b/blocksuite/affine/widgets/drag-handle/package.json
index 33ad908da8..82e205cb07 100644
--- a/blocksuite/affine/widgets/drag-handle/package.json
+++ b/blocksuite/affine/widgets/drag-handle/package.json
@@ -17,6 +17,7 @@
"@blocksuite/affine-block-paragraph": "workspace:*",
"@blocksuite/affine-block-surface": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
+ "@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/global": "workspace:*",
diff --git a/blocksuite/affine/widgets/drag-handle/src/helpers/preview-helper.ts b/blocksuite/affine/widgets/drag-handle/src/helpers/preview-helper.ts
index f1037f675b..39fafe5553 100644
--- a/blocksuite/affine/widgets/drag-handle/src/helpers/preview-helper.ts
+++ b/blocksuite/affine/widgets/drag-handle/src/helpers/preview-helper.ts
@@ -1,10 +1,10 @@
+import { ViewExtensionManagerIdentifier } from '@blocksuite/affine-ext-loader';
import {
DocModeExtension,
DocModeProvider,
EditorSettingExtension,
EditorSettingProvider,
} from '@blocksuite/affine-shared/services';
-import { SpecProvider } from '@blocksuite/affine-shared/utils';
import { BlockStdScope, BlockViewIdentifier } from '@blocksuite/std';
import type {
BlockModel,
@@ -69,7 +69,9 @@ export class PreviewHelper {
const editorSetting = std.get(EditorSettingProvider);
const query = this._calculateQuery(blockIds as string[]);
const store = widget.doc.doc.getStore({ query });
- const previewSpec = SpecProvider._.getSpec('preview:page');
+ let previewSpec = widget.std
+ .get(ViewExtensionManagerIdentifier)
+ .get('preview-page');
const settingSignal = signal({ ...editorSetting.setting$.peek() });
const extensions = [
DocModeExtension(docModeService),
@@ -99,7 +101,7 @@ export class PreviewHelper {
} as ExtensionType,
];
- previewSpec.extend(extensions);
+ previewSpec = previewSpec.concat(extensions);
settingSignal.value = {
...settingSignal.value,
@@ -108,7 +110,7 @@ export class PreviewHelper {
const previewStd = new BlockStdScope({
store,
- extensions: previewSpec.value,
+ extensions: previewSpec,
});
let width: number = 500;
diff --git a/blocksuite/affine/widgets/drag-handle/tsconfig.json b/blocksuite/affine/widgets/drag-handle/tsconfig.json
index 51215efe4e..89887c9a74 100644
--- a/blocksuite/affine/widgets/drag-handle/tsconfig.json
+++ b/blocksuite/affine/widgets/drag-handle/tsconfig.json
@@ -14,6 +14,7 @@
{ "path": "../../blocks/paragraph" },
{ "path": "../../blocks/surface" },
{ "path": "../../components" },
+ { "path": "../../ext-loader" },
{ "path": "../../model" },
{ "path": "../../shared" },
{ "path": "../../../framework/global" },
diff --git a/blocksuite/integration-test/package.json b/blocksuite/integration-test/package.json
index d1b0d35711..569eacc957 100644
--- a/blocksuite/integration-test/package.json
+++ b/blocksuite/integration-test/package.json
@@ -28,7 +28,9 @@
},
"exports": {
".": "./src/index.ts",
- "./effects": "./src/effects.ts"
+ "./effects": "./src/effects.ts",
+ "./store": "./src/store.ts",
+ "./view": "./src/view.ts"
},
"files": [
"src",
diff --git a/blocksuite/integration-test/src/__tests__/utils/setup.ts b/blocksuite/integration-test/src/__tests__/utils/setup.ts
index cfc27eaee0..af06acd4df 100644
--- a/blocksuite/integration-test/src/__tests__/utils/setup.ts
+++ b/blocksuite/integration-test/src/__tests__/utils/setup.ts
@@ -1,13 +1,7 @@
import '@toeverything/theme/style.css';
import '@toeverything/theme/fonts.css';
-import { effects as blocksEffects } from '@blocksuite/affine/effects';
-import {
- EdgelessEditorBlockSpecs,
- PageEditorBlockSpecs,
- registerStoreSpecs,
- StoreExtensions,
-} from '@blocksuite/affine/extensions';
+import { registerStoreSpecs } from '@blocksuite/affine/extensions';
import type { DocMode } from '@blocksuite/affine/model';
import { AffineSchemas } from '@blocksuite/affine/schemas';
import {
@@ -27,11 +21,17 @@ import {
import { effects } from '../../effects.js';
import { TestAffineEditorContainer } from '../../index.js';
+import { getTestStoreManager } from '../../store.js';
+import { getTestViewManager } from '../../view.js';
+// FIXME: used for test import/export
registerStoreSpecs();
-blocksEffects();
+const storeManager = getTestStoreManager();
+const viewManager = getTestViewManager();
effects();
+const storeExtensions = storeManager.get('store');
+
export function getRenderer() {
return editor.std.get(
ViewportTurboRendererIdentifier
@@ -85,12 +85,12 @@ async function createEditor(
editor.doc = doc;
editor.mode = mode;
editor.pageSpecs = [
- ...PageEditorBlockSpecs,
+ ...viewManager.get('page'),
FontConfigExtension(CommunityCanvasTextFonts),
...extensions,
];
editor.edgelessSpecs = [
- ...EdgelessEditorBlockSpecs,
+ ...viewManager.get('edgeless'),
FontConfigExtension(CommunityCanvasTextFonts),
...extensions,
];
@@ -123,7 +123,7 @@ export async function setupEditor(
extensions: ExtensionType[] = []
) {
const collection = new TestWorkspace(createCollectionOptions());
- collection.storeExtensions = StoreExtensions;
+ collection.storeExtensions = storeExtensions;
collection.meta.initialize();
window.collection = collection;
diff --git a/blocksuite/integration-test/src/store.ts b/blocksuite/integration-test/src/store.ts
new file mode 100644
index 0000000000..63afa01014
--- /dev/null
+++ b/blocksuite/integration-test/src/store.ts
@@ -0,0 +1,7 @@
+import { StoreExtensionManager } from '@blocksuite/affine/ext-loader';
+import { MigratingStoreExtension } from '@blocksuite/affine/extensions/store';
+
+export function getTestStoreManager() {
+ const manager = new StoreExtensionManager([MigratingStoreExtension]);
+ return manager;
+}
diff --git a/blocksuite/integration-test/src/view.ts b/blocksuite/integration-test/src/view.ts
new file mode 100644
index 0000000000..fa89a7c729
--- /dev/null
+++ b/blocksuite/integration-test/src/view.ts
@@ -0,0 +1,7 @@
+import { ViewExtensionManager } from '@blocksuite/affine/ext-loader';
+import { MigratingViewExtension } from '@blocksuite/affine/extensions/view';
+
+export function getTestViewManager() {
+ const manager = new ViewExtensionManager([MigratingViewExtension]);
+ return manager;
+}
diff --git a/blocksuite/playground/apps/starter/main.ts b/blocksuite/playground/apps/starter/main.ts
index e477728413..b59c2fad0e 100644
--- a/blocksuite/playground/apps/starter/main.ts
+++ b/blocksuite/playground/apps/starter/main.ts
@@ -2,7 +2,6 @@ import '../../style.css';
import * as databaseBlocks from '@blocksuite/affine/blocks/database';
import * as noteBlocks from '@blocksuite/affine/blocks/note';
-import { effects as blocksEffects } from '@blocksuite/affine/effects';
import { registerStoreSpecs } from '@blocksuite/affine/extensions';
import * as globalUtils from '@blocksuite/affine/global/utils';
import * as services from '@blocksuite/affine/shared/services';
@@ -10,7 +9,8 @@ import * as blockStd from '@blocksuite/affine/std';
import * as store from '@blocksuite/affine/store';
import * as affineModel from '@blocksuite/affine-model';
import * as editor from '@blocksuite/integration-test';
-import { effects as presetsEffects } from '@blocksuite/integration-test/effects';
+import { effects as itEffects } from '@blocksuite/integration-test/effects';
+import { getTestStoreManager } from '@blocksuite/integration-test/store';
import { setupEdgelessTemplate } from '../_common/setup.js';
import { effects as commentEffects } from '../comment/effects.js';
@@ -22,8 +22,8 @@ import { mountDefaultDocEditor } from './utils/setup-playground';
import { prepareTestApp } from './utils/test';
registerStoreSpecs();
-blocksEffects();
-presetsEffects();
+itEffects();
+const storeManager = getTestStoreManager();
commentEffects();
async function main() {
@@ -34,7 +34,7 @@ async function main() {
const params = new URLSearchParams(location.search);
const room = params.get('room') ?? Math.random().toString(16).slice(2, 8);
const isE2E = room.startsWith('playwright');
- const collection = createStarterDocCollection();
+ const collection = createStarterDocCollection(storeManager);
if (isE2E) {
Object.defineProperty(window, '$blocksuite', {
diff --git a/blocksuite/playground/apps/starter/utils/collection.ts b/blocksuite/playground/apps/starter/utils/collection.ts
index 3a4dd30dd8..4fed772180 100644
--- a/blocksuite/playground/apps/starter/utils/collection.ts
+++ b/blocksuite/playground/apps/starter/utils/collection.ts
@@ -1,5 +1,5 @@
+import type { StoreExtensionManager } from '@blocksuite/affine/ext-loader';
import { AffineSchemas } from '@blocksuite/affine/schemas';
-import { SpecProvider } from '@blocksuite/affine/shared/utils';
import { nanoid, Schema, Transformer } from '@blocksuite/affine/store';
import {
createAutoIncrementIdGenerator,
@@ -23,7 +23,9 @@ const room = params.get('room');
const isE2E = room?.startsWith('playwright');
const blobSourceArgs = (params.get('blobSource') ?? '').split(',');
-export function createStarterDocCollection() {
+export function createStarterDocCollection(
+ storeExtensionManager: StoreExtensionManager
+) {
const collectionId = room ?? 'starter';
const schema = new Schema();
schema.register(AffineSchemas);
@@ -56,7 +58,7 @@ export function createStarterDocCollection() {
blobSources,
};
const collection = new TestWorkspace(options);
- collection.storeExtensions = SpecProvider._.getSpec('store').value;
+ collection.storeExtensions = storeExtensionManager.get('store');
collection.start();
// debug info
diff --git a/blocksuite/playground/apps/starter/utils/extensions.ts b/blocksuite/playground/apps/starter/utils/extensions.ts
index 8a44f24c38..1c4fc84bb5 100644
--- a/blocksuite/playground/apps/starter/utils/extensions.ts
+++ b/blocksuite/playground/apps/starter/utils/extensions.ts
@@ -1,7 +1,3 @@
-import {
- EdgelessEditorBlockSpecs,
- PageEditorBlockSpecs,
-} from '@blocksuite/affine/extensions';
import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference';
import {
CommunityCanvasTextFonts,
@@ -13,6 +9,7 @@ import {
} from '@blocksuite/affine/shared/services';
import type { ExtensionType, Store, Workspace } from '@blocksuite/affine/store';
import { type TestAffineEditorContainer } from '@blocksuite/integration-test';
+import { getTestViewManager } from '@blocksuite/integration-test/view';
import {
mockDocModeService,
@@ -20,6 +17,8 @@ import {
mockParseDocUrlService,
} from '../../_common/mock-services';
+const viewManager = getTestViewManager();
+
export function getTestCommonExtensions(
editor: TestAffineEditorContainer
): ExtensionType[] {
@@ -48,8 +47,8 @@ export function createTestEditor(store: Store, workspace: Workspace) {
editor.doc = store;
const defaultExtensions = getTestCommonExtensions(editor);
- editor.pageSpecs = [...PageEditorBlockSpecs, ...defaultExtensions];
- editor.edgelessSpecs = [...EdgelessEditorBlockSpecs, ...defaultExtensions];
+ editor.pageSpecs = [...viewManager.get('page'), ...defaultExtensions];
+ editor.edgelessSpecs = [...viewManager.get('edgeless'), ...defaultExtensions];
editor.std
.get(RefNodeSlotsProvider)
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-messages.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-messages.ts
index 424a53e0d7..5c84a48ca0 100644
--- a/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-messages.ts
+++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/chat-panel-messages.ts
@@ -3,10 +3,9 @@ import {
DocModeProvider,
FeatureFlagService,
} from '@blocksuite/affine/shared/services';
-import { type SpecBuilder } from '@blocksuite/affine/shared/utils';
import type { EditorHost } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std';
-import type { BaseSelection } from '@blocksuite/affine/store';
+import type { BaseSelection, ExtensionType } from '@blocksuite/affine/store';
import { ArrowDownBigIcon as ArrowDownIcon } from '@blocksuite/icons/lit';
import { css, html, nothing, type PropertyValues } from 'lit';
import { property, query, state } from 'lit/decorators.js';
@@ -157,7 +156,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
accessor updateContext!: (context: Partial
) => void;
@property({ attribute: false })
- accessor previewSpecBuilder!: SpecBuilder;
+ accessor extensions!: ExtensionType[];
@query('.chat-panel-messages-container')
accessor messagesContainer: HTMLDivElement | null = null;
@@ -271,7 +270,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
.isLast=${isLast}
.status=${isLast ? status : 'idle'}
.error=${isLast ? error : null}
- .previewSpecBuilder=${this.previewSpecBuilder}
+ .extensions=${this.extensions}
.getSessionId=${this.getSessionId}
.retry=${() => this.retry()}
>`;
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/content/rich-text.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/content/rich-text.ts
index 0668154f24..13d807524e 100644
--- a/packages/frontend/core/src/blocksuite/ai/chat-panel/content/rich-text.ts
+++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/content/rich-text.ts
@@ -1,7 +1,7 @@
import { WithDisposable } from '@blocksuite/affine/global/lit';
-import type { SpecBuilder } from '@blocksuite/affine/shared/utils';
import type { EditorHost } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std';
+import type { ExtensionType } from '@blocksuite/affine/store';
import { html } from 'lit';
import { property } from 'lit/decorators.js';
@@ -18,13 +18,13 @@ export class ChatContentRichText extends WithDisposable(ShadowlessElement) {
accessor state: 'finished' | 'generating' = 'finished';
@property({ attribute: false })
- accessor previewSpecBuilder!: SpecBuilder;
+ accessor extensions!: ExtensionType[];
protected override render() {
const { text, host } = this;
return html`${createTextRenderer(host, {
customHeading: true,
- extensions: this.previewSpecBuilder.value,
+ extensions: this.extensions,
})(text, this.state)}`;
}
}
diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts
index 7e49bf4bfb..ce1f2623fa 100644
--- a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts
+++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts
@@ -2,10 +2,9 @@ import './chat-panel-messages';
import type { ContextEmbedStatus } from '@affine/graphql';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
-import type { SpecBuilder } from '@blocksuite/affine/shared/utils';
import type { EditorHost } from '@blocksuite/affine/std';
import { ShadowlessElement } from '@blocksuite/affine/std';
-import type { Store } from '@blocksuite/affine/store';
+import type { ExtensionType, Store } from '@blocksuite/affine/store';
import { HelpIcon } from '@blocksuite/icons/lit';
import { type Signal, signal } from '@preact/signals-core';
import { css, html, type PropertyValues } from 'lit';
@@ -208,7 +207,7 @@ export class ChatPanel extends SignalWatcher(
accessor docDisplayConfig!: DocDisplayConfig;
@property({ attribute: false })
- accessor previewSpecBuilder!: SpecBuilder;
+ accessor extensions!: ExtensionType[];
@state()
accessor isLoading = false;
@@ -401,7 +400,7 @@ export class ChatPanel extends SignalWatcher(
.updateContext=${this.updateContext}
.host=${this.host}
.isLoading=${this.isLoading}
- .previewSpecBuilder=${this.previewSpecBuilder}
+ .extensions=${this.extensions}
>
Promise;
@@ -90,7 +91,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
.host=${host}
.text=${item.content}
.state=${state}
- .previewSpecBuilder=${this.previewSpecBuilder}
+ .extensions=${this.extensions}
>
${shouldRenderError ? AIChatErrorRenderer(host, error) : nothing}
${this.renderEditorActions()}
diff --git a/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts b/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts
index 2532f37751..5146257242 100644
--- a/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts
+++ b/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts
@@ -43,6 +43,18 @@ import type {
AffineAIPanelWidgetConfig,
} from '../widgets/ai-panel/type';
+export const CustomPageEditorBlockSpecs: ExtensionType[] = [
+ ...PageEditorBlockSpecs,
+ {
+ setup: di => {
+ di.override(
+ BlockViewIdentifier('affine:page'),
+ () => literal`affine-page-root`
+ );
+ },
+ },
+];
+
const customHeadingStyles = css`
.custom-heading {
.h1 {
@@ -91,18 +103,6 @@ export type TextRendererOptions = {
testId?: string;
};
-export const CustomPageEditorBlockSpecs: ExtensionType[] = [
- ...PageEditorBlockSpecs,
- {
- setup: di => {
- di.override(
- BlockViewIdentifier('affine:page'),
- () => literal`affine-page-root`
- );
- },
- },
-];
-
// todo: refactor it for more general purpose usage instead of AI only?
export class TextRenderer extends WithDisposable(ShadowlessElement) {
static override styles = css`
diff --git a/packages/frontend/core/src/blocksuite/ai/extensions/ai-code.ts b/packages/frontend/core/src/blocksuite/ai/extensions/ai-code.ts
index 5d27584bcd..795102e246 100644
--- a/packages/frontend/core/src/blocksuite/ai/extensions/ai-code.ts
+++ b/packages/frontend/core/src/blocksuite/ai/extensions/ai-code.ts
@@ -7,7 +7,7 @@ import type { ExtensionType } from '@blocksuite/affine/store';
import { setupCodeToolbarAIEntry } from '../entries/code-toolbar/setup-code-toolbar';
-class AICodeBlockWatcher extends LifeCycleWatcher {
+export class AICodeBlockWatcher extends LifeCycleWatcher {
static override key = 'ai-code-block-watcher';
override mounted() {
diff --git a/packages/frontend/core/src/blocksuite/ai/extensions/ai-edgeless-root.ts b/packages/frontend/core/src/blocksuite/ai/extensions/ai-edgeless-root.ts
index d2f8bbcf62..a570648f7c 100644
--- a/packages/frontend/core/src/blocksuite/ai/extensions/ai-edgeless-root.ts
+++ b/packages/frontend/core/src/blocksuite/ai/extensions/ai-edgeless-root.ts
@@ -47,7 +47,7 @@ export function createAIEdgelessRootBlockSpec(
];
}
-function getAIEdgelessRootWatcher(framework: FrameworkProvider) {
+export function getAIEdgelessRootWatcher(framework: FrameworkProvider) {
class AIEdgelessRootWatcher extends LifeCycleWatcher {
static override key = 'ai-edgeless-root-watcher';
diff --git a/packages/frontend/core/src/blocksuite/ai/extensions/ai-page-root.ts b/packages/frontend/core/src/blocksuite/ai/extensions/ai-page-root.ts
index b221fdc7af..dccd3aaa1f 100644
--- a/packages/frontend/core/src/blocksuite/ai/extensions/ai-page-root.ts
+++ b/packages/frontend/core/src/blocksuite/ai/extensions/ai-page-root.ts
@@ -16,7 +16,7 @@ import {
} from '../widgets/ai-panel/ai-panel';
import { AiSlashMenuConfigExtension } from './ai-slash-menu';
-function getAIPageRootWatcher(framework: FrameworkProvider) {
+export function getAIPageRootWatcher(framework: FrameworkProvider) {
class AIPageRootWatcher extends LifeCycleWatcher {
static override key = 'ai-page-root-watcher';
diff --git a/packages/frontend/core/src/blocksuite/ai/messages/slides-renderer.ts b/packages/frontend/core/src/blocksuite/ai/messages/slides-renderer.ts
index ad7aaf93b9..6c4bf6d3f5 100644
--- a/packages/frontend/core/src/blocksuite/ai/messages/slides-renderer.ts
+++ b/packages/frontend/core/src/blocksuite/ai/messages/slides-renderer.ts
@@ -1,6 +1,6 @@
import { WorkspaceImpl } from '@affine/core/modules/workspace/impls/workspace';
+import { ViewExtensionManagerIdentifier } from '@blocksuite/affine/ext-loader';
import { WithDisposable } from '@blocksuite/affine/global/lit';
-import { SpecProvider } from '@blocksuite/affine/shared/utils';
import { BlockStdScope, type EditorHost } from '@blocksuite/affine/std';
import type { Store } from '@blocksuite/affine/store';
import { css, html, LitElement, nothing } from 'lit';
@@ -91,6 +91,12 @@ export class AISlidesRenderer extends WithDisposable(LitElement) {
});
}
+ protected _getExtensions() {
+ return this.host.std
+ .get(ViewExtensionManagerIdentifier)
+ .get('preview-edgeless');
+ }
+
protected override render() {
return html`