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 +74 -195
- package/dist/module.json +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,32 +1,14 @@
|
|
|
1
1
|
# nuxt-state
|
|
2
2
|
|
|
3
|
-
> Define shared Nuxt state
|
|
3
|
+
> Define shared Nuxt state with the Vue Composition API.
|
|
4
4
|
|
|
5
|
-
`nuxt-state` is
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
|
67
|
-
are auto-imported. `defineState` is also auto-imported and
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
99
|
+
return { count, user, double }
|
|
181
100
|
})
|
|
182
101
|
```
|
|
183
102
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
by the factory rather than serialized.
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
- `ref
|
|
211
|
-
-
|
|
212
|
-
-
|
|
213
|
-
|
|
214
|
-
-
|
|
215
|
-
|
|
216
|
-
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-state",
|
|
3
|
-
"version": "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
|
}
|