Skip to content

Commit fa1517a

Browse files
feat: add support for custom types path configuration and implement E2E tests
1 parent 76b23ad commit fa1517a

6 files changed

Lines changed: 472 additions & 10 deletions

File tree

src/nitro/codegen.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import type { Nitro } from 'nitro/types'
88
import { existsSync, readFileSync } from 'node:fs'
99
import { loadFilesSync } from '@graphql-tools/load-files'
1010
import { mergeTypeDefs } from '@graphql-tools/merge'
11+
import { makeExecutableSchema } from '@graphql-tools/schema'
1112
import { printSchemaWithDirectives } from '@graphql-tools/utils'
1213
import consola from 'consola'
13-
import { buildSchema, lexicographicSortSchema, parse, print } from 'graphql'
14+
import { parse, print } from 'graphql'
1415
import { join, resolve } from 'pathe'
1516
import {
1617
downloadAndSaveSchema,
@@ -31,6 +32,7 @@ import { getDefaultPaths, getSdkConfig, getTypesConfig, resolveFilePath, shouldG
3132
const logger = consola.withTag(LOG_TAG)
3233

3334
// Helper: Build schema with optional federation support
35+
// Uses @graphql-tools to ensure same graphql instance is used throughout
3436
async function buildSchemaFromString(source: string, federation: boolean): Promise<GraphQLSchema> {
3537
if (federation) {
3638
const buildSubgraph = await loadFederationSupport()
@@ -39,7 +41,9 @@ async function buildSchemaFromString(source: string, federation: boolean): Promi
3941
}
4042
return buildSubgraph([{ typeDefs: parse(source) }])
4143
}
42-
return buildSchema(source)
44+
// Use makeExecutableSchema from @graphql-tools to ensure same graphql instance
45+
// This allows printSchemaWithDirectives to work without "different module" errors
46+
return makeExecutableSchema({ typeDefs: source })
4347
}
4448

4549
/**
@@ -84,15 +88,17 @@ export async function generateServerTypes(
8488
if (!validateNoDuplicateTypes(validSchemas, strings))
8589
return
8690

87-
const merged = mergeTypeDefs([strings.join('\n\n')], { throwOnConflict: true })
91+
// mergeTypeDefs with sort: true provides deterministic ordering
92+
const merged = mergeTypeDefs([strings.join('\n\n')], { throwOnConflict: true, sort: true })
93+
// print(merged) preserves directives from the merged DocumentNode
8894
const mergedSchemaString = print(merged)
8995
const federation = nitro.options.graphql?.federation?.enabled === true
9096
const schema = await buildSchemaFromString(mergedSchemaString, federation)
9197

92-
// Sort schema for deterministic output
93-
const sortedSchema = lexicographicSortSchema(schema)
94-
// Get sorted schema string with directives preserved
95-
const sortedSchemaString = printSchemaWithDirectives(sortedSchema)
98+
// Use printSchemaWithDirectives to preserve custom directives in the output
99+
// Note: We skip lexicographicSortSchema because it causes graphql instance mismatch errors
100+
// The schema is already sorted by mergeTypeDefs with sort: true
101+
const sortedSchemaString = printSchemaWithDirectives(schema)
96102

97103
// Generate types - pass schemaString to avoid graphql instance mismatch
98104
const result = await generateServerTypesCore({
@@ -223,8 +229,6 @@ async function generateExternalTypes(
223229
consola.warn(`[${service.name}] Failed to load schema`)
224230
continue
225231
}
226-
const sortedSchema = lexicographicSortSchema(schema)
227-
228232
const docs = service.documents?.length
229233
? await loadGraphQLDocuments(service.documents).catch(() => [])
230234
: []
@@ -234,7 +238,8 @@ async function generateExternalTypes(
234238
continue
235239
}
236240

237-
const types = await generateExternalClientTypesCore(service as any, sortedSchema, docs)
241+
// Use schema directly without lexicographicSortSchema to avoid graphql instance mismatch
242+
const types = await generateExternalClientTypesCore(service as any, schema, docs)
238243
if (types === false)
239244
continue
240245

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/**
2+
* E2E tests for custom types path configuration
3+
*
4+
* This test verifies that types files are generated at custom paths
5+
* when configured via the `types` option.
6+
*
7+
* Scenarios tested:
8+
* - Custom server types path with {rootDir} placeholder
9+
* - Custom client types path with {typesDir} placeholder
10+
* - Disabled client types (client: false)
11+
* - Default paths when types is true
12+
*/
13+
import type { Nitro } from 'nitro/types'
14+
import { existsSync, readFileSync, rmSync } from 'node:fs'
15+
import { build, createNitro, prepare } from 'nitro/builder'
16+
import { join, resolve } from 'pathe'
17+
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'
18+
import graphql from '../../src'
19+
20+
const fixturesDir = resolve(__dirname, '../fixtures')
21+
const projectDir = resolve(fixturesDir, 'custom-types-path')
22+
23+
// Clean up generated files
24+
function cleanupGeneratedFiles() {
25+
const dirsToClean = [
26+
join(projectDir, '.nitro'),
27+
join(projectDir, '.output'),
28+
join(projectDir, 'generated'),
29+
join(projectDir, 'custom-types'),
30+
]
31+
32+
for (const dir of dirsToClean) {
33+
if (existsSync(dir)) {
34+
rmSync(dir, { recursive: true, force: true })
35+
}
36+
}
37+
}
38+
39+
describe('custom Types Path E2E', () => {
40+
// Note: cleanup is done in beforeAll of each describe block, not afterEach
41+
// This prevents files from being deleted between tests in the same group
42+
43+
describe('custom server and client types paths', () => {
44+
let nitro: Nitro
45+
46+
beforeAll(async () => {
47+
cleanupGeneratedFiles()
48+
49+
nitro = await createNitro({
50+
rootDir: projectDir,
51+
dev: true,
52+
modules: [
53+
graphql({
54+
framework: 'graphql-yoga',
55+
types: {
56+
enabled: true,
57+
server: '{rootDir}/generated/server-types.d.ts',
58+
client: '{rootDir}/generated/client-types.d.ts',
59+
},
60+
}),
61+
],
62+
})
63+
64+
await prepare(nitro)
65+
await build(nitro)
66+
67+
// Generate types
68+
const { generateServerTypes, generateClientTypes } = await import('../../src/nitro/codegen')
69+
await generateServerTypes(nitro)
70+
await generateClientTypes(nitro)
71+
}, 60000)
72+
73+
afterAll(async () => {
74+
await nitro?.close()
75+
})
76+
77+
it('should generate server types at custom path', () => {
78+
const customServerTypesPath = join(projectDir, 'generated/server-types.d.ts')
79+
80+
expect(existsSync(customServerTypesPath)).toBe(true)
81+
82+
const content = readFileSync(customServerTypesPath, 'utf-8')
83+
expect(content).toContain('Query')
84+
expect(content).toContain('User')
85+
})
86+
87+
it('should generate client types at custom path', () => {
88+
const customClientTypesPath = join(projectDir, 'generated/client-types.d.ts')
89+
90+
expect(existsSync(customClientTypesPath)).toBe(true)
91+
92+
const content = readFileSync(customClientTypesPath, 'utf-8')
93+
expect(content).toContain('GetUser')
94+
expect(content).toContain('GetHello')
95+
})
96+
97+
it('should NOT generate types at default paths', () => {
98+
const defaultServerPath = join(projectDir, '.nitro/types/nitro-graphql-server.d.ts')
99+
const defaultClientPath = join(projectDir, '.nitro/types/nitro-graphql-client.d.ts')
100+
101+
// Default paths should NOT exist when custom paths are configured
102+
expect(existsSync(defaultServerPath)).toBe(false)
103+
expect(existsSync(defaultClientPath)).toBe(false)
104+
})
105+
})
106+
107+
describe('disabled client types', () => {
108+
let nitro: Nitro
109+
110+
beforeAll(async () => {
111+
cleanupGeneratedFiles()
112+
113+
nitro = await createNitro({
114+
rootDir: projectDir,
115+
dev: true,
116+
modules: [
117+
graphql({
118+
framework: 'graphql-yoga',
119+
types: {
120+
enabled: true,
121+
server: '{rootDir}/custom-types/server.d.ts',
122+
client: false, // Disable client types
123+
},
124+
}),
125+
],
126+
})
127+
128+
await prepare(nitro)
129+
await build(nitro)
130+
131+
const { generateServerTypes, generateClientTypes } = await import('../../src/nitro/codegen')
132+
await generateServerTypes(nitro)
133+
await generateClientTypes(nitro)
134+
}, 60000)
135+
136+
afterAll(async () => {
137+
await nitro?.close()
138+
})
139+
140+
it('should generate server types when enabled', () => {
141+
const serverTypesPath = join(projectDir, 'custom-types/server.d.ts')
142+
143+
expect(existsSync(serverTypesPath)).toBe(true)
144+
145+
const content = readFileSync(serverTypesPath, 'utf-8')
146+
expect(content).toContain('Query')
147+
})
148+
149+
it('should NOT generate client types when disabled', () => {
150+
const defaultClientPath = join(projectDir, '.nitro/types/nitro-graphql-client.d.ts')
151+
const customClientPath = join(projectDir, 'custom-types/client.d.ts')
152+
153+
expect(existsSync(defaultClientPath)).toBe(false)
154+
expect(existsSync(customClientPath)).toBe(false)
155+
})
156+
})
157+
158+
describe('typesDir placeholder', () => {
159+
let nitro: Nitro
160+
161+
beforeAll(async () => {
162+
cleanupGeneratedFiles()
163+
164+
nitro = await createNitro({
165+
rootDir: projectDir,
166+
dev: true,
167+
modules: [
168+
graphql({
169+
framework: 'graphql-yoga',
170+
types: {
171+
enabled: true,
172+
server: '{typesDir}/custom-server.d.ts',
173+
client: '{typesDir}/custom-client.d.ts',
174+
},
175+
}),
176+
],
177+
})
178+
179+
await prepare(nitro)
180+
await build(nitro)
181+
182+
const { generateServerTypes, generateClientTypes } = await import('../../src/nitro/codegen')
183+
await generateServerTypes(nitro)
184+
await generateClientTypes(nitro)
185+
}, 60000)
186+
187+
afterAll(async () => {
188+
await nitro?.close()
189+
})
190+
191+
it('should resolve {typesDir} placeholder correctly', () => {
192+
// typesDir defaults to {buildDir}/types - buildDir is node_modules/.nitro in test env
193+
const serverTypesPath = join(nitro.options.buildDir, 'types/custom-server.d.ts')
194+
const clientTypesPath = join(nitro.options.buildDir, 'types/custom-client.d.ts')
195+
196+
expect(existsSync(serverTypesPath)).toBe(true)
197+
expect(existsSync(clientTypesPath)).toBe(true)
198+
})
199+
200+
it('should contain correct type definitions', () => {
201+
const serverTypesPath = join(nitro.options.buildDir, 'types/custom-server.d.ts')
202+
const clientTypesPath = join(nitro.options.buildDir, 'types/custom-client.d.ts')
203+
204+
const serverContent = readFileSync(serverTypesPath, 'utf-8')
205+
expect(serverContent).toContain('Query')
206+
expect(serverContent).toContain('User')
207+
208+
const clientContent = readFileSync(clientTypesPath, 'utf-8')
209+
expect(clientContent).toContain('GetUser')
210+
})
211+
})
212+
})
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// THIS FILE IS GENERATED, DO NOT EDIT!
2+
/* eslint-disable eslint-comments/no-unlimited-disable */
3+
/* tslint:disable */
4+
/* eslint-disable */
5+
/* prettier-ignore */
6+
import type * as Types from '#graphql/client';
7+
8+
import type { ExecutionResult } from 'graphql';
9+
10+
export const GetUserDocument = /*#__PURE__*/ `
11+
query GetUser($id: ID!) {
12+
user(id: $id) {
13+
id
14+
name
15+
email
16+
}
17+
}
18+
`;
19+
export const GetHelloDocument = /*#__PURE__*/ `
20+
query GetHello {
21+
hello
22+
}
23+
`;
24+
export type Requester<C = {}, E = unknown> = <R, V>(doc: string, vars?: V, options?: C) => Promise<ExecutionResult<R, E>> | AsyncIterable<ExecutionResult<R, E>>
25+
export function getSdk<C, E>(requester: Requester<C, E>) {
26+
return {
27+
GetUser(variables: Types.GetUserQueryVariables, options?: C): Promise<ExecutionResult<Types.GetUserQuery, E>> {
28+
return requester<Types.GetUserQuery, Types.GetUserQueryVariables>(GetUserDocument, variables, options) as Promise<ExecutionResult<Types.GetUserQuery, E>>;
29+
},
30+
GetHello(variables?: Types.GetHelloQueryVariables, options?: C): Promise<ExecutionResult<Types.GetHelloQuery, E>> {
31+
return requester<Types.GetHelloQuery, Types.GetHelloQueryVariables>(GetHelloDocument, variables, options) as Promise<ExecutionResult<Types.GetHelloQuery, E>>;
32+
}
33+
};
34+
}
35+
export type Sdk = ReturnType<typeof getSdk>;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
query GetUser($id: ID!) {
2+
user(id: $id) {
3+
id
4+
name
5+
email
6+
}
7+
}
8+
9+
query GetHello {
10+
hello
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
type Query {
2+
hello: String!
3+
user(id: ID!): User
4+
}
5+
6+
type User {
7+
id: ID!
8+
name: String!
9+
email: String!
10+
}

0 commit comments

Comments
 (0)