nitro-graphql 1.1.2 → 1.2.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.
@@ -1,6 +1,6 @@
1
- import * as h31 from "h3";
1
+ import * as h30 from "h3";
2
2
 
3
3
  //#region src/routes/graphql-yoga.d.ts
4
- declare const _default: h31.EventHandler<h31.EventHandlerRequest, Promise<Response>>;
4
+ declare const _default: h30.EventHandler<h30.EventHandlerRequest, Promise<Response>>;
5
5
  //#endregion
6
6
  export { _default as default };
@@ -57,6 +57,33 @@ declare module 'nitropack' {
57
57
  graphql?: NitroGraphQLOptions;
58
58
  }
59
59
  }
60
+ interface ExternalGraphQLService {
61
+ /** Unique name for this service (used for file naming and type generation) */
62
+ name: string;
63
+ /** Schema source - can be URL(s) for remote schemas or file path(s) for local schemas */
64
+ schema: string | string[];
65
+ /** GraphQL endpoint for this service */
66
+ endpoint: string;
67
+ /** Optional headers for schema introspection and client requests */
68
+ headers?: Record<string, string> | (() => Record<string, string>);
69
+ /** Optional: specific document patterns for this service */
70
+ documents?: string[];
71
+ /**
72
+ * Optional: Download and cache schema locally for offline usage
73
+ * - true or 'once': Download if file doesn't exist, then use cached version (offline-friendly)
74
+ * - 'always': Check for updates on every build (current behavior)
75
+ * - 'manual': Never download automatically, user manages schema files manually
76
+ * - false: Disable schema downloading
77
+ */
78
+ downloadSchema?: boolean | 'once' | 'always' | 'manual';
79
+ /** Optional: Custom path to save downloaded schema (default: .nitro/graphql/schemas/[serviceName].graphql) */
80
+ downloadPath?: string;
81
+ /** Optional: service-specific codegen configuration */
82
+ codegen?: {
83
+ client?: CodegenClientConfig;
84
+ clientSDK?: GenericSdkConfig;
85
+ };
86
+ }
60
87
  interface NitroGraphQLOptions {
61
88
  framework: 'graphql-yoga' | 'apollo-server';
62
89
  endpoint?: {
@@ -76,6 +103,8 @@ interface NitroGraphQLOptions {
76
103
  client?: CodegenClientConfig;
77
104
  clientSDK?: GenericSdkConfig;
78
105
  };
106
+ /** External GraphQL services to generate types and SDKs for */
107
+ externalServices?: ExternalGraphQLService[];
79
108
  }
80
109
  //#endregion
81
- export { CodegenClientConfig, CodegenServerConfig, GenImport, GenericSdkConfig, NitroGraphQLOptions };
110
+ export { CodegenClientConfig, CodegenServerConfig, ExternalGraphQLService, GenImport, GenericSdkConfig, NitroGraphQLOptions };
@@ -1,4 +1,4 @@
1
- import { CodegenClientConfig, GenericSdkConfig } from "../types/index.js";
1
+ import { CodegenClientConfig, ExternalGraphQLService, GenericSdkConfig } from "../types/index.js";
2
2
  import { GraphQLSchema } from "graphql";
3
3
  import { Source } from "@graphql-tools/utils";
4
4
  import { LoadSchemaOptions, UnnormalizedTypeDefPointer } from "@graphql-tools/load";
@@ -14,10 +14,25 @@ type GraphQLTypeDefPointer = UnnormalizedTypeDefPointer | UnnormalizedTypeDefPoi
14
14
  */
15
15
  type GraphQLLoadSchemaOptions = Partial<LoadSchemaOptions>;
16
16
  declare function graphQLLoadSchemaSync(schemaPointers: GraphQLTypeDefPointer, data?: GraphQLLoadSchemaOptions): Promise<GraphQLSchema | undefined>;
17
+ /**
18
+ * Load schema from external GraphQL service
19
+ */
20
+ declare function loadExternalSchema(service: ExternalGraphQLService, buildDir?: string): Promise<GraphQLSchema | undefined>;
21
+ /**
22
+ * Download and save schema from external service
23
+ */
24
+ declare function downloadAndSaveSchema(service: ExternalGraphQLService, buildDir: string): Promise<string | undefined>;
17
25
  declare function loadGraphQLDocuments(patterns: string | string[]): Promise<Source[]>;
18
- declare function generateClientTypes(schema: GraphQLSchema, docs: Source[], config?: CodegenClientConfig, sdkConfig?: GenericSdkConfig, outputPath?: string): Promise<false | {
26
+ declare function generateClientTypes(schema: GraphQLSchema, docs: Source[], config?: CodegenClientConfig, sdkConfig?: GenericSdkConfig, outputPath?: string, serviceName?: string): Promise<false | {
19
27
  types: string;
20
28
  sdk: string;
21
29
  }>;
30
+ /**
31
+ * Generate client types for external GraphQL service
32
+ */
33
+ declare function generateExternalClientTypes(service: ExternalGraphQLService, schema: GraphQLSchema, docs: Source[]): Promise<{
34
+ types: string;
35
+ sdk: string;
36
+ } | false>;
22
37
  //#endregion
23
- export { GraphQLLoadSchemaOptions, GraphQLTypeDefPointer, generateClientTypes, graphQLLoadSchemaSync, loadGraphQLDocuments };
38
+ export { GraphQLLoadSchemaOptions, GraphQLTypeDefPointer, downloadAndSaveSchema, generateClientTypes, generateExternalClientTypes, graphQLLoadSchemaSync, loadExternalSchema, loadGraphQLDocuments };
@@ -1,14 +1,18 @@
1
1
  import { preset } from "../node_modules/.pnpm/@graphql-codegen_import-types-preset@3.0.1_graphql@16.11.0/node_modules/@graphql-codegen/import-types-preset/esm/index.js";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
3
  import { consola as consola$1 } from "consola";
3
4
  import { defu as defu$1 } from "defu";
5
+ import { dirname, resolve } from "pathe";
4
6
  import { parse } from "graphql";
5
7
  import { printSchemaWithDirectives } from "@graphql-tools/utils";
8
+ import { createHash } from "node:crypto";
6
9
  import { codegen } from "@graphql-codegen/core";
7
10
  import { plugin } from "@graphql-codegen/typescript";
8
11
  import { plugin as plugin$1 } from "@graphql-codegen/typescript-generic-sdk";
9
12
  import { plugin as plugin$2 } from "@graphql-codegen/typescript-operations";
10
13
  import { GraphQLFileLoader } from "@graphql-tools/graphql-file-loader";
11
14
  import { loadDocuments, loadSchemaSync } from "@graphql-tools/load";
15
+ import { UrlLoader } from "@graphql-tools/url-loader";
12
16
  import { CurrencyResolver, DateTimeISOResolver, DateTimeResolver, JSONObjectResolver, JSONResolver, NonEmptyStringResolver, UUIDResolver } from "graphql-scalars";
13
17
 
14
18
  //#region src/utils/client-codegen.ts
@@ -31,7 +35,11 @@ async function graphQLLoadSchemaSync(schemaPointers, data = {}) {
31
35
  try {
32
36
  result = loadSchemaSync(filteredPointers, {
33
37
  ...data,
34
- loaders: [new GraphQLFileLoader(), ...data.loaders || []]
38
+ loaders: [
39
+ new GraphQLFileLoader(),
40
+ new UrlLoader(),
41
+ ...data.loaders || []
42
+ ]
35
43
  });
36
44
  } catch (e) {
37
45
  if ((e.message || "").includes("Unable to find any GraphQL type definitions for the following pointers:")) consola$1.info("No server GraphQL files found. If you need server-side GraphQL, add .graphql files to your server directory.");
@@ -39,6 +47,92 @@ async function graphQLLoadSchemaSync(schemaPointers, data = {}) {
39
47
  }
40
48
  return result;
41
49
  }
50
+ /**
51
+ * Load schema from external GraphQL service
52
+ */
53
+ async function loadExternalSchema(service, buildDir) {
54
+ try {
55
+ const headers = typeof service.headers === "function" ? service.headers() : service.headers || {};
56
+ const schemas = Array.isArray(service.schema) ? service.schema : [service.schema];
57
+ if (service.downloadSchema && buildDir) {
58
+ const defaultPath = resolve(buildDir, "graphql", "schemas", `${service.name}.graphql`);
59
+ const schemaFilePath = service.downloadPath ? resolve(service.downloadPath) : defaultPath;
60
+ if (existsSync(schemaFilePath)) {
61
+ consola$1.info(`[graphql:${service.name}] Loading schema from local file: ${schemaFilePath}`);
62
+ try {
63
+ const result$1 = loadSchemaSync([schemaFilePath], { loaders: [new GraphQLFileLoader()] });
64
+ consola$1.info(`[graphql:${service.name}] External schema loaded successfully from local file`);
65
+ return result$1;
66
+ } catch (localError) {
67
+ consola$1.warn(`[graphql:${service.name}] Failed to load local schema, falling back to remote:`, localError);
68
+ }
69
+ }
70
+ }
71
+ consola$1.info(`[graphql:${service.name}] Loading external schema from: ${schemas.join(", ")}`);
72
+ const result = loadSchemaSync(schemas, {
73
+ loaders: [new GraphQLFileLoader(), new UrlLoader()],
74
+ ...Object.keys(headers).length > 0 && { headers }
75
+ });
76
+ consola$1.info(`[graphql:${service.name}] External schema loaded successfully`);
77
+ return result;
78
+ } catch (error) {
79
+ consola$1.error(`[graphql:${service.name}] Failed to load external schema:`, error);
80
+ return void 0;
81
+ }
82
+ }
83
+ /**
84
+ * Download and save schema from external service
85
+ */
86
+ async function downloadAndSaveSchema(service, buildDir) {
87
+ const downloadMode = service.downloadSchema;
88
+ if (!downloadMode || downloadMode === "manual") return void 0;
89
+ const defaultPath = resolve(buildDir, "graphql", "schemas", `${service.name}.graphql`);
90
+ const schemaFilePath = service.downloadPath ? resolve(service.downloadPath) : defaultPath;
91
+ try {
92
+ const headers = typeof service.headers === "function" ? service.headers() : service.headers || {};
93
+ const schemas = Array.isArray(service.schema) ? service.schema : [service.schema];
94
+ let shouldDownload = false;
95
+ const fileExists = existsSync(schemaFilePath);
96
+ if (downloadMode === "always") {
97
+ shouldDownload = true;
98
+ if (fileExists) try {
99
+ const remoteSchema = loadSchemaSync(schemas, {
100
+ loaders: [new UrlLoader()],
101
+ ...Object.keys(headers).length > 0 && { headers }
102
+ });
103
+ const remoteSchemaString = printSchemaWithDirectives(remoteSchema);
104
+ const remoteHash = createHash("md5").update(remoteSchemaString).digest("hex");
105
+ const localSchemaString = readFileSync(schemaFilePath, "utf-8");
106
+ const localHash = createHash("md5").update(localSchemaString).digest("hex");
107
+ if (remoteHash === localHash) {
108
+ shouldDownload = false;
109
+ consola$1.info(`[graphql:${service.name}] Schema is up-to-date, using cached version`);
110
+ }
111
+ } catch {
112
+ consola$1.warn(`[graphql:${service.name}] Unable to compare with remote schema, updating local cache`);
113
+ shouldDownload = true;
114
+ }
115
+ } else if (downloadMode === true || downloadMode === "once") {
116
+ shouldDownload = !fileExists;
117
+ if (fileExists) consola$1.info(`[graphql:${service.name}] Using cached schema from: ${schemaFilePath}`);
118
+ }
119
+ if (shouldDownload) {
120
+ consola$1.info(`[graphql:${service.name}] Downloading schema to: ${schemaFilePath}`);
121
+ const schema = loadSchemaSync(schemas, {
122
+ loaders: [new UrlLoader()],
123
+ ...Object.keys(headers).length > 0 && { headers }
124
+ });
125
+ const schemaString = printSchemaWithDirectives(schema);
126
+ mkdirSync(dirname(schemaFilePath), { recursive: true });
127
+ writeFileSync(schemaFilePath, schemaString, "utf-8");
128
+ consola$1.success(`[graphql:${service.name}] Schema downloaded and saved successfully`);
129
+ }
130
+ return schemaFilePath;
131
+ } catch (error) {
132
+ consola$1.error(`[graphql:${service.name}] Failed to download schema:`, error);
133
+ return void 0;
134
+ }
135
+ }
42
136
  async function loadGraphQLDocuments(patterns) {
43
137
  try {
44
138
  const result = await loadDocuments(patterns, { loaders: [new GraphQLFileLoader()] });
@@ -48,12 +142,14 @@ async function loadGraphQLDocuments(patterns) {
48
142
  else throw e;
49
143
  }
50
144
  }
51
- async function generateClientTypes(schema, docs, config = {}, sdkConfig = {}, outputPath) {
145
+ async function generateClientTypes(schema, docs, config = {}, sdkConfig = {}, outputPath, serviceName) {
52
146
  if (docs.length === 0) {
53
- consola$1.info("[graphql] No client GraphQL files found. Skipping client type generation.");
147
+ const serviceLabel$1 = serviceName ? `:${serviceName}` : "";
148
+ consola$1.info(`[graphql${serviceLabel$1}] No client GraphQL files found. Skipping client type generation.`);
54
149
  return false;
55
150
  }
56
- consola$1.info(`[graphql] Found ${docs.length} client GraphQL documents`);
151
+ const serviceLabel = serviceName ? `:${serviceName}` : "";
152
+ consola$1.info(`[graphql${serviceLabel}] Found ${docs.length} client GraphQL documents`);
57
153
  const defaultConfig = {
58
154
  emitLegacyCommonJSImports: false,
59
155
  useTypeImports: true,
@@ -98,12 +194,13 @@ async function generateClientTypes(schema, docs, config = {}, sdkConfig = {}, ou
98
194
  typescriptOperations: { plugin: plugin$2 }
99
195
  }
100
196
  });
197
+ const typesPath = serviceName ? `#graphql/client/${serviceName}` : "#graphql/client";
101
198
  const sdkOutput = await preset.buildGeneratesSection({
102
199
  baseOutputDir: outputPath || "client-types.generated.ts",
103
200
  schema: parse(printSchemaWithDirectives(schema)),
104
201
  documents: [...docs],
105
202
  config: mergedSdkConfig,
106
- presetConfig: { typesPath: "#graphql/client" },
203
+ presetConfig: { typesPath },
107
204
  plugins: [{ pluginContent: {} }, { typescriptGenericSdk: {} }],
108
205
  pluginMap: {
109
206
  pluginContent: { plugin: pluginContent },
@@ -116,15 +213,24 @@ async function generateClientTypes(schema, docs, config = {}, sdkConfig = {}, ou
116
213
  content: await codegen(config$1)
117
214
  };
118
215
  }));
216
+ const sdkContent = results[0]?.content || "";
119
217
  return {
120
218
  types: output,
121
- sdk: results[0]?.content || ""
219
+ sdk: sdkContent
122
220
  };
123
221
  } catch (error) {
124
- consola$1.warn("[graphql] Client type generation failed:", error);
222
+ consola$1.warn(`[graphql${serviceLabel}] Client type generation failed:`, error);
125
223
  return false;
126
224
  }
127
225
  }
226
+ /**
227
+ * Generate client types for external GraphQL service
228
+ */
229
+ async function generateExternalClientTypes(service, schema, docs) {
230
+ const config = service.codegen?.client || {};
231
+ const sdkConfig = service.codegen?.clientSDK || {};
232
+ return generateClientTypes(schema, docs, config, sdkConfig, void 0, service.name);
233
+ }
128
234
 
129
235
  //#endregion
130
- export { generateClientTypes, graphQLLoadSchemaSync, loadGraphQLDocuments };
236
+ export { downloadAndSaveSchema, generateClientTypes, generateExternalClientTypes, graphQLLoadSchemaSync, loadExternalSchema, loadGraphQLDocuments };
@@ -0,0 +1,80 @@
1
+ //#region src/utils/directive-parser.d.ts
2
+ interface ParsedDirective {
3
+ name: string;
4
+ locations: string[];
5
+ args?: Record<string, {
6
+ type: string;
7
+ defaultValue?: any;
8
+ }>;
9
+ description?: string;
10
+ isRepeatable?: boolean;
11
+ }
12
+ /**
13
+ * Clean AST-based directive parser using oxc-parser
14
+ */
15
+ declare class DirectiveParser {
16
+ private oxc;
17
+ init(): Promise<void>;
18
+ /**
19
+ * Parse directives from a TypeScript/JavaScript file
20
+ */
21
+ parseDirectives(fileContent: string, filePath: string): Promise<ParsedDirective[]>;
22
+ /**
23
+ * Extract directive definitions from AST
24
+ */
25
+ private extractDirectiveDefinitions;
26
+ /**
27
+ * Traverse AST nodes recursively
28
+ */
29
+ private traverse;
30
+ /**
31
+ * Check if node is a defineDirective call
32
+ */
33
+ private isDefineDirectiveCall;
34
+ /**
35
+ * Extract directive configuration from defineDirective call
36
+ */
37
+ private extractDirectiveFromCall;
38
+ /**
39
+ * Extract directive properties from object expression
40
+ */
41
+ private extractDirectiveFromObject;
42
+ /**
43
+ * Extract string literal value
44
+ */
45
+ private extractStringLiteral;
46
+ /**
47
+ * Extract boolean literal value
48
+ */
49
+ private extractBooleanLiteral;
50
+ /**
51
+ * Extract array of strings
52
+ */
53
+ private extractStringArray;
54
+ /**
55
+ * Extract arguments object
56
+ */
57
+ private extractArgsObject;
58
+ /**
59
+ * Extract argument configuration
60
+ */
61
+ private extractArgConfig;
62
+ /**
63
+ * Extract literal value (string, number, boolean)
64
+ */
65
+ private extractLiteralValue;
66
+ }
67
+ /**
68
+ * Generate GraphQL directive schema from parsed directive
69
+ */
70
+ declare function generateDirectiveSchema(directive: ParsedDirective): string;
71
+ /**
72
+ * Generate directive schemas file from scanned directives
73
+ */
74
+ declare function generateDirectiveSchemas(nitro: any, directives: any[]): Promise<void>;
75
+ /**
76
+ * Singleton instance for reuse
77
+ */
78
+ declare const directiveParser: DirectiveParser;
79
+ //#endregion
80
+ export { DirectiveParser, ParsedDirective, directiveParser, generateDirectiveSchema, generateDirectiveSchemas };
@@ -0,0 +1,235 @@
1
+ //#region src/utils/directive-parser.ts
2
+ /**
3
+ * Clean AST-based directive parser using oxc-parser
4
+ */
5
+ var DirectiveParser = class {
6
+ oxc;
7
+ async init() {
8
+ if (!this.oxc) this.oxc = await import("oxc-parser");
9
+ }
10
+ /**
11
+ * Parse directives from a TypeScript/JavaScript file
12
+ */
13
+ async parseDirectives(fileContent, filePath) {
14
+ await this.init();
15
+ try {
16
+ const result = this.oxc.parseSync(filePath, fileContent, {
17
+ lang: filePath.endsWith(".ts") ? "ts" : "js",
18
+ sourceType: "module",
19
+ astType: "ts"
20
+ });
21
+ if (result.errors.length > 0) {
22
+ console.warn(`Parse errors in ${filePath}:`, result.errors.map((e) => e.message));
23
+ return [];
24
+ }
25
+ return this.extractDirectiveDefinitions(result.program);
26
+ } catch (error) {
27
+ console.warn(`Failed to parse ${filePath} with oxc:`, error);
28
+ return [];
29
+ }
30
+ }
31
+ /**
32
+ * Extract directive definitions from AST
33
+ */
34
+ extractDirectiveDefinitions(program) {
35
+ const directives = [];
36
+ this.traverse(program, (node) => {
37
+ if (this.isDefineDirectiveCall(node)) {
38
+ const directive = this.extractDirectiveFromCall(node);
39
+ if (directive) directives.push(directive);
40
+ }
41
+ });
42
+ return directives;
43
+ }
44
+ /**
45
+ * Traverse AST nodes recursively
46
+ */
47
+ traverse(node, visitor) {
48
+ if (!node || typeof node !== "object") return;
49
+ visitor(node);
50
+ for (const key in node) {
51
+ const child = node[key];
52
+ if (Array.isArray(child)) child.forEach((item) => this.traverse(item, visitor));
53
+ else if (child && typeof child === "object") this.traverse(child, visitor);
54
+ }
55
+ }
56
+ /**
57
+ * Check if node is a defineDirective call
58
+ */
59
+ isDefineDirectiveCall(node) {
60
+ return node.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "defineDirective" && node.arguments?.length > 0;
61
+ }
62
+ /**
63
+ * Extract directive configuration from defineDirective call
64
+ */
65
+ extractDirectiveFromCall(node) {
66
+ const arg = node.arguments[0];
67
+ if (arg?.type !== "ObjectExpression") return null;
68
+ return this.extractDirectiveFromObject(arg);
69
+ }
70
+ /**
71
+ * Extract directive properties from object expression
72
+ */
73
+ extractDirectiveFromObject(objNode) {
74
+ let name = "";
75
+ let locations = [];
76
+ let args = {};
77
+ let description;
78
+ let isRepeatable;
79
+ for (const prop of objNode.properties || []) {
80
+ if (prop.type !== "Property" || prop.key?.type !== "Identifier") continue;
81
+ switch (prop.key.name) {
82
+ case "name":
83
+ name = this.extractStringLiteral(prop.value) || "";
84
+ break;
85
+ case "locations":
86
+ locations = this.extractStringArray(prop.value);
87
+ break;
88
+ case "args":
89
+ args = this.extractArgsObject(prop.value);
90
+ break;
91
+ case "description":
92
+ description = this.extractStringLiteral(prop.value);
93
+ break;
94
+ case "isRepeatable":
95
+ isRepeatable = this.extractBooleanLiteral(prop.value);
96
+ break;
97
+ }
98
+ }
99
+ return name && locations.length > 0 ? {
100
+ name,
101
+ locations,
102
+ args,
103
+ description,
104
+ isRepeatable
105
+ } : null;
106
+ }
107
+ /**
108
+ * Extract string literal value
109
+ */
110
+ extractStringLiteral(node) {
111
+ if (node?.type === "Literal" && typeof node.value === "string") return node.value;
112
+ return void 0;
113
+ }
114
+ /**
115
+ * Extract boolean literal value
116
+ */
117
+ extractBooleanLiteral(node) {
118
+ if (node?.type === "Literal" && typeof node.value === "boolean") return node.value;
119
+ return void 0;
120
+ }
121
+ /**
122
+ * Extract array of strings
123
+ */
124
+ extractStringArray(node) {
125
+ if (node?.type !== "ArrayExpression") return [];
126
+ return (node.elements || []).filter((el) => el?.type === "Literal" && typeof el.value === "string").map((el) => el.value);
127
+ }
128
+ /**
129
+ * Extract arguments object
130
+ */
131
+ extractArgsObject(node) {
132
+ if (node?.type !== "ObjectExpression") return {};
133
+ const args = {};
134
+ for (const prop of node.properties || []) {
135
+ if (prop.type !== "Property" || prop.key?.type !== "Identifier") continue;
136
+ const argName = prop.key.name;
137
+ const argConfig = this.extractArgConfig(prop.value);
138
+ if (argConfig) args[argName] = argConfig;
139
+ }
140
+ return args;
141
+ }
142
+ /**
143
+ * Extract argument configuration
144
+ */
145
+ extractArgConfig(node) {
146
+ if (node?.type !== "ObjectExpression") return null;
147
+ let type = "String";
148
+ let defaultValue;
149
+ for (const prop of node.properties || []) {
150
+ if (prop.type !== "Property" || prop.key?.type !== "Identifier") continue;
151
+ switch (prop.key.name) {
152
+ case "type": {
153
+ const typeValue = this.extractStringLiteral(prop.value);
154
+ if (typeValue) type = typeValue;
155
+ break;
156
+ }
157
+ case "defaultValue":
158
+ defaultValue = this.extractLiteralValue(prop.value);
159
+ break;
160
+ }
161
+ }
162
+ return {
163
+ type,
164
+ ...defaultValue !== void 0 && { defaultValue }
165
+ };
166
+ }
167
+ /**
168
+ * Extract literal value (string, number, boolean)
169
+ */
170
+ extractLiteralValue(node) {
171
+ if (node?.type === "Literal") return node.value;
172
+ return void 0;
173
+ }
174
+ };
175
+ /**
176
+ * Generate GraphQL directive schema from parsed directive
177
+ */
178
+ function generateDirectiveSchema(directive) {
179
+ let args = "";
180
+ if (directive.args && Object.keys(directive.args).length > 0) {
181
+ const argDefs = Object.entries(directive.args).map(([name, arg]) => {
182
+ let defaultValue = "";
183
+ if (arg.defaultValue !== void 0) if (typeof arg.defaultValue === "string") defaultValue = ` = "${arg.defaultValue}"`;
184
+ else defaultValue = ` = ${arg.defaultValue}`;
185
+ return `${name}: ${arg.type}${defaultValue}`;
186
+ });
187
+ args = `(${argDefs.join(", ")})`;
188
+ }
189
+ const locations = directive.locations.join(" | ");
190
+ return `directive @${directive.name}${args} on ${locations}`;
191
+ }
192
+ /**
193
+ * Generate directive schemas file from scanned directives
194
+ */
195
+ async function generateDirectiveSchemas(nitro, directives) {
196
+ if (directives.length === 0) return;
197
+ const { existsSync, readFileSync, writeFileSync } = await import("node:fs");
198
+ const { readFile } = await import("node:fs/promises");
199
+ const { resolve } = await import("pathe");
200
+ const directiveSchemas = [];
201
+ const seenDirectives = /* @__PURE__ */ new Set();
202
+ const parser = new DirectiveParser();
203
+ for (const dir of directives) for (const _imp of dir.imports) {
204
+ const fileContent = await readFile(dir.specifier, "utf-8");
205
+ const directiveDefs = await parser.parseDirectives(fileContent, dir.specifier);
206
+ for (const def of directiveDefs) {
207
+ if (seenDirectives.has(def.name)) continue;
208
+ seenDirectives.add(def.name);
209
+ const schema = generateDirectiveSchema(def);
210
+ directiveSchemas.push(schema);
211
+ }
212
+ }
213
+ if (directiveSchemas.length > 0) {
214
+ const directivesPath = resolve(nitro.graphql.serverDir, "_directives.graphql");
215
+ const content = `# WARNING: This file is auto-generated by nitro-graphql
216
+ # Do not modify this file directly. It will be overwritten.
217
+ # To define custom directives, create .directive.ts files using defineDirective()
218
+
219
+ ${directiveSchemas.join("\n\n")}`;
220
+ let shouldWrite = true;
221
+ if (existsSync(directivesPath)) {
222
+ const existingContent = readFileSync(directivesPath, "utf-8");
223
+ shouldWrite = existingContent !== content;
224
+ }
225
+ if (shouldWrite) writeFileSync(directivesPath, content, "utf-8");
226
+ if (!nitro.scanSchemas.includes(directivesPath)) nitro.scanSchemas.push(directivesPath);
227
+ }
228
+ }
229
+ /**
230
+ * Singleton instance for reuse
231
+ */
232
+ const directiveParser = new DirectiveParser();
233
+
234
+ //#endregion
235
+ export { DirectiveParser, directiveParser, generateDirectiveSchema, generateDirectiveSchemas };
@@ -1,4 +1,5 @@
1
1
  import { GenImport } from "../types/index.js";
2
+ import { directiveParser, generateDirectiveSchema, generateDirectiveSchemas } from "./directive-parser.js";
2
3
  import { Nitro } from "nitropack";
3
4
 
4
5
  //#region src/utils/index.d.ts
@@ -11,5 +12,13 @@ declare function scanDirectives(nitro: Nitro): Promise<GenImport[]>;
11
12
  declare function scanTypeDefs(nitro: Nitro): Promise<string[]>;
12
13
  declare function scanSchemas(nitro: Nitro): Promise<string[]>;
13
14
  declare function scanDocs(nitro: Nitro): Promise<string[]>;
15
+ /**
16
+ * Scan documents for a specific external service
17
+ */
18
+ declare function scanExternalServiceDocs(nitro: Nitro, serviceName: string, patterns: string[]): Promise<string[]>;
19
+ /**
20
+ * Validate external GraphQL service configuration
21
+ */
22
+ declare function validateExternalServices(services: any[]): string[];
14
23
  //#endregion
15
- export { GLOB_SCAN_PATTERN, getImportId, relativeWithDot, scanDirectives, scanDocs, scanGraphql, scanResolvers, scanSchemas, scanTypeDefs };
24
+ export { GLOB_SCAN_PATTERN, directiveParser, generateDirectiveSchema, generateDirectiveSchemas, getImportId, relativeWithDot, scanDirectives, scanDocs, scanExternalServiceDocs, scanGraphql, scanResolvers, scanSchemas, scanTypeDefs, validateExternalServices };
@@ -1,5 +1,6 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { directiveParser, generateDirectiveSchema, generateDirectiveSchemas } from "./directive-parser.js";
2
2
  import { join, relative } from "pathe";
3
+ import { readFile } from "node:fs/promises";
3
4
  import { hash } from "ohash";
4
5
  import { parseAsync } from "oxc-parser";
5
6
  import { glob } from "tinyglobby";
@@ -103,7 +104,48 @@ async function scanSchemas(nitro) {
103
104
  }
104
105
  async function scanDocs(nitro) {
105
106
  const files = await scanDir(nitro, nitro.options.rootDir, nitro.graphql.dir.client, "**/*.graphql");
106
- return files.map((f) => f.fullPath);
107
+ return files.filter((f) => !f.path.startsWith("external/")).map((f) => f.fullPath);
108
+ }
109
+ /**
110
+ * Scan documents for a specific external service
111
+ */
112
+ async function scanExternalServiceDocs(nitro, serviceName, patterns) {
113
+ if (!patterns.length) return [];
114
+ const files = [];
115
+ for (const pattern of patterns) try {
116
+ const serviceFiles = await glob(pattern, {
117
+ cwd: nitro.options.rootDir,
118
+ dot: true,
119
+ ignore: nitro.options.ignore,
120
+ absolute: true
121
+ });
122
+ files.push(...serviceFiles);
123
+ } catch (error) {
124
+ nitro.logger.warn(`[graphql:${serviceName}] Error scanning documents with pattern "${pattern}":`, error);
125
+ }
126
+ return files.filter((file, index, self) => self.indexOf(file) === index);
127
+ }
128
+ /**
129
+ * Validate external GraphQL service configuration
130
+ */
131
+ function validateExternalServices(services) {
132
+ const errors = [];
133
+ const serviceNames = /* @__PURE__ */ new Set();
134
+ for (const [index, service] of services.entries()) {
135
+ const prefix = `externalServices[${index}]`;
136
+ if (!service.name || typeof service.name !== "string") errors.push(`${prefix}.name is required and must be a string`);
137
+ else if (serviceNames.has(service.name)) errors.push(`${prefix}.name "${service.name}" must be unique`);
138
+ else serviceNames.add(service.name);
139
+ if (!service.schema) errors.push(`${prefix}.schema is required`);
140
+ if (!service.endpoint || typeof service.endpoint !== "string") errors.push(`${prefix}.endpoint is required and must be a string`);
141
+ else try {
142
+ const url = new URL(service.endpoint);
143
+ } catch {
144
+ errors.push(`${prefix}.endpoint "${service.endpoint}" must be a valid URL`);
145
+ }
146
+ if (service.name && !/^[a-z]\w*$/i.test(service.name)) errors.push(`${prefix}.name "${service.name}" must be a valid identifier (letters, numbers, underscore, starting with letter)`);
147
+ }
148
+ return errors;
107
149
  }
108
150
  async function scanFiles(nitro, name, globPattern = GLOB_SCAN_PATTERN) {
109
151
  const files = await Promise.all(nitro.options.scanDirs.map((dir) => scanDir(nitro, dir, name, globPattern))).then((r) => r.flat());
@@ -131,4 +173,4 @@ async function scanDir(nitro, dir, name, globPattern = GLOB_SCAN_PATTERN) {
131
173
  }
132
174
 
133
175
  //#endregion
134
- export { GLOB_SCAN_PATTERN, getImportId, relativeWithDot, scanDirectives, scanDocs, scanGraphql, scanResolvers, scanSchemas, scanTypeDefs };
176
+ export { GLOB_SCAN_PATTERN, directiveParser, generateDirectiveSchema, generateDirectiveSchemas, getImportId, relativeWithDot, scanDirectives, scanDocs, scanExternalServiceDocs, scanGraphql, scanResolvers, scanSchemas, scanTypeDefs, validateExternalServices };