From 363f2130bc6857ecd509d623eca2e35e36c36741 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Sat, 4 May 2024 23:19:19 +0100 Subject: [PATCH 1/7] Make rootPath input to loaders required --- packages/internal/src/loaders/index.ts | 2 +- packages/internal/src/loaders/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/internal/src/loaders/index.ts b/packages/internal/src/loaders/index.ts index 90e8a9a1..eecf2fb0 100644 --- a/packages/internal/src/loaders/index.ts +++ b/packages/internal/src/loaders/index.ts @@ -61,7 +61,7 @@ export function loadRef( const teardowns: (() => void)[] = []; let _loaders: { input: SingleSchemaInput; loader: SchemaLoader }[] | undefined; - const getLoaders = (config?: BaseLoadConfig) => { + const getLoaders = (config: BaseLoadConfig) => { if (!_loaders) { _loaders = (('schemas' in input && input.schemas) || []).map((input) => ({ input, diff --git a/packages/internal/src/loaders/types.ts b/packages/internal/src/loaders/types.ts index 51b0ad5a..ed4a4e75 100644 --- a/packages/internal/src/loaders/types.ts +++ b/packages/internal/src/loaders/types.ts @@ -26,7 +26,7 @@ export interface SchemaLoader { } export interface BaseLoadConfig { - rootPath?: string; + rootPath: string; fetchInterval?: number; assumeValid?: boolean; } From 44a726f8f1ad3e22f35001f61d8b52dc9b3d58ba Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Sat, 4 May 2024 23:46:19 +0100 Subject: [PATCH 2/7] Expand SDL loader for multiple files --- packages/internal/src/loaders/index.ts | 12 ++--- packages/internal/src/loaders/sdl.ts | 66 +++++++++++++++++++------- packages/internal/src/loaders/types.ts | 1 + 3 files changed, 55 insertions(+), 24 deletions(-) diff --git a/packages/internal/src/loaders/index.ts b/packages/internal/src/loaders/index.ts index eecf2fb0..89d53a3a 100644 --- a/packages/internal/src/loaders/index.ts +++ b/packages/internal/src/loaders/index.ts @@ -1,6 +1,5 @@ export type * from './types'; -import path from 'node:path'; import { loadFromSDL } from './sdl'; import { loadFromURL } from './url'; @@ -18,7 +17,7 @@ export { loadFromSDL, loadFromURL }; export const getURLConfig = (origin: SchemaOrigin | null) => { try { - return origin + return origin && !Array.isArray(origin) ? { url: new URL(typeof origin === 'object' ? origin.url : origin), headers: typeof origin === 'object' ? origin.headers : undefined, @@ -42,13 +41,12 @@ export function load(config: LoadConfig): SchemaLoader { interval: config.fetchInterval, name: config.name, }); - } else if (typeof config.origin === 'string') { - const file = config.rootPath ? path.resolve(config.rootPath, config.origin) : config.origin; - const assumeValid = config.assumeValid != null ? config.assumeValid : true; + } else if (typeof config.origin === 'string' || Array.isArray(config.origin)) { return loadFromSDL({ - file, - assumeValid, + assumeValid: config.assumeValid != null ? config.assumeValid : true, name: config.name, + rootPath: config.rootPath, + include: config.origin, }); } else { throw new Error(`Configuration contains an invalid "schema" option`); diff --git a/packages/internal/src/loaders/sdl.ts b/packages/internal/src/loaders/sdl.ts index 5ed2db13..62ac8c0c 100644 --- a/packages/internal/src/loaders/sdl.ts +++ b/packages/internal/src/loaders/sdl.ts @@ -1,6 +1,7 @@ import type { IntrospectionQuery } from 'graphql'; import { buildSchema, buildClientSchema, executeSync } from 'graphql'; import { CombinedError } from '@urql/core'; +import ts from 'typescript'; import fs from 'node:fs/promises'; import path from 'node:path'; @@ -8,22 +9,45 @@ import { makeIntrospectionQuery, getPeerSupportedFeatures } from './introspectio import type { SchemaLoader, SchemaLoaderResult, OnSchemaUpdate } from './types'; +const EXTENSIONS = ['.graphql', '.gql', '.json'] as const; +const EXCLUDE = ['**/node_modules'] as const; + +const readInclude = (rootPath: string, include: string[] | string) => { + const files = ts.sys.readDirectory( + rootPath, + EXTENSIONS, + EXCLUDE, + typeof include === 'string' ? [include] : include + ); + if (files.length === 0) { + throw new Error(`No schema input was found at "${rootPath}".`); + } else if (files.length > 1 && files.some((file) => path.extname(file) === '.json')) { + throw new Error( + 'Multiple schema inputs were passed, but at least one is a JSON file.\n' + + 'A JSON introspection schema cannot be combined with other schemas.' + ); + } + return files; +}; + interface LoadFromSDLConfig { + rootPath: string; + include: string | string[]; name?: string; assumeValid?: boolean; - file: string; } export function loadFromSDL(config: LoadFromSDLConfig): SchemaLoader { const subscriptions = new Set(); + let files: string[] | undefined; let controller: AbortController | null = null; let result: SchemaLoaderResult | null = null; const load = async (): Promise => { - const ext = path.extname(config.file); - const data = await fs.readFile(config.file, { encoding: 'utf8' }); - if (ext === '.json') { + files = readInclude(config.rootPath, config.include); + if (path.extname(files[0]) === '.json') { + const data = await fs.readFile(files[0], { encoding: 'utf8' }); const introspection = JSON.parse(data) as IntrospectionQuery | null; if (!introspection || !introspection.__schema) { throw new Error( @@ -39,6 +63,9 @@ export function loadFromSDL(config: LoadFromSDLConfig): SchemaLoader { schema: buildClientSchema(introspection, { assumeValid: !!config.assumeValid }), }; } else { + const data = (await Promise.all(files.map((file) => fs.readFile(file, 'utf-8')))).join( + '\n\n' + ); const schema = buildSchema(data, { assumeValidSDL: !!config.assumeValid }); const query = makeIntrospectionQuery(getPeerSupportedFeatures()); const queryResult = executeSync({ schema, document: query }); @@ -60,21 +87,26 @@ export function loadFromSDL(config: LoadFromSDLConfig): SchemaLoader { }; const watch = async () => { + if (!files) return; controller = new AbortController(); - const watcher = fs.watch(config.file, { - signal: controller.signal, - persistent: false, - }); - try { - for await (const _event of watcher) { - if ((result = await load())) { - for (const subscriber of subscriptions) subscriber(result); + for (const file of files) { + const watcher = fs.watch(file, { + signal: controller!.signal, + persistent: false, + }); + (async () => { + try { + for await (const _event of watcher) { + if ((result = await load())) { + for (const subscriber of subscriptions) subscriber(result); + } + } + } catch (error: any) { + if (error.name !== 'AbortError') throw error; + } finally { + controller = null; } - } - } catch (error: any) { - if (error.name !== 'AbortError') throw error; - } finally { - controller = null; + })(); } }; diff --git a/packages/internal/src/loaders/types.ts b/packages/internal/src/loaders/types.ts index ed4a4e75..3c73d183 100644 --- a/packages/internal/src/loaders/types.ts +++ b/packages/internal/src/loaders/types.ts @@ -56,6 +56,7 @@ export interface SchemaRef { export type SchemaOrigin = | string + | string[] | { url: string; headers?: HeadersInit; From 06c289b0d2e23c38168c06513a052e26b872b7df Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Sat, 4 May 2024 23:48:28 +0100 Subject: [PATCH 3/7] Update config validation --- packages/internal/src/config.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/internal/src/config.ts b/packages/internal/src/config.ts index e5027945..9c3a50d4 100644 --- a/packages/internal/src/config.ts +++ b/packages/internal/src/config.ts @@ -48,6 +48,11 @@ const parseSchemaConfig = (input: unknown, rootPath: string): SchemaConfig => { throw new TadaError(`Schema is not configured properly (Received: ${input})`); } + if ('schema' in input && input.schema && Array.isArray(input.schema)) { + if (input.schema.some((include) => typeof include !== 'string')) { + throw new TadaError('All entries in `schema` array must be file paths'); + } + } if ('schema' in input && input.schema && typeof input.schema === 'object') { const { schema } = input; if (!('url' in schema)) { From 79aa7138d16a2a07e6652d18ba492ffc5acda848 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Sun, 5 May 2024 00:01:12 +0100 Subject: [PATCH 4/7] Support multiple file inputs in `generate schema` command --- packages/cli-utils/src/commands/generate-schema/index.ts | 4 ++-- .../cli-utils/src/commands/generate-schema/runner.ts | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/cli-utils/src/commands/generate-schema/index.ts b/packages/cli-utils/src/commands/generate-schema/index.ts index 21c13f04..c52b0332 100644 --- a/packages/cli-utils/src/commands/generate-schema/index.ts +++ b/packages/cli-utils/src/commands/generate-schema/index.ts @@ -24,9 +24,9 @@ const parseHeaders = ( export class GenerateSchema extends Command { static paths = [['generate-schema'], ['generate', 'schema']]; - input = Option.String({ + input = Option.Rest({ name: 'schema', - required: true, + required: 1, }); tsconfig = Option.String('--tsconfig,-c', { diff --git a/packages/cli-utils/src/commands/generate-schema/runner.ts b/packages/cli-utils/src/commands/generate-schema/runner.ts index bfad005c..55f683e0 100644 --- a/packages/cli-utils/src/commands/generate-schema/runner.ts +++ b/packages/cli-utils/src/commands/generate-schema/runner.ts @@ -11,7 +11,7 @@ import * as logger from './logger'; export interface SchemaOptions { /** The filename to a `.graphql` SDL file, introspection JSON, or URL to a GraphQL API to introspect. */ - input: string; + input: string | string[]; /** Object of headers to send when introspection a GraphQL API. */ headers: Record | undefined; /** The filename to write the GraphQL SDL file to. @@ -23,7 +23,9 @@ export interface SchemaOptions { } export async function* run(tty: TTY, opts: SchemaOptions): AsyncIterable { - const origin = opts.headers ? { url: opts.input, headers: opts.headers } : opts.input; + const origin = opts.headers + ? { url: Array.isArray(opts.input) ? opts.input[0] : opts.input, headers: opts.headers } + : opts.input; const loader = load({ rootPath: process.cwd(), origin }); let schema: GraphQLSchema; @@ -52,7 +54,8 @@ export async function* run(tty: TTY, opts: SchemaOptions): AsyncIterable Date: Sun, 5 May 2024 00:02:54 +0100 Subject: [PATCH 5/7] Update CLI reference --- website/reference/gql-tada-cli.md | 52 +++++++++++++++---------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/website/reference/gql-tada-cli.md b/website/reference/gql-tada-cli.md index eb98708b..094eee6b 100644 --- a/website/reference/gql-tada-cli.md +++ b/website/reference/gql-tada-cli.md @@ -68,12 +68,12 @@ When this command is run inside a GitHub Action, [workflow commands](https://doc ### `generate-schema` -| Option | Description | -| --------------- | ---------------------------------------------------------------------------------------------------- | -| `schema` | URL to a GraphQL API or a path to a `.graphql` SDL file or introspection JSON. | -| `--tsconfig,-c` | Optionally, a `tsconfig.json` file to use instead of an automatically discovered one. | -| `--output,-o` | An output location to write the `.graphql` SDL file to. (Default: The `schema` configuration option) | -| `--header,` | A `key:value` header entry to use when retrieving the introspection from a GraphQL API. | +| Option | Description | +| --------------- | ----------------------------------------------------------------------------------------------------- | +| `schema` | URL to a GraphQL API or a path/glob to `.graphql` SDL files, or a path to an introspection JSON file. | +| `--tsconfig,-c` | Optionally, a `tsconfig.json` file to use instead of an automatically discovered one. | +| `--output,-o` | An output location to write the `.graphql` SDL file to. (Default: The `schema` configuration option) | +| `--header,` | A `key:value` header entry to use when retrieving the introspection from a GraphQL API. | Oftentimes, an API may not be running in development, is maintained in a separate repository, or requires authorization headers, and specifying a URL in the `schema` configuration can slow down development. @@ -121,12 +121,12 @@ When this command is run inside a GitHub Action, [workflow commands](https://doc ### `generate-persisted` -| Option | Description | -| ------------------- | ------------------------------------------------------------------------------------------------ | -| `--disable-normalization` | Whether to disable normalizing the GraphQL document. (Default: false) | -| `--tsconfig,-c` | Optionally, a `tsconfig.json` file to use instead of an automatically discovered one. | -| `--fail-on-warn,-w` | Triggers an error and a non-zero exit code if any warnings have been reported. | -| `--output,-o` | Specify where to output the file to. (Default: The `tadaPersistedLocation` configuration option) | +| Option | Description | +| ------------------------- | ------------------------------------------------------------------------------------------------ | +| `--disable-normalization` | Whether to disable normalizing the GraphQL document. (Default: false) | +| `--tsconfig,-c` | Optionally, a `tsconfig.json` file to use instead of an automatically discovered one. | +| `--fail-on-warn,-w` | Triggers an error and a non-zero exit code if any warnings have been reported. | +| `--output,-o` | Specify where to output the file to. (Default: The `tadaPersistedLocation` configuration option) | The `gql.tada generate-persisted` command will scan your code for `graphql.persisted()` calls and generate a JSON manifest file containing a mapping of document IDs to the GraphQL document strings. @@ -171,13 +171,13 @@ await generateOutput({ ### `generatePersisted()` -| | Description | -| ------------------- | --------------------------------------------------------------------------------------------------------------------- | -| `disableNormalization` | Disables normalizing the GraphQL document | -| `output` option | The filename to write the persisted JSON manifest file to (Default: the `tadaPersistedLocation` configuration option) | -| `tsconfig` option | The `tsconfig.json` to use instead of an automatically discovered one. | -| `failOnWarn` option | Whether to throw an error instead of logging warnings. | -| returns | A `Promise` that resolves when the task completes. | +| | Description | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------- | +| `disableNormalization` | Disables normalizing the GraphQL document | +| `output` option | The filename to write the persisted JSON manifest file to (Default: the `tadaPersistedLocation` configuration option) | +| `tsconfig` option | The `tsconfig.json` to use instead of an automatically discovered one. | +| `failOnWarn` option | Whether to throw an error instead of logging warnings. | +| returns | A `Promise` that resolves when the task completes. | The `generatePersisted()` function will scan your code for `graphql.persisted()` calls and generate a JSON manifest file containing a mapping of document IDs to the GraphQL document strings. @@ -200,13 +200,13 @@ await generatePersisted({ ### `generateSchema()` -| | Description | -| ----------------- | ------------------------------------------------------------------------------------------------------ | -| `input` option | The filename to a `.graphql` SDL file, introspection JSON, or URL to a GraphQL API to introspect. | -| `headers` option | Optionally, an object of headers to send when introspecting a GraphQL API. | -| `output` option | The filename to write the persisted JSON manifest file to (Default: the `schema` configuration option) | -| `tsconfig` option | The `tsconfig.json` to use instead of an automatically discovered one. | -| returns | A `Promise` that resolves when the task completes. | +| | Description | +| ----------------- | ------------------------------------------------------------------------------------------------------------------- | +| `input` option | The path/glob to `.graphql` SDL files, a path to an introspection JSON file, or URL to a GraphQL API to introspect. | +| `headers` option | Optionally, an object of headers to send when introspecting a GraphQL API. | +| `output` option | The filename to write the persisted JSON manifest file to (Default: the `schema` configuration option) | +| `tsconfig` option | The `tsconfig.json` to use instead of an automatically discovered one. | +| returns | A `Promise` that resolves when the task completes. | The `generateSchema()` function introspects a targeted GraphQL API by URL, a `.graphql` SDL or introspection JSON file, and outputs a `.graphql` SDL file. Generating a `.graphql` SDL file is From 11ebd7793503deee248bee2dad8d9e863a9da5d3 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Sun, 5 May 2024 00:04:53 +0100 Subject: [PATCH 6/7] Update config format doc --- website/reference/config-format.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/reference/config-format.md b/website/reference/config-format.md index 2726848c..1b1c16b2 100644 --- a/website/reference/config-format.md +++ b/website/reference/config-format.md @@ -86,7 +86,7 @@ the main plugin config or inside the `schemas[]` array items. The `schema` option specifies how to load your GraphQL schema and currently allows for three different schema formats. It accepts either: -- a path to a `.graphql` file containing a schema definition (in GraphQL SDL format) +- a path or glob to a `.graphql` file containing a schema definition (in GraphQL SDL format) - a path to a `.json` file containing a schema’s introspection query data - a URL to a GraphQL API that can be introspected @@ -149,6 +149,10 @@ for three different schema formats. It accepts either: ``` ::: +If your schema consists of multiple `.graphql` files, you may pass +an array of paths, a glob, or an array of mixed paths and globs +to your SDL files. + This option is used by both the `gql.tada` CLI and `@0no-co/graphqlsp` and is required for all diagnostics and in-editor support to work. From fd4e02f71e3c10232f47184c33e636eae5a34bc4 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Sun, 5 May 2024 00:07:03 +0100 Subject: [PATCH 7/7] Add changeset --- .changeset/angry-dancers-unite.md | 5 +++++ .changeset/two-parents-tie.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/angry-dancers-unite.md create mode 100644 .changeset/two-parents-tie.md diff --git a/.changeset/angry-dancers-unite.md b/.changeset/angry-dancers-unite.md new file mode 100644 index 00000000..c59baa0b --- /dev/null +++ b/.changeset/angry-dancers-unite.md @@ -0,0 +1,5 @@ +--- +"@gql.tada/cli-utils": minor +--- + +Allow `generate-schema` to accept globs or multiple paths/globs. The command will therefore now be able to merge separate SDL files into a single one. diff --git a/.changeset/two-parents-tie.md b/.changeset/two-parents-tie.md new file mode 100644 index 00000000..35d691c7 --- /dev/null +++ b/.changeset/two-parents-tie.md @@ -0,0 +1,5 @@ +--- +"@gql.tada/internal": minor +--- + +Support globs and arrays of paths/globs to be passed to the `schema` option. This allows the local schema to be extended or a schema with split SDL files to be configured with `gql.tada` without any extra tooling.