nitro-graphql 1.1.2 → 1.1.3
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 +158 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -102
- package/dist/rollup.js +1 -1
- package/dist/routes/apollo-server.d.ts +2 -2
- package/dist/routes/graphql-yoga.d.ts +2 -2
- package/dist/routes/health.d.ts +2 -2
- package/dist/utils/directive-parser.d.ts +80 -0
- package/dist/utils/directive-parser.js +233 -0
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
- 🔄 **Hot Reload**: Development mode with automatic schema and resolver updates
|
|
31
31
|
- 📦 **Optimized Bundling**: Smart chunking and dynamic imports for production
|
|
32
32
|
- 🌐 **Nuxt Integration**: First-class Nuxt.js support with dedicated module
|
|
33
|
+
- 🎭 **Custom Directives**: Create reusable GraphQL directives with automatic schema generation
|
|
33
34
|
|
|
34
35
|
## 🚀 Quick Start
|
|
35
36
|
|
|
@@ -181,6 +182,10 @@ server/
|
|
|
181
182
|
├── graphql/
|
|
182
183
|
│ ├── schema.graphql # Main schema with scalars and base types
|
|
183
184
|
│ ├── hello.resolver.ts # Global resolvers (use named exports)
|
|
185
|
+
│ ├── directives/ # Custom GraphQL directives
|
|
186
|
+
│ │ ├── auth.directive.ts # Authentication directive
|
|
187
|
+
│ │ ├── cache.directive.ts # Caching directive
|
|
188
|
+
│ │ └── validate.directive.ts # Validation directive
|
|
184
189
|
│ ├── users/
|
|
185
190
|
│ │ ├── user.graphql # User schema definitions
|
|
186
191
|
│ │ ├── user-queries.resolver.ts # User query resolvers (use named exports)
|
|
@@ -615,6 +620,90 @@ export const postTypes = defineType({
|
|
|
615
620
|
|
|
616
621
|
</details>
|
|
617
622
|
|
|
623
|
+
<details>
|
|
624
|
+
<summary><strong>defineDirective</strong> - Create custom GraphQL directives</summary>
|
|
625
|
+
|
|
626
|
+
```ts
|
|
627
|
+
import { defineDirective } from 'nitro-graphql/utils/define'
|
|
628
|
+
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils'
|
|
629
|
+
import { defaultFieldResolver, GraphQLError } from 'graphql'
|
|
630
|
+
|
|
631
|
+
export const authDirective = defineDirective({
|
|
632
|
+
name: 'auth',
|
|
633
|
+
locations: ['FIELD_DEFINITION', 'OBJECT'],
|
|
634
|
+
args: {
|
|
635
|
+
requires: {
|
|
636
|
+
type: 'String',
|
|
637
|
+
defaultValue: 'USER',
|
|
638
|
+
description: 'Required role to access this field',
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
description: 'Directive to check authentication and authorization',
|
|
642
|
+
transformer: (schema) => {
|
|
643
|
+
return mapSchema(schema, {
|
|
644
|
+
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
|
|
645
|
+
const authDirectiveConfig = getDirective(schema, fieldConfig, 'auth')?.[0]
|
|
646
|
+
|
|
647
|
+
if (authDirectiveConfig) {
|
|
648
|
+
const { resolve = defaultFieldResolver } = fieldConfig
|
|
649
|
+
|
|
650
|
+
fieldConfig.resolve = async function (source, args, context, info) {
|
|
651
|
+
if (!context.user) {
|
|
652
|
+
throw new GraphQLError('You must be logged in')
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (context.user.role !== authDirectiveConfig.requires) {
|
|
656
|
+
throw new GraphQLError('Insufficient permissions')
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return resolve(source, args, context, info)
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return fieldConfig
|
|
664
|
+
},
|
|
665
|
+
})
|
|
666
|
+
},
|
|
667
|
+
})
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
**Usage in Schema:**
|
|
671
|
+
```graphql
|
|
672
|
+
type User {
|
|
673
|
+
id: ID!
|
|
674
|
+
name: String!
|
|
675
|
+
email: String! @auth(requires: "ADMIN")
|
|
676
|
+
secretData: String @auth(requires: "SUPER_ADMIN")
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
type Query {
|
|
680
|
+
users: [User!]! @auth
|
|
681
|
+
adminStats: AdminStats @auth(requires: "ADMIN")
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
**Available Argument Types:**
|
|
686
|
+
- Basic scalars: `String`, `Int`, `Float`, `Boolean`, `ID`, `JSON`, `DateTime`
|
|
687
|
+
- Non-nullable: `String!`, `Int!`, `Float!`, `Boolean!`, `ID!`, `JSON!`, `DateTime!`
|
|
688
|
+
- Arrays: `[String]`, `[String!]`, `[String]!`, `[String!]!` (and all combinations for other types)
|
|
689
|
+
- Custom types: Any string for your custom GraphQL types
|
|
690
|
+
|
|
691
|
+
**Helper Function:**
|
|
692
|
+
```ts
|
|
693
|
+
export const validateDirective = defineDirective({
|
|
694
|
+
name: 'validate',
|
|
695
|
+
locations: ['FIELD_DEFINITION', 'ARGUMENT_DEFINITION'],
|
|
696
|
+
args: {
|
|
697
|
+
minLength: arg('Int', { description: 'Minimum length' }),
|
|
698
|
+
maxLength: arg('Int', { description: 'Maximum length' }),
|
|
699
|
+
pattern: arg('String', { description: 'Regex pattern' }),
|
|
700
|
+
},
|
|
701
|
+
// ... transformer implementation
|
|
702
|
+
})
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
</details>
|
|
706
|
+
|
|
618
707
|
<details>
|
|
619
708
|
<summary><strong>defineSchema</strong> - Define custom schema with validation</summary>
|
|
620
709
|
|
|
@@ -741,6 +830,75 @@ export default defineNitroConfig({
|
|
|
741
830
|
|
|
742
831
|
## 🔥 Advanced Features
|
|
743
832
|
|
|
833
|
+
<details>
|
|
834
|
+
<summary><strong>Custom Directives</strong></summary>
|
|
835
|
+
|
|
836
|
+
Create reusable GraphQL directives with automatic schema generation:
|
|
837
|
+
|
|
838
|
+
```ts
|
|
839
|
+
// server/graphql/directives/auth.directive.ts
|
|
840
|
+
import { defineDirective } from 'nitro-graphql/utils/define'
|
|
841
|
+
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils'
|
|
842
|
+
|
|
843
|
+
export const authDirective = defineDirective({
|
|
844
|
+
name: 'auth',
|
|
845
|
+
locations: ['FIELD_DEFINITION', 'OBJECT'],
|
|
846
|
+
args: {
|
|
847
|
+
requires: {
|
|
848
|
+
type: 'String',
|
|
849
|
+
defaultValue: 'USER',
|
|
850
|
+
description: 'Required role to access this field',
|
|
851
|
+
},
|
|
852
|
+
},
|
|
853
|
+
description: 'Authentication and authorization directive',
|
|
854
|
+
transformer: (schema) => {
|
|
855
|
+
return mapSchema(schema, {
|
|
856
|
+
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
|
|
857
|
+
const authConfig = getDirective(schema, fieldConfig, 'auth')?.[0]
|
|
858
|
+
if (authConfig) {
|
|
859
|
+
// Transform field resolvers to check authentication
|
|
860
|
+
const { resolve = defaultFieldResolver } = fieldConfig
|
|
861
|
+
fieldConfig.resolve = async (source, args, context, info) => {
|
|
862
|
+
if (!context.user || context.user.role !== authConfig.requires) {
|
|
863
|
+
throw new GraphQLError('Access denied')
|
|
864
|
+
}
|
|
865
|
+
return resolve(source, args, context, info)
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return fieldConfig
|
|
869
|
+
},
|
|
870
|
+
})
|
|
871
|
+
},
|
|
872
|
+
})
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
**Common Directive Examples:**
|
|
876
|
+
- `@auth(requires: "ADMIN")` - Role-based authentication
|
|
877
|
+
- `@cache(ttl: 300, scope: "PUBLIC")` - Field-level caching
|
|
878
|
+
- `@rateLimit(limit: 10, window: 60)` - Rate limiting
|
|
879
|
+
- `@validate(minLength: 5, maxLength: 100)` - Input validation
|
|
880
|
+
- `@transform(upper: true, trim: true)` - Data transformation
|
|
881
|
+
- `@permission(roles: ["ADMIN", "MODERATOR"])` - Multi-role permissions
|
|
882
|
+
|
|
883
|
+
**Usage in Schema:**
|
|
884
|
+
```graphql
|
|
885
|
+
type User {
|
|
886
|
+
id: ID!
|
|
887
|
+
name: String!
|
|
888
|
+
email: String! @auth(requires: "ADMIN")
|
|
889
|
+
posts: [Post!]! @cache(ttl: 300)
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
type Query {
|
|
893
|
+
users: [User!]! @rateLimit(limit: 100, window: 3600)
|
|
894
|
+
sensitiveData: String @auth(requires: "SUPER_ADMIN")
|
|
895
|
+
}
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
The module automatically generates the directive schema definitions and integrates them with both GraphQL Yoga and Apollo Server.
|
|
899
|
+
|
|
900
|
+
</details>
|
|
901
|
+
|
|
744
902
|
<details>
|
|
745
903
|
<summary><strong>Custom Scalars</strong></summary>
|
|
746
904
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { StandardSchemaV1 } from "./types/standard-schema.js";
|
|
2
2
|
import { CodegenClientConfig, CodegenServerConfig, GenImport, GenericSdkConfig, NitroGraphQLOptions } from "./types/index.js";
|
|
3
|
-
import * as
|
|
3
|
+
import * as nitropack1 from "nitropack";
|
|
4
4
|
|
|
5
5
|
//#region src/index.d.ts
|
|
6
|
-
declare const _default:
|
|
6
|
+
declare const _default: nitropack1.NitroModule;
|
|
7
7
|
//#endregion
|
|
8
8
|
export { CodegenClientConfig, CodegenServerConfig, GenImport, GenericSdkConfig, NitroGraphQLOptions, StandardSchemaV1, _default as default };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { generateDirectiveSchemas } from "./utils/directive-parser.js";
|
|
1
2
|
import { relativeWithDot, scanDirectives, scanDocs, scanResolvers, scanSchemas } from "./utils/index.js";
|
|
2
3
|
import { clientTypeGeneration, serverTypeGeneration } from "./utils/type-generation.js";
|
|
3
4
|
import { rollupConfig } from "./rollup.js";
|
|
4
|
-
import { existsSync, mkdirSync,
|
|
5
|
-
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { watch } from "chokidar";
|
|
8
8
|
import consola from "consola";
|
|
@@ -81,56 +81,7 @@ var src_default = defineNitroModule({
|
|
|
81
81
|
nitro.scanResolvers = resolvers;
|
|
82
82
|
const directives = await scanDirectives(nitro);
|
|
83
83
|
nitro.scanDirectives = directives;
|
|
84
|
-
|
|
85
|
-
const directiveSchemas = [];
|
|
86
|
-
for (const dir of directives) for (const _imp of dir.imports) {
|
|
87
|
-
const fileContent = await readFile(dir.specifier, "utf-8");
|
|
88
|
-
const nameMatch = fileContent.match(/name:\s*['"`](\w+)['"`]/);
|
|
89
|
-
const locationsMatch = fileContent.match(/locations:\s*\[([\s\S]*?)\]/);
|
|
90
|
-
const argsMatch = fileContent.match(/args:\s*\{([\s\S]*?)\}\s*,\s*(?:description|transformer)/);
|
|
91
|
-
if (nameMatch && locationsMatch) {
|
|
92
|
-
const name = nameMatch[1];
|
|
93
|
-
const locations = locationsMatch?.[1]?.split(",").map((l) => l.trim().replace(/['"`]/g, "")).filter(Boolean).join(" | ") || "";
|
|
94
|
-
let args = "";
|
|
95
|
-
if (argsMatch && argsMatch[1] && argsMatch[1].trim()) {
|
|
96
|
-
const argDefs = [];
|
|
97
|
-
const argMatches = argsMatch[1].matchAll(/(\w+):\s*\{([^}]+)\}/g);
|
|
98
|
-
for (const argMatch of argMatches) {
|
|
99
|
-
const argName = argMatch[1];
|
|
100
|
-
const argBody = argMatch[2];
|
|
101
|
-
const typeMatch = argBody?.match(/type:\s*['"`](\[[\w!]+\]|\w+)['"`]/);
|
|
102
|
-
const type = typeMatch ? typeMatch[1] : "String";
|
|
103
|
-
const defaultMatch = argBody?.match(/defaultValue:\s*(['"`]([^'"`]+)['"`]|(\d+)|true|false)/);
|
|
104
|
-
let defaultValue = "";
|
|
105
|
-
if (defaultMatch) {
|
|
106
|
-
const value = defaultMatch[2] || defaultMatch[3] || defaultMatch[1]?.replace(/['"`]/g, "");
|
|
107
|
-
if (type === "String") defaultValue = ` = "${value}"`;
|
|
108
|
-
else if (type === "Int" || type === "Float") defaultValue = ` = ${value}`;
|
|
109
|
-
else if (type === "Boolean") defaultValue = ` = ${value}`;
|
|
110
|
-
}
|
|
111
|
-
argDefs.push(`${argName}: ${type}${defaultValue}`);
|
|
112
|
-
}
|
|
113
|
-
if (argDefs.length > 0) args = `(${argDefs.join(", ")})`;
|
|
114
|
-
}
|
|
115
|
-
directiveSchemas.push(`directive @${name}${args} on ${locations}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
if (directiveSchemas.length > 0) {
|
|
119
|
-
const directivesPath = resolve(nitro.graphql.serverDir, "_directives.graphql");
|
|
120
|
-
const content = `# WARNING: This file is auto-generated by nitro-graphql
|
|
121
|
-
# Do not modify this file directly. It will be overwritten.
|
|
122
|
-
# To define custom directives, create .directive.ts files using defineDirective()
|
|
123
|
-
|
|
124
|
-
${directiveSchemas.join("\n\n")}`;
|
|
125
|
-
let shouldWrite = true;
|
|
126
|
-
if (existsSync(directivesPath)) {
|
|
127
|
-
const existingContent = readFileSync(directivesPath, "utf-8");
|
|
128
|
-
shouldWrite = existingContent !== content;
|
|
129
|
-
}
|
|
130
|
-
if (shouldWrite) writeFileSync(directivesPath, content, "utf-8");
|
|
131
|
-
if (!nitro.scanSchemas.includes(directivesPath)) nitro.scanSchemas.push(directivesPath);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
84
|
+
await generateDirectiveSchemas(nitro, directives);
|
|
134
85
|
nitro.hooks.hook("dev:start", async () => {
|
|
135
86
|
const schemas$1 = await scanSchemas(nitro);
|
|
136
87
|
nitro.scanSchemas = schemas$1;
|
|
@@ -138,56 +89,7 @@ ${directiveSchemas.join("\n\n")}`;
|
|
|
138
89
|
nitro.scanResolvers = resolvers$1;
|
|
139
90
|
const directives$1 = await scanDirectives(nitro);
|
|
140
91
|
nitro.scanDirectives = directives$1;
|
|
141
|
-
|
|
142
|
-
const directiveSchemas = [];
|
|
143
|
-
for (const dir of directives$1) for (const _imp of dir.imports) {
|
|
144
|
-
const fileContent = await readFile(dir.specifier, "utf-8");
|
|
145
|
-
const nameMatch = fileContent.match(/name:\s*['"`](\w+)['"`]/);
|
|
146
|
-
const locationsMatch = fileContent.match(/locations:\s*\[([\s\S]*?)\]/);
|
|
147
|
-
const argsMatch = fileContent.match(/args:\s*\{([\s\S]*?)\}\s*,\s*(?:description|transformer)/);
|
|
148
|
-
if (nameMatch && locationsMatch) {
|
|
149
|
-
const name = nameMatch[1];
|
|
150
|
-
const locations = locationsMatch?.[1]?.split(",").map((l) => l.trim().replace(/['"`]/g, "")).filter(Boolean).join(" | ") || "";
|
|
151
|
-
let args = "";
|
|
152
|
-
if (argsMatch && argsMatch[1] && argsMatch[1].trim()) {
|
|
153
|
-
const argDefs = [];
|
|
154
|
-
const argMatches = argsMatch[1].matchAll(/(\w+):\s*\{([^}]+)\}/g);
|
|
155
|
-
for (const argMatch of argMatches) {
|
|
156
|
-
const argName = argMatch[1];
|
|
157
|
-
const argBody = argMatch[2];
|
|
158
|
-
const typeMatch = argBody?.match(/type:\s*['"`](\[?\w+!?\]?)['"`]/);
|
|
159
|
-
const type = typeMatch ? typeMatch[1] : "String";
|
|
160
|
-
const defaultMatch = argBody?.match(/defaultValue:\s*(['"`]([^'"`]+)['"`]|(\d+)|true|false)/);
|
|
161
|
-
let defaultValue = "";
|
|
162
|
-
if (defaultMatch) {
|
|
163
|
-
const value = defaultMatch[2] || defaultMatch[3] || defaultMatch[1]?.replace(/['"`]/g, "");
|
|
164
|
-
if (type === "String") defaultValue = ` = "${value}"`;
|
|
165
|
-
else if (type === "Int" || type === "Float") defaultValue = ` = ${value}`;
|
|
166
|
-
else if (type === "Boolean") defaultValue = ` = ${value}`;
|
|
167
|
-
}
|
|
168
|
-
argDefs.push(`${argName}: ${type}${defaultValue}`);
|
|
169
|
-
}
|
|
170
|
-
if (argDefs.length > 0) args = `(${argDefs.join(", ")})`;
|
|
171
|
-
}
|
|
172
|
-
directiveSchemas.push(`directive @${name}${args} on ${locations}`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
if (directiveSchemas.length > 0) {
|
|
176
|
-
const directivesPath = resolve(nitro.graphql.serverDir, "_directives.graphql");
|
|
177
|
-
const content = `# WARNING: This file is auto-generated by nitro-graphql
|
|
178
|
-
# Do not modify this file directly. It will be overwritten.
|
|
179
|
-
# To define custom directives, create .directive.ts files using defineDirective()
|
|
180
|
-
|
|
181
|
-
${directiveSchemas.join("\n\n")}`;
|
|
182
|
-
let shouldWrite = true;
|
|
183
|
-
if (existsSync(directivesPath)) {
|
|
184
|
-
const existingContent = readFileSync(directivesPath, "utf-8");
|
|
185
|
-
shouldWrite = existingContent !== content;
|
|
186
|
-
}
|
|
187
|
-
if (shouldWrite) writeFileSync(directivesPath, content, "utf-8");
|
|
188
|
-
if (!nitro.scanSchemas.includes(directivesPath)) nitro.scanSchemas.push(directivesPath);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
92
|
+
await generateDirectiveSchemas(nitro, directives$1);
|
|
191
93
|
const docs$1 = await scanDocs(nitro);
|
|
192
94
|
nitro.scanDocuments = docs$1;
|
|
193
95
|
});
|
package/dist/rollup.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getImportId, scanGraphql } from "./utils/index.js";
|
|
2
2
|
import { clientTypeGeneration, serverTypeGeneration } from "./utils/type-generation.js";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
4
3
|
import { resolve } from "pathe";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { parse } from "graphql";
|
|
6
6
|
import { genImport } from "knitwork";
|
|
7
7
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as h34 from "h3";
|
|
2
2
|
|
|
3
3
|
//#region src/routes/apollo-server.d.ts
|
|
4
|
-
declare const _default:
|
|
4
|
+
declare const _default: h34.EventHandler<h34.EventHandlerRequest, any>;
|
|
5
5
|
//#endregion
|
|
6
6
|
export { _default as default };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as h36 from "h3";
|
|
2
2
|
|
|
3
3
|
//#region src/routes/graphql-yoga.d.ts
|
|
4
|
-
declare const _default:
|
|
4
|
+
declare const _default: h36.EventHandler<h36.EventHandlerRequest, Promise<Response>>;
|
|
5
5
|
//#endregion
|
|
6
6
|
export { _default as default };
|
package/dist/routes/health.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as h32 from "h3";
|
|
2
2
|
|
|
3
3
|
//#region src/routes/health.d.ts
|
|
4
|
-
declare const _default:
|
|
4
|
+
declare const _default: h32.EventHandler<h32.EventHandlerRequest, Promise<{
|
|
5
5
|
status: string;
|
|
6
6
|
message: string;
|
|
7
7
|
timestamp: string;
|
|
@@ -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,233 @@
|
|
|
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
|
+
case "defaultValue":
|
|
157
|
+
defaultValue = this.extractLiteralValue(prop.value);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
type,
|
|
163
|
+
...defaultValue !== void 0 && { defaultValue }
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Extract literal value (string, number, boolean)
|
|
168
|
+
*/
|
|
169
|
+
extractLiteralValue(node) {
|
|
170
|
+
if (node?.type === "Literal") return node.value;
|
|
171
|
+
return void 0;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Generate GraphQL directive schema from parsed directive
|
|
176
|
+
*/
|
|
177
|
+
function generateDirectiveSchema(directive) {
|
|
178
|
+
let args = "";
|
|
179
|
+
if (directive.args && Object.keys(directive.args).length > 0) {
|
|
180
|
+
const argDefs = Object.entries(directive.args).map(([name, arg]) => {
|
|
181
|
+
let defaultValue = "";
|
|
182
|
+
if (arg.defaultValue !== void 0) if (typeof arg.defaultValue === "string") defaultValue = ` = "${arg.defaultValue}"`;
|
|
183
|
+
else defaultValue = ` = ${arg.defaultValue}`;
|
|
184
|
+
return `${name}: ${arg.type}${defaultValue}`;
|
|
185
|
+
});
|
|
186
|
+
args = `(${argDefs.join(", ")})`;
|
|
187
|
+
}
|
|
188
|
+
const locations = directive.locations.join(" | ");
|
|
189
|
+
return `directive @${directive.name}${args} on ${locations}`;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Generate directive schemas file from scanned directives
|
|
193
|
+
*/
|
|
194
|
+
async function generateDirectiveSchemas(nitro, directives) {
|
|
195
|
+
if (directives.length === 0) return;
|
|
196
|
+
const { existsSync, readFileSync, writeFileSync } = await import("node:fs");
|
|
197
|
+
const { readFile } = await import("node:fs/promises");
|
|
198
|
+
const { resolve } = await import("pathe");
|
|
199
|
+
const directiveSchemas = [];
|
|
200
|
+
const seenDirectives = /* @__PURE__ */ new Set();
|
|
201
|
+
for (const dir of directives) for (const _imp of dir.imports) {
|
|
202
|
+
const fileContent = await readFile(dir.specifier, "utf-8");
|
|
203
|
+
const directiveDefs = await directiveParser.parseDirectives(fileContent, dir.specifier);
|
|
204
|
+
for (const def of directiveDefs) {
|
|
205
|
+
if (seenDirectives.has(def.name)) continue;
|
|
206
|
+
seenDirectives.add(def.name);
|
|
207
|
+
const schema = generateDirectiveSchema(def);
|
|
208
|
+
directiveSchemas.push(schema);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (directiveSchemas.length > 0) {
|
|
212
|
+
const directivesPath = resolve(nitro.graphql.serverDir, "_directives.graphql");
|
|
213
|
+
const content = `# WARNING: This file is auto-generated by nitro-graphql
|
|
214
|
+
# Do not modify this file directly. It will be overwritten.
|
|
215
|
+
# To define custom directives, create .directive.ts files using defineDirective()
|
|
216
|
+
|
|
217
|
+
${directiveSchemas.join("\n\n")}`;
|
|
218
|
+
let shouldWrite = true;
|
|
219
|
+
if (existsSync(directivesPath)) {
|
|
220
|
+
const existingContent = readFileSync(directivesPath, "utf-8");
|
|
221
|
+
shouldWrite = existingContent !== content;
|
|
222
|
+
}
|
|
223
|
+
if (shouldWrite) writeFileSync(directivesPath, content, "utf-8");
|
|
224
|
+
if (!nitro.scanSchemas.includes(directivesPath)) nitro.scanSchemas.push(directivesPath);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Singleton instance for reuse
|
|
229
|
+
*/
|
|
230
|
+
const directiveParser = new DirectiveParser();
|
|
231
|
+
|
|
232
|
+
//#endregion
|
|
233
|
+
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
|
|
@@ -12,4 +13,4 @@ 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[]>;
|
|
14
15
|
//#endregion
|
|
15
|
-
export { GLOB_SCAN_PATTERN, getImportId, relativeWithDot, scanDirectives, scanDocs, scanGraphql, scanResolvers, scanSchemas, scanTypeDefs };
|
|
16
|
+
export { GLOB_SCAN_PATTERN, directiveParser, generateDirectiveSchema, generateDirectiveSchemas, getImportId, relativeWithDot, scanDirectives, scanDocs, scanGraphql, scanResolvers, scanSchemas, scanTypeDefs };
|
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";
|
|
@@ -131,4 +132,4 @@ async function scanDir(nitro, dir, name, globPattern = GLOB_SCAN_PATTERN) {
|
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
//#endregion
|
|
134
|
-
export { GLOB_SCAN_PATTERN, getImportId, relativeWithDot, scanDirectives, scanDocs, scanGraphql, scanResolvers, scanSchemas, scanTypeDefs };
|
|
135
|
+
export { GLOB_SCAN_PATTERN, directiveParser, generateDirectiveSchema, generateDirectiveSchemas, getImportId, relativeWithDot, scanDirectives, scanDocs, scanGraphql, scanResolvers, scanSchemas, scanTypeDefs };
|