pinia-plugin-subscription 0.2.0-beta.0 → 0.2.1

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 CHANGED
@@ -5,7 +5,34 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.2.1]
9
+
10
+ ### Fixed
11
+ - Kept deferred action subscribers attached to the correct store after hydration, and scoped action/reset callbacks to each store so disposal and recreation do not leak callbacks.
12
+ - Preserved existing refs with falsy values when adding setup-store state.
13
+ - Made `stateKeys: []` restore the full state in both explicit and automatic rollback.
14
+
15
+ ## [0.2.0]
16
+
17
+ ### Added
18
+ - Added `StoreOptionsExtensions`, a centralized extension point for per-store plugin options: subscriber plugins can now type their own `storeOptions` keys through interface merging (`declare module 'pinia-plugin-subscription/types'`) instead of redeclaring pinia's `DefineStoreOptionsBase`, which would raise TS2717 type-conflict errors in consumer projects.
19
+ - Enabled Vitest typecheck tests (`*.test-d.ts`) so the `StoreOptionsExtensions` merging contract is validated at compile time alongside the runtime test suite.
20
+ - Added `docs/using-plugins.md`, a consumer-oriented guide: plugin registration, `defineAStore` / `defineAStoreCtx` reference (including TypeScript generics with JavaScript-first examples), `$reset`, state rollback, and SSR / Nuxt registration through `createHydrationPlugin`.
21
+ - Added `docs/authoring-plugins.md`, a plugin-author guide: `PluginSubscriberInterface` reference, the `PluginSubscriber` abstract class (including its `Instance` type parameter), the `Store` base class, execution policy & SSR best practices, and `StoreOptionsExtensions` augmentation.
22
+ - Documented the testing setup and the trust & supply-chain practices (GPG-signed commits, review-gated merges, npm provenance) in the README.
23
+
24
+ ### Changed
25
+ - Restructured the documentation around the package's two audiences: the README is now a lightweight entry point that routes readers to the dedicated consumer and plugin-author guides.
26
+ - Added the `docs/` directory to the published package files so both guides ship with the npm tarball.
27
+
28
+ ### Fixed
29
+ - Aligned the `$rollbackAfterFailure` public type with its implementation: action arguments are now passed as a single array (`$rollbackAfterFailure(params, [arg1, arg2])`, forwarded via `apply()`), instead of the documented variadic form that threw `TypeError: CreateListFromArrayLike called on non-object` at runtime.
30
+ - Fixed store typings lost when defining stores with `defineAStore`, `defineAStoreCtx`, `defineAStoreSetup` or `defineAStoreOptionApi`: state, getters and actions are now inferred from the store definition (setup function return or options object), exactly like pinia's `defineStore`, so IDEs list store members and display action prototypes. Explicit store/state generics (e.g. `defineAStore<Sto, Sta>`) remain supported for plugin-augmented stores and now type the store returned by `useStore()` — they were previously intersected onto the store definition itself, which left the returned store loosely typed.
31
+ - Added the public `DefineAStoreDefinition<Sto, Sta>` type: the store definition returned when explicit store/state generics are provided. It extends pinia's `StoreDefinition` so it stays assignable wherever a standard store definition is expected.
32
+ - Widened the `getDefineAStoreSetupContext` parameter to accept any store object, so fully inferred stores (without index signatures) can be passed without a cast.
33
+ - Fixed stores defined with explicit generics (`defineAStore<Sto, Sta>` / `defineAStoreCtx<Sto, Sta>`) exposing every non-state member as `any`: the state was intersected with pinia's `StateTree`, whose `any` index signature widened actions and broke IDE autocompletion. The state is now mapped through the new public `StrictStateTree<Sta>` type.
34
+
35
+ ## [0.2.0-beta.0]
9
36
 
10
37
  ### Added
11
38
  - Added automatic state rollback on action failure via the `rollbackAfterFailure` store option: the declared state keys are snapshotted before the action runs and restored through `$patch` when the action throws (sync or async).
package/README.md CHANGED
@@ -1,377 +1,85 @@
1
- # pinia-plugin-subscription
1
+ # pinia-plugin-subscription
2
2
 
3
- Pinia plugin for Vue.js that helps building Pinia plugins by centralizing subscriber registration and providing a `Store` base class for store helpers.
3
+ Pinia plugin for Vue.js that helps building Pinia plugins by centralizing subscriber registration and providing a `Store` base class for store helpers.
4
4
 
5
- This project provides:
6
- - a lightweight mechanism to declare "subscribers" that are invoked when stores are registered or updated by Pinia;
7
- - a `Store` base class (helper wrapper) to ease interacting with Pinia stores from subscribers or other plugin code;
8
- - an API to create a Pinia plugin from a list of subscribers;
9
- - the $reset method to all stores modified by the plugin;
10
- - automatic state rollback on action failure via the `rollbackAfterFailure` store option and the `$rollbackAfterFailure` method.
5
+ This project provides:
6
+ - a lightweight mechanism to declare "subscribers" that are invoked when stores are registered or updated by Pinia;
7
+ - a `Store` base class (helper wrapper) to ease interacting with Pinia stores from subscribers or other plugin code;
8
+ - an API to create a Pinia plugin from a list of subscribers;
9
+ - the `$reset` method on all stores modified by the plugin;
10
+ - automatic state rollback on action failure via the `rollbackAfterFailure` store option and the `$rollbackAfterFailure` method.
11
11
 
12
- The main goal is to offer a clear API for writing reusable Pinia plugins and to make it easy to extend stores from plugin code.
12
+ Deferred subscribers attach their action and reset callbacks to their own store after hydration; disposing a store removes its callbacks. For both rollback APIs, `stateKeys: []` restores the full state on failure.
13
13
 
14
- ## Installation
14
+ ## Tested & maintained
15
15
 
16
- Ensure Pinia is installed, then register the plugin in your `main.ts`:
16
+ The package is fully tested with [Vitest](https://vitest.dev/): runtime behavior, type-level contracts (`*.test-d.ts`) and performance regressions are covered, and coverage reports are available in `coverage/`. The full test suite and the build run on every push and pull request through [GitHub Actions](.github/workflows/ci.yml).
17
17
 
18
- For local development and runtime demos, use the dedicated demo app with `npm run dev`. The library build remains `npm run build` and is intentionally kept separate from the demo entry.
18
+ ## Trust & supply chain
19
19
 
20
- For helper-only imports, the package also exposes an additive subpath that is kept separate from the default library API:
20
+ This package is **open source (MIT)** and developed in a [public repository](https://github.com/AlxBDo/pinia-plugin-subscription). You can read, audit, fork and self-host the code at any time — no vendor lock-in, no service-continuity risk.
21
21
 
22
- ```typescript
23
- import { PluginSubscriber, Store } from 'pinia-plugin-subscription/helpers'
24
- ```
22
+ Development follows these practices:
25
23
 
26
- For type-only imports, prefer the dedicated public types entry:
24
+ - **No direct pushes to `main`** — every change lands through a reviewed pull request (branch protection enforced).
25
+ - **Commits are GPG-signed** and verified on GitHub.
26
+ - **Tests and build gate every merge** — a pull request cannot merge with a failing CI run.
27
+ - **npm releases ship with provenance** — each published package is built and signed on GitHub Actions and cryptographically linked to the exact commit and CI run that built it ([example transparency log entry](https://search.sigstore.dev/?logIndex=2811656622)), so you can verify what you install.
28
+ - **Security issues are handled privately** — see [SECURITY.md](SECURITY.md) for the reporting process.
27
29
 
28
- ```typescript
29
- import type { PluginSubscriptionOptions } from 'pinia-plugin-subscription/types'
30
+ ## Documentation
31
+
32
+ The package serves two audiences:
33
+
34
+ | You want to… | Guide |
35
+ | --- | --- |
36
+ | Use a compatible plugin (e.g. `pinia-plugin-extending-store`, `pinia-plugin-persist-state`) or this package's built-in store features (`defineAStore`, `$reset`, state rollback) in your app | [Using compatible plugins](docs/using-plugins.md) |
37
+ | Build your own pinia plugin on top of `pinia-plugin-subscription` | [Plugin authoring guide](docs/authoring-plugins.md) |
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ npm install pinia pinia-plugin-subscription
30
43
  ```
31
44
 
32
- ```typescript
45
+ Register the plugin in your `main.ts`:
46
+
47
+ ```js
33
48
  import { createApp } from 'vue'
34
49
  import { createPinia } from 'pinia'
35
- import { createPlugin, PLUGIN_NAME as PPS } from 'pinia-plugin-subscription'
36
- import { myStoreSubscriber } from './src/core/my-store'
50
+ import { createPlugin, PLUGIN_NAME } from 'pinia-plugin-subscription'
51
+ import { myStoreSubscriber } from './plugins/my-store'
37
52
  import App from './App.vue'
38
53
 
39
54
  const app = createApp(App)
40
55
  const pinia = createPinia()
41
56
 
42
- // Register plugin (subscribers array, debug mode)
43
- pinia.use(createPlugin([myStoreSubscriber], [PPS]))
57
+ // Register plugin (subscribers array, plugins to debug)
58
+ pinia.use(createPlugin([myStoreSubscriber], [PLUGIN_NAME]))
44
59
 
45
60
  app.use(pinia)
46
61
  app.mount('#app')
47
62
  ```
48
63
 
49
- ## Usage — Examples
50
-
51
- **1) Subscriber using a `Store` subclass:**
52
-
53
- ```typescript
54
- import PluginSubscriber from 'pinia-plugin-subscription'
55
- import { Store } from 'pinia-plugin-subscription'
56
-
57
- class MyPlugin extends Store {
58
- protected override _className: string = 'MyPlugin'
59
- protected static override _requiredKeys?: string[] | undefined = ['my-plugin-option']
60
-
61
- constructor(store, options, debug = false) {
62
- super(store, options, debug)
63
- this.doSomething()
64
- }
65
- }
66
-
67
- class MyPluginSubscriber extends PluginSubscriber<MyPlugin> {
68
- constructor() {
69
- super('my-plugin', MyPlugin.customizeStore.bind(MyPlugin))
70
- }
71
- }
72
-
73
- export const myStoreSubscriber = new MyPluginSubscriber()
74
- ```
75
-
76
- **2) Simple subscriber implementing `PluginSubscriberInterface`:**
77
-
78
- ```typescript
79
- import type { PluginSubscriberInterface } from 'pinia-plugin-subscription'
80
-
81
- export const myStoreSubscriber: PluginSubscriberInterface = {
82
- name: 'my-plugin',
83
-
84
- invoke: (context, debug) => {
85
- // context contains `store`, `options`, `pinia`
86
- console.log('store registered', context.store.$id)
87
-
88
- return true
89
- },
90
-
91
- resetStoreCallback: (store) => {
92
- console.log('store reset:', store.$id)
93
- }
94
- }
95
- ```
96
-
97
- ## Advanced Features
64
+ ### Import subpaths
98
65
 
99
- - **Debug mode:** Specifies the name of the plugin(s) to debug as the second argument to `createPlugin` to enable detailed logging.
100
- - **Reset callbacks:** Define `resetStoreCallback` to run custom logic when a store is reset.
101
- - **SSR and hydration control:** Configure whether each subscribed plugin runs on the server, the client, or only after client hydration.
102
- - **Lifecycle safety:** Internal hydration ordering is now guarded so `hydrate()` completes before `afterHydration()` and disposed stores do not retain stale subscriber metadata.
103
-
104
- ## API Reference
105
-
106
- ### `createPlugin(subscribers: PluginSubscriber[], debug?: string[]): PiniaPlugin`
107
-
108
- Creates and returns a Pinia plugin from the provided `subscribers`. Each subscriber is invoked when a store is registered.
109
-
110
- ### `createHydrationPlugin(subscribers: PluginSubscriber[], options?: PluginSubscriptionOptions): PiniaPlugin`
111
-
112
- Creates a Pinia plugin dedicated to SSR / Nuxt hydration orchestration. This helper is useful when a store plugin needs a runtime-specific scheduler or environment override without forcing every app to re-declare the plugin’s execution policy.
113
-
114
- ```typescript
115
- import { createHydrationPlugin, PLUGIN_NAME as PPS } from 'pinia-plugin-subscription'
116
-
117
- pinia.use(createHydrationPlugin([
118
- persistedStateSubscriber
119
- ], {
120
- debug: [PPS],
121
- hydrationScheduler: (run) => {
122
- nextTick(() => run())
123
- }
124
- }))
125
- ```
126
-
127
- ### SSR / Nuxt hydration control
128
-
129
- The source of truth for hydration policy stays on the subscriber itself. For example, a client-only plugin should declare its execution directly:
130
-
131
- ```typescript
132
- export const persistedStateSubscriber: PluginSubscriberInterface = {
133
- name: 'persisted-state',
134
- execution: {
135
- environment: 'client',
136
- hydration: 'defer',
137
- },
138
- hydrationScheduler: (run) => {
139
- nextTick(() => run())
140
- },
141
- invoke: (context, debug) => {
142
- // ...
143
- return true
144
- }
145
- }
146
- ```
147
-
148
- Available execution options:
149
-
150
- - `environment: 'both' | 'client' | 'server'` — controls where the subscriber is allowed to run.
151
- - `hydration: 'immediate' | 'defer'` — on the client, `defer` schedules execution after hydration.
152
- - `hydrationScheduler` can be provided either on a subscriber or via `createHydrationPlugin()` to override the execution timing when needed.
153
-
154
- ### SSR / Nuxt best practices
155
-
156
- `pinia-plugin-subscription` is SSR-safe by design, but the runtime policy still needs to match the actual app lifecycle.
157
-
158
- Use `hydration: 'defer'` when the subscriber depends on browser-only APIs, client-side cookies, localStorage, DOM access, or a hydrated app state that must exist only after the app is mounted.
159
-
160
- ```ts
161
- const persistedStateSubscriber: PluginSubscriberInterface = {
162
- name: 'persisted-state',
163
- execution: {
164
- environment: 'client',
165
- hydration: 'defer',
166
- },
167
- hydrationScheduler: (run) => nextTick(() => run()),
168
- invoke: () => true,
169
- }
170
- ```
171
-
172
- Use `runtimeEnvironment: 'server'` or `execution.environment: 'server'` when a subscriber is only valid during server-side rendering or when it should not touch browser-only objects.
66
+ For helper-only imports, the package exposes an additive subpath kept separate from the default library API:
173
67
 
174
68
  ```ts
175
- pinia.use(createHydrationPlugin([serverOnlySubscriber], {
176
- runtimeEnvironment: 'server'
177
- }))
69
+ import { PluginSubscriber, Store } from 'pinia-plugin-subscription/helpers'
178
70
  ```
179
71
 
180
- Avoid browser-only code in stores during build-time SSR. In practice, that means:
181
-
182
- - do not access `window`, `document`, `localStorage`, or `matchMedia` at module scope or during store setup when the code may run on the server,
183
- - keep browser-specific bootstrap in `hydrate()` or `afterHydration()`,
184
- - prefer environment-aware guards such as `typeof window !== 'undefined'`,
185
- - avoid doing network access or hydration reads before the client is ready unless the code is explicitly intended to run on both environments.
186
-
187
- A safe pattern is:
72
+ For type-only imports, prefer the dedicated public types entry:
188
73
 
189
74
  ```ts
190
- const mySubscriber: PluginSubscriberInterface = {
191
- name: 'safe-browser-access',
192
- execution: {
193
- environment: 'client',
194
- hydration: 'defer',
195
- },
196
- hydrate: (context) => {
197
- // safe browser access here after hydration is allowed
198
- const saved = localStorage.getItem('demo')
199
- if (saved) {
200
- context.store.$patch({ value: JSON.parse(saved) })
201
- }
202
- },
203
- invoke: () => true,
204
- }
205
- ```
206
-
207
- This keeps the server render deterministic and prevents browser-only initialization from crashing Nuxt or SSR builds.
208
-
209
- ### State rollback on action failure
210
-
211
- The plugin can automatically restore the previous state when an action throws. Declare the state keys to snapshot per action via the `rollbackAfterFailure` store option:
212
-
213
- ```typescript
214
- import { defineAStore } from 'pinia-plugin-subscription'
215
-
216
- export const useMyStore = defineAStore('myStore', () => {
217
- const count = ref(0)
218
-
219
- function riskyAction() {
220
- count.value += 1
221
- throw new Error('Something went wrong')
222
- }
223
-
224
- return { count, riskyAction }
225
- }, {
226
- rollbackAfterFailure: {
227
- // only the listed keys are snapshotted / restored (deep-cloned)
228
- riskyAction: ['count']
229
- }
230
- })
231
- ```
232
-
233
- Before a configured action runs, the declared state keys are snapshotted. If the action throws (synchronously or by rejecting its returned promise), the snapshot is restored through `$patch`, so partial mutations are rolled back. The snapshot is always discarded once the action completes or fails.
234
-
235
- Only the actions listed in `rollbackAfterFailure` are snapshotted, so unrelated actions pay no cloning cost. Use `'all'` instead of a key list to snapshot the full state — prefer explicit keys for large states.
236
-
237
- Action names starting with `_` or `$` are rejected to protect internal store methods.
238
-
239
- #### Manual rollback with `$rollbackAfterFailure`
240
-
241
- Every store registered by the plugin also exposes a `$rollbackAfterFailure` method to run any action with an explicit snapshot/rollback wrapper, without declaring it in the store options:
242
-
243
- ```typescript
244
- const store = useMyStore()
245
-
246
- try {
247
- await store.$rollbackAfterFailure(
248
- { action: 'riskyAction', stateKeys: ['count'] },
249
- // ...args forwarded to the action
250
- )
251
- } catch (error) {
252
- // the action failed and the state was restored
253
- }
254
- ```
255
-
256
- - `params.action` — the name of the action to execute.
257
- - `params.stateKeys` — optional; the state keys to snapshot, or `'all'` (default when omitted or empty).
258
- - The original error is rethrown after the state is restored.
259
-
260
- ### `defineAStoreCtx(id, setup, options?)`
261
-
262
- Use actions or access the state, added by one or more plugins, when defining the store.
263
-
264
- ```typescript
265
- import { defineAStoreCtx, getEnhancedStore } from 'pinia-plugin-subscription'
266
-
267
- type EnhancedStore = { addItem: (value: unknown) => void }
268
-
269
- export const useMyStore = defineAStoreCtx('myStore', (ctx) => {
270
- const enhanced = getEnhancedStore<EnhancedStore>(ctx)
271
-
272
- return {
273
- addViaEnhanced: (value: unknown) => enhanced.addItem(value)
274
- }
275
- })
276
- ```
277
-
278
- `ctx.extensions` now supports:
279
- - `enhancedStore`
280
-
281
- The setup context is indexed internally only when `storeOptions.enhancedStore` is set to `true` (this is enabled automatically by `defineAStoreCtx`).
282
- The temporary lookup by store id is now cleaned as soon as the weak-map association is established (and on store dispose), which limits map growth over time.
283
- When `storeOptions.debug` is `true`, setup-context map size transitions are logged for observability.
284
-
285
- ### `PluginSubscriberInterface`
286
-
287
- An object with at least an `invoke(context: PiniaPluginContext, debug?: boolean)` method, plus optional properties:
288
-
289
- - `execution?: { environment?: 'both' | 'client' | 'server'; hydration?: 'immediate' | 'defer' }`
290
- - `hydrationScheduler?: (run: () => void) => void`
291
- - `hydrate?: (context, debug) => void | Promise<void>` — optional hook for SSR-safe store initialization after the subscriber is accepted for execution
292
- - `afterHydration?: (context, debug) => void | Promise<void>` — optional lifecycle hook for post-hydration work
293
- - `resetStoreCallback?: (store?: any) => void`
294
- - `storeOnActionSubscription?: { store, callback }` (getter)
295
- - `storeMutationSubscription?: { store, callback }` (getter)
296
- - `subscriptions?: Record<string, Function>` (plugin-specific subscription functions)
297
-
298
- ## The `PluginSubscriber` Abstract Class
299
-
300
- The project provides an abstract `PluginSubscriber` implementation (see [src/core/PluginSubscriber.ts](src/core/PluginSubscriber.ts)) to simplify creating reusable subscribers.
301
-
302
- **Typical usage:**
303
-
304
- - The subscriber instantiates a `Store` (or subclass) via a factory (`MyStore.customizeStore`).
305
- - The instance exposes:
306
- - `subscriptions` (from `getSubscriptions()`)
307
- - `storeMutationSubscription` (from `storeSubscribe`)
308
- - `storeOnActionSubscription` (from `onAction`)
309
- - optional `pluginCreated(store)` hook called after initialization
310
-
311
- **Example:**
312
-
313
- ```typescript
314
- import { nextTick } from 'vue'
315
- import PluginSubscriber from 'pinia-plugin-subscription'
316
- import StoreExtension from './src/extending-pinia-store/core/StoreExtension'
317
- import { addStore } from './src/extending-pinia-store/plugins/stores'
318
-
319
- class ExtendingStoreSubscriber extends PluginSubscriber<StoreExtension> {
320
- constructor() {
321
- super('extendsPiniaStore', StoreExtension.customizeStore.bind(StoreExtension))
322
-
323
- // Controls when this subscriber may run and whether it should wait for hydration on the client.
324
- this.execution = {
325
- environment: 'client',
326
- hydration: 'defer',
327
- }
328
-
329
- // Optional framework-specific scheduler for deferred execution.
330
- this.hydrationScheduler = (run) => {
331
- nextTick(() => run())
332
- }
333
-
334
- this.pluginCreated = addStore
335
- }
336
-
337
- override hydrate() {
338
- if (typeof window === 'undefined') {
339
- return
340
- }
341
-
342
- // Safe browser-only initialization. This is only called when the policy allows it.
343
- return this.storeInstance?.hydrate?.()
344
- }
345
- }
346
-
347
- export const extendingStoreSubscriber = new ExtendingStoreSubscriber()
75
+ import type { PluginSubscriptionOptions } from 'pinia-plugin-subscription/types'
348
76
  ```
349
77
 
350
- This example shows the recommended pattern for SSR / Nuxt-safe plugins:
351
-
352
- - `execution.environment` restricts the subscriber to `'both'`, `'client'`, or `'server'`
353
- - `execution.hydration` is `'immediate'` by default and can be set to `'defer'` when the plugin must wait for client hydration
354
- - `hydrationScheduler` lets you override the runtime scheduling behavior, for example with Nuxt/Vue `nextTick()`
355
- - `hydrate()` / `afterHydration()` are optional lifecycle hooks for browser-only initialization and post-hydration work; they help keep SSR-sensitive logic out of constructors
356
-
357
- ## The `Store` Class — Summary
358
-
359
- The `Store` class (see [src/core/Store.ts](src/core/Store.ts)) is a wrapper around a `PiniaStore` providing:
360
-
361
- - **Properties:** `debug`, `options`, `state`, `store`.
362
- - **Useful methods:**
363
- - `addToState(name, value?)` — adds a property to store state and exposes it as a `Ref` when appropriate.
364
- - `addSubscription(pluginName, subscription)` — registers a plugin-specific subscription function.
365
- - `getSubscriptions()` — returns registered subscriptions.
366
- - `storeSubscribe` (getter/setter) — factory for `store.$subscribe` ({ store, callback }).
367
- - `onAction` (getter/setter) — factory for `onAction` ({ store, callback }).
368
- - `static customizeStore(store, options, debug?)` — recommended factory for class instantiation.
369
- - `debugLog(message, args)` — conditional logging.
370
- - Helpers: `stateHas()`, `storeHas()`, `getValue()`.
371
-
372
- ## Testing
78
+ ## Development
373
79
 
374
- This plugin is tested with Vitest. Coverage reports are available in the `coverage/` directory.
80
+ - `npm run dev` — runs the dedicated demo app. The library build remains `npm run build` and is intentionally kept separate from the demo entry. The demo ships a complete plugin-authoring example in [`src/demo/extending-pinia-store/`](src/demo/extending-pinia-store/).
81
+ - `npm test` — runs the Vitest suite, including typecheck tests (`*.test-d.ts`) validating type-level contracts such as `StoreOptionsExtensions` merging. Coverage reports are available in `coverage/`.
82
+ - `npm run bundle:check` — measures the emitted bundle size.
375
83
 
376
84
  ## License
377
85
 
@@ -379,4 +87,4 @@ MIT
379
87
 
380
88
  ## Publishing
381
89
 
382
- Releases are published to npm via GitHub Actions. GitHub releases marked as pre-releases are published with the `beta` dist-tag using `npm publish --tag beta`; stable releases use the default `latest` tag. The workflow uses npm provenance for better supply-chain security and requires a `NPM_TOKEN` secret in the repository settings.
90
+ Releases are published to npm via GitHub Actions. GitHub releases marked as pre-releases are published with the `beta` dist-tag using `npm publish --tag beta`; stable releases use the default `latest` tag. The workflow uses npm provenance for better supply-chain security and requires a `NPM_TOKEN` secret in the repository settings.