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.
- package/README.md +416 -1
- package/dist/ecosystem/nuxt.d.ts +2 -2
- package/dist/ecosystem/nuxt.js +8 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +27 -103
- package/dist/rollup.js +1 -1
- package/dist/routes/graphql-yoga.d.ts +2 -2
- package/dist/types/index.d.ts +30 -1
- package/dist/utils/client-codegen.d.ts +18 -3
- package/dist/utils/client-codegen.js +114 -8
- package/dist/utils/directive-parser.d.ts +80 -0
- package/dist/utils/directive-parser.js +235 -0
- package/dist/utils/index.d.ts +10 -1
- package/dist/utils/index.js +45 -3
- package/dist/utils/type-generation.js +145 -21
- package/package.json +4 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as h30 from "h3";
|
|
2
2
|
|
|
3
3
|
//#region src/routes/graphql-yoga.d.ts
|
|
4
|
-
declare const _default:
|
|
4
|
+
declare const _default: h30.EventHandler<h30.EventHandlerRequest, Promise<Response>>;
|
|
5
5
|
//#endregion
|
|
6
6
|
export { _default as default };
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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: [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
219
|
+
sdk: sdkContent
|
|
122
220
|
};
|
|
123
221
|
} catch (error) {
|
|
124
|
-
consola$1.warn(
|
|
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 };
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -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 };
|
package/dist/utils/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
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 };
|