Closes: BS-3186
# @blocksuite/affine-ext-loader
Blocksuite extension loader system for AFFiNE, providing a structured way to manage and load extensions in different contexts.
## Usage
### Basic Extension Provider
```typescript
import { BaseExtensionProvider } from '@blocksuite/affine-ext-loader';
import { z } from 'zod';
// Create a custom provider with options
class MyProvider extends BaseExtensionProvider<'my-scope', { enabled: boolean }> {
name = 'MyProvider';
schema = z.object({
enabled: z.boolean(),
});
setup(context: Context<'my-scope'>, options?: { enabled: boolean }) {
super.setup(context, options);
// Custom setup logic
}
}
```
### Store Extensions
```typescript
import { StoreExtensionProvider, StoreExtensionManager } from '@blocksuite/affine-ext-loader';
import { z } from 'zod';
// Create a store provider with custom options
class MyStoreProvider extends StoreExtensionProvider<{ cacheSize: number }> {
override name = 'MyStoreProvider';
override schema = z.object({
cacheSize: z.number().min(0),
});
override setup(context: StoreExtensionContext, options?: { cacheSize: number }) {
super.setup(context, options);
context.register([Ext1, Ext2, Ext3]);
}
}
// Create and use the store extension manager
const manager = new StoreExtensionManager([MyStoreProvider]);
manager.configure(MyStoreProvider, { cacheSize: 100 });
const extensions = manager.get('store');
```
### View Extensions
```typescript
import { ViewExtensionProvider, ViewExtensionManager } from '@blocksuite/affine-ext-loader';
import { z } from 'zod';
// Create a view provider with custom options
class MyViewProvider extends ViewExtensionProvider<{ theme: string }> {
override name = 'MyViewProvider';
override schema = z.object({
theme: z.enum(['light', 'dark']),
});
override setup(context: ViewExtensionContext, options?: { theme: string }) {
super.setup(context, options);
context.register([CommonExt]);
if (context.scope === 'page') {
context.register([PageExt]);
} else if (context.scope === 'edgeless') {
context.register([EdgelessExt]);
}
if (options?.theme === 'dark') {
context.register([DarkModeExt]);
}
}
// Override effect to run one-time initialization logic
override effect() {
// This will only run once per provider class
console.log('Initializing MyViewProvider');
// Register lit elements
this.registerLitElements();
}
}
// Create and use the view extension manager
const manager = new ViewExtensionManager([MyViewProvider]);
manager.configure(MyViewProvider, { theme: 'dark' });
// Get extensions for different view scopes
const pageExtensions = manager.get('page');
const edgelessExtensions = manager.get('edgeless');
```
### One-time Initialization with Effect
View extensions support one-time initialization through the `effect` method. This method is called automatically during setup, but only once per provider class. It's useful for:
- Initializing global state
- Registering lit elements
- Setting up shared resources
```typescript
class MyViewProvider extends ViewExtensionProvider {
override effect() {
// This will only run once, even if multiple instances are created
initializeGlobalState();
registerLitElements();
setupGlobalEventListeners();
}
}
```
### Available View Scopes
The view extension system supports the following scopes:
- `page` - Standard page view
- `edgeless` - Edgeless (whiteboard) view
- `preview-page` - Page preview view
- `preview-edgeless` - Edgeless preview view
- `mobile-page` - Mobile page view
- `mobile-edgeless` - Mobile edgeless view
### Extension Configuration
Extensions can be configured using the `configure` method:
```typescript
// Set configuration directly
manager.configure(MyProvider, { enabled: true });
// Update configuration using a function
manager.configure(MyProvider, prev => {
if (!prev) return prev;
return {
...prev,
enabled: !prev.enabled,
};
});
// Remove configuration
manager.configure(MyProvider, undefined);
```
### Dependency Injection
Both store and view extension managers support dependency injection:
```typescript
// Access the manager through the di container
const viewManager = std.get(ViewExtensionManagerIdentifier);
const pagePreviewExtension = viewManager.get('preview-page');
```
This PR contains the following updates:
| Update | Change |
|---|---|
| lockFileMaintenance | All locks refreshed |
🔧 This Pull Request updates lock files to use the latest dependency versions.
---
### Configuration
📅 **Schedule**: Branch creation - "* 0-3 * * 1" (UTC), Automerge - At any time (no schedule defined).
🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMjcuMyIsInVwZGF0ZWRJblZlciI6IjM5LjIzOC4wIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5IiwibGFiZWxzIjpbImRlcGVuZGVuY2llcyJdfQ==-->
This PR contains the following updates:
| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [jotai-effect](https://jotai.org/docs/extensions/effect) ([source](https://redirect.github.com/jotaijs/jotai-effect)) | [`^1.0.5` -> `^2.0.0`](https://renovatebot.com/diffs/npm/jotai-effect/1.1.6/2.0.1) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) |
---
### Release Notes
<details>
<summary>jotaijs/jotai-effect (jotai-effect)</summary>
### [`v2.0.1`](https://redirect.github.com/jotaijs/jotai-effect/compare/v2.0.0...v2.0.1)
[Compare Source](https://redirect.github.com/jotaijs/jotai-effect/compare/v2.0.0...v2.0.1)
### [`v2.0.0`](https://redirect.github.com/jotaijs/jotai-effect/releases/tag/v2.0.0)
[Compare Source](https://redirect.github.com/jotaijs/jotai-effect/compare/v1.1.6...v2.0.0)
We’re excited to announce the release of **jotai-effect v2**, which brings a single but significant change to the core API: **`atomEffect` now runs synchronously** whenever it mounts or its dependencies change. This update improves consistency, helps avoid race conditions, and keeps related state changes in sync.
***
#### What’s New?
##### Synchronous `atomEffect`
- In v1, `atomEffect` would run **asynchronously** in the next microtask.
- In v2, `atomEffect` runs **synchronously** on mount and whenever the dependencies it uses have changed.
- **Batching is still supported** when you update multiple dependencies in a single writable atom. The effect runs only after that writable atom has finished all its updates, preventing partial updates or intermediate states.
**Example:**
```ts
const syncEffect = atomEffect((get, set) => {
get(someAtom)
set(anotherAtom)
})
const store = createStore()
store.set(someAtom, (v) => v + 1)
// The effect above runs immediately, so anotherAtom is updated in the same microtask
console.log(store.get(anotherAtom)) // Updated by atomEffect synchronously
```
When `someAtom` is updated, the effect runs **immediately**, updating `anotherAtom` in the same turn. If you update multiple atoms in the same writable atom, these changes are batched together, and `atomEffect` runs after those updates complete.
***
#### Migration Guide
For most users, **no change is required**. If you depended on the old microtask delay or cross-atom batching, read on.
##### 1. Adding back the microtask delay
If your logic explicitly relied on `atomEffect` running in a separate microtask, you can reintroduce the delay yourself:
**Before (v1)**
```ts
const effect = atomEffect((get, set) => {
console.log('effect')
return () => {
console.log('cleanup')
}
})
```
**After (v2)**
```ts
const effect = atomEffect((get, set) => {
queueMicrotask(() => {
console.log('effect')
})
return () => {
queueMicrotask(() => {
console.log('cleanup')
})
}
})
```
##### 2. Batching updates
In v1, updates to separate atoms were implicitly batched in the next microtask. In v2, **batching only occurs within a single writable atom update**:
**Before (v1)**
```ts
store.set(atomA, (v) => v + 1)
store.set(atomB, (v) => v + 1)
// atomEffect would 'see' both changes together in the next microtask
```
**After (v2)**
```ts
const actionAtom = atom(null, (get, set) => {
set(atomA, (v) => v + 1)
set(atomB, (v) => v + 1)
})
store.set(actionAtom)
// atomEffect now runs after both updates, in one batch
```
***
##### A Special Thanks to Daishi Kato
I’d like to extend my deepest gratitude to **Daishi Kato**, author of Jotai. Daishi dedicated months of tireless work to rework and rewrite significant parts of the Jotai core—primarily to empower community library authors such as myself to implement features such as **synchronous effects** in jotai-effect. His willingness to refine Jotai’s internals and his thoughtfulness in API design made this effort possible. Thank you.
##### Final Thoughts
- **Most code will just work** without any changes.
- If you have specialized scenarios relying on microtask delays or separate updates to multiple atoms, you’ll need to wrap them in a single writable atom or manually queue the microtask.
We hope these improvements make your state management more predictable and easier to reason about. If you have any issues, please feel free to open a GitHub [Discussion](https://redirect.github.com/jotaijs/jotai-effect/discussions). Happy coding!
</details>
---
### Configuration
📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).
🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xNjcuMSIsInVwZGF0ZWRJblZlciI6IjM5LjE2Ny4xIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5IiwibGFiZWxzIjpbImRlcGVuZGVuY2llcyJdfQ==-->
This PR contains the following updates:
| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [animejs](https://animejs.com) ([source](https://redirect.github.com/juliangarnier/anime)) | [`^3.2.2` -> `^4.0.0`](https://renovatebot.com/diffs/npm/animejs/3.2.2/4.0.0) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) | [](https://docs.renovatebot.com/merge-confidence/) |
---
### Release Notes
<details>
<summary>juliangarnier/anime (animejs)</summary>
### [`v4.0.0`](https://redirect.github.com/juliangarnier/anime/releases/tag/4.0.0)
[Compare Source](https://redirect.github.com/juliangarnier/anime/compare/v3.2.2...4.0.0)
> **I'm still finalizing the release notes as there are MANY changes, but in the meantime, you can check out the brand new documentation [here](https://animejs.com/documentation).**
The brand new Anime.js.
### API Breaking changes
Every Anime.js feature is now exported as an ES Module.
This is great for tree shaking, you don't have to ship the entire library anymore, only what you need.
#### Animation
The `anime(parameters)` function has been replaced with the `animate(targets, parameters)` module.
The `targets` parameter has been replaced with a dedicated function parameter: `animate(targets, parameters)`.
V3:
```javascript
import anime from 'animejs';
const animation = anime({
targets: 'div',
translateX: 100,
});
```
V4:
```javascript
import { animate } from 'animejs';
const animation = animate('div', {
translateX: 100,
});
```
#### Easings names
The `ease` prefix has been removed: 'easeInOutQuad' -> 'inOutQuad'.
#### Callbacks
Callbacks have have been renamed like this:
- `begin()` -> `onBegin()`
- `update()` -> `onUpdate()`
Here's all the change to the API
```diff
- import anime from 'animejs';
+ import { animate, createSpring, utils } from 'animejs';
- anime({
- targets: 'div',
+ animate('div', {
translateX: 100,
rotate: {
- value: 360,
+ to: 360,
- easing: 'spring(.7, 80, 10, .5)',
+ ease: createSpring({ mass: .7, damping: 80, stiffness: 10, velocity: .5}),
},
- easing: 'easeinOutExpo',
+ ease: 'inOutExpo',
- easing: () => t => Math.cos(t),
+ ease: t => Math.cos(t),
- direction: 'reverse',
+ reversed: true,
- direction: 'alternate',
+ alternate: true,
- loop: 1,
+ loop: 0,
- round: 100,
+ modifier: utils.round(2),
- begin: () => {},
+ onBegin: () => {},
- update: () => {},
+ onUpdate: () => {},
- change: () => {},
+ onRender: () => {},
- changeBegin: () => {},
- changeComplete: () => {},
- loopBegin: () => {},
- loopComplete: () => {},
+ onLoop: () => {},
- complete: () => {},
+ onComplete: () => {},
});
```
#### Promises
No more `.finished` property, promises are now handled directly with `animation.then()`:
```diff
- import anime from 'animejs';
+ import { animate, utils } from 'animejs';
- anime({ targets: target, prop: x }).finished.then(() => {});
+ animate(target, { prop: x }).then(() => {});
```
#### Values
##### To
The object syntax `value` property has been renamed `to`:
```diff
- translateX: { value: 100 }
+ translateX: { to: 100 }
```
#### Animation parameters
##### Default `easing`
The new default easing is `'outQuad'` instead of `'easeOutElastic(1, .5)'`.
##### `composition`
In V3 all animations coexist and overlaps with each other. This can cause animations with the same targets and animated properties to create weird results.
V4 cancels a running tween if a new one is created on the same target with the same property. This behaviour can be confifugred using the new `composition` parameter.
`composition: 'none'` // The old V3 behaviour, animations can overlaps
`composition: 'replace'` // The new V4 default
`composition: 'add'` // Creates additive animations by adding the values of the currently running animations with the new ones
##### `round` -> `modifier`
The `round` parameter has been replaced with a more flexible parameters that allows you to define custom functions to transform the numerical value of an animation just before the rendering.
```diff
- round: 100
+ modifier: utils.round(2)
```
You can of course defines your own modifier functions like this:
```javascript
const animation = animate('div', {
translateX: '100rem',
modifier: v => v % 10 // Note that the unit 'rem' will automatically be passed to the rendered value
});
```
#### Playback parameters
##### `direction`
The `direction` parameter has been replaced with an `alternate` and `reversed` parameters
V3:
```javascript
const animation = anime({
targets: 'div',
direction: 'reverse',
// direction: 'alternate' It wasn't possible to combined reverse and alternate direction before
});
```
V4:
```javascript
import { animate } from 'animejs';
const animation = animate('div', {
translateX: 100,
reversed: true,
alternate: true,
});
```
#### Timelines:
```diff
- import anime from 'animejs';
+ import { createTimeline, stagger } from 'animejs';
- anime.timeline({
+ createTimeline({
- duration: 500,
- easing: 'easeInOutQuad',
+ defaults: {
+ duration: 500,
+ ease: 'inOutQuad',
+ }
- loop: 2,
+ loop: 1,
- }).add({
- targets: 'div',
+ }).add('div', {
rotate: 90,
})
- .add('.target:nth-child(1)', { opacity: 0, onComplete }, 0)
- .add('.target:nth-child(2)', { opacity: 0, onComplete }, 100)
- .add('.target:nth-child(3)', { opacity: 0, onComplete }, 200)
- .add('.target:nth-child(4)', { opacity: 0, onComplete }, 300)
+ .add('.target', { opacity: 0, onComplete }, stagger(100))
```
##### Stagger
```diff
- import anime from 'animejs';
+ import { animate, stagger } from 'animejs';
- anime({
- targets: 'div',
+ animate('div', {
- translateX: anime.stagger(100),
+ translateX: stagger(100),
- delay: anime.stagger(100, { direction: 'reversed' }),
+ translateX: stagger(100, { reversed: true }),
});
```
#### SVG
```diff
- import anime from 'animejs';
+ import { animate, svg } from 'animejs';
- const path = anime.path('path');
+ const { x, y, angle } = svg.createMotionPath('path');
- anime({
- targets: '#shape1',
+ animate('#shape1', {
- points: '70 41 118.574 59.369 111.145 132.631 60.855 84.631 20.426 60.369',
+ points: svg.morphTo('#shape2'),
- strokeDashoffset: [anime.setDashoffset, 0],
+ strokeDashoffset: svg.drawLine(),
- translateX: path('x'),
- translateY: path('y'),
- rotate: path('angle'),
+ translateX: x,
+ translateY: y,
+ rotate: angle,
});
```
#### Utils
```diff
- import anime from 'animejs';
+ import { utils } from 'animejs';
- const value = anime.get('#target1', 'translateX');
+ const value = utils.get('#target1', 'translateX');
- anime.set('#target1', { translateX: 100 });
+ utils.set('#target1', { translateX: 100 });
- anime.remove('#target1');
+ utils.remove('#target1');
- const rounded = anime.round(value);
+ const rounded = utils.round(value, 0);
```
#### Engine
```diff
- import anime from 'animejs';
+ import { engine } from 'animejs';
- anime.suspendWhenDocumentHidden = false;
+ engine.pauseWhenHidden = false;
- anime.speed = .5;
+ engine.playbackRate = .5;
```
### Improvements
#### Performances
Major performance boost and lower memory footprint.
V4 has bee re-written from scratch by keeping performance in mind at every steps.
#### Better tween composition
The tween system has been refactored to improve animations behaviours when they overlaps.
This fix lots of issues, especially when creating multiple animations with the same property on the same target.
#### Additive animations
You can also blend animations together with the new `composition: 'add'` parameter.
#### Improved Timelines
- Child animations can new be looped and reversed
- Add supports for labels
- Add supports for `.set()` in timeline
- New position operators for more flexibility
- Multi-target child animation can be positioned using the `stagger` function
- Easier children defaults configuration
- Greatly improved support for CSS transforms composition from one child animation to another
```javascript
const tl = createTimeline({
playbackRate: .2,
defaults: {
duration: 500,
easing: 'outQuad',
}
});
tl.add('START', 100) // Add a label a 100ms
.set('.target', { opacity: 0 })
.add('.target', {
translateY: 100,
opacity: 1,
onComplete: () => {},
}, stagger(100))
.add('.target', {
scale: .75,
}, 'START')
.add('.target', {
rotate: '1turn',
}, '<<+=200')
```
#### Properties
##### CSS Variables
You can now use CSS variables directly like any other property:
```javascript
// Animate the values of the CSS variables '--radius'
animate('#target', { '--radius': '20px' });
```
##### Animating *from*
Animate *from* a value
```diff
+ translateX: { from: 50 }
```
##### From -> To
Even if the `[from, to]` shortcut is still valid in V4, you can now also write it like this:
```diff
+ translateX: { from: 50, to: 100 }
```
##### Colors
You can now animate hex colors with an alpha channel like '#F443' or '#FF444433'.
#### Timers
You can now create timers with the `createTimer` module.
Timers can be use as replacement for `setTimeout`or `setInterval` but with all the playbacks parameters, callbacks and the `Promise` system provided by anime.js.
```
const interval = createTimer({
onLoop: () => { // do something every 500ms },
duration: 500,
});
const timeout = createTimer({
onComplete: () => { // do something in 500ms },
duration: 500,
});
const gameLogicLoop = createTimer({
frameRate: 30,
onUpdate: gameSystems,
});
const gameRenderLoop = createTimer({
frameRate: 60,
onUpdate: gameRender,
});
```
#### Variable frame rate
You can now change the frame rate to all animations or to a specific Timeline / Animation / Timer
</details>
---
### Configuration
📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).
🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMjcuMyIsInVwZGF0ZWRJblZlciI6IjM5LjIyNy4zIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5IiwibGFiZWxzIjpbImRlcGVuZGVuY2llcyJdfQ==-->
This PR contains the following updates:
| Update | Change |
|---|---|
| lockFileMaintenance | All locks refreshed |
🔧 This Pull Request updates lock files to use the latest dependency versions.
---
### Configuration
📅 **Schedule**: Branch creation - "* 0-3 * * 1" (UTC), Automerge - At any time (no schedule defined).
🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://redirect.github.com/renovatebot/renovate/discussions) if that's undesired.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xODUuNCIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4xIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5IiwibGFiZWxzIjpbImRlcGVuZGVuY2llcyJdfQ==-->