Skip to content

Commit 2a97e17

Browse files
refactor: streamline file change handling by centralizing GraphQL scanning logic
1 parent 01d2b8c commit 2a97e17

File tree

2 files changed

+188
-16
lines changed

2 files changed

+188
-16
lines changed

src/nitro/setup/file-watcher.ts

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,12 @@ import {
1616
LOG_TAG,
1717
RESOLVER_EXTENSIONS,
1818
} from '../../core/constants'
19-
import { generateDirectiveSchemas } from '../../core/utils/directive-parser'
20-
import { NitroAdapter } from '../adapter'
2119
import { generateClientTypes, generateServerTypes } from '../codegen'
2220
import {
2321
DEFAULT_WATCHER_IGNORE_INITIAL,
2422
DEFAULT_WATCHER_PERSISTENT,
2523
} from '../config'
26-
import { resolveExtendConfig } from './extend-loader'
27-
import { shouldScanLocalFiles } from './scanner'
24+
import { performGraphQLScan, shouldScanLocalFiles } from './scanner'
2825

2926
const logger = consola.withTag(LOG_TAG)
3027

@@ -70,18 +67,8 @@ export function setupFileWatcher(nitro: Nitro, watchDirs: string[]): FSWatcher {
7067
pending.server = pending.client = false
7168

7269
if (changes.server) {
73-
const directivesResult = await NitroAdapter.scanDirectives(nitro)
74-
nitro.scanDirectives = directivesResult.items
75-
76-
// Generate directive schemas and write to .graphql/directives.graphql
77-
const directiveSchemas = await generateDirectiveSchemas(directivesResult.items, nitro.graphql.buildDir)
78-
nitro.graphql.directiveSchemas = directiveSchemas
79-
80-
const schemasResult = await NitroAdapter.scanSchemas(nitro)
81-
nitro.scanSchemas = schemasResult.items
82-
nitro.scanResolvers = (await NitroAdapter.scanResolvers(nitro)).items
83-
84-
await resolveExtendConfig(nitro, { silent: true })
70+
// Use centralized scan function that respects skipLocalScan
71+
await performGraphQLScan(nitro, { silent: true, isRescan: true })
8572

8673
logger.success('Types regenerated')
8774
await generateServerTypes(nitro, { silent: true })

tests/unit/setup/file-watcher.test.ts

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ vi.mock('../../../src/nitro/setup/extend-loader', () => ({
7474
// Mock scanner
7575
vi.mock('../../../src/nitro/setup/scanner', () => ({
7676
shouldScanLocalFiles: vi.fn(() => true),
77+
performGraphQLScan: vi.fn().mockResolvedValue(undefined),
7778
}))
7879

7980
/**
@@ -385,6 +386,190 @@ describe('setupFileWatcher', () => {
385386
expect(ignored('/project/server/graphql/config.json')).toBe(true)
386387
})
387388
})
389+
390+
describe('file change handling (processChanges)', () => {
391+
it('should call performGraphQLScan when server file changes', async () => {
392+
const { watch } = await import('chokidar')
393+
const { performGraphQLScan } = await import('../../../src/nitro/setup/scanner')
394+
395+
// Create a mock watcher that captures the 'all' event handler
396+
let allEventHandler: ((event: string, path: string) => void) | null = null
397+
const mockWatcher = {
398+
on: vi.fn((event: string, handler: (event: string, path: string) => void) => {
399+
if (event === 'all') {
400+
allEventHandler = handler
401+
}
402+
return mockWatcher
403+
}),
404+
close: vi.fn(),
405+
}
406+
vi.mocked(watch).mockReturnValue(mockWatcher as any)
407+
408+
const nitro = createMockNitro()
409+
const watchDirs = ['/project/server/graphql']
410+
411+
setupFileWatcher(nitro, watchDirs)
412+
413+
// Simulate a .graphql file change
414+
expect(allEventHandler).not.toBeNull()
415+
allEventHandler!('change', '/project/server/graphql/schema.graphql')
416+
417+
// Wait for debounced function to be called
418+
await new Promise(resolve => setTimeout(resolve, 10))
419+
420+
expect(performGraphQLScan).toHaveBeenCalledWith(nitro, { silent: true, isRescan: true })
421+
})
422+
423+
it('should call performGraphQLScan when resolver file changes', async () => {
424+
const { watch } = await import('chokidar')
425+
const { performGraphQLScan } = await import('../../../src/nitro/setup/scanner')
426+
427+
let allEventHandler: ((event: string, path: string) => void) | null = null
428+
const mockWatcher = {
429+
on: vi.fn((event: string, handler: (event: string, path: string) => void) => {
430+
if (event === 'all') {
431+
allEventHandler = handler
432+
}
433+
return mockWatcher
434+
}),
435+
close: vi.fn(),
436+
}
437+
vi.mocked(watch).mockReturnValue(mockWatcher as any)
438+
439+
const nitro = createMockNitro()
440+
const watchDirs = ['/project/server/graphql']
441+
442+
setupFileWatcher(nitro, watchDirs)
443+
444+
// Simulate a .resolver.ts file change
445+
allEventHandler!('change', '/project/server/graphql/user.resolver.ts')
446+
447+
await new Promise(resolve => setTimeout(resolve, 10))
448+
449+
expect(performGraphQLScan).toHaveBeenCalledWith(nitro, { silent: true, isRescan: true })
450+
})
451+
452+
it('should NOT call performGraphQLScan for non-graphql files', async () => {
453+
const { watch } = await import('chokidar')
454+
const { performGraphQLScan } = await import('../../../src/nitro/setup/scanner')
455+
456+
vi.mocked(performGraphQLScan).mockClear()
457+
458+
let allEventHandler: ((event: string, path: string) => void) | null = null
459+
const mockWatcher = {
460+
on: vi.fn((event: string, handler: (event: string, path: string) => void) => {
461+
if (event === 'all') {
462+
allEventHandler = handler
463+
}
464+
return mockWatcher
465+
}),
466+
close: vi.fn(),
467+
}
468+
vi.mocked(watch).mockReturnValue(mockWatcher as any)
469+
470+
const nitro = createMockNitro()
471+
const watchDirs = ['/project/server/graphql']
472+
473+
setupFileWatcher(nitro, watchDirs)
474+
475+
// Simulate a non-graphql file change
476+
allEventHandler!('change', '/project/server/graphql/utils.ts')
477+
478+
await new Promise(resolve => setTimeout(resolve, 10))
479+
480+
expect(performGraphQLScan).not.toHaveBeenCalled()
481+
})
482+
483+
it('should NOT call performGraphQLScan for sdk.ts files', async () => {
484+
const { watch } = await import('chokidar')
485+
const { performGraphQLScan } = await import('../../../src/nitro/setup/scanner')
486+
487+
vi.mocked(performGraphQLScan).mockClear()
488+
489+
let allEventHandler: ((event: string, path: string) => void) | null = null
490+
const mockWatcher = {
491+
on: vi.fn((event: string, handler: (event: string, path: string) => void) => {
492+
if (event === 'all') {
493+
allEventHandler = handler
494+
}
495+
return mockWatcher
496+
}),
497+
close: vi.fn(),
498+
}
499+
vi.mocked(watch).mockReturnValue(mockWatcher as any)
500+
501+
const nitro = createMockNitro()
502+
const watchDirs = ['/project/server/graphql']
503+
504+
setupFileWatcher(nitro, watchDirs)
505+
506+
// Simulate sdk.ts file change (should be ignored)
507+
allEventHandler!('change', '/project/graphql/sdk.ts')
508+
509+
await new Promise(resolve => setTimeout(resolve, 10))
510+
511+
expect(performGraphQLScan).not.toHaveBeenCalled()
512+
})
513+
514+
it('should use performGraphQLScan with isRescan:true to respect skipLocalScan during file changes', async () => {
515+
// This test verifies the fix for the bug where skipLocalScan was ignored during rescan.
516+
// Previously, processChanges called NitroAdapter.scanSchemas directly which ignored skipLocalScan.
517+
// Now it calls performGraphQLScan which properly handles skipLocalScan.
518+
const { watch } = await import('chokidar')
519+
const { performGraphQLScan } = await import('../../../src/nitro/setup/scanner')
520+
const { NitroAdapter } = await import('../../../src/nitro/adapter')
521+
522+
vi.mocked(performGraphQLScan).mockClear()
523+
vi.mocked(NitroAdapter.scanSchemas).mockClear()
524+
vi.mocked(NitroAdapter.scanResolvers).mockClear()
525+
vi.mocked(NitroAdapter.scanDirectives).mockClear()
526+
527+
let allEventHandler: ((event: string, path: string) => void) | null = null
528+
const mockWatcher = {
529+
on: vi.fn((event: string, handler: (event: string, path: string) => void) => {
530+
if (event === 'all') {
531+
allEventHandler = handler
532+
}
533+
return mockWatcher
534+
}),
535+
close: vi.fn(),
536+
}
537+
vi.mocked(watch).mockReturnValue(mockWatcher as any)
538+
539+
// Create nitro with skipLocalScan: true
540+
const nitro = createMockNitro({
541+
options: {
542+
rootDir: '/project',
543+
dev: true,
544+
framework: { name: 'nitro' },
545+
graphql: { skipLocalScan: true },
546+
ignore: [],
547+
} as any,
548+
graphql: {
549+
buildDir: '/project/.nitro',
550+
serverDir: '/project/server/graphql',
551+
clientDir: '/project/graphql',
552+
} as any,
553+
})
554+
// Watch extend package directory (simulating extend: ['./auth'])
555+
const watchDirs = ['/packages/auth/server/graphql']
556+
557+
setupFileWatcher(nitro, watchDirs)
558+
559+
// Simulate a file change in extend package (path includes 'server/graphql' so it's detected as server file)
560+
allEventHandler!('change', '/packages/auth/server/graphql/schema.graphql')
561+
562+
await new Promise(resolve => setTimeout(resolve, 10))
563+
564+
// Should call performGraphQLScan (which respects skipLocalScan)
565+
expect(performGraphQLScan).toHaveBeenCalledWith(nitro, { silent: true, isRescan: true })
566+
567+
// Should NOT call NitroAdapter methods directly (old buggy behavior)
568+
expect(NitroAdapter.scanSchemas).not.toHaveBeenCalled()
569+
expect(NitroAdapter.scanResolvers).not.toHaveBeenCalled()
570+
expect(NitroAdapter.scanDirectives).not.toHaveBeenCalled()
571+
})
572+
})
388573
})
389574

390575
describe('getWatchDirectories', () => {

0 commit comments

Comments
 (0)