nuxt-state 0.3.0 → 1.0.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/README.md CHANGED
@@ -1,32 +1,14 @@
1
1
  # nuxt-state
2
2
 
3
- > Define shared Nuxt state using the same Composition API you already use in composables.
3
+ > Define shared Nuxt state with the Vue Composition API.
4
4
 
5
- `nuxt-state` is an experimental Nuxt 4 module that explores one small primitive:
5
+ `nuxt-state` is a focused Nuxt 4 module. A composable created with `defineState` runs lazily
6
+ once per Nuxt application and returns the same result to every caller in that app. Its stable
7
+ 1.x public API is `defineState(factory)`.
6
8
 
7
- ```ts
8
- defineState(() => {
9
- // standard Vue Composition API
10
- return {/* public state */}
11
- })
12
- ```
13
-
14
- A regular composable runs its factory for every invocation. A composable created by
15
- `defineState` runs its factory lazily, once for the current Nuxt application instance,
16
- and returns that exact result to every caller in that app.
17
-
18
- This is a working open-source prototype for discussion and possible future contribution to
19
- Nuxt. The API is intentionally narrow and the project is not yet presented as production-ready.
20
-
21
- ## Why
22
-
23
- Nuxt's `useState()` is excellent for simple SSR-aware values. Pinia is a strong choice
24
- when an application wants a dedicated state-management library and its ecosystem.
25
- This project does not replace either one. It explores a lightweight native abstraction
26
- between a raw `useState()` value and a full state-management library.
27
-
28
- There are no stores, actions, getters, mutations, IDs, configuration objects, or special
29
- wrappers. The factory and its return value use normal Vue semantics.
9
+ It sits between a simple `useState()` value and a full state-management library such as
10
+ Pinia. There are no stores, IDs, actions, getters, or special wrappers—only standard Vue
11
+ primitives.
30
12
 
31
13
  ## Install
32
14
 
@@ -43,7 +25,7 @@ export default defineNuxtConfig({
43
25
 
44
26
  ## Usage
45
27
 
46
- Create a state in Nuxt 4's application source directory:
28
+ Create states in `app/states/`:
47
29
 
48
30
  ```ts
49
31
  // app/states/counter.ts
@@ -55,211 +37,110 @@ export const useCounter = defineState(() => {
55
37
  count.value++
56
38
  }
57
39
 
58
- return {
59
- count,
60
- double,
61
- increment,
62
- }
40
+ return { count, double, increment }
63
41
  })
64
42
  ```
65
43
 
66
- Exports from `app/states/`, including nested directories and multiple exports per file,
67
- are auto-imported. `defineState` is also auto-imported and can be used elsewhere, such
68
- as in `app/composables/`.
44
+ Exports from this directory, including nested directories and multiple exports per file,
45
+ are auto-imported. `defineState` is also auto-imported and may be used elsewhere.
69
46
 
70
47
  ```vue
71
48
  <script setup lang="ts">
72
49
  const { count, double, increment } = useCounter()
73
50
 
74
- count.value++
75
51
  increment()
76
52
  console.log(double.value)
77
53
  </script>
78
54
  ```
79
55
 
80
- Calling `useCounter()` in ten components returns the same object for that Nuxt app.
81
- Different server requests and different client apps receive different objects.
82
-
83
- The returned object is not cloned, wrapped, or transformed. Refs remain refs, computed
84
- refs remain computed refs, reactive objects remain reactive, and functions are unchanged.
56
+ All callers within one Nuxt app receive the exact factory result. Refs, reactive objects,
57
+ computed refs, and functions retain their normal Vue behavior. Server requests and separate
58
+ client apps never share instances.
85
59
 
86
60
  ## Nuxt Layers
87
61
 
88
- Reusable Nuxt Layers can provide states from their resolved application directory:
89
-
90
- ```text
91
- layers/
92
- └── admin/
93
- ├── nuxt.config.ts
94
- └── app/
95
- └── states/
96
- └── permissions.ts
97
- ```
98
-
99
- ```ts
100
- // layers/admin/app/states/permissions.ts
101
- export const usePermissions = defineState(() => {
102
- const roles = ref(['reader'])
103
- return { roles }
104
- })
105
- ```
106
-
107
- `usePermissions()` is auto-imported in the consuming application. Local auto-discovered layers,
108
- layers listed in `extends`, package-provided layers, nested state directories, hydration, and
109
- request isolation use the same behavior as project states.
110
-
111
- nuxt-state follows Nuxt's normal layer priority. The project wins over every extended layer;
112
- higher-priority layers win over lower-priority layers when exports collide. No aliases or custom
113
- override API are generated.
62
+ States in the resolved `app/states/` directory of local, extended, or package-provided
63
+ [Nuxt Layers](https://nuxt.com/docs/4.x/guide/going-further/layers) are auto-imported too.
64
+ Normal Nuxt priority applies: project exports override layer exports, and higher-priority
65
+ layers override lower-priority ones.
114
66
 
115
67
  ## DevTools
116
68
 
117
- Enable Nuxt DevTools normally:
69
+ With Nuxt DevTools enabled, a development-only **Nuxt State** tab provides read-only views
70
+ of active states and discovered state exports. It shows hydration status, safe bounded value
71
+ previews, source paths, and layer origins without executing lazy factories.
118
72
 
119
- ```ts
120
- export default defineNuxtConfig({
121
- devtools: {
122
- enabled: true,
123
- },
124
- })
125
- ```
73
+ The inspector cannot mutate state or invoke functions. It polls only while visible, adds no
74
+ production runtime code, and labels active instances by their internal hydration key because
75
+ Nuxt does not expose a stable mapping from that key to auto-import metadata.
126
76
 
127
- In development, the **Nuxt State** tab provides a read-only inspector. It shows active state
128
- instances, internal hydration keys, Hydrated/Client-only status, member classifications, bounded
129
- value previews, and a separate list of statically discovered states with project-relative source
130
- and layer origin.
131
-
132
- Opening the panel never executes a state factory. Values are queried while the view is visible;
133
- there are no permanent deep watchers. Functions are descriptors and cannot be invoked, large
134
- values are truncated, and cyclic/shared graphs use reference markers. v0.3.0 does not support
135
- editing, patching, resetting, deleting, or disposing application state.
136
-
137
- Nuxt DevTools 3.4's stable iframe integration is used. No alpha/nightly override is required.
138
- Runtime hydration hashes cannot currently be mapped safely back to export names without another
139
- compiler transform, so active cards use `State $<internal-key>` while the separate Known states
140
- list shows names, sources, and layer origins. The internal key is debug information, not API.
141
-
142
- ## Semantics
143
-
144
- - The factory is synchronous and takes no arguments.
145
- - The generated composable takes no arguments.
146
- - The factory is lazy and runs at first use.
147
- - It runs once per Nuxt app instance.
148
- - The exact factory result is returned to all callers in that app.
149
- - The factory runs in a detached Vue effect scope owned by the Nuxt app, so effects and Nuxt
150
- composables are not disposed with the first consuming component.
151
- - A module-local `WeakMap` keys instances by `NuxtApp`, providing request isolation while
152
- allowing old application instances to be garbage-collected.
153
- - Separate `defineState()` calls have separate closure-owned caches, including calls in
154
- the same file.
155
- - Mutable state used during SSR is restored into the client-created refs and reactive proxies
156
- before Vue hydrates the component tree.
157
-
158
- Async factories are rejected by TypeScript and guarded at runtime for JavaScript users.
159
- Expose an async function from synchronous state or use Nuxt's data-fetching APIs instead.
77
+ ## Behavior
160
78
 
161
- ## SSR hydration
79
+ - The factory and generated composable are synchronous and take no arguments.
80
+ - The factory runs lazily, once per Nuxt app, inside an app-lived Vue effect scope.
81
+ - Instances are stored in a `WeakMap` keyed by `NuxtApp` for SSR request isolation and garbage
82
+ collection.
83
+ - Separate `defineState()` calls always have separate caches.
84
+ - HMR recreates the state instead of preserving it.
85
+ - Async work should be exposed as a function or use Nuxt data composables such as `useFetch`.
162
86
 
163
- Since v0.1.0, nuxt-state transparently hydrates mutable top-level members returned by the factory.
164
- Nuxt injects an internal call-site key at build time; the developer-facing call remains exactly
165
- `defineState(factory)`.
87
+ ## SSR hydration
166
88
 
167
- On the server, the module captures the final values after rendering in one namespaced Nuxt
168
- payload entry. On the client, it runs the factory normally and patches those values into the
169
- new refs and reactive proxies before Vue hydration. The returned object is never replaced, so
170
- computed refs, functions, watchers, and closures created by the client factory remain wired to
171
- the hydrated state.
89
+ Mutable top-level refs and reactive objects returned by the factory are snapshotted after SSR
90
+ and restored into client-created values before Vue hydration. The factory result is never
91
+ replaced, so its computed refs, watchers, functions, and closures remain connected.
172
92
 
173
93
  ```ts
174
94
  export const useAccount = defineState(() => {
175
95
  const count = ref(0)
176
96
  const user = reactive({ name: 'Guest', roles: [] as string[] })
177
97
  const double = computed(() => count.value * 2)
178
- const increment = () => count.value++
179
98
 
180
- return { count, user, double, increment }
99
+ return { count, user, double }
181
100
  })
182
101
  ```
183
102
 
184
- Nested serializable values inside supported refs and reactive objects are handled by Nuxt's
185
- payload serializer. Functions, readonly computed refs, and plain runtime objects are recreated
186
- by the factory rather than serialized. Concurrent SSR requests retain separate Nuxt-app
187
- registries and cannot share user state.
188
-
189
- Snapshot discovery is intentionally limited to mutable members exposed by the factory. Reactive
190
- state that must survive SSR hydration currently needs to be exposed from the `defineState`
191
- factory. A private ref captured only by a computed value or function is not visible to the
192
- snapshot layer; if it is mutated during SSR, its client value can differ and cause a hydration
193
- mismatch. Discovering closure-private Vue state would require a new explicit API, compiler-level
194
- analysis, or undocumented reactivity inspection, none of which belongs in v0.2.0.
103
+ Nuxt's payload serializer handles nested serializable values, including supported `Date`,
104
+ `Map`, `Set`, shared-reference, and cyclic graphs. Functions and readonly computed refs are
105
+ recreated by the client factory rather than serialized.
195
106
 
196
- `useFetch()` can remain inside a synchronous state factory. Its request caching and payload
197
- hydration still belong to Nuxt; `nuxt-state` neither replaces nor triggers a second fetch
198
- mechanism. If multiple sibling SSR components must all render completed data, await the returned
199
- Nuxt `AsyncData` promise in a parent/page as you would with normal Nuxt data fetching.
107
+ Reactive state that must survive SSR hydration currently needs to be exposed from the
108
+ `defineState` factory. Closure-private refs cannot be discovered; mutating one during SSR may
109
+ therefore cause a hydration mismatch.
200
110
 
201
- When a returned `useFetch().data` ref is snapshotted, both Nuxt's data payload and the internal
202
- state snapshot refer to it. Nuxt's graph serializer preserves the shared object identity, so the
203
- response body is emitted once rather than copied into the HTML twice. There is still a small
204
- snapshot-metadata overhead.
111
+ `useFetch()` and `useAsyncData()` retain Nuxt's request caching and payload ownership. When
112
+ their returned data ref is exposed, Nuxt's graph serializer keeps the shared payload object
113
+ instead of duplicating the response body. The state snapshot adds only metadata overhead.
205
114
 
206
115
  ## Compatibility
207
116
 
208
- ### Supported
209
-
210
- - `ref()` and `reactive()`, including nested serializable objects and arrays;
211
- - `shallowRef()` and `shallowReactive()` with their shallow semantics preserved;
212
- - `Date`, `Map`, `Set`, shared references, and cyclic graphs supported by Nuxt's payload
213
- serializer;
214
- - readonly computed chains and functions as client-recreated runtime state;
215
- - readonly views when their mutable source is also returned and hydrated;
216
- - `useFetch()`, `useAsyncData()`, `callOnce()`, `useCookie()`, `useRuntimeConfig()`, `useRoute()`,
217
- and `useRouter()` in valid Nuxt contexts;
218
- - project, local, explicit-extends, and package-provided Nuxt Layer states with native priority;
219
- - first use from plugins, route middleware, layouts, pages, and components;
220
- - state lifetime across client navigation and repeated component mount/unmount.
221
-
222
- `useFetch` and `useAsyncData` continue to own their request/payload behavior. nuxt-state's
223
- snapshot metadata points at the same payload graph rather than serializing response bodies again.
224
- `callOnce({ mode: 'navigation' })` retains Nuxt's normal per-navigation behavior.
225
-
226
- ### Characterized or limited
227
-
228
- - A readonly view is not independent mutable state. If its mutable source is private, it follows
229
- the private-state limitation below.
230
- - Writable computed refs are not supported hydration state. Vue exposes no public `isComputed`
231
- check, and a writable computed currently looks like a mutable ref; restoring it invokes its
232
- setter and may cause side effects. Return and hydrate its source refs instead.
233
- - Mutable reactive values that must survive SSR hydration need to be reachable through the
234
- enumerable object returned from the factory.
235
- - The intended return is a composable-style object. Arbitrary ref, reactive-root, function, or
236
- primitive returns still share per app, but the current member-based snapshot format does not
237
- hydrate them.
238
- - Cycles are supported for Nuxt-serializable object graphs, not arbitrary native resources or
239
- custom class instances.
240
-
241
- ## Current limitations
242
-
243
- - Nuxt 4 and Vue 3 only.
244
- - Synchronous factories only.
245
- - No persistence or browser-storage integration.
246
- - Hydration is guaranteed for standard and shallow refs/reactives. `customRef()` and writable
247
- computed hydration are not supported.
248
- - Hydrated values must be serializable by Nuxt's payload system; DOM nodes, sockets, functions
249
- inside refs, symbols, and arbitrary native/class resources are unsupported.
250
- - Closure-private mutable state is not discoverable; return any ref/reactive value whose SSR
251
- mutations must hydrate.
252
- - State resets when its module is hot-reloaded; HMR preservation is not implemented.
253
- - Context-sensitive composables must still be called while normal Nuxt context is available.
254
- - DevTools is read-only and development-only. Active hashes are not safely correlated with
255
- static export names; there is no public or process-global state registry.
256
- - There is no reset API or keyed/multi-instance state.
257
- - The keyed transform is source-sensitive. The supported path is the module's auto-imported
258
- `defineState`; a barrel re-export or unrelated manual wrapper is not guaranteed to receive an
259
- internal hydration key.
260
-
261
- See [the architecture notes](./docs/architecture.md) and [roadmap](./docs/roadmap.md) for
262
- implementation constraints and deferred work.
117
+ Supported:
118
+
119
+ - `ref`, `reactive`, `shallowRef`, and `shallowReactive`;
120
+ - readonly computed values and functions recreated by the client factory;
121
+ - readonly views whose mutable source is also returned;
122
+ - Nuxt-serializable nested values, shared references, and cycles;
123
+ - `useFetch`, `useAsyncData`, `callOnce`, `useCookie`, `useRuntimeConfig`, `useRoute`, and
124
+ `useRouter` in valid Nuxt contexts;
125
+ - first use from plugins, middleware, layouts, pages, and components;
126
+ - project and Nuxt Layer states.
127
+
128
+ Limitations:
129
+
130
+ - Nuxt 4 and Vue 3 only; synchronous factories only.
131
+ - Hydrated state must be an enumerable object containing supported mutable members.
132
+ - Closure-private mutable state, `customRef`, and writable computed hydration are unsupported.
133
+ Restoring a writable computed may invoke its setter, so return its source refs instead.
134
+ - Payload values must be serializable by Nuxt; DOM nodes, sockets, symbols, functions inside
135
+ refs, and arbitrary class/native resources are unsupported.
136
+ - No persistence, reset API, keyed instances, or HMR state preservation.
137
+ - Context-sensitive Nuxt composables still require a valid Nuxt context.
138
+ - The supported transform path is the module's auto-imported `defineState`; barrel re-exports
139
+ and unrelated wrappers are not guaranteed to receive a hydration key.
140
+ - DevTools is development-only and read-only.
141
+
142
+ See [architecture](./docs/architecture.md) for implementation details and the
143
+ [roadmap](./docs/roadmap.md) for deferred work.
263
144
 
264
145
  ## Development
265
146
 
@@ -277,9 +158,7 @@ pnpm prepack
277
158
  pnpm dev:build
278
159
  ```
279
160
 
280
- The browser suite requires Chromium, installed with
281
- `pnpm exec playwright-core install chromium`. The playground contains two counter components,
282
- a reactive object example, a nested state, and two independent states exported from one file.
161
+ Install Chromium for browser tests with `pnpm exec playwright-core install chromium`.
283
162
 
284
163
  ## License
285
164
 
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": "^4.0.0"
5
5
  },
6
6
  "configKey": "nuxt-state",
7
- "version": "0.3.0",
7
+ "version": "1.0.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-state",
3
- "version": "0.3.0",
3
+ "version": "1.0.0",
4
4
  "description": "Define shared Nuxt state using the same Composition API you already use in composables.",
5
5
  "keywords": [
6
6
  "composable",
@@ -53,12 +53,14 @@
53
53
  "@nuxt/module-builder": "^1.0.3",
54
54
  "@nuxt/schema": "^4.5.2",
55
55
  "@nuxt/test-utils": "^4.1.0",
56
+ "@pinia/nuxt": "1.0.2",
56
57
  "@types/node": "latest",
57
58
  "changelogen": "^0.6.2",
58
59
  "happy-dom": "^20.8.3",
59
60
  "nuxt": "^4.5.2",
60
61
  "oxfmt": "^0.65.0",
61
62
  "oxlint": "^1.80.0",
63
+ "pinia": "4.0.3",
62
64
  "playwright-core": "^1.62.1",
63
65
  "typescript": "^6.0.3",
64
66
  "vitest": "^4.1.11",
@@ -69,6 +71,7 @@
69
71
  "node": ">=22"
70
72
  },
71
73
  "scripts": {
74
+ "bench:bundle-size": "pnpm run prepack && node benchmarks/bundle-size/run.mjs",
72
75
  "dev": "pnpm dev:prepare && nuxt dev playground",
73
76
  "dev:build": "nuxt build playground",
74
77
  "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
@@ -81,6 +84,6 @@
81
84
  "test:stress": "vitest run --config vitest.stress.config.ts",
82
85
  "check": "pnpm run fmt:check && pnpm run lint",
83
86
  "test:watch": "vitest",
84
- "test:types": "nuxt-module-build prepare && nuxt-module-build build && vue-tsc --noEmit && pnpm --dir playground exec vue-tsc --noEmit"
87
+ "test:types": "nuxt-module-build prepare && nuxt-module-build build && vue-tsc --noEmit && nuxt prepare playground && pnpm --dir playground exec vue-tsc --noEmit"
85
88
  }
86
89
  }