@treygrr/nuxt-airplane 0.0.0-stage → 0.1.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.
Files changed (35) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +177 -2
  3. package/dist/module.d.mts +34 -0
  4. package/dist/module.json +12 -0
  5. package/dist/module.mjs +271 -0
  6. package/dist/runtime/client.d.ts +229 -0
  7. package/dist/runtime/client.js +330 -0
  8. package/dist/runtime/composables/defineAirplaneRequest.d.ts +7 -0
  9. package/dist/runtime/composables/defineAirplaneRequest.js +4 -0
  10. package/dist/runtime/composables/defineAirplaneStores.d.ts +8 -0
  11. package/dist/runtime/composables/defineAirplaneStores.js +4 -0
  12. package/dist/runtime/composables/defineMutationHandler.d.ts +6 -0
  13. package/dist/runtime/composables/defineMutationHandler.js +4 -0
  14. package/dist/runtime/composables/defineMutationHandlers.d.ts +6 -0
  15. package/dist/runtime/composables/defineMutationHandlers.js +4 -0
  16. package/dist/runtime/composables/defineQueryHandler.d.ts +6 -0
  17. package/dist/runtime/composables/defineQueryHandler.js +4 -0
  18. package/dist/runtime/composables/defineQueryHandlers.d.ts +6 -0
  19. package/dist/runtime/composables/defineQueryHandlers.js +4 -0
  20. package/dist/runtime/composables/useAirplane.d.ts +3 -0
  21. package/dist/runtime/composables/useAirplane.js +4 -0
  22. package/dist/runtime/composables/useMutation.d.ts +14 -0
  23. package/dist/runtime/composables/useMutation.js +21 -0
  24. package/dist/runtime/composables/useQuery.d.ts +19 -0
  25. package/dist/runtime/composables/useQuery.js +63 -0
  26. package/dist/runtime/composables/useSync.d.ts +35 -0
  27. package/dist/runtime/composables/useSync.js +46 -0
  28. package/dist/runtime/db.d.ts +19 -0
  29. package/dist/runtime/db.js +86 -0
  30. package/dist/runtime/plugin.client.d.ts +415 -0
  31. package/dist/runtime/plugin.client.js +43 -0
  32. package/dist/runtime/types.d.ts +216 -0
  33. package/dist/runtime/types.js +8 -0
  34. package/dist/types.d.mts +3 -0
  35. package/package.json +96 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 treygrr
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 CHANGED
@@ -1,3 +1,178 @@
1
- # Temporary Holding Version
1
+ <p align="center">
2
+ <img src="./docs/public/logo.png" width="140" height="140" alt="nuxt-airplane logo">
3
+ </p>
2
4
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
5
+ # nuxt-airplane
6
+
7
+ [![npm version](https://img.shields.io/npm/v/%40treygrr%2Fnuxt-airplane?color=00DC82&labelColor=020420)](https://www.npmjs.com/package/@treygrr/nuxt-airplane)
8
+ [![License](https://img.shields.io/npm/l/%40treygrr%2Fnuxt-airplane?color=00DC82&labelColor=020420)](./LICENSE)
9
+ [![Nuxt](https://img.shields.io/badge/Nuxt-4-00DC82?logo=nuxt&labelColor=020420)](https://nuxt.com)
10
+
11
+ Airplane mode for your GraphQL app.
12
+
13
+ 📖 **[Documentation](https://treygrr.github.io/nuxt-airplane/)**
14
+
15
+ Give it an endpoint and your `.gql` files. Every operation becomes a typed
16
+ `useQuery` or `useMutation`, results live in IndexedDB (via Dexie), and anything
17
+ you change mid-flight queues up and lands on the server once you're back on the
18
+ ground.
19
+
20
+ ## Setup
21
+
22
+ ```ts
23
+ // nuxt.config.ts
24
+ export default defineNuxtConfig({
25
+ modules: ['@treygrr/nuxt-airplane'],
26
+ airplane: {
27
+ endpoint: 'https://api.example.com/graphql',
28
+ },
29
+ })
30
+ ```
31
+
32
+ | Option | Default | |
33
+ | --- | --- | --- |
34
+ | `endpoint` | `/api/graphql` | GraphQL endpoint. A relative path is resolved against the app itself. |
35
+ | `schema` | – | SDL file or URL. When omitted the endpoint is introspected. In dev a relative endpoint is introspected once the dev server is up; the last good schema is cached in `node_modules/.cache/nuxt-airplane`. |
36
+ | `documents` | `['**/*.gql', '**/*.graphql']` | Globs (relative to `srcDir`) of the files holding your operations. |
37
+ | `handlersDir` | `airplane` | Directory (relative to `srcDir`) scanned for mutation handler files. |
38
+ | `dbName` | `nuxt-airplane` | IndexedDB database name. |
39
+ | `cacheKey` | – | Deploy key (git SHA, build id). A change wipes cached data on next load, outbox kept; a hash of your documents is always mixed in. `NUXT_PUBLIC_AIRPLANE_CACHE_KEY` overrides it. |
40
+ | `codegen` | `{}` | Extra config for the `typescript` / `typescript-operations` codegen plugins. |
41
+
42
+ ## Operations
43
+
44
+ Every named operation in your documents becomes available by name. Fragments are
45
+ resolved across files.
46
+
47
+ ```graphql
48
+ # app/graphql/sites.gql
49
+ query GetSites { sites { id name archived } }
50
+ mutation RenameSite($id: ID!, $name: String!) { renameSite(id: $id, name: $name) { id name } }
51
+ ```
52
+
53
+ ```vue
54
+ <script setup lang="ts">
55
+ const { data, loading, fetching, error, refresh } = useQuery('GetSites')
56
+ const rename = useMutation('RenameSite')
57
+
58
+ await rename.mutate({ id: 'site-1', name: 'New name' })
59
+ </script>
60
+ ```
61
+
62
+ Both are fully typed from the schema: the document name is checked, variables
63
+ and `data` get the generated operation types.
64
+
65
+ `useQuery` returns **live data from the local cache**, not the network response.
66
+ A cached result renders immediately and a refetch runs in the background when
67
+ online; any mutation handler that touches the cache updates `data` right away.
68
+
69
+ `useMutation().mutate()` resolves as soon as the change is applied locally and
70
+ queued. Sending it to the server happens in the sync loop.
71
+
72
+ ## Mutation handlers
73
+
74
+ Handlers describe how a queued mutation shows up in the local cache. Put files
75
+ under `app/airplane/` and they are picked up automatically.
76
+
77
+ ```ts
78
+ // app/airplane/sites.ts
79
+ export default defineMutationHandlers({
80
+ RenameSite: {
81
+ // Runs when the mutation is queued, so the UI reflects it immediately.
82
+ apply: ({ cache, variables }) =>
83
+ cache.update('GetSites', (data) => {
84
+ const site = data.sites.find(s => s.id === variables.id)
85
+ if (site) site.name = variables.name
86
+ }),
87
+ // Optional: summary for the pending list, computed before `apply`.
88
+ describe: ({ variables }) => `Rename site to ${variables.name}`,
89
+ // Optional: merge the server's response once it has been sent.
90
+ reconcile: ({ cache, variables }, result) => { /* ... */ },
91
+ // Optional: undo `apply`. When omitted, the rows `apply` touched are
92
+ // snapshotted and restored on rollback.
93
+ revert: ({ cache, variables }) => { /* ... */ },
94
+ // Queries to refetch after the server accepted it. Defaults to every active query.
95
+ refetch: ['GetSites'],
96
+ },
97
+ })
98
+ ```
99
+
100
+ The handler context gives you:
101
+
102
+ - `cache.update(name, updater)` / `cache.update(name, variables, updater)`: edit cached responses in place or return a replacement.
103
+ - `cache.get(name, variables?)`, `cache.set(name, variables, data)`, `cache.invalidate(name)`.
104
+ - `db`: the Dexie instance (`queries` and `mutations` tables) if you need to go lower.
105
+ - `variables`: the mutation's input, typed.
106
+
107
+ ## Auth and headers
108
+
109
+ ```ts
110
+ // app/airplane/request.ts
111
+ export default defineAirplaneRequest({
112
+ headers: () => ({ Authorization: `Bearer ${useCookie('token').value}` }), // runs per request, composables work
113
+ credentials: 'include',
114
+ onResponse: res => { if (res.status === 401) navigateTo('/login') },
115
+ })
116
+ ```
117
+
118
+ Headers are computed when a request is sent, so a mutation queued offline goes out later with the token valid at that moment. Static headers go in the `headers` module option; `schemaHeaders` covers build-time introspection of a protected endpoint.
119
+
120
+ ## Your own tables
121
+
122
+ Opt a query into normalized storage: declare Dexie tables with `defineAirplaneStores`, then tell the module how to `store` a response and `read` it back with `defineQueryHandlers`. Mutation handlers write to the tables directly and every query reading them updates on its own. Rollback still works: writes made during `apply` are recorded by an undo log.
123
+
124
+ ```ts
125
+ // app/airplane/stores.ts
126
+ declare global { interface NuxtAirplaneStores { sites: { id: string, name: string, archived: boolean } } }
127
+ export default defineAirplaneStores({ sites: '&id, name, archived' })
128
+
129
+ // app/airplane/queries.ts
130
+ export default defineQueryHandlers({
131
+ GetSites: {
132
+ store: ({ db }, data) => db.sites.bulkPut(data.sites),
133
+ read: async ({ db }) => ({ sites: await db.sites.toArray() }),
134
+ },
135
+ })
136
+
137
+ // app/airplane/sites.ts
138
+ export default defineMutationHandlers({
139
+ RenameSite: { apply: ({ db, variables }) => db.sites.update(variables.id, { name: variables.name }) },
140
+ })
141
+ ```
142
+
143
+ One file per operation also works: `defineMutationHandler('RenameSite', { … })` and `defineQueryHandler('GetSites', { … })` declare a single operation, and a folder with an `index.ts` is imported through its index only, so you can compose there.
144
+
145
+ Paginated queries can opt into fetching every page in the background so the whole set is available offline: `prefetch: { next: (data, vars) => data.pageInfo.hasNextPage ? { ...vars, after: data.pageInfo.endCursor } : null, when: () => settings.cacheCatalog }`. `useSync().prefetching` counts the runs in progress.
146
+
147
+ ## Sync state
148
+
149
+ ```ts
150
+ const { online, syncing, synced, pending, syncTotal, syncCount, lastError, sync, rollback, retry, goOffline, goOnline } = useSync()
151
+ ```
152
+
153
+ - `pending` lists the queued mutations oldest first, each with a human-readable `description` (from the handler's `describe` hook, or the humanized document name), its variables and status.
154
+ - `rollback(id)` undoes a queued mutation locally and drops it from the queue. Mutations queued after it are re-applied on top of the restored data.
155
+ - A mutation the server rejects is marked `failed` and left in the queue; `retry(id)` or `rollback(id)` it.
156
+ - A network failure stops the batch and keeps everything queued; the next `online` event (or `sync()`) resumes it.
157
+ - `goOffline()` holds the client offline whatever the network says: queries serve the cache only and mutations stay queued. `goOnline()` releases it and syncs. `online` reflects the effective state; `networkOnline` and `forcedOffline` expose the two inputs. Handy for a "work offline" switch or for testing.
158
+
159
+ ## Security
160
+
161
+ Cached data and the outbox live unencrypted in IndexedDB under your origin: clear them on sign-out with `useAirplane().clear()`, don't queue mutations whose variables you wouldn't store (change-password, card numbers), and render server error messages as text. Credentials are never persisted; `schemaHeaders` never ships. Full notes in the [Security guide](https://treygrr.github.io/nuxt-airplane/guide/security).
162
+
163
+ ## Contributing
164
+
165
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) and the [pull request guidelines](https://treygrr.github.io/nuxt-airplane/contributing/pull-requests).
166
+
167
+ ## Development
168
+
169
+ ```sh
170
+ npm install
171
+ npm run dev # playground with an in-memory Apollo server at /api/graphql
172
+ npm run lint
173
+ npm run test
174
+ npm run test:types
175
+ ```
176
+
177
+ The playground (`playground/`) has a sites/items inventory demo: toggle the
178
+ network in devtools, make changes, roll some back, and go online again.
@@ -0,0 +1,34 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ interface ModuleOptions {
4
+ /** URL of the GraphQL endpoint. A path like `/api/graphql` is resolved against the app's own origin. */
5
+ endpoint: string;
6
+ /**
7
+ * Where the schema comes from: an SDL file or a URL. When omitted the
8
+ * endpoint is introspected (in dev, once the server is listening).
9
+ */
10
+ schema?: string;
11
+ /** Globs, relative to `srcDir`, of the `.gql` / `.graphql` files holding the operations. */
12
+ documents: string[];
13
+ /** Directory, relative to `srcDir`, scanned for `defineMutationHandlers` / `defineQueryHandlers` / `defineAirplaneStores` files. */
14
+ handlersDir: string;
15
+ /** Static headers sent with every request, e.g. an API key. Public: they ship to the browser. */
16
+ headers?: Record<string, string>;
17
+ /** Headers for build-time introspection of a protected endpoint, e.g. from `process.env`. Never shipped. */
18
+ schemaHeaders?: Record<string, string>;
19
+ /** Name of the IndexedDB database. */
20
+ dbName: string;
21
+ /**
22
+ * Changes to this value wipe the local cache on the next load (the outbox is
23
+ * kept). Set it per deploy, e.g. a git SHA or build id. A hash of the GraphQL
24
+ * documents is always mixed in, so editing a query busts the cache on its own.
25
+ * Overridable at runtime with `NUXT_PUBLIC_AIRPLANE_CACHE_KEY`.
26
+ */
27
+ cacheKey?: string;
28
+ /** Extra options passed to the `typescript` / `typescript-operations` codegen plugins. */
29
+ codegen: Record<string, unknown>;
30
+ }
31
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
32
+
33
+ export { _default as default };
34
+ export type { ModuleOptions };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "nuxt-airplane",
3
+ "configKey": "airplane",
4
+ "compatibility": {
5
+ "nuxt": ">=4.0.0"
6
+ },
7
+ "version": "0.1.1",
8
+ "builder": {
9
+ "@nuxt/module-builder": "1.0.3",
10
+ "unbuild": "3.6.1"
11
+ }
12
+ }
@@ -0,0 +1,271 @@
1
+ import { join, dirname, resolve, sep, basename } from 'node:path';
2
+ import { defineNuxtModule, createResolver, useLogger, addTemplate, addTypeTemplate, addImportsDir, addPlugin, updateTemplates } from '@nuxt/kit';
3
+ import { hash } from 'ohash';
4
+ import { glob } from 'tinyglobby';
5
+ import { existsSync } from 'node:fs';
6
+ import { readFile, mkdir, writeFile } from 'node:fs/promises';
7
+ import { codegen } from '@graphql-codegen/core';
8
+ import * as typescriptPlugin from '@graphql-codegen/typescript';
9
+ import * as typescriptOperationsPlugin from '@graphql-codegen/typescript-operations';
10
+ import { buildSchema, printSchema, parse, Kind, print, getIntrospectionQuery, buildClientSchema, visit } from 'graphql';
11
+
12
+ function isUrl(value) {
13
+ return /^https?:\/\//.test(value);
14
+ }
15
+ async function introspect(url, headers = {}, attempts = 10) {
16
+ const res = await fetch(url, {
17
+ method: "POST",
18
+ headers: { "content-type": "application/json", ...headers },
19
+ body: JSON.stringify({ query: getIntrospectionQuery() }),
20
+ signal: AbortSignal.timeout(15e3)
21
+ });
22
+ if (res.status === 503 && attempts > 1) {
23
+ await new Promise((r) => setTimeout(r, 1e3));
24
+ return introspect(url, headers, attempts - 1);
25
+ }
26
+ if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
27
+ const body = await res.json();
28
+ if (body.errors?.length) throw new Error(body.errors.map((e) => e.message).join(", "));
29
+ return buildClientSchema(body.data);
30
+ }
31
+ async function loadSchema(source, log) {
32
+ const cacheFile = join(source.cacheDir, "schema.graphql");
33
+ let schema = null;
34
+ try {
35
+ if (source.schema && !isUrl(source.schema)) {
36
+ schema = buildSchema(await readFile(source.schema, "utf8"));
37
+ } else {
38
+ const url = source.schema ?? source.endpoint;
39
+ if (isUrl(url)) schema = await introspect(url, source.headers);
40
+ }
41
+ } catch (error) {
42
+ log.warn(`Could not load GraphQL schema: ${error.message}`);
43
+ }
44
+ if (schema) {
45
+ await mkdir(dirname(cacheFile), { recursive: true });
46
+ await writeFile(cacheFile, printSchema(schema));
47
+ } else if (existsSync(cacheFile)) {
48
+ schema = buildSchema(await readFile(cacheFile, "utf8"));
49
+ }
50
+ return schema;
51
+ }
52
+ function fragmentSpreads(node) {
53
+ const names = /* @__PURE__ */ new Set();
54
+ visit(node, { FragmentSpread: (s) => void names.add(s.name.value) });
55
+ return names;
56
+ }
57
+ async function loadDocuments(patterns, cwd, log) {
58
+ const files = (await glob(patterns, { cwd, absolute: true, ignore: ["**/node_modules/**"] })).sort();
59
+ const fragments = /* @__PURE__ */ new Map();
60
+ const ops = [];
61
+ for (const file of files) {
62
+ let doc;
63
+ try {
64
+ doc = parse(await readFile(file, "utf8"));
65
+ } catch (error) {
66
+ log.warn(`Skipping ${file}: ${error.message}`);
67
+ continue;
68
+ }
69
+ for (const def of doc.definitions) {
70
+ if (def.kind === Kind.FRAGMENT_DEFINITION) fragments.set(def.name.value, def);
71
+ else if (def.kind === Kind.OPERATION_DEFINITION) {
72
+ if (!def.name) log.warn(`Skipping anonymous ${def.operation} in ${file}: operations must be named`);
73
+ else ops.push({ node: def, file });
74
+ }
75
+ }
76
+ }
77
+ const operations = {};
78
+ const documentNodes = [];
79
+ for (const { node, file } of ops) {
80
+ const name = node.name.value;
81
+ if (operations[name]) {
82
+ log.warn(`Duplicate operation "${name}" in ${file}, keeping the first one`);
83
+ continue;
84
+ }
85
+ const needed = /* @__PURE__ */ new Map();
86
+ const queue = [...fragmentSpreads(node)];
87
+ while (queue.length) {
88
+ const spread = queue.shift();
89
+ if (needed.has(spread)) continue;
90
+ const fragment = fragments.get(spread);
91
+ if (!fragment) {
92
+ log.warn(`Operation "${name}" spreads unknown fragment "${spread}"`);
93
+ continue;
94
+ }
95
+ needed.set(spread, fragment);
96
+ queue.push(...fragmentSpreads(fragment));
97
+ }
98
+ const documentNode = { kind: Kind.DOCUMENT, definitions: [node, ...needed.values()] };
99
+ documentNodes.push(documentNode);
100
+ operations[name] = { name, kind: node.operation, source: print(documentNode) };
101
+ }
102
+ return { operations, documentNodes, files };
103
+ }
104
+ function operationMap(ops, kind, suffix) {
105
+ return ops.filter((op) => op.kind === kind).map((op) => ` ${JSON.stringify(op.name)}: { result: ${op.name}${suffix}, variables: ${op.name}${suffix}Variables }`).join("\n");
106
+ }
107
+ const virtualModules = `
108
+ declare module '#nuxt-airplane/documents' {
109
+ export const documents: Record<string, { name: string, kind: 'query' | 'mutation' | 'subscription', source: string }>
110
+ export const documentsHash: string
111
+ }
112
+ declare module '#nuxt-airplane/handlers' {
113
+ export const handlers: unknown[]
114
+ }
115
+ `;
116
+ async function generateTypes(schema, docs, config = {}) {
117
+ const ops = Object.values(docs.operations);
118
+ if (!schema) {
119
+ return [
120
+ "// Generated by nuxt-airplane. No GraphQL schema could be loaded, so operations are untyped.",
121
+ "export {}",
122
+ "declare global {",
123
+ " interface NuxtAirplaneQueries { [name: string]: { result: any, variables: any } }",
124
+ " interface NuxtAirplaneMutations { [name: string]: { result: any, variables: any } }",
125
+ "}",
126
+ virtualModules
127
+ ].join("\n");
128
+ }
129
+ const generated = await codegen({
130
+ filename: "nuxt-airplane.ts",
131
+ schema: parse(printSchema(schema)),
132
+ documents: docs.documentNodes.map((document) => ({ document })),
133
+ config: {
134
+ namingConvention: "keep",
135
+ // `ID` comes back as a string over the wire; typing it as such keeps variables simple.
136
+ scalars: { ID: { input: "string", output: "string" } },
137
+ ...config
138
+ },
139
+ plugins: [{ typescript: {} }, { typescriptOperations: {} }],
140
+ pluginMap: { typescript: typescriptPlugin, typescriptOperations: typescriptOperationsPlugin }
141
+ });
142
+ return [
143
+ "// Generated by nuxt-airplane from your GraphQL documents. Do not edit.",
144
+ generated,
145
+ "declare global {",
146
+ " interface NuxtAirplaneQueries {",
147
+ operationMap(ops, "query", "Query"),
148
+ " }",
149
+ " interface NuxtAirplaneMutations {",
150
+ operationMap(ops, "mutation", "Mutation"),
151
+ " }",
152
+ "}",
153
+ virtualModules
154
+ ].join("\n");
155
+ }
156
+
157
+ const module$1 = defineNuxtModule({
158
+ meta: {
159
+ name: "nuxt-airplane",
160
+ configKey: "airplane",
161
+ compatibility: { nuxt: ">=4.0.0" }
162
+ },
163
+ defaults: {
164
+ endpoint: "/api/graphql",
165
+ documents: ["**/*.gql", "**/*.graphql"],
166
+ handlersDir: "airplane",
167
+ dbName: "nuxt-airplane",
168
+ codegen: {}
169
+ },
170
+ async setup(options, nuxt) {
171
+ const resolver = createResolver(import.meta.url);
172
+ const logger = useLogger("nuxt-airplane");
173
+ const { srcDir } = nuxt.options;
174
+ const cacheDir = join(nuxt.options.rootDir, "node_modules/.cache/nuxt-airplane");
175
+ nuxt.options.runtimeConfig.public.airplane = {
176
+ endpoint: options.endpoint,
177
+ dbName: options.dbName,
178
+ cacheKey: options.cacheKey ?? "",
179
+ headers: options.headers ?? {}
180
+ };
181
+ nuxt.options.build.transpile.push(resolver.resolve("./runtime"));
182
+ nuxt.options.vite.optimizeDeps ||= {};
183
+ nuxt.options.vite.optimizeDeps.include ||= [];
184
+ nuxt.options.vite.optimizeDeps.include.push("dexie", "ohash");
185
+ let introspectionUrl = /^https?:\/\//.test(options.endpoint) ? options.endpoint : void 0;
186
+ let docs = { operations: {}, documentNodes: [], files: [] };
187
+ async function reloadDocuments() {
188
+ docs = await loadDocuments(options.documents, srcDir, logger);
189
+ return docs;
190
+ }
191
+ async function handlerFiles() {
192
+ const dir = join(srcDir, options.handlersDir);
193
+ const files = (await glob(["**/*.{ts,js,mts,mjs}"], { cwd: dir, absolute: true })).sort();
194
+ const isIndex = (file) => /^index\.[mc]?[tj]s$/.test(basename(file));
195
+ const indexDirs = files.filter(isIndex).map((file) => dirname(file));
196
+ return files.filter((file) => {
197
+ const owner = indexDirs.find((d) => file.startsWith(d + sep));
198
+ return !owner || dirname(file) === owner && isIndex(file);
199
+ });
200
+ }
201
+ await reloadDocuments();
202
+ nuxt.options.watch.push(...docs.files);
203
+ const documentsTemplate = addTemplate({
204
+ filename: "nuxt-airplane/documents.mjs",
205
+ write: true,
206
+ getContents: () => [
207
+ `export const documents = ${JSON.stringify(docs.operations, null, 2)}`,
208
+ // Part of the cache key: any change to an operation invalidates cached responses.
209
+ `export const documentsHash = ${JSON.stringify(hash(Object.values(docs.operations).map((op) => op.source)))}`,
210
+ ""
211
+ ].join("\n")
212
+ });
213
+ const handlersTemplate = addTemplate({
214
+ filename: "nuxt-airplane/handlers.mjs",
215
+ write: true,
216
+ getContents: async () => {
217
+ const files = await handlerFiles();
218
+ const imports = files.map((file, i) => `import h${i} from ${JSON.stringify(file)}`);
219
+ const list = files.map((_, i) => `h${i}`).join(", ");
220
+ return `${imports.join("\n")}
221
+ export const handlers = [${list}]
222
+ `;
223
+ }
224
+ });
225
+ nuxt.options.alias["#nuxt-airplane"] = resolver.resolve("./runtime/types");
226
+ nuxt.options.alias["#nuxt-airplane/documents"] = documentsTemplate.dst;
227
+ nuxt.options.alias["#nuxt-airplane/handlers"] = handlersTemplate.dst;
228
+ addTypeTemplate({
229
+ filename: "types/nuxt-airplane.d.ts",
230
+ getContents: async () => {
231
+ const schemaSource = options.schema && !/^https?:\/\//.test(options.schema) ? resolve(nuxt.options.rootDir, options.schema) : options.schema;
232
+ const schema = await loadSchema({ schema: schemaSource, endpoint: introspectionUrl ?? "", cacheDir, headers: options.schemaHeaders }, logger);
233
+ if (!schema) logger.warn("No GraphQL schema available yet, operations are untyped");
234
+ return generateTypes(schema, docs, options.codegen);
235
+ }
236
+ });
237
+ async function regenerate() {
238
+ await reloadDocuments();
239
+ await updateTemplates({ filter: (t) => t.filename.includes("nuxt-airplane") });
240
+ }
241
+ if (nuxt.options.dev) {
242
+ let introspectDevServer = function() {
243
+ if (introspectionUrl || options.schema || !devUrl || !nitroBuilt) return;
244
+ introspectionUrl = new URL(options.endpoint, devUrl).href;
245
+ regenerate().catch((error) => logger.error("Failed to generate GraphQL types", error));
246
+ };
247
+ let devUrl;
248
+ let nitroBuilt = false;
249
+ nuxt.hook("listen", (_server, listener) => {
250
+ devUrl = listener.url;
251
+ introspectDevServer();
252
+ });
253
+ nuxt.hook("nitro:init", (nitro) => {
254
+ nitro.hooks.hook("dev:reload", () => {
255
+ nitroBuilt = true;
256
+ introspectDevServer();
257
+ });
258
+ });
259
+ nuxt.hook("builder:watch", async (_event, path) => {
260
+ const abs = join(srcDir, path);
261
+ const isDocument = /\.(?:gql|graphql)$/.test(abs);
262
+ const isHandler = abs.startsWith(join(srcDir, options.handlersDir));
263
+ if (isDocument || isHandler) await regenerate();
264
+ });
265
+ }
266
+ addImportsDir(resolver.resolve("./runtime/composables"));
267
+ addPlugin({ src: resolver.resolve("./runtime/plugin.client"), mode: "client" });
268
+ }
269
+ });
270
+
271
+ export { module$1 as default };