From 2af94dff3b2cae476408c0c87d1f427f380c90fc Mon Sep 17 00:00:00 2001 From: Niktia Nigmatullin Date: Thu, 21 Nov 2024 02:12:47 +0500 Subject: [PATCH] feat: support glob paths in schema --- README.md | 2 +- src/index.ts | 2 +- src/utils/configPaths.ts | 16 ++-- .../match-on-glob-schema.spec.ts.snap | 33 +++++++ test/match-on-glob-schema/codegen.yml | 9 ++ .../match-on-glob-schema.spec.ts | 95 +++++++++++++++++++ 6 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 test/match-on-glob-schema/__snapshots__/match-on-glob-schema.spec.ts.snap create mode 100644 test/match-on-glob-schema/codegen.yml create mode 100644 test/match-on-glob-schema/match-on-glob-schema.spec.ts diff --git a/README.md b/README.md index 4f83ec1..ab4da71 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ codegen({ */ matchOnDocuments?: boolean; /** - * Run codegen when a schema matches. Only supports file path based schemas. + * Run codegen when a schema matches. * * @defaultValue `false` */ diff --git a/src/index.ts b/src/index.ts index e47fe53..b755544 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,7 +52,7 @@ export interface Options { */ matchOnDocuments?: boolean; /** - * Run codegen when a schema matches. Only supports file path based schemas. + * Run codegen when a schema matches. * * @defaultValue `false` */ diff --git a/src/utils/configPaths.ts b/src/utils/configPaths.ts index b2971e6..8ea6001 100644 --- a/src/utils/configPaths.ts +++ b/src/utils/configPaths.ts @@ -1,6 +1,6 @@ import { normalizePath } from 'vite'; -import { resolve } from 'node:path'; import type { CodegenContext } from '@graphql-codegen/cli'; +import type { Types } from '@graphql-codegen/plugin-helpers/typings/types'; export async function getDocumentPaths( context: CodegenContext, @@ -44,15 +44,19 @@ export async function getSchemaPaths( sourceSchemas.unshift(config.schema); } - const schemas = sourceSchemas + const normalized = sourceSchemas .filter((item): item is NonNullable => !!item) .flat(); - if (!schemas.length) return []; + if (!normalized.length) return []; + + const schemas = await context.loadSchema( + // loadSchema supports array of string, but typings are wrong + normalized as unknown as Types.Schema, + ); - return schemas - .filter((schema): schema is string => typeof schema === 'string') + return (schemas.extensions.sources as { name: string }[]) + .map(({ name = '' }) => name) .filter(Boolean) - .map((schema) => resolve(schema)) .map(normalizePath); } diff --git a/test/match-on-glob-schema/__snapshots__/match-on-glob-schema.spec.ts.snap b/test/match-on-glob-schema/__snapshots__/match-on-glob-schema.spec.ts.snap new file mode 100644 index 0000000..b2273f1 --- /dev/null +++ b/test/match-on-glob-schema/__snapshots__/match-on-glob-schema.spec.ts.snap @@ -0,0 +1,33 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`match-on-glob-schema > generates on schema change 1`] = ` +"export type Maybe = T | null; +export type InputMaybe = Maybe; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: { input: string; output: string; } + String: { input: string; output: string; } + Boolean: { input: boolean; output: boolean; } + Int: { input: number; output: number; } + Float: { input: number; output: number; } +}; + +export type Query = { + __typename?: 'Query'; + bar?: Maybe; + baz?: Maybe; + foo?: Maybe; + qux?: Maybe; +}; + +export type FooQueryVariables = Exact<{ [key: string]: never; }>; + + +export type FooQuery = { __typename?: 'Query', foo?: number | null }; +" +`; diff --git a/test/match-on-glob-schema/codegen.yml b/test/match-on-glob-schema/codegen.yml new file mode 100644 index 0000000..6b6bd00 --- /dev/null +++ b/test/match-on-glob-schema/codegen.yml @@ -0,0 +1,9 @@ +schema: + - ./test/match-on-glob-schema/dir-1/**/*.graphql + - ./test/match-on-glob-schema/dir-2/**/*.graphql +documents: ./test/match-on-glob-schema/graphql/**/*.graphql +generates: + ./test/match-on-glob-schema/generated/graphql.ts: + plugins: + - typescript + - typescript-operations 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 new file mode 100644 index 0000000..fa8ab20 --- /dev/null +++ b/test/match-on-glob-schema/match-on-glob-schema.spec.ts @@ -0,0 +1,95 @@ +import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; +import { createServer, UserConfig, ViteDevServer } from 'vite'; +import { promises as fs } from 'node:fs'; +import codegen from '../../src/index'; + +const TEST_PATH = './test/match-on-glob-schema' as const; +const DIR_PATH_1 = `${TEST_PATH}/dir-1` as const; +const DIR_PATH_2 = `${TEST_PATH}/dir-2` as const; +const SCHEMA_PATH_1 = `${DIR_PATH_1}/schema-1.graphql` as const; +const SCHEMA_PATH_2 = `${DIR_PATH_1}/schema-2.graphql` as const; +const SCHEMA_PATH_3 = `${DIR_PATH_2}/schema-1.graphql` as const; +const SCHEMA_PATH_4 = `${DIR_PATH_2}/schema-2.graphql` as const; +const DOCUMENT_PATH = `${TEST_PATH}/graphql` as const; +const OUTPUT_PATH = `${TEST_PATH}/generated` as const; +const OUTPUT_FILE_NAME = 'graphql.ts' as const; +const OUTPUT_FILE = `${OUTPUT_PATH}/${OUTPUT_FILE_NAME}` as const; + +const viteConfig = { + plugins: [ + codegen({ + runOnStart: false, + matchOnDocuments: false, + matchOnSchemas: true, + configFilePathOverride: `${TEST_PATH}/codegen.yml`, + }), + ], +} satisfies UserConfig; + +describe('match-on-glob-schema', () => { + let viteServer: ViteDevServer | null = null; + + const isFileGenerated = async (): Promise => { + try { + await fs.access(OUTPUT_FILE); + return true; + } catch (error) { + // ignore + } + + return new Promise((resolve, reject) => { + if (!viteServer) reject('Vite server not started'); + + viteServer?.watcher.on('add', (path: string) => { + if (path.includes(OUTPUT_FILE_NAME)) resolve(true); + }); + + setTimeout(() => reject('Generated file not found'), 5000); + }); + }; + + beforeAll(async () => { + // Files in dir1 + await fs.mkdir(DIR_PATH_1, { recursive: true }); + await fs.writeFile(SCHEMA_PATH_1, 'type Query { foo: String }'); + await fs.writeFile(SCHEMA_PATH_2, 'type Query { bar: String }'); + + // Files in dir2 + await fs.mkdir(DIR_PATH_2, { recursive: true }); + await fs.writeFile(SCHEMA_PATH_3, 'type Query { baz: String }'); + await fs.writeFile(SCHEMA_PATH_4, 'type Query { qux: String }'); + + await fs.mkdir(DOCUMENT_PATH, { recursive: true }); + await fs.writeFile(`${DOCUMENT_PATH}/Foo.graphql`, 'query Foo { foo }'); + viteServer = await createServer(viteConfig).then((s) => s.listen()); + }); + + afterAll(async () => { + await viteServer?.close(); + viteServer = null; + await fs.rm(DIR_PATH_1, { recursive: true }); + await fs.rm(DIR_PATH_2, { recursive: true }); + + await fs.rm(DOCUMENT_PATH, { recursive: true }); + }); + + afterEach(async () => { + await fs.rm(OUTPUT_PATH, { recursive: true }); + }); + + it('generates on schema change', async () => { + // Files in dir1 + await fs.writeFile(SCHEMA_PATH_1, 'type Query { foo: Int }'); + await fs.writeFile(SCHEMA_PATH_2, 'type Query { bar: Int }'); + + // Files in dir2 + await fs.writeFile(SCHEMA_PATH_3, 'type Query { baz: Int }'); + await fs.writeFile(SCHEMA_PATH_4, 'type Query { qux: Int }'); + + await isFileGenerated(); + + const file = await fs.readFile(OUTPUT_FILE, 'utf-8'); + + expect(file).toMatchSnapshot(); + }); +});