diff --git a/src/index.ts b/src/index.ts index ce052e6..91bf4b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,14 +5,10 @@ import { generate, loadContext, } from '@graphql-codegen/cli'; -import { - type FileMatcher, - isCodegenConfig, - isGraphQLDocument, - isGraphQLSchema, -} from './utils/fileMatchers'; +import { isCodegenConfig } from './utils/fileMatchers'; import { isBuildMode, isServeMode, type ViteMode } from './utils/viteModes'; import { debugLog } from './utils/debugLog'; +import { createMatchCache } from './utils/matchCache'; export interface Options { /** @@ -181,67 +177,69 @@ export function GraphQLCodegen(options?: Options): Plugin { configureServer(server) { if (!enableWatcher) return; - const listener = async (filePath = '') => { - log('File changed:', filePath); + const matchCache = createMatchCache(codegenContext, { + matchOnDocuments, + matchOnSchemas, + }); - const isConfig = await isCodegenConfig(filePath, codegenContext); + async function checkFile(filePath: string) { + log(`Checking file: ${filePath}`); + + if (matchCache.has(filePath)) { + log('File is in match cache'); + + try { + await generateWithOverride(configOverrideWatcher); + log('Generation successful in file watcher'); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + // GraphQL Codegen handles logging useful errors + log('Generation failed in file watcher'); + } - if (isConfig) { - log('Codegen config file changed, restarting vite'); - server.restart(); return; } - const matchers: [ - enabled: boolean, - name: string, - matcher: FileMatcher, - ][] = [ - [matchOnDocuments, 'document', isGraphQLDocument], - [matchOnSchemas, 'schema', isGraphQLSchema], - ]; - - const matcherResults = await Promise.all( - matchers.map(async ([enabled, name, matcher]) => { - if (!enabled) { - log(`Check for ${name} file skipped in file watcher by config`); - return false; - } + if (isCodegenConfig(filePath, codegenContext)) { + log('Codegen config file matched, restarting vite'); + server.restart(); + return; + } - try { - const isMatch = await matcher(filePath, codegenContext); + log('File did not match'); + } - log(`Check for ${name} file successful in file watcher`); + async function initializeWatcher() { + try { + log('Match cache initialing'); + await matchCache.init(); + log('Match cache initialized'); + } catch (error) { + log('Match cache initialization failed', error); + } - if (isMatch) log(`File matched a graphql ${name}`); - else log(`File did not match a graphql ${name}`); + server.watcher.on('add', async (filePath) => { + log(`File added: ${filePath}`); - return isMatch; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { - // GraphQL Codegen handles logging useful errors - log(`Check for ${name} file failed in file watcher`); - return false; - } - }), - ); + try { + log('Match cache refreshing'); + await matchCache.refresh(); + log('Match cache refreshed'); + } catch (error) { + log('Match cache refresh failed', error); + } - const isMatch = matcherResults.some((result) => result); + await checkFile(filePath); + }); - if (!isMatch) return; + server.watcher.on('change', async (filePath) => { + log(`File changed: ${filePath}`); - try { - await generateWithOverride(configOverrideWatcher); - log('Generation successful in file watcher'); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { - // GraphQL Codegen handles logging useful errors - log('Generation failed in file watcher'); - } - }; + await checkFile(filePath); + }); + } - server.watcher.on('add', listener); - server.watcher.on('change', listener); + initializeWatcher(); }, } as const satisfies Plugin; } diff --git a/src/utils/debugLog.ts b/src/utils/debugLog.ts index 5a70c73..4a6d65d 100644 --- a/src/utils/debugLog.ts +++ b/src/utils/debugLog.ts @@ -3,8 +3,9 @@ const BRIGHT = '\x1b[1m'; const DIM = '\x1b[2m'; const FG_CYAN = '\x1b[36m'; +const LOG_PREFIX = + `${FG_CYAN}${BRIGHT}VITE PLUGIN GRAPHQL CODEGEN${RESET} ` as const; + export function debugLog(...args: unknown[]) { - const LOG_PREFIX = - `${FG_CYAN}${BRIGHT}VITE PLUGIN GRAPHQL CODEGEN${RESET} ` as const; console.log(LOG_PREFIX, DIM, ...args, RESET); } diff --git a/src/utils/fileMatchers.ts b/src/utils/fileMatchers.ts index ca8db5b..ac588ff 100644 --- a/src/utils/fileMatchers.ts +++ b/src/utils/fileMatchers.ts @@ -1,34 +1,8 @@ import { normalizePath } from 'vite'; import type { CodegenContext } from '@graphql-codegen/cli'; -import { getDocumentPaths, getSchemaPaths } from '../utils/configPaths'; -export type FileMatcher = ( - filePath: string, - context: CodegenContext, -) => Promise; - -export const isCodegenConfig: FileMatcher = async (filePath, context) => { +export function isCodegenConfig(filePath: string, context: CodegenContext) { if (!context.filepath) return false; return normalizePath(filePath) === normalizePath(context.filepath); -}; - -export const isGraphQLDocument: FileMatcher = async (filePath, context) => { - const documentPaths = await getDocumentPaths(context); - - if (!documentPaths.length) return false; - - const normalizedFilePath = normalizePath(filePath); - - return documentPaths.some((path) => normalizedFilePath.includes(path)); -}; - -export const isGraphQLSchema: FileMatcher = async (filePath, context) => { - const schemaPaths = await getSchemaPaths(context); - - if (!schemaPaths.length) return false; - - const normalizedFilePath = normalizePath(filePath); - - return schemaPaths.some((path) => normalizedFilePath.includes(path)); -}; +} diff --git a/src/utils/matchCache.ts b/src/utils/matchCache.ts new file mode 100644 index 0000000..2255764 --- /dev/null +++ b/src/utils/matchCache.ts @@ -0,0 +1,33 @@ +import type { CodegenContext } from '@graphql-codegen/cli'; +import { getDocumentPaths, getSchemaPaths } from './configPaths'; +import { normalizePath } from 'vite'; +import type { Options } from '..'; + +export function createMatchCache( + context: CodegenContext, + options: Pick, 'matchOnDocuments' | 'matchOnSchemas'>, +) { + const cache = new Set(); + + const refresh = async () => { + const matchers = [] as Promise[]; + if (options.matchOnDocuments) matchers.push(getDocumentPaths(context)); + if (options.matchOnSchemas) matchers.push(getSchemaPaths(context)); + + const results = await Promise.all(matchers); + + const entries = results.flat().map(normalizePath); + + cache.clear(); + + for (const entry of entries) { + cache.add(entry); + } + }; + + return { + init: refresh, + refresh, + has: (filePath: string) => cache.has(normalizePath(filePath)), + }; +} diff --git a/test/codegen-config-file/codegen-config-file.spec.ts b/test/codegen-config-file/codegen-config-file.spec.ts index a04cb2f..34f14c3 100644 --- a/test/codegen-config-file/codegen-config-file.spec.ts +++ b/test/codegen-config-file/codegen-config-file.spec.ts @@ -32,6 +32,7 @@ describe('codegen-config-file', () => { }); it('generates', async () => { + await new Promise((resolve) => setTimeout(resolve, 200)); const file = await fs.readFile(OUTPUT_FILE, 'utf-8'); expect(file).toMatchSnapshot(); diff --git a/test/graphql-config-file/graphql-config-file.spec.ts b/test/graphql-config-file/graphql-config-file.spec.ts index c637eb5..b340fb6 100644 --- a/test/graphql-config-file/graphql-config-file.spec.ts +++ b/test/graphql-config-file/graphql-config-file.spec.ts @@ -32,6 +32,7 @@ describe('graphql-config-file', () => { }); it('generates', async () => { + await new Promise((resolve) => setTimeout(resolve, 200)); const file = await fs.readFile(OUTPUT_FILE, 'utf-8'); expect(file).toMatchSnapshot(); diff --git a/test/inline-config/inline-config.spec.ts b/test/inline-config/inline-config.spec.ts index 8a7feaf..366db80 100644 --- a/test/inline-config/inline-config.spec.ts +++ b/test/inline-config/inline-config.spec.ts @@ -51,6 +51,7 @@ describe('inline-config', () => { }); it('generates', async () => { + await new Promise((resolve) => setTimeout(resolve, 200)); const file = await fs.readFile(OUTPUT_FILE, 'utf-8'); expect(file).toMatchSnapshot(); diff --git a/test/main/main.spec.ts b/test/main/main.spec.ts index a520b67..30118bf 100644 --- a/test/main/main.spec.ts +++ b/test/main/main.spec.ts @@ -22,6 +22,7 @@ describe('main', () => { const isFileGenerated = async (): Promise => { try { + await new Promise((resolve) => setTimeout(resolve, 200)); await fs.access(OUTPUT_FILE); return true; // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/test/match-on-glob-schema/match-on-glob-schema.spec.ts b/test/match-on-glob-schema/match-on-glob-schema.spec.ts index d64f8ab..445216c 100644 --- a/test/match-on-glob-schema/match-on-glob-schema.spec.ts +++ b/test/match-on-glob-schema/match-on-glob-schema.spec.ts @@ -31,6 +31,7 @@ describe('match-on-glob-schema', () => { const isFileGenerated = async (): Promise => { try { + await new Promise((resolve) => setTimeout(resolve, 200)); await fs.access(OUTPUT_FILE); return true; // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/test/match-on-schema/match-on-schema.spec.ts b/test/match-on-schema/match-on-schema.spec.ts index 33409dd..5dacf66 100644 --- a/test/match-on-schema/match-on-schema.spec.ts +++ b/test/match-on-schema/match-on-schema.spec.ts @@ -26,6 +26,7 @@ describe('match-on-schema', () => { const isFileGenerated = async (): Promise => { try { + await new Promise((resolve) => setTimeout(resolve, 200)); await fs.access(OUTPUT_FILE); return true; // eslint-disable-next-line @typescript-eslint/no-unused-vars