nuxt-state 0.2.0 → 0.3.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
@@ -83,6 +83,62 @@ Different server requests and different client apps receive different objects.
83
83
  The returned object is not cloned, wrapped, or transformed. Refs remain refs, computed
84
84
  refs remain computed refs, reactive objects remain reactive, and functions are unchanged.
85
85
 
86
+ ## Nuxt Layers
87
+
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.
114
+
115
+ ## DevTools
116
+
117
+ Enable Nuxt DevTools normally:
118
+
119
+ ```ts
120
+ export default defineNuxtConfig({
121
+ devtools: {
122
+ enabled: true,
123
+ },
124
+ })
125
+ ```
126
+
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
+
86
142
  ## Semantics
87
143
 
88
144
  - The factory is synchronous and takes no arguments.
@@ -159,6 +215,7 @@ snapshot-metadata overhead.
159
215
  - readonly views when their mutable source is also returned and hydrated;
160
216
  - `useFetch()`, `useAsyncData()`, `callOnce()`, `useCookie()`, `useRuntimeConfig()`, `useRoute()`,
161
217
  and `useRouter()` in valid Nuxt contexts;
218
+ - project, local, explicit-extends, and package-provided Nuxt Layer states with native priority;
162
219
  - first use from plugins, route middleware, layouts, pages, and components;
163
220
  - state lifetime across client navigation and repeated component mount/unmount.
164
221
 
@@ -192,11 +249,11 @@ snapshot metadata points at the same payload graph rather than serializing respo
192
249
  inside refs, symbols, and arbitrary native/class resources are unsupported.
193
250
  - Closure-private mutable state is not discoverable; return any ref/reactive value whose SSR
194
251
  mutations must hydrate.
195
- - No Nuxt Layers support yet.
196
252
  - State resets when its module is hot-reloaded; HMR preservation is not implemented.
197
253
  - Context-sensitive composables must still be called while normal Nuxt context is available.
198
- - There is no reset API, keyed/multi-instance state, DevTools integration, or central
199
- user-facing registry.
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.
200
257
  - The keyed transform is source-sensitive. The supported path is the module's auto-imported
201
258
  `defineState`; a barrel re-export or unrelated manual wrapper is not guaranteed to receive an
202
259
  internal hydration key.
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.2.0",
7
+ "version": "0.3.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,5 +1,22 @@
1
- import { resolve } from 'node:path';
2
- import { defineNuxtModule, createResolver, addImports, addPlugin, addImportsDir } from '@nuxt/kit';
1
+ import { resolve, sep, relative, basename } from 'node:path';
2
+ import { defineNuxtModule, createResolver, addImports, addPlugin, getLayerDirectories, addTemplate, addDevServerHandler, addImportsDir } from '@nuxt/kit';
3
+ import { addCustomTab } from '@nuxt/devtools-kit';
4
+
5
+ function registerDevtools(route, nuxt) {
6
+ addCustomTab(
7
+ {
8
+ name: "nuxt-state",
9
+ title: "Nuxt State",
10
+ icon: "carbon:data-vis-4",
11
+ category: "app",
12
+ view: {
13
+ type: "iframe",
14
+ src: route
15
+ }
16
+ },
17
+ nuxt
18
+ );
19
+ }
3
20
 
4
21
  const module$1 = defineNuxtModule({
5
22
  meta: {
@@ -22,8 +39,147 @@ const module$1 = defineNuxtModule({
22
39
  argumentLength: 2
23
40
  });
24
41
  addPlugin(resolver.resolve("./runtime/app/plugins/hydration"));
25
- addImportsDir(resolve(nuxt.options.srcDir, "states/**"));
42
+ const devtools = nuxt.options.devtools;
43
+ const devtoolsEnabled = nuxt.options.dev && devtools !== false && !(typeof devtools === "object" && devtools.enabled === false);
44
+ const layers = getLayerDirectories(nuxt);
45
+ const stateDirectories = layers.map((layer) => resolve(layer.app, "states"));
46
+ if (devtoolsEnabled) {
47
+ const devtoolsRoute = "/__nuxt_state_devtools__/";
48
+ let knownStates = [];
49
+ nuxt.hook("imports:extend", (imports) => {
50
+ const winners = /* @__PURE__ */ new Map();
51
+ for (const item of imports) {
52
+ const layerIndex = stateDirectories.findIndex(
53
+ (directory) => item.from === directory || item.from.startsWith(`${directory}${sep}`)
54
+ );
55
+ const name = item.as || item.name;
56
+ if (layerIndex < 0 || winners.has(name)) continue;
57
+ const layer = layers[layerIndex];
58
+ const projectRelative = relative(nuxt.options.rootDir, item.from);
59
+ const source = projectRelative.startsWith("..") ? basename(item.from) : projectRelative;
60
+ const layerRelative = relative(nuxt.options.rootDir, layer.root).replace(/\/$/, "");
61
+ winners.set(name, {
62
+ name,
63
+ source,
64
+ origin: layerIndex === 0 ? "Project" : layerRelative || basename(layer.root)
65
+ });
66
+ }
67
+ knownStates = [...winners.values()].sort((a, b) => a.name.localeCompare(b.name));
68
+ });
69
+ addTemplate({
70
+ filename: "nuxt-state/metadata.mjs",
71
+ getContents: () => `export default ${JSON.stringify(knownStates)}`
72
+ });
73
+ registerDevtools(devtoolsRoute, nuxt);
74
+ addPlugin(resolver.resolve("./runtime/app/plugins/devtools.client"));
75
+ addDevServerHandler({
76
+ route: devtoolsRoute,
77
+ handler(event) {
78
+ event.node.res.setHeader("content-type", "text/html; charset=utf-8");
79
+ return renderDevtoolsView();
80
+ }
81
+ });
82
+ }
83
+ addImportsDir(layers.map((layer) => resolve(layer.app, "states/**")));
26
84
  }
27
85
  });
86
+ function renderDevtoolsView() {
87
+ return `<!doctype html>
88
+ <html lang="en">
89
+ <head>
90
+ <meta charset="utf-8">
91
+ <meta name="viewport" content="width=device-width,initial-scale=1">
92
+ <title>Nuxt State</title>
93
+ <style>
94
+ :root { color-scheme: light dark; font: 14px/1.5 ui-sans-serif, system-ui, sans-serif; }
95
+ * { box-sizing: border-box; }
96
+ body { margin: 0; color: #d9e1ea; background: #101418; }
97
+ header { position: sticky; top: 0; z-index: 1; display: flex; gap: 12px; align-items: center; padding: 14px 18px; border-bottom: 1px solid #2a343e; background: #101418ee; }
98
+ h1 { margin: 0 auto 0 0; font-size: 18px; }
99
+ input, button { border: 1px solid #34414d; border-radius: 7px; background: #182027; color: inherit; padding: 7px 10px; }
100
+ input { width: min(280px, 42vw); }
101
+ button { cursor: pointer; }
102
+ main { display: grid; gap: 12px; padding: 16px; }
103
+ article { border: 1px solid #2a343e; border-radius: 10px; background: #151b21; overflow: hidden; }
104
+ article > div { padding: 12px 14px; }
105
+ h2 { margin: 0; font-size: 15px; }
106
+ .meta { color: #94a3b1; font-size: 12px; overflow-wrap: anywhere; }
107
+ table { width: 100%; border-collapse: collapse; }
108
+ th, td { padding: 8px 14px; border-top: 1px solid #27313a; text-align: left; vertical-align: top; }
109
+ th { color: #91a0ad; font-size: 11px; text-transform: uppercase; }
110
+ td:nth-child(1) { width: 22%; font-weight: 600; }
111
+ td:nth-child(2) { width: 16%; color: #65d8a5; }
112
+ pre { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; font: 12px/1.45 ui-monospace, monospace; }
113
+ .empty, .error { padding: 36px; text-align: center; color: #91a0ad; }
114
+ .badge { display: inline-block; margin-left: 7px; padding: 1px 6px; border-radius: 999px; background: #263a32; color: #7ee2ad; font-size: 11px; }
115
+ @media (prefers-color-scheme: light) {
116
+ body { color: #202832; background: #f7f9fb; }
117
+ header { border-color: #d9e0e6; background: #f7f9fbee; }
118
+ input, button, article { border-color: #d7dfe6; background: white; }
119
+ th, td { border-color: #e3e8ed; }
120
+ .badge { background: #dff7ea; color: #176b43; }
121
+ }
122
+ </style>
123
+ </head>
124
+ <body>
125
+ <header>
126
+ <h1>Nuxt State <span class="badge">Read only</span></h1>
127
+ <input id="filter" type="search" placeholder="Filter active states">
128
+ <button id="refresh" type="button">Refresh</button>
129
+ </header>
130
+ <main id="states"><p class="empty">Connecting to the Nuxt app\u2026</p></main>
131
+ <script>
132
+ const root = document.querySelector('#states')
133
+ const filter = document.querySelector('#filter')
134
+ let activeStates = []
135
+ let knownStates = []
136
+ let timer
137
+ let visible = true
138
+ let refreshing = false
139
+
140
+ const escapeHTML = value => String(value).replace(/[&<>"']/g, char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[char])
141
+ const render = () => {
142
+ const query = filter.value.trim().toLowerCase()
143
+ const shown = activeStates.filter(state => (state.name + ' ' + state.key).toLowerCase().includes(query))
144
+ const known = knownStates.filter(state => (state.name + ' ' + state.source + ' ' + state.origin).toLowerCase().includes(query))
145
+ const activeHTML = shown.length
146
+ ? shown.map(state => '<article><div><h2>' + escapeHTML(state.name) + '<span class="badge">' + escapeHTML(state.hydration) + '</span></h2><div class="meta">Internal hydration key: ' + escapeHTML(state.key) + '</div></div><table><thead><tr><th>Member</th><th>Kind</th><th>Value preview</th></tr></thead><tbody>' + state.members.map(member => '<tr><td>' + escapeHTML(member.name) + '</td><td>' + escapeHTML(member.kind) + '</td><td><pre>' + escapeHTML(JSON.stringify(member.value, null, 2)) + '</pre></td></tr>').join('') + '</tbody></table></article>').join('')
147
+ : '<p class="empty">' + (activeStates.length ? 'No matching active states.' : 'No active states yet. Opening this panel does not instantiate lazy state.') + '</p>'
148
+ const knownHTML = known.length
149
+ ? '<article><div><h2>Known states</h2><div class="meta">Discovered statically; factories remain lazy.</div></div><table><thead><tr><th>Name</th><th>Origin</th><th>Source</th></tr></thead><tbody>' + known.map(state => '<tr><td>' + escapeHTML(state.name) + '</td><td>' + escapeHTML(state.origin) + '</td><td><pre>' + escapeHTML(state.source) + '</pre></td></tr>').join('') + '</tbody></table></article>'
150
+ : ''
151
+ root.innerHTML = activeHTML + knownHTML
152
+ }
153
+ const refresh = async () => {
154
+ if (!visible || refreshing) return
155
+ refreshing = true
156
+ try {
157
+ const client = window.__NUXT_DEVTOOLS__
158
+ if (!client?.host?.nuxt) throw new Error('Nuxt DevTools host is not connected.')
159
+ const request = { result: undefined }
160
+ await client.host.nuxt.callHook('nuxt-state:inspect', request)
161
+ activeStates = request.result?.active || []
162
+ knownStates = request.result?.known || []
163
+ render()
164
+ } catch (error) {
165
+ root.innerHTML = '<p class="error">' + escapeHTML(error?.message || error) + '</p>'
166
+ } finally {
167
+ refreshing = false
168
+ }
169
+ }
170
+ const observer = new IntersectionObserver(entries => {
171
+ visible = entries.some(entry => entry.isIntersecting)
172
+ if (visible) refresh()
173
+ })
174
+ observer.observe(document.documentElement)
175
+ filter.addEventListener('input', render)
176
+ document.querySelector('#refresh').addEventListener('click', refresh)
177
+ timer = setInterval(refresh, 1000)
178
+ addEventListener('pagehide', () => { clearInterval(timer); observer.disconnect() }, { once: true })
179
+ refresh()
180
+ <\/script>
181
+ </body>
182
+ </html>`;
183
+ }
28
184
 
29
185
  export { module$1 as default };
@@ -30,11 +30,23 @@ export function defineState(factory, internalKey) {
30
30
  const app = nuxtApp;
31
31
  app.vueApp?.onUnmount?.(() => scope.stop());
32
32
  if (internalKey) {
33
- registerHydratableState(nuxtApp, internalKey, {
33
+ const entry = {
34
34
  snapshot: () => snapshotState(instance),
35
35
  restore: (snapshot) => restoreState(instance, snapshot),
36
36
  dispose: () => scope.stop()
37
- });
37
+ };
38
+ if (import.meta.dev) {
39
+ entry.debug = {
40
+ state: instance,
41
+ hydration: import.meta.server ? "Server" : "Client-only"
42
+ };
43
+ const restore = entry.restore;
44
+ entry.restore = (snapshot) => {
45
+ restore(snapshot);
46
+ entry.debug.hydration = "Hydrated";
47
+ };
48
+ }
49
+ registerHydratableState(nuxtApp, internalKey, entry);
38
50
  }
39
51
  return instance;
40
52
  };
@@ -0,0 +1,4 @@
1
+ declare module '#build/nuxt-state/metadata' {
2
+ const states: Array<{ name: string; source: string; origin: string }>
3
+ export default states
4
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
2
+ export default _default;
@@ -0,0 +1,17 @@
1
+ import { defineNuxtPlugin } from "#app";
2
+ import knownStates from "#build/nuxt-state/metadata";
3
+ import { inspectActiveStates } from "../state-inspector.js";
4
+ export default defineNuxtPlugin({
5
+ name: "nuxt-state:devtools",
6
+ setup(nuxtApp) {
7
+ nuxtApp.hook(
8
+ "nuxt-state:inspect",
9
+ ((request) => {
10
+ request.result = {
11
+ active: inspectActiveStates(nuxtApp),
12
+ known: knownStates
13
+ };
14
+ })
15
+ );
16
+ }
17
+ });
@@ -0,0 +1,16 @@
1
+ export interface StateInspectorMember {
2
+ name: string;
3
+ kind: string;
4
+ value: unknown;
5
+ }
6
+ export interface StateInspectorEntry {
7
+ key: string;
8
+ name: string;
9
+ source?: string;
10
+ origin?: string;
11
+ hydration: 'Hydrated' | 'Client-only' | 'Server';
12
+ members: StateInspectorMember[];
13
+ }
14
+ export declare function inspectActiveStates(nuxtApp: object): StateInspectorEntry[];
15
+ export declare function classifyStateMember(value: unknown): string;
16
+ export declare function previewValue(value: unknown): unknown;
@@ -0,0 +1,149 @@
1
+ import { isReactive, isReadonly, isRef, isShallow, toRaw } from "vue";
2
+ import { getStateRegistry } from "./state-registry.js";
3
+ const MAX_DEPTH = 4;
4
+ const MAX_ITEMS = 40;
5
+ const MAX_NODES = 200;
6
+ const MAX_STRING = 500;
7
+ export function inspectActiveStates(nuxtApp) {
8
+ const entries = [];
9
+ for (const [key, entry] of getStateRegistry(nuxtApp).active) {
10
+ if (!entry.debug) continue;
11
+ const state = entry.debug.state;
12
+ const members = inspectMembers(state);
13
+ entries.push({
14
+ key,
15
+ name: entry.debug.name || `State ${key}`,
16
+ source: entry.debug.source,
17
+ origin: entry.debug.origin,
18
+ hydration: entry.debug.hydration,
19
+ members
20
+ });
21
+ }
22
+ return entries;
23
+ }
24
+ export function classifyStateMember(value) {
25
+ if (typeof value === "function") return "function";
26
+ if (isRef(value)) {
27
+ if (isReadonly(value)) return "readonly ref";
28
+ return isShallow(value) ? "shallowRef" : "ref";
29
+ }
30
+ if (isReactive(value)) {
31
+ if (isReadonly(value)) return "readonly";
32
+ return isShallow(value) ? "shallowReactive" : "reactive";
33
+ }
34
+ if (isReadonly(value)) return "readonly";
35
+ return "other";
36
+ }
37
+ export function previewValue(value) {
38
+ try {
39
+ const context = { seen: /* @__PURE__ */ new WeakMap(), nodes: 0, truncated: false };
40
+ const preview = visit(value, 0, context);
41
+ return context.truncated ? { preview, truncated: true } : preview;
42
+ } catch (error) {
43
+ return { unavailable: safeErrorMessage(error) };
44
+ }
45
+ }
46
+ function inspectMembers(state) {
47
+ if (!isObjectLike(state)) {
48
+ return [{ name: "value", kind: classifyStateMember(state), value: previewValue(state) }];
49
+ }
50
+ try {
51
+ return Object.keys(state).map((name) => {
52
+ try {
53
+ const value = state[name];
54
+ return {
55
+ name,
56
+ kind: classifyStateMember(value),
57
+ value: previewValue(isRef(value) ? value.value : value)
58
+ };
59
+ } catch (error) {
60
+ return { name, kind: "unavailable", value: { unavailable: safeErrorMessage(error) } };
61
+ }
62
+ });
63
+ } catch (error) {
64
+ return [
65
+ {
66
+ name: "value",
67
+ kind: "unavailable",
68
+ value: { unavailable: safeErrorMessage(error) }
69
+ }
70
+ ];
71
+ }
72
+ }
73
+ function visit(value, depth, context) {
74
+ if (typeof value === "string") {
75
+ if (value.length <= MAX_STRING) return value;
76
+ context.truncated = true;
77
+ return `${value.slice(0, MAX_STRING)}\u2026`;
78
+ }
79
+ if (value === null || ["number", "boolean", "undefined"].includes(typeof value)) return value;
80
+ if (typeof value === "bigint") return `${value}n`;
81
+ if (typeof value === "symbol") return value.toString();
82
+ if (typeof value === "function") return `\u0192 ${value.name || "anonymous"}()`;
83
+ if (typeof value !== "object") return String(value);
84
+ const raw = isReactive(value) || isReadonly(value) ? toRaw(value) : value;
85
+ const known = context.seen.get(raw);
86
+ if (known) return { reference: `#${known}` };
87
+ const id = ++context.nodes;
88
+ context.seen.set(raw, id);
89
+ if (context.nodes > MAX_NODES || depth >= MAX_DEPTH) {
90
+ context.truncated = true;
91
+ return { id: `#${id}`, truncated: true };
92
+ }
93
+ if (raw instanceof Date) {
94
+ return {
95
+ id: `#${id}`,
96
+ type: "Date",
97
+ value: Number.isNaN(raw.valueOf()) ? "Invalid Date" : raw.toISOString()
98
+ };
99
+ }
100
+ if (raw instanceof Map) {
101
+ const items = [...raw.entries()];
102
+ if (items.length > MAX_ITEMS) context.truncated = true;
103
+ return {
104
+ id: `#${id}`,
105
+ type: "Map",
106
+ entries: items.slice(0, MAX_ITEMS).map(([key, item]) => [visit(key, depth + 1, context), visit(item, depth + 1, context)]),
107
+ ...items.length > MAX_ITEMS ? { truncated: true, total: items.length } : {}
108
+ };
109
+ }
110
+ if (raw instanceof Set) {
111
+ const items = [...raw];
112
+ if (items.length > MAX_ITEMS) context.truncated = true;
113
+ return {
114
+ id: `#${id}`,
115
+ type: "Set",
116
+ values: items.slice(0, MAX_ITEMS).map((item) => visit(item, depth + 1, context)),
117
+ ...items.length > MAX_ITEMS ? { truncated: true, total: items.length } : {}
118
+ };
119
+ }
120
+ if (Array.isArray(raw)) {
121
+ if (raw.length > MAX_ITEMS) context.truncated = true;
122
+ return {
123
+ id: `#${id}`,
124
+ type: "Array",
125
+ values: raw.slice(0, MAX_ITEMS).map((item) => visit(item, depth + 1, context)),
126
+ ...raw.length > MAX_ITEMS ? { truncated: true, total: raw.length } : {}
127
+ };
128
+ }
129
+ if (Object.getPrototypeOf(raw) !== Object.prototype && Object.getPrototypeOf(raw) !== null) {
130
+ const constructor = raw.constructor;
131
+ return { id: `#${id}`, unsupported: constructor?.name || "Object" };
132
+ }
133
+ const properties = Object.entries(raw);
134
+ if (properties.length > MAX_ITEMS) context.truncated = true;
135
+ return {
136
+ id: `#${id}`,
137
+ properties: Object.fromEntries(
138
+ properties.slice(0, MAX_ITEMS).map(([key, item]) => [key, visit(item, depth + 1, context)])
139
+ ),
140
+ ...properties.length > MAX_ITEMS ? { truncated: true, total: properties.length } : {}
141
+ };
142
+ }
143
+ function isObjectLike(value) {
144
+ return typeof value === "object" && value !== null || typeof value === "function";
145
+ }
146
+ function safeErrorMessage(error) {
147
+ if (error instanceof Error) return error.message.slice(0, 200);
148
+ return "Value could not be inspected";
149
+ }
@@ -3,6 +3,13 @@ export interface HydratableStateEntry {
3
3
  snapshot: () => unknown;
4
4
  restore: (snapshot: unknown) => void;
5
5
  dispose?: () => void;
6
+ debug?: {
7
+ state: unknown;
8
+ name?: string;
9
+ source?: string;
10
+ origin?: string;
11
+ hydration: 'Hydrated' | 'Client-only' | 'Server';
12
+ };
6
13
  }
7
14
  interface StateRegistry {
8
15
  active: Map<string, HydratableStateEntry>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-state",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Define shared Nuxt state using the same Composition API you already use in composables.",
5
5
  "keywords": [
6
6
  "composable",
@@ -44,6 +44,7 @@
44
44
  "access": "public"
45
45
  },
46
46
  "dependencies": {
47
+ "@nuxt/devtools-kit": "^3.4.2",
47
48
  "@nuxt/kit": "^4.5.2"
48
49
  },
49
50
  "devDependencies": {