@lewebsimple/nuxt-graphql 0.1.5 → 0.1.7

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
@@ -12,6 +12,8 @@ Opinionated Nuxt module for using GraphQL Yoga with graphql-request / graphql-ss
12
12
 
13
13
  ## Features
14
14
  - 🧘‍♂️ GraphQL Yoga server handler with user-provided schema / context
15
+ - 📄 Auto-import GraphQL documents from `**/*.gql` (configurable)
16
+ - 🧩 Type-safe composables to call operations by name, i.e. `useGraphQLQuery("Hello")`
15
17
 
16
18
  ## Quick Setup
17
19
 
package/dist/module.d.mts CHANGED
@@ -1,12 +1,14 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
2
 
3
- interface GraphQLYogaConfig {
4
- endpoint?: string;
5
- }
6
3
  interface ModuleOptions {
7
- yoga?: GraphQLYogaConfig;
4
+ endpoint?: string;
5
+ codegen?: {
6
+ enabled?: boolean;
7
+ pattern?: string;
8
+ schemaOutput?: string;
9
+ };
8
10
  }
9
11
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
10
12
 
11
13
  export { _default as default };
12
- export type { GraphQLYogaConfig, ModuleOptions };
14
+ export type { ModuleOptions };
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lewebsimple/nuxt-graphql",
3
3
  "configKey": "graphql",
4
- "version": "0.1.5",
4
+ "version": "0.1.7",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,22 +1,214 @@
1
1
  import { useLogger, defineNuxtModule, createResolver, getLayerDirectories, addServerTemplate, addServerHandler, addImportsDir, addPlugin, addTypeTemplate } from '@nuxt/kit';
2
- import { existsSync, readFileSync } from 'node:fs';
3
- import { join } from 'node:path';
2
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
3
+ import { join, dirname, relative } from 'node:path';
4
+ import { glob } from 'tinyglobby';
5
+ import { generate } from '@graphql-codegen/cli';
6
+ import { parse, Kind } from 'graphql';
4
7
 
5
8
  const logger = useLogger("@lewebsimple/nuxt-graphql");
9
+ const blue = "\x1B[34m";
6
10
  const cyan = "\x1B[36m";
11
+ const dim = "\x1B[2m";
12
+ const green = "\x1B[32m";
13
+ const magenta = "\x1B[35m";
7
14
  const reset = "\x1B[0m";
15
+ const yellow = "\x1B[33m";
8
16
 
9
- function findServerFile(layerDirs, relativePath) {
10
- const extensions = ["ts", "mjs"];
11
- for (const dir of layerDirs) {
12
- const candidates = extensions.map((ext) => join(dir.server, `${relativePath}.${ext}`));
13
- const fullPath = candidates.find(existsSync);
14
- if (fullPath) {
15
- return fullPath;
17
+ async function findSingleFile(dirs, pattern, isRequired = false) {
18
+ for (const dir of dirs) {
19
+ const fullPattern = join(dir, pattern);
20
+ const files = await glob(fullPattern, { absolute: true });
21
+ if (files.length > 0) {
22
+ return files[0];
16
23
  }
17
24
  }
18
- throw new Error(`Could not find required server file ${cyan}${relativePath}${reset} in layers:
19
- ${layerDirs.map(({ server }) => server).join("\n")}`);
25
+ if (isRequired) {
26
+ throw new Error(`File not found: ${cyan}${pattern}${reset} in directories:
27
+ ${dirs.join("\n")}`);
28
+ }
29
+ }
30
+ async function findMultipleFiles(dirs, pattern) {
31
+ const files = [];
32
+ for (const dir of dirs) {
33
+ const foundFiles = await glob(pattern, { cwd: dir, absolute: true });
34
+ files.push(...foundFiles);
35
+ }
36
+ return Array.from(new Set(files));
37
+ }
38
+ function writeFileIfChanged(path, content) {
39
+ if (existsSync(path) && readFileSync(path, "utf-8") === content) {
40
+ return false;
41
+ }
42
+ mkdirSync(dirname(path), { recursive: true });
43
+ writeFileSync(path, content, "utf-8");
44
+ return true;
45
+ }
46
+
47
+ function analyzeGraphQLDocuments(docs) {
48
+ const byFile = /* @__PURE__ */ new Map();
49
+ const operationsByType = {
50
+ query: [],
51
+ mutation: [],
52
+ subscription: []
53
+ };
54
+ const operationNameToFile = /* @__PURE__ */ new Map();
55
+ const fragmentNameToFile = /* @__PURE__ */ new Map();
56
+ for (const doc of docs) {
57
+ const ast = parse(doc.content);
58
+ const defs = [];
59
+ for (const def of ast.definitions) {
60
+ if (def.kind === Kind.FRAGMENT_DEFINITION) {
61
+ const name2 = def.name.value;
62
+ const prev2 = fragmentNameToFile.get(name2);
63
+ if (prev2) {
64
+ throw new Error(`Duplicate fragment name '${name2}' in:
65
+ - ${prev2}
66
+ - ${doc.path}`);
67
+ }
68
+ fragmentNameToFile.set(name2, doc.path);
69
+ defs.push({ kind: "fragment", name: name2 });
70
+ continue;
71
+ }
72
+ if (def.kind !== Kind.OPERATION_DEFINITION) continue;
73
+ const type = def.operation;
74
+ if (!["query", "mutation", "subscription"].includes(type)) continue;
75
+ const name = def.name?.value;
76
+ if (!name) {
77
+ throw new Error(`Unnamed ${type} operation in ${doc.path}`);
78
+ }
79
+ const prev = operationNameToFile.get(name);
80
+ if (prev) {
81
+ throw new Error(`Duplicate ${type} operation name '${name}' in:
82
+ - ${prev}
83
+ - ${doc.path}`);
84
+ }
85
+ operationNameToFile.set(name, doc.path);
86
+ const op = { kind: "operation", type, name };
87
+ defs.push(op);
88
+ operationsByType[type].push(op);
89
+ }
90
+ byFile.set(doc.path, defs);
91
+ }
92
+ return { byFile, operationsByType };
93
+ }
94
+ function generateRegistryByTypeSource(analysis) {
95
+ const q = analysis.query.map((o) => o.name);
96
+ const m = analysis.mutation.map((o) => o.name);
97
+ const s = analysis.subscription.map((o) => o.name);
98
+ const lines = [
99
+ `import type { TypedDocumentNode } from "@graphql-typed-document-node/core";`,
100
+ `import * as ops from "#graphql/operations";`,
101
+ ``,
102
+ `type ResultOf<T> = T extends { __apiType?: (variables: infer _) => infer R } ? R : never;`,
103
+ `type VariablesOf<T> = T extends { __apiType?: (variables: infer V) => infer _ } ? V : never;`
104
+ ];
105
+ if (q.length > 0) {
106
+ lines.push(
107
+ ``,
108
+ `export const queries = {`,
109
+ ...q.map((name) => ` ${name}: ops.${name}Document,`),
110
+ `} as const;`
111
+ );
112
+ } else {
113
+ lines.push(``, `export const queries = {} as const;`);
114
+ }
115
+ lines.push(
116
+ `export type QueryName = keyof typeof queries;`,
117
+ `export type QueryResult<N extends QueryName> = ResultOf<(typeof queries)[N]>;`,
118
+ `export type QueryVariables<N extends QueryName> = VariablesOf<(typeof queries)[N]>;`
119
+ );
120
+ if (m.length > 0) {
121
+ lines.push(
122
+ ``,
123
+ `export const mutations = {`,
124
+ ...m.map((name) => ` ${name}: ops.${name}Document,`),
125
+ `} as const;`
126
+ );
127
+ } else {
128
+ lines.push(``, `export const mutations = {} as const;`);
129
+ }
130
+ lines.push(
131
+ `export type MutationName = keyof typeof mutations;`,
132
+ `export type MutationResult<N extends MutationName> = ResultOf<(typeof mutations)[N]>;`,
133
+ `export type MutationVariables<N extends MutationName> = VariablesOf<(typeof mutations)[N]>;`
134
+ );
135
+ if (s.length > 0) {
136
+ lines.push(
137
+ ``,
138
+ `export const subscriptions = {`,
139
+ ...s.map((name) => ` ${name}: ops.${name}Document,`),
140
+ `} as const;`
141
+ );
142
+ } else {
143
+ lines.push(``, `export const subscriptions = {} as const;`);
144
+ }
145
+ lines.push(
146
+ `export type SubscriptionName = keyof typeof subscriptions;`,
147
+ `export type SubscriptionResult<N extends SubscriptionName> = ResultOf<(typeof subscriptions)[N]>;`,
148
+ `export type SubscriptionVariables<N extends SubscriptionName> = VariablesOf<(typeof subscriptions)[N]>;`
149
+ );
150
+ return lines.join("\n") + "\n";
151
+ }
152
+ function formatDefinitions(defs) {
153
+ if (defs.length === 0) return "";
154
+ const colorOf = (def) => {
155
+ if (def.kind === "fragment") return green;
156
+ switch (def.type) {
157
+ case "query":
158
+ return blue;
159
+ case "mutation":
160
+ return magenta;
161
+ case "subscription":
162
+ return yellow;
163
+ }
164
+ };
165
+ return defs.map((def) => `${colorOf(def)}${def.name}${reset}`).join(`${dim} / ${reset}`);
166
+ }
167
+ async function loadGraphQLSchema(schemaPath) {
168
+ const { createJiti } = await import('jiti');
169
+ const jiti = createJiti(import.meta.url, { interopDefault: true });
170
+ const module = await jiti.import(schemaPath);
171
+ if (!module.schema) {
172
+ throw new Error(`${schemaPath} must export a 'schema' variable`);
173
+ }
174
+ const { printSchema, lexicographicSortSchema } = await import('graphql');
175
+ return printSchema(lexicographicSortSchema(module.schema));
176
+ }
177
+ async function runCodegen(options) {
178
+ const { sdl, documents, operationsFile } = options;
179
+ if (documents.length === 0) {
180
+ logger.warn("No GraphQL documents found");
181
+ return;
182
+ }
183
+ try {
184
+ await generate(
185
+ {
186
+ schema: sdl,
187
+ documents,
188
+ generates: {
189
+ [operationsFile]: {
190
+ plugins: ["typescript", "typescript-operations", "typed-document-node"],
191
+ config: {
192
+ useTypeImports: true,
193
+ enumsAsTypes: true,
194
+ skipTypename: true,
195
+ documentVariableSuffix: "Document",
196
+ documentMode: "documentNode",
197
+ strictScalars: true,
198
+ defaultScalarType: "never"
199
+ // TODO: Make codegen config customizable, e.g. for custom scalars
200
+ }
201
+ }
202
+ },
203
+ silent: true,
204
+ errorsOnly: true
205
+ },
206
+ true
207
+ );
208
+ logger.success(`Generated types for ${documents.length} document(s)`);
209
+ } catch (error) {
210
+ logger.error("GraphQL codegen failed:", error instanceof Error ? error.message : error);
211
+ }
20
212
  }
21
213
 
22
214
  const module$1 = defineNuxtModule({
@@ -25,29 +217,85 @@ const module$1 = defineNuxtModule({
25
217
  configKey: "graphql"
26
218
  },
27
219
  defaults: {
28
- yoga: {
29
- endpoint: "/api/graphql"
220
+ endpoint: "/api/graphql",
221
+ codegen: {
222
+ enabled: true,
223
+ pattern: "**/*.gql",
224
+ schemaOutput: "server/graphql/schema.graphql"
30
225
  }
31
226
  },
32
227
  async setup(options, nuxt) {
33
228
  const { resolve } = createResolver(import.meta.url);
34
229
  const { rootDir, serverDir } = nuxt.options;
35
- const layerDirs = [...getLayerDirectories(nuxt), { server: serverDir.replace(rootDir, `${rootDir}/playground`) }];
36
- nuxt.hook("nitro:config", (nitroConfig) => {
37
- nitroConfig.alias ||= {};
38
- nitroConfig.alias["#graphql/schema"] = findServerFile(layerDirs, "graphql/schema");
39
- nitroConfig.alias["#graphql/context"] = findServerFile(layerDirs, "graphql/context");
230
+ const layerDirs = [
231
+ ...getLayerDirectories(nuxt),
232
+ { root: rootDir, server: serverDir.replace(rootDir, `${rootDir}/playground`) }
233
+ ];
234
+ const layerServerDirs = layerDirs.map(({ server }) => server);
235
+ const layerRootDirs = layerDirs.map(({ root }) => root);
236
+ const schemaPath = await findSingleFile(layerServerDirs, "graphql/schema.{ts,mjs}", true);
237
+ const contextPath = await findSingleFile(layerServerDirs, "graphql/context.{ts,mjs}") || resolve("./runtime/server/default-context.ts");
238
+ nuxt.hook("nitro:config", (config) => {
239
+ config.alias ||= {};
240
+ config.alias["#graphql/schema"] = schemaPath;
241
+ config.alias["#graphql/context"] = contextPath;
40
242
  });
41
- const endpoint = options.yoga?.endpoint ?? "/api/graphql";
243
+ const endpoint = options.endpoint ?? "/api/graphql";
42
244
  addServerTemplate({
43
245
  filename: "graphql/yoga-handler",
44
- getContents: () => readFileSync(resolve("./templates/yoga-handler.ts"), "utf-8").replace("{{endpoint}}", endpoint)
246
+ getContents: () => readFileSync(resolve("./templates/yoga-handler.mjs"), "utf-8").replace("{{endpoint}}", endpoint)
45
247
  });
46
248
  addServerHandler({ route: endpoint, handler: "graphql/yoga-handler" });
47
- nuxt.hook("listen", (_server, { url }) => {
48
- logger.success(`GraphQL Yoga available at ${cyan}${url.replace(/\/$/, "") + endpoint}${reset}`);
249
+ nuxt.hook("listen", (_, { url }) => {
250
+ logger.success(`GraphQL Yoga ready at ${cyan}${url.replace(/\/$/, "")}${endpoint}${reset}`);
49
251
  });
50
252
  nuxt.options.runtimeConfig.public.graphql = { endpoint };
253
+ if (options.codegen?.enabled !== false) {
254
+ const codegenPattern = options.codegen?.pattern ?? "**/*.gql";
255
+ const graphqlrcFile = join(rootDir, ".graphqlrc");
256
+ const operationsFile = join(nuxt.options.buildDir, "graphql/operations.ts");
257
+ const registryFile = join(nuxt.options.buildDir, "graphql/registry.ts");
258
+ const schemaOutput = options.codegen?.schemaOutput ?? "server/graphql/schema.graphql";
259
+ const schemaFile = join(rootDir, schemaOutput);
260
+ const generate = async () => {
261
+ const [sdl, documents] = await Promise.all([
262
+ loadGraphQLSchema(schemaPath),
263
+ findMultipleFiles(layerRootDirs, codegenPattern)
264
+ ]);
265
+ const docs = documents.map((document) => ({ path: document, content: readFileSync(document, "utf-8") }));
266
+ const analysis = analyzeGraphQLDocuments(docs);
267
+ for (const doc of docs) {
268
+ const relativePath = doc.path.startsWith(rootDir) ? doc.path.slice(rootDir.length + 1) : doc.path;
269
+ const defs = analysis.byFile.get(doc.path) ?? [];
270
+ logger.info(`${cyan}${relativePath}${reset} [${formatDefinitions(defs)}]`);
271
+ }
272
+ await runCodegen({ sdl, documents, operationsFile });
273
+ if (writeFileIfChanged(schemaFile, sdl)) {
274
+ logger.info(`GraphQL schema saved to ${cyan}${schemaOutput}${reset}`);
275
+ }
276
+ const config = JSON.stringify({ schema: relative(rootDir, schemaFile), documents: codegenPattern }, null, 2);
277
+ if (writeFileIfChanged(graphqlrcFile, config)) {
278
+ logger.info(`GraphQL config saved to ${cyan}.graphqlrc${reset}`);
279
+ }
280
+ if (writeFileIfChanged(registryFile, generateRegistryByTypeSource(analysis.operationsByType))) {
281
+ logger.info(`GraphQL registry saved to ${cyan}${relative(rootDir, registryFile)}${reset}`);
282
+ }
283
+ };
284
+ nuxt.hook("prepare:types", async ({ references }) => {
285
+ await generate();
286
+ if (existsSync(operationsFile)) references.push({ path: operationsFile });
287
+ if (existsSync(registryFile)) references.push({ path: registryFile });
288
+ });
289
+ if (nuxt.options.dev) {
290
+ nuxt.hook("builder:watch", async (event, path) => {
291
+ if (path.endsWith(".gql")) {
292
+ await generate();
293
+ }
294
+ });
295
+ }
296
+ nuxt.options.alias["#graphql/operations"] = operationsFile;
297
+ nuxt.options.alias["#graphql/registry"] = registryFile;
298
+ }
51
299
  addImportsDir(resolve("./runtime/composables"));
52
300
  addPlugin(resolve("./runtime/plugins/graphql"));
53
301
  addTypeTemplate({
@@ -1,4 +1,4 @@
1
- import type { RequestDocument, Variables } from "graphql-request";
1
+ import type { TypedDocumentNode } from "@graphql-typed-document-node/core";
2
2
  export declare function useGraphQL(): {
3
- request: <T>(document: RequestDocument, variables?: Variables) => any;
3
+ request: <TResult, TVariables extends Record<string, unknown> | undefined>(document: TypedDocumentNode<TResult, TVariables>, variables?: TVariables) => Promise<TResult>;
4
4
  };
@@ -1,13 +1,8 @@
1
- import { useNuxtApp, useRequestHeaders } from "#imports";
1
+ import { useNuxtApp } from "#imports";
2
2
  export function useGraphQL() {
3
3
  const { $graphql } = useNuxtApp();
4
- if (import.meta.server) {
5
- const headers = useRequestHeaders(["cookie"]);
6
- if (headers.cookie) {
7
- $graphql.setHeader("cookie", headers.cookie);
8
- }
4
+ async function request(document, variables) {
5
+ return $graphql.request({ document, variables });
9
6
  }
10
- return {
11
- request: (document, variables) => $graphql.request(document, variables)
12
- };
7
+ return { request };
13
8
  }
@@ -0,0 +1,10 @@
1
+ import { type Ref } from "vue";
2
+ import { type MutationName, type MutationResult, type MutationVariables } from "#graphql/registry";
3
+ type IsEmptyObject<T> = T extends Record<string, never> ? true : keyof T extends never ? true : false;
4
+ export declare function useGraphQLMutation<N extends MutationName>(operationName: N): {
5
+ mutate: (...args: IsEmptyObject<MutationVariables<N>> extends true ? [variables?: MutationVariables<N>] : [variables: MutationVariables<N>]) => Promise<MutationResult<N>>;
6
+ data: Ref<MutationResult<N> | null, MutationResult<N> | null>;
7
+ error: Ref<Error | null, Error | null>;
8
+ pending: Ref<boolean, boolean>;
9
+ };
10
+ export {};
@@ -0,0 +1,25 @@
1
+ import { ref } from "vue";
2
+ import { useGraphQL } from "./useGraphQL.js";
3
+ import { mutations } from "#graphql/registry";
4
+ export function useGraphQLMutation(operationName) {
5
+ const document = mutations[operationName];
6
+ const { request } = useGraphQL();
7
+ const data = ref(null);
8
+ const error = ref(null);
9
+ const pending = ref(false);
10
+ async function mutate(...args) {
11
+ pending.value = true;
12
+ error.value = null;
13
+ try {
14
+ const result = await request(document, args[0]);
15
+ data.value = result;
16
+ return result;
17
+ } catch (e) {
18
+ error.value = e instanceof Error ? e : new Error(String(e));
19
+ throw e;
20
+ } finally {
21
+ pending.value = false;
22
+ }
23
+ }
24
+ return { mutate, data, error, pending };
25
+ }
@@ -0,0 +1,5 @@
1
+ import type { AsyncData, AsyncDataOptions } from "#app";
2
+ import { type QueryName, type QueryResult, type QueryVariables } from "#graphql/registry";
3
+ type IsEmptyObject<T> = T extends Record<string, never> ? true : keyof T extends never ? true : false;
4
+ export declare function useGraphQLQuery<N extends QueryName>(operationName: N, ...args: IsEmptyObject<QueryVariables<N>> extends true ? [variables?: QueryVariables<N>, opts?: AsyncDataOptions<QueryResult<N>>] : [variables: QueryVariables<N>, opts?: AsyncDataOptions<QueryResult<N>>]): AsyncData<QueryResult<N>, Error | null>;
5
+ export {};
@@ -0,0 +1,11 @@
1
+ import { useAsyncData } from "#imports";
2
+ import { hash } from "ohash";
3
+ import { useGraphQL } from "./useGraphQL.js";
4
+ import { queries } from "#graphql/registry";
5
+ export function useGraphQLQuery(operationName, ...args) {
6
+ const document = queries[operationName];
7
+ const [variables, opts] = args;
8
+ const { request } = useGraphQL();
9
+ const key = `gql:q:${operationName}:${hash(variables ?? {})}`;
10
+ return useAsyncData(key, () => request(document, variables), opts);
11
+ }
@@ -0,0 +1,7 @@
1
+ import type { H3Event } from "h3";
2
+ export declare const createContext: (event: H3Event) => {
3
+ event: H3Event<import("h3").EventHandlerRequest>;
4
+ };
5
+ export type GraphQLContext = {
6
+ event: H3Event;
7
+ };
@@ -0,0 +1 @@
1
+ export const createContext = (event) => ({ event });
@@ -1,18 +1,24 @@
1
1
  import { createYoga } from "graphql-yoga";
2
2
  import { defineEventHandler, toWebRequest, sendWebResponse } from "h3";
3
- /** @ts-expect-error - No type declarations in module context */
4
3
  import { schema } from "#graphql/schema";
5
- /** @ts-expect-error - No type declarations in module context */
6
4
  import { createContext } from "#graphql/context";
7
5
 
8
- const yoga = createYoga({
9
- schema,
10
- graphqlEndpoint: "{{endpoint}}",
11
- });
6
+ let yoga = null;
7
+
8
+ function getYoga() {
9
+ if (!yoga) {
10
+ yoga = createYoga({
11
+ schema,
12
+ graphqlEndpoint: "{{endpoint}}",
13
+ fetchAPI: globalThis,
14
+ });
15
+ }
16
+ return yoga;
17
+ }
12
18
 
13
19
  export default defineEventHandler(async (event) => {
14
- const context = await createContext(event);
15
20
  const request = toWebRequest(event);
16
- const response = await yoga.handleRequest(request, context);
21
+ const context = await createContext(event);
22
+ const response = await getYoga().handleRequest(request, context);
17
23
  return sendWebResponse(event, response);
18
24
  });
package/dist/types.d.mts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { default } from './module.mjs'
2
2
 
3
- export { type GraphQLYogaConfig, type ModuleOptions } from './module.mjs'
3
+ export { type ModuleOptions } from './module.mjs'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lewebsimple/nuxt-graphql",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Opinionated Nuxt module for using GraphQL",
5
5
  "repository": "lewebsimple/nuxt-graphql",
6
6
  "license": "MIT",
@@ -34,10 +34,18 @@
34
34
  "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
35
35
  },
36
36
  "dependencies": {
37
+ "@graphql-codegen/cli": "^5.0.7",
38
+ "@graphql-codegen/typed-document-node": "^5.1.2",
39
+ "@graphql-codegen/typescript": "^4.1.6",
40
+ "@graphql-codegen/typescript-operations": "^4.6.1",
41
+ "@graphql-typed-document-node/core": "^3.2.0",
37
42
  "@nuxt/kit": "^4.2.2",
38
43
  "graphql": "^16.12.0",
39
44
  "graphql-request": "^7.4.0",
40
- "graphql-yoga": "^5.17.1"
45
+ "graphql-yoga": "^5.17.1",
46
+ "jiti": "^2.6.1",
47
+ "ohash": "^2.0.11",
48
+ "tinyglobby": "^0.2.15"
41
49
  },
42
50
  "devDependencies": {
43
51
  "@nuxt/devtools": "^3.1.1",