nuxt-convex-module 0.14.1 → 0.14.2
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/dist/devtools-client/index.html +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +5 -2
- package/dist/runtime/better-auth/nuxt/chunk-reload.client.d.ts +2 -0
- package/dist/runtime/better-auth/nuxt/chunk-reload.client.js +41 -0
- package/dist/runtime/better-auth/vue/auth-client-loader.js +25 -4
- package/dist/runtime/vue/composables/use-upload-queue.d.ts +49 -13
- package/dist/runtime/vue/composables/use-upload-queue.js +4 -2
- package/dist/runtime/vue/index.d.ts +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><script type="importmap">{"imports":{"#entry":"/__nuxt-convex-module/_nuxt/CtC_CmOY.js"}}</script><link rel="stylesheet" href="/__nuxt-convex-module/_nuxt/entry.jOWrhpYd.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__nuxt-convex-module/_nuxt/CtC_CmOY.js"><script type="module" src="/__nuxt-convex-module/_nuxt/CtC_CmOY.js" crossorigin></script><link rel="prefetch" as="script" crossorigin href="/__nuxt-convex-module/_nuxt/C-J9qZjr.js"></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-convex-module",buildId:"
|
|
1
|
+
<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><script type="importmap">{"imports":{"#entry":"/__nuxt-convex-module/_nuxt/CtC_CmOY.js"}}</script><link rel="stylesheet" href="/__nuxt-convex-module/_nuxt/entry.jOWrhpYd.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__nuxt-convex-module/_nuxt/CtC_CmOY.js"><script type="module" src="/__nuxt-convex-module/_nuxt/CtC_CmOY.js" crossorigin></script><link rel="prefetch" as="script" crossorigin href="/__nuxt-convex-module/_nuxt/C-J9qZjr.js"></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-convex-module",buildId:"45bb65c1-9f29-4c02-bccc-cf2e35308dd6",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1794326993439,false]</script></body></html>
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -535,7 +535,7 @@ function registerIntegrations(resolver, nuxt, options, policy, authRoute) {
|
|
|
535
535
|
const resolve = (key, pkg) => report(key, pkg, resolveIntegrationState(options[key], isPackageInstalled(pkg, rootDir), isDeclaredDependency(pkg, rootDir)));
|
|
536
536
|
const betterAuth = resolve("betterAuth", "@convex-dev/better-auth");
|
|
537
537
|
if (betterAuth) {
|
|
538
|
-
registerBetterAuth(resolver, authRoute, policy);
|
|
538
|
+
registerBetterAuth(resolver, authRoute, policy, nuxt.options.experimental);
|
|
539
539
|
} else {
|
|
540
540
|
registerBaseConvexClient(resolver);
|
|
541
541
|
}
|
|
@@ -684,9 +684,12 @@ const AUTH_PROXY_SECURITY_RULES = {
|
|
|
684
684
|
xssValidator: false,
|
|
685
685
|
allowedMethodsRestricter: { methods: ["GET", "HEAD", "POST", "OPTIONS"] }
|
|
686
686
|
};
|
|
687
|
-
function registerBetterAuth(resolver, authRoute, policy) {
|
|
687
|
+
function registerBetterAuth(resolver, authRoute, policy, experimental) {
|
|
688
688
|
addPlugin(resolver.resolve("./runtime/better-auth/vue/plugin.client"));
|
|
689
689
|
addPlugin(resolver.resolve("./runtime/better-auth/vue/plugin.server"));
|
|
690
|
+
if (experimental.emitRouteChunkError === "automatic" && experimental.appManifest) {
|
|
691
|
+
addPlugin(resolver.resolve("./runtime/better-auth/nuxt/chunk-reload.client"));
|
|
692
|
+
}
|
|
690
693
|
const noSessionVary = resolver.resolve("./runtime/better-auth/nuxt/no-session-vary");
|
|
691
694
|
addNitroPlugin({ nitro2: noSessionVary, nitro3: noSessionVary });
|
|
692
695
|
registerIntegrationImports(resolver, "betterAuth", policy);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { defineNuxtPlugin, reloadNuxtApp, useRuntimeConfig } from "#app";
|
|
2
|
+
import { onAuthClientLoadFailure } from "../vue/auth-client-loader.js";
|
|
3
|
+
function latestBuildURL(app) {
|
|
4
|
+
const root = (app.cdnURL || app.baseURL).replace(/\/+$/, "");
|
|
5
|
+
const dir = app.buildAssetsDir.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
6
|
+
return `${root}/${dir}/builds/latest.json?${Date.now()}`;
|
|
7
|
+
}
|
|
8
|
+
async function newerBuild(url, buildId) {
|
|
9
|
+
try {
|
|
10
|
+
const response = await fetch(url, { cache: "no-store" });
|
|
11
|
+
if (!response.ok) return void 0;
|
|
12
|
+
const latest = await response.json();
|
|
13
|
+
return typeof latest.id === "string" && latest.id !== buildId ? latest : void 0;
|
|
14
|
+
} catch {
|
|
15
|
+
return void 0;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export default defineNuxtPlugin({
|
|
19
|
+
name: "nuxt-convex-module:better-auth:chunk-reload",
|
|
20
|
+
setup(nuxtApp) {
|
|
21
|
+
const { app } = useRuntimeConfig();
|
|
22
|
+
const chunkErrors = /* @__PURE__ */ new WeakSet();
|
|
23
|
+
nuxtApp.hook("app:chunkError", ({ error }) => {
|
|
24
|
+
if (typeof error === "object" && error !== null) chunkErrors.add(error);
|
|
25
|
+
});
|
|
26
|
+
let checking = false;
|
|
27
|
+
let announced = false;
|
|
28
|
+
onAuthClientLoadFailure(({ error, used }) => {
|
|
29
|
+
if (checking) return;
|
|
30
|
+
checking = true;
|
|
31
|
+
void newerBuild(latestBuildURL(app), app.buildId).then((build) => {
|
|
32
|
+
checking = false;
|
|
33
|
+
if (!build || typeof error !== "object" || error === null || !chunkErrors.has(error)) return;
|
|
34
|
+
if (used) return reloadNuxtApp();
|
|
35
|
+
if (announced) return;
|
|
36
|
+
announced = true;
|
|
37
|
+
void nuxtApp.callHook("app:manifest:update", build);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
});
|
|
@@ -3,9 +3,15 @@ let loaded;
|
|
|
3
3
|
let loading;
|
|
4
4
|
let deferred = false;
|
|
5
5
|
const waiting = /* @__PURE__ */ new Set();
|
|
6
|
+
const failing = /* @__PURE__ */ new Set();
|
|
7
|
+
let used = false;
|
|
6
8
|
let evaluated;
|
|
7
9
|
let stopSignal;
|
|
8
10
|
export function loadAuthClient() {
|
|
11
|
+
used = true;
|
|
12
|
+
return load();
|
|
13
|
+
}
|
|
14
|
+
function load() {
|
|
9
15
|
if (evaluated) registerAuthClient(evaluated);
|
|
10
16
|
loading ??= import("#convex/auth-client").then(
|
|
11
17
|
({ authClient }) => {
|
|
@@ -14,6 +20,9 @@ export function loadAuthClient() {
|
|
|
14
20
|
},
|
|
15
21
|
(error) => {
|
|
16
22
|
loading = void 0;
|
|
23
|
+
const failure = { error, used };
|
|
24
|
+
used = false;
|
|
25
|
+
notify(failing, failure);
|
|
17
26
|
throw error;
|
|
18
27
|
}
|
|
19
28
|
);
|
|
@@ -33,11 +42,15 @@ export function registerAuthClient(client) {
|
|
|
33
42
|
evaluated = void 0;
|
|
34
43
|
loaded = client;
|
|
35
44
|
loading ??= Promise.resolve(client);
|
|
45
|
+
failing.clear();
|
|
36
46
|
const listeners = [...waiting];
|
|
37
47
|
waiting.clear();
|
|
38
|
-
|
|
48
|
+
notify(listeners, client);
|
|
49
|
+
}
|
|
50
|
+
function notify(listeners, value) {
|
|
51
|
+
for (const listener of [...listeners]) {
|
|
39
52
|
try {
|
|
40
|
-
listener(
|
|
53
|
+
listener(value);
|
|
41
54
|
} catch (error) {
|
|
42
55
|
report(error);
|
|
43
56
|
}
|
|
@@ -61,12 +74,18 @@ export function onAuthClient(listener) {
|
|
|
61
74
|
waiting.add(listener);
|
|
62
75
|
return () => waiting.delete(listener);
|
|
63
76
|
}
|
|
77
|
+
export function onAuthClientLoadFailure(listener) {
|
|
78
|
+
if (loaded) return () => {
|
|
79
|
+
};
|
|
80
|
+
failing.add(listener);
|
|
81
|
+
return () => failing.delete(listener);
|
|
82
|
+
}
|
|
64
83
|
export function deferAuthClient() {
|
|
65
84
|
if (deferred || loaded) return;
|
|
66
85
|
deferred = true;
|
|
67
86
|
const onVisible = () => {
|
|
68
87
|
if (document.visibilityState !== "visible") return;
|
|
69
|
-
void
|
|
88
|
+
void load().then(() => document.removeEventListener("visibilitychange", onVisible), () => {
|
|
70
89
|
});
|
|
71
90
|
};
|
|
72
91
|
document.addEventListener("visibilitychange", onVisible);
|
|
@@ -75,7 +94,7 @@ export function retryAuthClientLoad() {
|
|
|
75
94
|
if (loaded || typeof window === "undefined") return;
|
|
76
95
|
const retry = () => {
|
|
77
96
|
if (document.visibilityState !== "visible") return;
|
|
78
|
-
void
|
|
97
|
+
void load().then(() => {
|
|
79
98
|
window.removeEventListener("online", retry);
|
|
80
99
|
document.removeEventListener("visibilitychange", retry);
|
|
81
100
|
}, () => {
|
|
@@ -183,4 +202,6 @@ export function __resetAuthClientLoaderForTests() {
|
|
|
183
202
|
stopSignal = void 0;
|
|
184
203
|
evaluated = void 0;
|
|
185
204
|
waiting.clear();
|
|
205
|
+
failing.clear();
|
|
206
|
+
used = false;
|
|
186
207
|
}
|
|
@@ -16,9 +16,12 @@ export type UploadItemStage = 'prepare' | 'upload' | 'after';
|
|
|
16
16
|
/**
|
|
17
17
|
* A single entry in the queue managed by {@link useUploadQueue}.
|
|
18
18
|
*
|
|
19
|
+
* @typeParam TData - The app's own data for the item, given to
|
|
20
|
+
* {@link VueUploadQueue.enqueue}. `undefined` when the queue takes none.
|
|
21
|
+
*
|
|
19
22
|
* @public
|
|
20
23
|
*/
|
|
21
|
-
export interface UploadQueueItem {
|
|
24
|
+
export interface UploadQueueItem<TData = undefined> {
|
|
22
25
|
/** Stable id for this queue entry (not the storage id). */
|
|
23
26
|
id: string;
|
|
24
27
|
/** The file or blob being uploaded. */
|
|
@@ -48,13 +51,26 @@ export interface UploadQueueItem {
|
|
|
48
51
|
* and for a preview.
|
|
49
52
|
*/
|
|
50
53
|
prepared: Blob | null;
|
|
54
|
+
/**
|
|
55
|
+
* The app's data for this item, as given to `enqueue`: what the file is
|
|
56
|
+
* for (a document id, a kind, a position). Every hook reads it off the
|
|
57
|
+
* item. Deeply reactive like the rest of the item: an object reads as
|
|
58
|
+
* Vue's proxy of the one given, not a copy, so a change through it
|
|
59
|
+
* reaches that object and every item the same call queued (`toRaw`
|
|
60
|
+
* returns the object itself). A ref held as one of its properties reads
|
|
61
|
+
* unwrapped; one inside an array or a `Map` stays a ref. `undefined`
|
|
62
|
+
* when `enqueue` was given none.
|
|
63
|
+
*/
|
|
64
|
+
data: TData;
|
|
51
65
|
}
|
|
52
66
|
/**
|
|
53
67
|
* Options for {@link useUploadQueue}.
|
|
54
68
|
*
|
|
69
|
+
* @typeParam TData - The data each item carries; see {@link UploadQueueItem}.
|
|
70
|
+
*
|
|
55
71
|
* @public
|
|
56
72
|
*/
|
|
57
|
-
export interface UseUploadQueueOptions {
|
|
73
|
+
export interface UseUploadQueueOptions<TData = undefined> {
|
|
58
74
|
/** Maximum number of uploads to run at once. Defaults to `3`. */
|
|
59
75
|
concurrency?: number;
|
|
60
76
|
/**
|
|
@@ -63,7 +79,7 @@ export interface UseUploadQueueOptions {
|
|
|
63
79
|
* item ends `error`, refused when `isRefusal` says so. Runs once per
|
|
64
80
|
* item: a retry reuses what it returned.
|
|
65
81
|
*/
|
|
66
|
-
prepare?: (file: Blob, item: UploadQueueItem
|
|
82
|
+
prepare?: (file: Blob, item: UploadQueueItem<TData>, signal: AbortSignal) => Blob | Promise<Blob>;
|
|
67
83
|
/**
|
|
68
84
|
* Runs once the file is stored, before the item counts as a success:
|
|
69
85
|
* attach the storage id to a document, run a check on it. The item stays
|
|
@@ -74,7 +90,7 @@ export interface UseUploadQueueOptions {
|
|
|
74
90
|
* in error, so `onItemError` can delete it; the queue never deletes files,
|
|
75
91
|
* and a retry forgets it before uploading again.
|
|
76
92
|
*/
|
|
77
|
-
after?: (storageId: StorageId, item: UploadQueueItem
|
|
93
|
+
after?: (storageId: StorageId, item: UploadQueueItem<TData>, signal: AbortSignal) => void | Promise<void>;
|
|
78
94
|
/**
|
|
79
95
|
* Whether an error is a refusal (the app turned the file away) rather than
|
|
80
96
|
* a failure (the network, the deployment, an abort). Defaults to: the
|
|
@@ -82,33 +98,46 @@ export interface UseUploadQueueOptions {
|
|
|
82
98
|
* An aborted item is never refused, and neither is an error this check
|
|
83
99
|
* throws on.
|
|
84
100
|
*/
|
|
85
|
-
isRefusal?: (error: Error, item: UploadQueueItem) => boolean;
|
|
101
|
+
isRefusal?: (error: Error, item: UploadQueueItem<TData>) => boolean;
|
|
86
102
|
/**
|
|
87
103
|
* Called when an individual item finishes uploading (after `after`). A
|
|
88
104
|
* throw ends the item `error` with its storage id kept; a retry then runs
|
|
89
105
|
* only this callback again, never `after` a second time.
|
|
90
106
|
*/
|
|
91
|
-
onItemSuccess?: (storageId: StorageId, item: UploadQueueItem) => void | Promise<void>;
|
|
107
|
+
onItemSuccess?: (storageId: StorageId, item: UploadQueueItem<TData>) => void | Promise<void>;
|
|
92
108
|
/** Called when an individual item fails or is refused (`item.refused` says which). */
|
|
93
|
-
onItemError?: (error: Error, item: UploadQueueItem) => void;
|
|
109
|
+
onItemError?: (error: Error, item: UploadQueueItem<TData>) => void;
|
|
94
110
|
/**
|
|
95
111
|
* Called once every queued item has settled (succeeded or failed), after
|
|
96
112
|
* every `onItemSuccess` has returned.
|
|
97
113
|
*/
|
|
98
|
-
onComplete?: (items: UploadQueueItem[]) => void;
|
|
114
|
+
onComplete?: (items: UploadQueueItem<TData>[]) => void;
|
|
99
115
|
}
|
|
100
116
|
/** Accepted inputs to {@link VueUploadQueue.enqueue}. @public */
|
|
101
117
|
export type EnqueueInput = Blob | Blob[] | FileList | null | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* The `data` argument of {@link VueUploadQueue.enqueue}: optional when the
|
|
120
|
+
* item data may be `undefined`, required otherwise.
|
|
121
|
+
*
|
|
122
|
+
* @public
|
|
123
|
+
*/
|
|
124
|
+
export type EnqueueData<TData> = undefined extends TData ? [data?: TData] : [data: TData];
|
|
102
125
|
/**
|
|
103
126
|
* The reactive queue returned by {@link useUploadQueue}.
|
|
104
127
|
*
|
|
128
|
+
* @typeParam TData - The data each item carries; see {@link UploadQueueItem}.
|
|
129
|
+
*
|
|
105
130
|
* @public
|
|
106
131
|
*/
|
|
107
|
-
export interface VueUploadQueue {
|
|
132
|
+
export interface VueUploadQueue<TData = undefined> {
|
|
108
133
|
/** Reactive list of queued items, in insertion order. */
|
|
109
|
-
items: Readonly<Ref<UploadQueueItem[]>>;
|
|
110
|
-
/**
|
|
111
|
-
|
|
134
|
+
items: Readonly<Ref<UploadQueueItem<TData>[]>>;
|
|
135
|
+
/**
|
|
136
|
+
* Add one or more files to the queue; uploading starts automatically.
|
|
137
|
+
* `data` goes on every item this call queues, as `item.data`: queue files
|
|
138
|
+
* one call at a time to give each its own.
|
|
139
|
+
*/
|
|
140
|
+
enqueue: (files: EnqueueInput, ...data: EnqueueData<TData>) => void;
|
|
112
141
|
/** `true` while any item is pending or uploading. */
|
|
113
142
|
isUploading: Readonly<Ref<boolean>>;
|
|
114
143
|
/** Aggregate progress across all items, from `0` to `1`. */
|
|
@@ -152,6 +181,11 @@ export interface VueUploadQueue {
|
|
|
152
181
|
* item says whether the app `refused` it or it failed, so the UI can show a
|
|
153
182
|
* reason for one and {@link VueUploadQueue.retry} the other.
|
|
154
183
|
*
|
|
184
|
+
* `enqueue(files, data)` attaches the app's own data to the items it
|
|
185
|
+
* queues, as `item.data`, so a hook knows what a file is for without a
|
|
186
|
+
* side map: type it with the `TData` parameter,
|
|
187
|
+
* `useUploadQueue<{ albumId: Id<'albums'> }>(…)`.
|
|
188
|
+
*
|
|
155
189
|
* @example
|
|
156
190
|
* ```vue
|
|
157
191
|
* <script setup lang="ts">
|
|
@@ -180,6 +214,8 @@ export interface VueUploadQueue {
|
|
|
180
214
|
* </template>
|
|
181
215
|
* ```
|
|
182
216
|
*
|
|
217
|
+
* @typeParam TData - The data each item carries, given to `enqueue`;
|
|
218
|
+
* `undefined` (no data) by default.
|
|
183
219
|
* @param generateUploadUrl - A `FunctionReference` for the public
|
|
184
220
|
* `generateUploadUrl` mutation.
|
|
185
221
|
* @param options - Optional {@link UseUploadQueueOptions}.
|
|
@@ -187,6 +223,6 @@ export interface VueUploadQueue {
|
|
|
187
223
|
*
|
|
188
224
|
* @public
|
|
189
225
|
*/
|
|
190
|
-
export declare function useUploadQueue(generateUploadUrl: GenerateUploadUrl, options?: UseUploadQueueOptions): VueUploadQueue
|
|
226
|
+
export declare function useUploadQueue<TData = undefined>(generateUploadUrl: GenerateUploadUrl, options?: UseUploadQueueOptions<TData>): VueUploadQueue<TData>;
|
|
191
227
|
/** @public */
|
|
192
228
|
export declare const useConvexUploadQueue: typeof useUploadQueue;
|
|
@@ -115,7 +115,7 @@ export function useUploadQueue(generateUploadUrl, options = {}) {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
-
function enqueue(files) {
|
|
118
|
+
function enqueue(files, ...[data]) {
|
|
119
119
|
const incoming = normalizeFiles(files);
|
|
120
120
|
if (incoming.length === 0) return;
|
|
121
121
|
items.value.push(
|
|
@@ -128,7 +128,9 @@ export function useUploadQueue(generateUploadUrl, options = {}) {
|
|
|
128
128
|
error: null,
|
|
129
129
|
refused: false,
|
|
130
130
|
stage: null,
|
|
131
|
-
prepared: null
|
|
131
|
+
prepared: null,
|
|
132
|
+
// `undefined` only when `TData` allows it: `EnqueueData` requires it otherwise.
|
|
133
|
+
data
|
|
132
134
|
}))
|
|
133
135
|
);
|
|
134
136
|
held = false;
|
|
@@ -88,6 +88,6 @@ export type { ConnectionState, OptimisticUpdate, QueryJournal, } from 'convex/br
|
|
|
88
88
|
export type { FunctionReference, FunctionReference_future, FunctionArgs, FunctionReturnType, OptionalRestArgs, ArgsAndOptions, } from 'convex/server';
|
|
89
89
|
export type { Value } from 'convex/values';
|
|
90
90
|
export { useUpload, useConvexUpload, uploadFile, type VueUpload, type UseUploadOptions, type UploadFileOptions, type GenerateUploadUrl, type StorageId, } from './composables/use-upload.js';
|
|
91
|
-
export { useUploadQueue, useConvexUploadQueue, type VueUploadQueue, type UseUploadQueueOptions, type UploadQueueItem, type UploadItemStage, type UploadItemStatus, type EnqueueInput, } from './composables/use-upload-queue.js';
|
|
91
|
+
export { useUploadQueue, useConvexUploadQueue, type VueUploadQueue, type UseUploadQueueOptions, type UploadQueueItem, type UploadItemStage, type UploadItemStatus, type EnqueueInput, type EnqueueData, } from './composables/use-upload-queue.js';
|
|
92
92
|
export { useStorageUrl, useConvexStorageUrl, type GetStorageUrl, } from './composables/use-storage-url.js';
|
|
93
93
|
export { ConvexImage } from './components/convex-image.js';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "nuxt-convex-module",
|
|
4
|
-
"version": "0.14.
|
|
4
|
+
"version": "0.14.2",
|
|
5
5
|
"description": "Convex backend integration with live queries, mutations, actions, pagination, file storage and SSR, auto-imported and typed against your deployment. The same Vue client runs without Nuxt; Better Auth, Clerk, Auth0 and Polar are opt-in.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -221,7 +221,7 @@
|
|
|
221
221
|
"changelogen": "^0.6.2",
|
|
222
222
|
"convex": "^1.49.0",
|
|
223
223
|
"eslint": "^10.13.0",
|
|
224
|
-
"fallow": "3.
|
|
224
|
+
"fallow": "3.41.0",
|
|
225
225
|
"happy-dom": "^21.0.4",
|
|
226
226
|
"installed-check": "^11.0.0",
|
|
227
227
|
"nuxt": "^4.6.3",
|