pinia-plugin-subscription 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +47 -289
- package/dist/chunks/{index-B-zO0yrr.js → index-VL_PgzVK.js} +53 -54
- package/dist/chunks/index-VL_PgzVK.js.map +1 -0
- package/dist/core/PluginSubscriber.d.ts +1 -1
- package/dist/core/PluginSubscription.d.ts +39 -4
- package/dist/core/RollbackStateSnapshotsHandler.d.ts +65 -0
- package/dist/core/Store.d.ts +4 -4
- package/dist/factories/state-snapshot.d.ts +3 -0
- package/dist/helpers.js +2 -1
- package/dist/helpers.js.map +1 -0
- package/dist/lib/main.d.ts +2 -1
- package/dist/pinia-plugin-subscription.js +344 -199
- package/dist/pinia-plugin-subscription.js.map +1 -0
- package/dist/plugins/createPlugin.d.ts +9 -1
- package/dist/tests/defineAStore.test-d.d.ts +1 -0
- package/dist/tests/storeOptionsExtensions.test-d.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/plugin.d.ts +5 -2
- package/dist/types/store.d.ts +43 -2
- package/dist/utils/deep-clone.d.ts +7 -0
- package/dist/utils/store.d.ts +84 -8
- package/docs/authoring-plugins.md +408 -0
- package/docs/using-plugins.md +374 -0
- package/package.json +2 -1
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# Plugin authoring guide
|
|
2
|
+
|
|
3
|
+
Guide for **plugin authors** building their own pinia plugin on top of
|
|
4
|
+
`pinia-plugin-subscription`.
|
|
5
|
+
|
|
6
|
+
If you only want to **use** compatible plugins in an application, read
|
|
7
|
+
[Using compatible plugins](using-plugins.md) instead.
|
|
8
|
+
|
|
9
|
+
> **Documentation convention:** JavaScript examples come first and are
|
|
10
|
+
> complete and runnable as-is. TypeScript versions follow, adding type
|
|
11
|
+
> annotations and generics. Types are always optional at runtime — they only
|
|
12
|
+
> add autocompletion and compile-time checks.
|
|
13
|
+
|
|
14
|
+
## Mental model
|
|
15
|
+
|
|
16
|
+
- A **subscriber** is invoked by the central `PluginSubscription` engine
|
|
17
|
+
each time pinia registers a store.
|
|
18
|
+
- A subscriber can extend the store, register callbacks (mutation
|
|
19
|
+
subscriptions, action subscriptions, reset callbacks), and declare an
|
|
20
|
+
execution policy for SSR.
|
|
21
|
+
- `createPlugin([...subscribers])` aggregates subscribers into a single
|
|
22
|
+
pinia plugin.
|
|
23
|
+
|
|
24
|
+
Two authoring styles are available:
|
|
25
|
+
|
|
26
|
+
1. **Plain object** implementing `PluginSubscriberInterface` — lightweight,
|
|
27
|
+
no classes.
|
|
28
|
+
2. **`PluginSubscriber` abstract class + `Store` subclass** — recommended
|
|
29
|
+
for richer plugins: the `Store` subclass wraps the pinia store with
|
|
30
|
+
helpers (state extension, subscriptions, debug logging).
|
|
31
|
+
|
|
32
|
+
## Your first subscriber: a plain object
|
|
33
|
+
|
|
34
|
+
### JavaScript
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
export const myStoreSubscriber = {
|
|
38
|
+
name: 'my-plugin',
|
|
39
|
+
|
|
40
|
+
invoke: (context, debug) => {
|
|
41
|
+
// context contains `store`, `options`, `pinia`
|
|
42
|
+
console.log('store registered', context.store.$id)
|
|
43
|
+
|
|
44
|
+
return true
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
resetStoreCallback: (store) => {
|
|
48
|
+
console.log('store reset:', store.$id)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### TypeScript
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import type { PluginSubscriberInterface } from 'pinia-plugin-subscription'
|
|
57
|
+
|
|
58
|
+
export const myStoreSubscriber: PluginSubscriberInterface = {
|
|
59
|
+
name: 'my-plugin',
|
|
60
|
+
|
|
61
|
+
invoke: (context, debug) => {
|
|
62
|
+
console.log('store registered', context.store.$id)
|
|
63
|
+
|
|
64
|
+
return true
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
resetStoreCallback: (store) => {
|
|
68
|
+
console.log('store reset:', store.$id)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The host app registers subscribers through `createPlugin()`:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import { createPlugin } from 'pinia-plugin-subscription'
|
|
77
|
+
|
|
78
|
+
pinia.use(createPlugin([myStoreSubscriber]))
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## `PluginSubscriberInterface` reference
|
|
82
|
+
|
|
83
|
+
| Member | Type | Description |
|
|
84
|
+
| --- | --- | --- |
|
|
85
|
+
| `name` (required) | `string` | Unique plugin name; also used by the debug filter. |
|
|
86
|
+
| `invoke` (required) | `(context: PiniaPluginContext, debug: boolean) => boolean` | Called when a store is registered. Return `false` to skip the store. |
|
|
87
|
+
| `execution` | `PluginExecutionOptions` | Where/when the subscriber runs — see [Execution policy & SSR](#execution-policy--ssr). |
|
|
88
|
+
| `hydrationScheduler` | `(run: () => void) => void` | Custom scheduler used when hydration is deferred. |
|
|
89
|
+
| `hydrate` | `(context, debug) => void \| Promise<void>` | SSR-safe initialization hook; guaranteed to run before `afterHydration`. |
|
|
90
|
+
| `afterHydration` | `(context, debug) => void \| Promise<void>` | Post-hydration lifecycle hook. |
|
|
91
|
+
| `resetStoreCallback` | `(store?: Store) => void` | Custom logic executed when a registered store is reset. |
|
|
92
|
+
| `storeOnActionSubscription` | `{ store, callback }` (getter) | Action subscription forwarded to pinia's `$onAction`. |
|
|
93
|
+
| `storeMutationSubscription` | `{ store, callback }` (getter) | Mutation subscription forwarded to pinia's `$subscribe`. |
|
|
94
|
+
| `subscriptions` | `Record<string, PluginSubscription>` | Plugin-specific subscription functions exposed to other plugins. |
|
|
95
|
+
| `console` | `Console` | Custom console used for debug logging. |
|
|
96
|
+
|
|
97
|
+
All members except `name` and `invoke` are optional.
|
|
98
|
+
|
|
99
|
+
## The `PluginSubscriber` abstract class
|
|
100
|
+
|
|
101
|
+
For class-based plugins, extend `PluginSubscriber<Instance>`. The subscriber
|
|
102
|
+
creates the instance through a factory — conventionally
|
|
103
|
+
`MyPlugin.customizeStore.bind(MyPlugin)`.
|
|
104
|
+
|
|
105
|
+
### Type parameter (TypeScript only)
|
|
106
|
+
|
|
107
|
+
| Generic | Constraint | Role |
|
|
108
|
+
| --- | --- | --- |
|
|
109
|
+
| `Instance` | `extends Store` | The `Store` subclass your plugin creates for each registered store. |
|
|
110
|
+
|
|
111
|
+
The generic parameter is optional — JavaScript code and untyped TypeScript
|
|
112
|
+
plugins can extend the class without it. Providing it types the
|
|
113
|
+
`storeInstance` getter, so overridden hooks (`hydrate()`,
|
|
114
|
+
`afterHydration()`, `pluginCreated`) get full autocompletion and compile-time
|
|
115
|
+
checks when they call your plugin's custom methods.
|
|
116
|
+
|
|
117
|
+
Constructor:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
constructor(pluginName: string, createInstance: CreateInstance, pluginConsole?: Console)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
| Member | Kind | Description |
|
|
124
|
+
| --- | --- | --- |
|
|
125
|
+
| `execution` | public field | Execution policy (see below). |
|
|
126
|
+
| `hydrationScheduler` | public field | Custom scheduler used when hydration is deferred. |
|
|
127
|
+
| `pluginCreated` | protected field | Optional hook called with the pinia store after a successful `invoke`. |
|
|
128
|
+
| `pluginOptions` | getter/setter | Extra options merged into the options passed to your `Store` factory. |
|
|
129
|
+
| `storeInstance` | getter | The `Store` subclass instance created for the current store. |
|
|
130
|
+
| `hydrate()` / `afterHydration()` | methods | Lifecycle stubs to override for SSR-safe initialization. |
|
|
131
|
+
| `invoke()` | method | Implemented for you: creates the instance, wires `subscriptions`, `storeSubscribe` and `onAction`, then calls `pluginCreated`. Returns `false` when the factory creates no instance. |
|
|
132
|
+
|
|
133
|
+
### JavaScript
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
import { PluginSubscriber, Store } from 'pinia-plugin-subscription'
|
|
137
|
+
|
|
138
|
+
class MyPlugin extends Store {
|
|
139
|
+
constructor(store, options, debug = false) {
|
|
140
|
+
super(store, options, debug)
|
|
141
|
+
this.doSomething()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
doSomething() {
|
|
145
|
+
this.debugLog('doSomething', this.store.$id)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
class MyPluginSubscriber extends PluginSubscriber {
|
|
150
|
+
constructor() {
|
|
151
|
+
super('my-plugin', (store, options, debug, customConsole) =>
|
|
152
|
+
new MyPlugin(store, options, debug, customConsole))
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export const myStoreSubscriber = new MyPluginSubscriber()
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### TypeScript
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import { PluginSubscriber, Store } from 'pinia-plugin-subscription'
|
|
163
|
+
|
|
164
|
+
class MyPlugin extends Store {
|
|
165
|
+
protected override _className: string = 'MyPlugin'
|
|
166
|
+
protected static override _requiredKeys?: string[] = ['my-plugin-option']
|
|
167
|
+
|
|
168
|
+
constructor(store, options, debug = false) {
|
|
169
|
+
super(store, options, debug)
|
|
170
|
+
this.doSomething()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
doSomething(): void {
|
|
174
|
+
this.debugLog('doSomething', this.store.$id)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
class MyPluginSubscriber extends PluginSubscriber<MyPlugin> {
|
|
179
|
+
constructor() {
|
|
180
|
+
super('my-plugin', MyPlugin.customizeStore.bind(MyPlugin))
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export const myStoreSubscriber = new MyPluginSubscriber()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
> **Important contract:** `Store.customizeStore()` only creates an instance
|
|
188
|
+
> when the store defines `storeOptions` **and** every key listed in the
|
|
189
|
+
> static `_requiredKeys` is present in those options. When no instance is
|
|
190
|
+
> created, `invoke()` returns `false` and the store is skipped by your
|
|
191
|
+
> plugin. Declare `_requiredKeys = []` to accept every store that defines
|
|
192
|
+
> `storeOptions`.
|
|
193
|
+
|
|
194
|
+
## The `Store` base class
|
|
195
|
+
|
|
196
|
+
`Store` wraps a pinia store (`PiniaStore`) and exposes:
|
|
197
|
+
|
|
198
|
+
**Properties:** `debug`, `options`, `state`, `store`.
|
|
199
|
+
|
|
200
|
+
**Methods:**
|
|
201
|
+
|
|
202
|
+
| Method | Description |
|
|
203
|
+
| --- | --- |
|
|
204
|
+
| `addToState(name, value?)` | Adds a property to the store state; exposed as a `Ref` for setup-style stores. |
|
|
205
|
+
| `addSubscription(pluginName, subscription, options?)` | Registers a plugin-specific subscription function. |
|
|
206
|
+
| `getSubscriptions()` | Returns registered subscriptions (`undefined` when empty). |
|
|
207
|
+
| `getOption(optionName)` | Reads one key from the store's `StoreOptions`. |
|
|
208
|
+
| `getStatePropertyValue(propertyName)` | Reads a state property, unwrapping refs. |
|
|
209
|
+
| `storeSubscribe` (getter/setter) | Factory for pinia's `$subscribe` (`{ store, callback }`). |
|
|
210
|
+
| `onAction` (getter/setter) | Factory for pinia's `$onAction` (`{ store, callback }`). |
|
|
211
|
+
| `static customizeStore(store, options, debug?)` | Recommended factory; guarded by `_requiredKeys` (see above). |
|
|
212
|
+
| `debugLog(message, args)` | Conditional logging, active in debug mode. |
|
|
213
|
+
| `stateHas(property)` / `storeHas(property)` | Safe `hasOwnProperty` checks on state / store. |
|
|
214
|
+
| `getValue(value)` | Unwraps a ref or returns the value as-is. |
|
|
215
|
+
| `isOptionApi()` | Whether the store uses the options API. |
|
|
216
|
+
| `hydrate()` / `afterHydration()` | Lifecycle stubs for SSR-safe initialization. |
|
|
217
|
+
|
|
218
|
+
## Execution policy & SSR
|
|
219
|
+
|
|
220
|
+
The source of truth for hydration policy stays on the subscriber itself.
|
|
221
|
+
For example, a client-only plugin declares its execution directly:
|
|
222
|
+
|
|
223
|
+
### JavaScript
|
|
224
|
+
|
|
225
|
+
```js
|
|
226
|
+
import { nextTick } from 'vue'
|
|
227
|
+
|
|
228
|
+
export const persistedStateSubscriber = {
|
|
229
|
+
name: 'persisted-state',
|
|
230
|
+
execution: {
|
|
231
|
+
environment: 'client',
|
|
232
|
+
hydration: 'defer',
|
|
233
|
+
},
|
|
234
|
+
hydrationScheduler: (run) => {
|
|
235
|
+
nextTick(() => run())
|
|
236
|
+
},
|
|
237
|
+
invoke: (context, debug) => {
|
|
238
|
+
// ...
|
|
239
|
+
return true
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### TypeScript
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import { nextTick } from 'vue'
|
|
248
|
+
import type { PluginSubscriberInterface } from 'pinia-plugin-subscription'
|
|
249
|
+
|
|
250
|
+
export const persistedStateSubscriber: PluginSubscriberInterface = {
|
|
251
|
+
name: 'persisted-state',
|
|
252
|
+
execution: {
|
|
253
|
+
environment: 'client',
|
|
254
|
+
hydration: 'defer',
|
|
255
|
+
},
|
|
256
|
+
hydrationScheduler: (run) => {
|
|
257
|
+
nextTick(() => run())
|
|
258
|
+
},
|
|
259
|
+
invoke: (context, debug) => {
|
|
260
|
+
// ...
|
|
261
|
+
return true
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Available execution options (`PluginExecutionOptions`):
|
|
267
|
+
|
|
268
|
+
- `environment: 'both' | 'client' | 'server'` — controls where the
|
|
269
|
+
subscriber is allowed to run.
|
|
270
|
+
- `hydration: 'immediate' | 'defer'` — on the client, `defer` schedules
|
|
271
|
+
execution after hydration.
|
|
272
|
+
- `hydrationScheduler` can be provided either on a subscriber or via
|
|
273
|
+
`createHydrationPlugin()` to override the execution timing when needed.
|
|
274
|
+
|
|
275
|
+
The class-based equivalent sets the same policy in the constructor and
|
|
276
|
+
overrides the lifecycle hooks:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import { nextTick } from 'vue'
|
|
280
|
+
import { PluginSubscriber } from 'pinia-plugin-subscription'
|
|
281
|
+
|
|
282
|
+
class ExtendingStoreSubscriber extends PluginSubscriber<StoreExtension> {
|
|
283
|
+
constructor() {
|
|
284
|
+
super('extendsPiniaStore', StoreExtension.customizeStore.bind(StoreExtension))
|
|
285
|
+
|
|
286
|
+
// Controls when this subscriber may run and whether it should wait
|
|
287
|
+
// for hydration on the client.
|
|
288
|
+
this.execution = {
|
|
289
|
+
environment: 'client',
|
|
290
|
+
hydration: 'defer',
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Optional framework-specific scheduler for deferred execution.
|
|
294
|
+
this.hydrationScheduler = (run) => {
|
|
295
|
+
nextTick(() => run())
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this.pluginCreated = addStore
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
override hydrate() {
|
|
302
|
+
if (typeof window === 'undefined') {
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Safe browser-only initialization, only called when the policy allows it.
|
|
307
|
+
return this.storeInstance?.hydrate?.()
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### SSR / Nuxt best practices
|
|
313
|
+
|
|
314
|
+
`pinia-plugin-subscription` is SSR-safe by design, but the runtime policy
|
|
315
|
+
still needs to match the actual app lifecycle.
|
|
316
|
+
|
|
317
|
+
Use `hydration: 'defer'` when the subscriber depends on browser-only APIs,
|
|
318
|
+
client-side cookies, `localStorage`, DOM access, or a hydrated app state
|
|
319
|
+
that must exist only after the app is mounted.
|
|
320
|
+
|
|
321
|
+
Use `runtimeEnvironment: 'server'` (on `createHydrationPlugin()`) or
|
|
322
|
+
`execution.environment: 'server'` when a subscriber is only valid during
|
|
323
|
+
server-side rendering or should not touch browser-only objects:
|
|
324
|
+
|
|
325
|
+
```js
|
|
326
|
+
pinia.use(createHydrationPlugin([serverOnlySubscriber], {
|
|
327
|
+
runtimeEnvironment: 'server'
|
|
328
|
+
}))
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Avoid browser-only code in stores during build-time SSR. In practice:
|
|
332
|
+
|
|
333
|
+
- do not access `window`, `document`, `localStorage`, or `matchMedia` at
|
|
334
|
+
module scope or during store setup when the code may run on the server;
|
|
335
|
+
- keep browser-specific bootstrap in `hydrate()` or `afterHydration()`;
|
|
336
|
+
- prefer environment-aware guards such as `typeof window !== 'undefined'`;
|
|
337
|
+
- avoid network access or hydration reads before the client is ready,
|
|
338
|
+
unless the code is explicitly intended to run on both environments.
|
|
339
|
+
|
|
340
|
+
A safe pattern:
|
|
341
|
+
|
|
342
|
+
```js
|
|
343
|
+
const mySubscriber = {
|
|
344
|
+
name: 'safe-browser-access',
|
|
345
|
+
execution: {
|
|
346
|
+
environment: 'client',
|
|
347
|
+
hydration: 'defer',
|
|
348
|
+
},
|
|
349
|
+
hydrate: (context) => {
|
|
350
|
+
// safe browser access here, after hydration is allowed
|
|
351
|
+
const saved = localStorage.getItem('demo')
|
|
352
|
+
if (saved) {
|
|
353
|
+
context.store.$patch({ value: JSON.parse(saved) })
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
invoke: () => true,
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
This keeps the server render deterministic and prevents browser-only
|
|
361
|
+
initialization from crashing Nuxt or SSR builds.
|
|
362
|
+
|
|
363
|
+
## Typing per-store options: `StoreOptionsExtensions` (TypeScript)
|
|
364
|
+
|
|
365
|
+
This package is the only one allowed to augment pinia's
|
|
366
|
+
`DefineStoreOptionsBase`. A subscriber plugin must **not** redeclare it:
|
|
367
|
+
merging `storeOptions` with a different (even compatible) type raises
|
|
368
|
+
`TS2717` in consumer code.
|
|
369
|
+
|
|
370
|
+
Instead, per-store plugin options are centralized behind the
|
|
371
|
+
`StoreOptionsExtensions` interface. Augment it through interface merging to
|
|
372
|
+
add your own typed keys:
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
// my-subscriber-plugin/types.d.ts (shipped with your plugin)
|
|
376
|
+
declare module 'pinia-plugin-subscription/types' {
|
|
377
|
+
interface StoreOptionsExtensions {
|
|
378
|
+
myPluginOption?: string
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
The keys merge into `StoreOptions`, so store authors get type-checking
|
|
384
|
+
directly in the store definition:
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
export const useMyStore = defineAStore('myStore', () => { /* ... */ }, {
|
|
388
|
+
rollbackAfterFailure: { save: 'all' }, // built-in option
|
|
389
|
+
myPluginOption: 'value', // typed via your augmentation
|
|
390
|
+
})
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Guidelines:
|
|
394
|
+
|
|
395
|
+
- Always declare **optional** keys with names specific enough to avoid
|
|
396
|
+
collisions (prefer `myPluginOption` over `options`).
|
|
397
|
+
- At runtime, merged options are delivered to your subscriber's
|
|
398
|
+
`invoke(context, debug)` through `context.options.storeOptions` — no
|
|
399
|
+
extra wiring needed.
|
|
400
|
+
- Never redeclare `DefineStoreOptionsBase` or `PiniaCustomProperties`; only
|
|
401
|
+
this package owns those augmentations.
|
|
402
|
+
|
|
403
|
+
## Reference implementation
|
|
404
|
+
|
|
405
|
+
The demo app ships a complete plugin implementation in
|
|
406
|
+
[`src/demo/extending-pinia-store/`](../src/demo/extending-pinia-store/): a
|
|
407
|
+
`PluginSubscriber` subclass, a `Store` subclass, option typing and SSR
|
|
408
|
+
guards. Run `npm run dev` to explore it live.
|