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 +60 -3
- package/dist/module.json +1 -1
- package/dist/module.mjs +159 -3
- package/dist/runtime/app/composables/defineState.js +14 -2
- package/dist/runtime/app/devtools-metadata.d.ts +4 -0
- package/dist/runtime/app/plugins/devtools.client.d.ts +2 -0
- package/dist/runtime/app/plugins/devtools.client.js +17 -0
- package/dist/runtime/app/state-inspector.d.ts +16 -0
- package/dist/runtime/app/state-inspector.js +149 -0
- package/dist/runtime/app/state-registry.d.ts +7 -0
- package/package.json +2 -1
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
|
-
-
|
|
199
|
-
|
|
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
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
|
-
|
|
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 => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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
|
-
|
|
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,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.
|
|
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": {
|