Skip to content

Commit da64003

Browse files
Merge branch 'main' into feat/define-directive
2 parents bd750c3 + 8b565af commit da64003

File tree

5 files changed

+190
-75
lines changed

5 files changed

+190
-75
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "nitro-graphql",
33
"type": "module",
4-
"version": "1.2.0",
4+
"version": "1.2.2",
55
"packageManager": "pnpm@10.13.1",
66
"description": "GraphQL integration for Nitro",
77
"license": "MIT",

src/ecosystem/nuxt.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { VueTSConfig } from '@nuxt/schema'
2+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
23
import { defineNuxtModule } from '@nuxt/kit'
34
import { join, resolve } from 'pathe'
45

@@ -56,5 +57,39 @@ export default defineNuxtModule<ModuleOptions>({
5657
nuxt.hook('imports:dirs', (dirs) => {
5758
dirs.push(resolve(nuxt.options.srcDir, 'graphql'))
5859
})
60+
61+
// Handle Nuxt-specific GraphQL setup
62+
nuxt.hook('nitro:config', () => {
63+
const clientDir = join(nuxt.options.buildDir, 'graphql')
64+
65+
// Check if app/graphql directory exists
66+
const appGraphqlDir = resolve(nuxt.options.rootDir, 'app/graphql')
67+
const hasAppGraphqlDir = existsSync(appGraphqlDir)
68+
69+
// Only create default setup if app/graphql directory doesn't exist
70+
if (!hasAppGraphqlDir) {
71+
// Create default subdirectory for the new folder structure
72+
const defaultDir = join(clientDir, 'default')
73+
if (!existsSync(defaultDir)) {
74+
mkdirSync(defaultDir, { recursive: true })
75+
}
76+
77+
// Create a sample GraphQL query file to help users get started
78+
const sampleQueryFile = join(defaultDir, 'queries.graphql')
79+
if (!existsSync(sampleQueryFile)) {
80+
writeFileSync(sampleQueryFile, `# Example GraphQL queries
81+
# Add your GraphQL queries here
82+
83+
# query GetUser($id: ID!) {
84+
# user(id: $id) {
85+
# id
86+
# name
87+
# email
88+
# }
89+
# }
90+
`, 'utf-8')
91+
}
92+
}
93+
})
5994
},
6095
})

src/index.ts

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -403,34 +403,5 @@ declare module 'h3' {
403403
consola.warn('nitro-graphql: Found context.d.ts file. Please rename it to context.ts for the new structure.')
404404
consola.info('The context file should now be context.ts instead of context.d.ts')
405405
}
406-
407-
// Create client directory for Nuxt projects
408-
if (nitro.options.framework.name === 'nuxt') {
409-
if (!existsSync(nitro.graphql.clientDir)) {
410-
mkdirSync(nitro.graphql.clientDir, { recursive: true })
411-
}
412-
413-
// Create default subdirectory for the new folder structure
414-
const defaultDir = join(nitro.graphql.clientDir, 'default')
415-
if (!existsSync(defaultDir)) {
416-
mkdirSync(defaultDir, { recursive: true })
417-
}
418-
419-
// Create a sample GraphQL query file to help users get started
420-
const sampleQueryFile = join(defaultDir, 'queries.graphql')
421-
if (!existsSync(sampleQueryFile)) {
422-
writeFileSync(sampleQueryFile, `# Example GraphQL queries
423-
# Add your GraphQL queries here
424-
425-
# query GetUser($id: ID!) {
426-
# user(id: $id) {
427-
# id
428-
# name
429-
# email
430-
# }
431-
# }
432-
`, 'utf-8')
433-
}
434-
}
435406
},
436407
})

src/utils/client-codegen.ts

Lines changed: 141 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -94,34 +94,40 @@ export async function loadExternalSchema(service: ExternalGraphQLService, buildD
9494
const schemaFilePath = service.downloadPath ? resolve(service.downloadPath) : defaultPath
9595

9696
if (existsSync(schemaFilePath)) {
97-
consola.info(`[graphql:${service.name}] Loading schema from local file: ${schemaFilePath}`)
98-
9997
try {
10098
const result = loadSchemaSync([schemaFilePath], {
10199
loaders: [new GraphQLFileLoader()],
102100
})
103-
consola.info(`[graphql:${service.name}] External schema loaded successfully from local file`)
104101
return result
105102
}
106-
catch (localError) {
107-
consola.warn(`[graphql:${service.name}] Failed to load local schema, falling back to remote:`, localError)
103+
catch {
104+
consola.warn(`[graphql:${service.name}] Cached schema invalid, loading from source`)
108105
}
109106
}
110107
}
111108

112-
consola.info(`[graphql:${service.name}] Loading external schema from: ${schemas.join(', ')}`)
109+
// Determine appropriate loaders based on schema sources
110+
const hasUrls = schemas.some(schema => isUrl(schema))
111+
const hasLocalFiles = schemas.some(schema => !isUrl(schema))
112+
const loaders = []
113+
if (hasLocalFiles) {
114+
loaders.push(new GraphQLFileLoader())
115+
}
116+
if (hasUrls) {
117+
loaders.push(new UrlLoader())
118+
}
119+
120+
if (loaders.length === 0) {
121+
throw new Error('No appropriate loaders found for schema sources')
122+
}
113123

114124
const result = loadSchemaSync(schemas, {
115-
loaders: [
116-
new GraphQLFileLoader(),
117-
new UrlLoader(),
118-
],
125+
loaders,
119126
...(Object.keys(headers).length > 0 && {
120127
headers,
121128
}),
122129
})
123130

124-
consola.info(`[graphql:${service.name}] External schema loaded successfully`)
125131
return result
126132
}
127133
catch (error) {
@@ -130,6 +136,13 @@ export async function loadExternalSchema(service: ExternalGraphQLService, buildD
130136
}
131137
}
132138

139+
/**
140+
* Check if a path is a URL (http/https)
141+
*/
142+
function isUrl(path: string): boolean {
143+
return path.startsWith('http://') || path.startsWith('https://')
144+
}
145+
133146
/**
134147
* Download and save schema from external service
135148
*/
@@ -149,6 +162,10 @@ export async function downloadAndSaveSchema(service: ExternalGraphQLService, bui
149162
const headers = typeof service.headers === 'function' ? service.headers() : service.headers || {}
150163
const schemas = Array.isArray(service.schema) ? service.schema : [service.schema]
151164

165+
// Check if any schemas are local files vs URLs
166+
const hasUrlSchemas = schemas.some(schema => isUrl(schema))
167+
const hasLocalSchemas = schemas.some(schema => !isUrl(schema))
168+
152169
// Determine download behavior based on mode
153170
let shouldDownload = false
154171
const fileExists = existsSync(schemaFilePath)
@@ -157,10 +174,10 @@ export async function downloadAndSaveSchema(service: ExternalGraphQLService, bui
157174
// Always check for updates (original behavior)
158175
shouldDownload = true
159176

160-
if (fileExists) {
161-
// Compare with remote schema
177+
if (fileExists && hasUrlSchemas) {
178+
// Only compare with remote schema if we have URL schemas
162179
try {
163-
const remoteSchema = loadSchemaSync(schemas, {
180+
const remoteSchema = loadSchemaSync(schemas.filter(isUrl), {
164181
loaders: [new UrlLoader()],
165182
...(Object.keys(headers).length > 0 && { headers }),
166183
})
@@ -180,39 +197,74 @@ export async function downloadAndSaveSchema(service: ExternalGraphQLService, bui
180197
shouldDownload = true
181198
}
182199
}
200+
else if (fileExists && hasLocalSchemas) {
201+
// For local schemas, check if source files are newer
202+
const localFiles = schemas.filter(schema => !isUrl(schema))
203+
let sourceIsNewer = false
204+
205+
for (const localFile of localFiles) {
206+
if (existsSync(localFile)) {
207+
const { statSync } = await import('node:fs')
208+
const sourceStats = statSync(localFile)
209+
const cachedStats = statSync(schemaFilePath)
210+
if (sourceStats.mtime > cachedStats.mtime) {
211+
sourceIsNewer = true
212+
break
213+
}
214+
}
215+
}
216+
217+
if (!sourceIsNewer) {
218+
shouldDownload = false
219+
}
220+
}
183221
}
184222
else if (downloadMode === true || downloadMode === 'once') {
185-
// Download only if file doesn't exist (offline-friendly)
223+
// Download/copy only if file doesn't exist (offline-friendly)
186224
shouldDownload = !fileExists
187225

188-
if (fileExists) {
189-
consola.info(`[graphql:${service.name}] Using cached schema from: ${schemaFilePath}`)
190-
}
226+
// File exists, will use cached version
191227
}
192228

193229
if (shouldDownload) {
194-
consola.info(`[graphql:${service.name}] Downloading schema to: ${schemaFilePath}`)
195-
196-
const schema = loadSchemaSync(schemas, {
197-
loaders: [new UrlLoader()],
198-
...(Object.keys(headers).length > 0 && { headers }),
199-
})
200-
201-
const schemaString = printSchemaWithDirectives(schema)
202-
203-
// Ensure directory exists
204-
mkdirSync(dirname(schemaFilePath), { recursive: true })
205-
206-
// Save schema to file
207-
writeFileSync(schemaFilePath, schemaString, 'utf-8')
208-
209-
consola.success(`[graphql:${service.name}] Schema downloaded and saved successfully`)
230+
if (hasUrlSchemas && hasLocalSchemas) {
231+
// Mixed schemas: load both URL and local schemas
232+
const schema = loadSchemaSync(schemas, {
233+
loaders: [new GraphQLFileLoader(), new UrlLoader()],
234+
...(Object.keys(headers).length > 0 && { headers }),
235+
})
236+
237+
const schemaString = printSchemaWithDirectives(schema)
238+
mkdirSync(dirname(schemaFilePath), { recursive: true })
239+
writeFileSync(schemaFilePath, schemaString, 'utf-8')
240+
}
241+
else if (hasUrlSchemas) {
242+
// URL schemas: download from remote
243+
const schema = loadSchemaSync(schemas, {
244+
loaders: [new UrlLoader()],
245+
...(Object.keys(headers).length > 0 && { headers }),
246+
})
247+
248+
const schemaString = printSchemaWithDirectives(schema)
249+
mkdirSync(dirname(schemaFilePath), { recursive: true })
250+
writeFileSync(schemaFilePath, schemaString, 'utf-8')
251+
}
252+
else if (hasLocalSchemas) {
253+
// Local schemas: copy/merge local files
254+
const schema = loadSchemaSync(schemas, {
255+
loaders: [new GraphQLFileLoader()],
256+
})
257+
258+
const schemaString = printSchemaWithDirectives(schema)
259+
mkdirSync(dirname(schemaFilePath), { recursive: true })
260+
writeFileSync(schemaFilePath, schemaString, 'utf-8')
261+
}
210262
}
211263

212264
return schemaFilePath
213265
}
214266
catch (error) {
215-
consola.error(`[graphql:${service.name}] Failed to download schema:`, error)
267+
consola.error(`[graphql:${service.name}] Failed to download/copy schema:`, error)
216268
return undefined
217269
}
218270
}
@@ -248,15 +300,13 @@ export async function generateClientTypes(
248300
outputPath?: string,
249301
serviceName?: string,
250302
) {
251-
if (docs.length === 0) {
252-
const serviceLabel = serviceName ? `:${serviceName}` : ''
253-
consola.info(`[graphql${serviceLabel}] No client GraphQL files found. Skipping client type generation.`)
303+
// For external services, allow schema-only generation (no documents required)
304+
if (docs.length === 0 && !serviceName) {
305+
consola.info('No client GraphQL files found. Skipping client type generation.')
254306
return false
255307
}
256308

257309
const serviceLabel = serviceName ? `:${serviceName}` : ''
258-
consola.info(`[graphql${serviceLabel}] Found ${docs.length} client GraphQL documents`)
259-
260310
const defaultConfig: CodegenClientConfig | GenericSdkConfig = {
261311
emitLegacyCommonJSImports: false,
262312
useTypeImports: true,
@@ -284,10 +334,60 @@ export async function generateClientTypes(
284334
}
285335

286336
const mergedConfig = defu(defaultConfig, config)
287-
288337
const mergedSdkConfig = defu(mergedConfig, sdkConfig)
289338

290339
try {
340+
// Schema-only generation (no documents)
341+
if (docs.length === 0) {
342+
const output = await codegen({
343+
filename: outputPath || 'client-types.generated.ts',
344+
schema: parse(printSchemaWithDirectives(schema)),
345+
documents: [],
346+
config: mergedConfig,
347+
plugins: [
348+
{ pluginContent: {} },
349+
{ typescript: {} },
350+
],
351+
pluginMap: {
352+
pluginContent: { plugin: pluginContent },
353+
typescript: { plugin: typescriptPlugin },
354+
},
355+
})
356+
357+
// For schema-only generation, create a generic SDK
358+
const sdkContent = `// THIS FILE IS GENERATED, DO NOT EDIT!
359+
/* eslint-disable eslint-comments/no-unlimited-disable */
360+
/* tslint:disable */
361+
/* eslint-disable */
362+
/* prettier-ignore */
363+
364+
import type { GraphQLResolveInfo } from 'graphql'
365+
export type RequireFields<T, K extends keyof T> = Omit<T, K> & { [P in K]-?: NonNullable<T[P]> }
366+
367+
export interface Requester<C = {}, E = unknown> {
368+
<R, V>(doc: string, vars?: V, options?: C): Promise<R> | AsyncIterable<R>
369+
}
370+
371+
export type Sdk = {
372+
request: <R, V = Record<string, any>>(document: string, variables?: V) => Promise<R>
373+
}
374+
375+
export function getSdk(requester: Requester): Sdk {
376+
return {
377+
request: <R, V = Record<string, any>>(document: string, variables?: V): Promise<R> => {
378+
return requester<R, V>(document, variables)
379+
}
380+
}
381+
}
382+
`
383+
384+
return {
385+
types: output,
386+
sdk: sdkContent,
387+
}
388+
}
389+
390+
// Full generation with documents
291391
const output = await codegen({
292392
filename: outputPath || 'client-types.generated.ts',
293393
schema: parse(printSchemaWithDirectives(schema)),

src/utils/type-generation.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,13 @@ export async function clientTypeGeneration(
150150
nitro: Nitro,
151151
) {
152152
try {
153-
// Generate main service types
154-
await generateMainClientTypes(nitro)
153+
// Generate main service types (only if server schema exists)
154+
const hasServerSchema = nitro.scanSchemas && nitro.scanSchemas.length > 0
155+
if (hasServerSchema) {
156+
await generateMainClientTypes(nitro)
157+
}
155158

156-
// Generate external service types
159+
// Generate external service types (can work independently)
157160
if (nitro.options.graphql?.externalServices?.length) {
158161
await generateExternalServicesTypes(nitro)
159162
}
@@ -228,7 +231,13 @@ async function generateMainClientTypes(nitro: Nitro) {
228231

229232
// Generate ofetch client for Nuxt framework
230233
if (nitro.options.framework?.name === 'nuxt') {
231-
generateNuxtOfetchClient(nitro.graphql.clientDir, 'default')
234+
// Check if user has their own app/graphql setup
235+
const appGraphqlDir = resolve(nitro.options.rootDir, 'app/graphql')
236+
const hasUserGraphqlSetup = existsSync(appGraphqlDir)
237+
238+
if (!hasUserGraphqlSetup) {
239+
generateNuxtOfetchClient(nitro.graphql.clientDir, 'default')
240+
}
232241
const externalServices = nitro.options.graphql?.externalServices || []
233242
generateGraphQLIndexFile(nitro.graphql.clientDir, externalServices)
234243
}

0 commit comments

Comments
 (0)