nuxt-state 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Navid Talebian
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # nuxt-state
2
+
3
+ > Define shared Nuxt state using the same Composition API you already use in composables.
4
+
5
+ `nuxt-state` is an experimental Nuxt 4 module that explores one small primitive:
6
+
7
+ ```ts
8
+ defineState(() => {
9
+ // standard Vue Composition API
10
+ return {
11
+ /* public state */
12
+ };
13
+ });
14
+ ```
15
+
16
+ A regular composable runs its factory for every invocation. A composable created by
17
+ `defineState` runs its factory lazily, once for the current Nuxt application instance,
18
+ and returns that exact result to every caller in that app.
19
+
20
+ This is a working prototype for discussion and possible future contribution to Nuxt.
21
+ It is not yet presented as production-ready.
22
+
23
+ ## Why
24
+
25
+ Nuxt's `useState()` is excellent for simple SSR-aware values. Pinia is a strong choice
26
+ when an application wants a dedicated state-management library and its ecosystem.
27
+ This project does not replace either one. It explores a lightweight native abstraction
28
+ between a raw `useState()` value and a full state-management library.
29
+
30
+ There are no stores, actions, getters, mutations, IDs, configuration objects, or special
31
+ wrappers. The factory and its return value use normal Vue semantics.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pnpm add nuxt-state
37
+ ```
38
+
39
+ ```ts
40
+ // nuxt.config.ts
41
+ export default defineNuxtConfig({
42
+ modules: ["nuxt-state"],
43
+ });
44
+ ```
45
+
46
+ The package has not been published yet; during development, use this repository as a
47
+ workspace dependency.
48
+
49
+ ## Usage
50
+
51
+ Create a state in Nuxt 4's application source directory:
52
+
53
+ ```ts
54
+ // app/states/counter.ts
55
+ export const useCounter = defineState(() => {
56
+ const count = ref(0);
57
+ const double = computed(() => count.value * 2);
58
+
59
+ function increment() {
60
+ count.value++;
61
+ }
62
+
63
+ return {
64
+ count,
65
+ double,
66
+ increment,
67
+ };
68
+ });
69
+ ```
70
+
71
+ Exports from `app/states/`, including nested directories and multiple exports per file,
72
+ are auto-imported. `defineState` is also auto-imported and can be used elsewhere, such
73
+ as in `app/composables/`.
74
+
75
+ ```vue
76
+ <script setup lang="ts">
77
+ const { count, double, increment } = useCounter();
78
+
79
+ count.value++;
80
+ increment();
81
+ console.log(double.value);
82
+ </script>
83
+ ```
84
+
85
+ Calling `useCounter()` in ten components returns the same object for that Nuxt app.
86
+ Different server requests and different client apps receive different objects.
87
+
88
+ The returned object is not cloned, wrapped, or transformed. Refs remain refs, computed
89
+ refs remain computed refs, reactive objects remain reactive, and functions are unchanged.
90
+
91
+ ## Semantics
92
+
93
+ - The factory is synchronous and takes no arguments.
94
+ - The generated composable takes no arguments.
95
+ - The factory is lazy and runs at first use.
96
+ - It runs once per Nuxt app instance.
97
+ - The exact factory result is returned to all callers in that app.
98
+ - A module-local `WeakMap` keys instances by `NuxtApp`, providing request isolation while
99
+ allowing old application instances to be garbage-collected.
100
+ - Separate `defineState()` calls have separate closure-owned caches, including calls in
101
+ the same file.
102
+
103
+ Async factories are rejected by TypeScript and guarded at runtime for JavaScript users.
104
+ Expose an async function from synchronous state or use Nuxt's data-fetching APIs instead.
105
+
106
+ ## SSR scope
107
+
108
+ v0 guarantees isolation between SSR requests: module-level state does not become a
109
+ process-wide user-state singleton. This is intentionally different from hydration.
110
+
111
+ v0 does **not** serialize or hydrate arbitrary factory results. A result may contain
112
+ functions, computed refs, class instances, and other runtime-only values. If a state is
113
+ mutated during SSR, the client may recreate the factory's initial state during hydration.
114
+ Do not rely on server mutations transferring to the client yet.
115
+
116
+ ## Current limitations
117
+
118
+ - Nuxt 4 and Vue 3 only.
119
+ - Synchronous factories only.
120
+ - No persistence or browser-storage integration.
121
+ - No arbitrary-state payload serialization or hydration yet.
122
+ - No Nuxt Layers support yet.
123
+ - State resets when its module is hot-reloaded; HMR preservation is not implemented.
124
+ - Compatibility with every context-sensitive Nuxt composable inside a factory is not
125
+ guaranteed yet.
126
+ - There is no reset API, keyed/multi-instance state, DevTools integration, or central
127
+ registry.
128
+
129
+ See [ROADMAP.md](./ROADMAP.md) for the technical questions behind future hydration and
130
+ context compatibility.
131
+
132
+ ## Development
133
+
134
+ Requires Node.js 22+ and pnpm.
135
+
136
+ ```bash
137
+ pnpm install
138
+ pnpm dev
139
+ pnpm lint
140
+ pnpm test:types
141
+ pnpm test
142
+ pnpm prepack
143
+ pnpm dev:build
144
+ ```
145
+
146
+ The playground contains two counter components, a reactive object example, a nested
147
+ state, and two independent states exported from one file.
148
+
149
+ ## License
150
+
151
+ [MIT](./LICENSE)
@@ -0,0 +1,7 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ type ModuleOptions = Record<string, never>;
4
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
5
+
6
+ export { _default as default };
7
+ export type { ModuleOptions };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "nuxt-state",
3
+ "compatibility": {
4
+ "nuxt": "^4.0.0"
5
+ },
6
+ "configKey": "nuxt-state",
7
+ "version": "0.0.1",
8
+ "builder": {
9
+ "@nuxt/module-builder": "1.0.3",
10
+ "unbuild": "3.6.1"
11
+ }
12
+ }
@@ -0,0 +1,22 @@
1
+ import { resolve } from 'node:path';
2
+ import { defineNuxtModule, createResolver, addImports, addImportsDir } from '@nuxt/kit';
3
+
4
+ const module$1 = defineNuxtModule({
5
+ meta: {
6
+ name: "nuxt-state",
7
+ compatibility: {
8
+ nuxt: "^4.0.0"
9
+ }
10
+ },
11
+ defaults: {},
12
+ setup(_options, nuxt) {
13
+ const resolver = createResolver(import.meta.url);
14
+ addImports({
15
+ name: "defineState",
16
+ from: resolver.resolve("./runtime/app/composables/defineState")
17
+ });
18
+ addImportsDir(resolve(nuxt.options.srcDir, "states/**"));
19
+ }
20
+ });
21
+
22
+ export { module$1 as default };
@@ -0,0 +1,6 @@
1
+ type NotPromise<T> = T extends PromiseLike<unknown> ? never : unknown;
2
+ /**
3
+ * Turns a synchronous composable factory into state shared by one Nuxt app.
4
+ */
5
+ export declare function defineState<T>(factory: () => T & NotPromise<T>): () => T;
6
+ export {};
@@ -0,0 +1,21 @@
1
+ import { useNuxtApp } from "#app";
2
+ function isPromiseLike(value) {
3
+ return (typeof value === "object" && value !== null || typeof value === "function") && "then" in value && typeof value.then === "function";
4
+ }
5
+ export function defineState(factory) {
6
+ const instances = /* @__PURE__ */ new WeakMap();
7
+ return function useDefinedState() {
8
+ const nuxtApp = useNuxtApp();
9
+ if (instances.has(nuxtApp)) {
10
+ return instances.get(nuxtApp);
11
+ }
12
+ const instance = factory();
13
+ if (isPromiseLike(instance)) {
14
+ throw new TypeError(
15
+ "[nuxt-state] State factories must be synchronous. Expose an async function from the state or use Nuxt data-fetching composables instead."
16
+ );
17
+ }
18
+ instances.set(nuxtApp, instance);
19
+ return instance;
20
+ };
21
+ }
@@ -0,0 +1,3 @@
1
+ export { default } from './module.mjs'
2
+
3
+ export { type ModuleOptions } from './module.mjs'
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "nuxt-state",
3
+ "version": "0.0.1",
4
+ "description": "Define shared Nuxt state using the same Composition API you already use in composables.",
5
+ "keywords": [
6
+ "nuxt",
7
+ "nuxt-module",
8
+ "vue",
9
+ "state",
10
+ "composable"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "Navid Talebian",
14
+ "type": "module",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/navidtm/nuxt-state.git"
18
+ },
19
+ "homepage": "https://github.com/navidtm/nuxt-state#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/navidtm/nuxt-state/issues"
22
+ },
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/types.d.mts",
26
+ "import": "./dist/module.mjs"
27
+ }
28
+ },
29
+ "main": "./dist/module.mjs",
30
+ "typesVersions": {
31
+ "*": {
32
+ ".": [
33
+ "./dist/types.d.mts"
34
+ ]
35
+ }
36
+ },
37
+ "files": [
38
+ "dist"
39
+ ],
40
+ "workspaces": [
41
+ "playground"
42
+ ],
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "engines": {
47
+ "node": ">=22"
48
+ },
49
+ "dependencies": {
50
+ "@nuxt/kit": "^4.5.2"
51
+ },
52
+ "peerDependencies": {
53
+ "nuxt": "^4.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "@nuxt/devtools": "^3.4.2",
57
+ "@nuxt/eslint-config": "^1.17.0",
58
+ "@nuxt/module-builder": "^1.0.3",
59
+ "@nuxt/schema": "^4.5.2",
60
+ "@nuxt/test-utils": "^4.1.0",
61
+ "@types/node": "latest",
62
+ "changelogen": "^0.6.2",
63
+ "eslint": "^10.9.0",
64
+ "happy-dom": "^20.8.3",
65
+ "nuxt": "^4.5.2",
66
+ "typescript": "^6.0.3",
67
+ "vitest": "^4.1.11",
68
+ "vue": "^3.5.31",
69
+ "vue-tsc": "^3.3.11"
70
+ },
71
+ "scripts": {
72
+ "dev": "pnpm dev:prepare && nuxt dev playground",
73
+ "dev:build": "nuxt build playground",
74
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
75
+ "lint": "eslint .",
76
+ "test": "vitest run",
77
+ "test:watch": "vitest",
78
+ "test:types": "vue-tsc --noEmit && pnpm --dir playground exec vue-tsc --noEmit"
79
+ }
80
+ }