Skip to content

Commit

Permalink
fix(internal): Improve loader by adding a merged loader and incorpora…
Browse files Browse the repository at this point in the history
…ting `SchemaOrigin` parsing (#165)
  • Loading branch information
kitten committed Mar 28, 2024
1 parent 5c645c6 commit 6d04115
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 159 deletions.
6 changes: 6 additions & 0 deletions .changeset/twelve-tips-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@gql.tada/cli-utils": patch
"@gql.tada/internal": patch
---

Update internal loader to merge them into one and incorporate `SchemaOrigin` parsing
7 changes: 4 additions & 3 deletions packages/cli-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { printSchema } from 'graphql';

import type { GraphQLSchema } from 'graphql';
import type { TsConfigJson } from 'type-fest';
import { resolveTypeScriptRootDir } from '@gql.tada/internal';
import { resolveTypeScriptRootDir, load } from '@gql.tada/internal';

import { getGraphQLSPConfig } from './lsp';
import { ensureTadaIntrospection, makeLoader } from './tada';
import { ensureTadaIntrospection } from './tada';

interface GenerateSchemaOptions {
headers?: Record<string, string>;
Expand All @@ -21,7 +21,8 @@ export async function generateSchema(
target: string,
{ headers, output, cwd = process.cwd() }: GenerateSchemaOptions
) {
const loader = makeLoader(cwd, headers ? { url: target, headers } : target);
const origin = headers ? { url: target, headers } : target;
const loader = load({ origin, rootPath: cwd });

let schema: GraphQLSchema | null;
try {
Expand Down
8 changes: 1 addition & 7 deletions packages/cli-utils/src/lsp.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import type { TsConfigJson } from 'type-fest';

export type SchemaOrigin =
| string
| {
url: string;
headers: HeadersInit;
};
import type { SchemaOrigin } from '@gql.tada/internal';

export type GraphQLSPConfig = {
name: string;
Expand Down
44 changes: 4 additions & 40 deletions packages/cli-utils/src/tada.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { promises as fs } from 'node:fs';
import path from 'node:path';
import type { IntrospectionQuery } from 'graphql';
import type { SchemaLoader } from '@gql.tada/internal';

import {
type SchemaOrigin,
minifyIntrospection,
outputIntrospectionFile,
loadFromSDL,
loadFromURL,
load,
} from '@gql.tada/internal';

import type { SchemaOrigin } from './lsp';

/**
* This function mimics the behavior of the LSP, this so we can ensure
* that gql.tada will work in any environment. The JetBrains IDE's do not
Expand All @@ -20,12 +17,12 @@ import type { SchemaOrigin } from './lsp';
* this function.
*/
export async function ensureTadaIntrospection(
schemaLocation: SchemaOrigin,
origin: SchemaOrigin,
outputLocation: string,
base: string = process.cwd(),
shouldPreprocess = true
) {
const loader = makeLoader(base, schemaLocation);
const loader = load({ origin, rootPath: base });

let introspection: IntrospectionQuery | null;
try {
Expand All @@ -52,36 +49,3 @@ export async function ensureTadaIntrospection(
console.error('Something went wrong while writing the introspection file', error);
}
}

const getURLConfig = (origin: SchemaOrigin) => {
if (typeof origin === 'string') {
try {
return { url: new URL(origin) };
} catch (_error) {
return null;
}
} else if (typeof origin.url === 'string') {
try {
return {
url: new URL(origin.url),
headers: origin.headers,
};
} catch (error) {
throw new Error(`Input URL "${origin.url}" is invalid`);
}
} else {
return null;
}
};

export function makeLoader(root: string, origin: SchemaOrigin): SchemaLoader {
const urlOrigin = getURLConfig(origin);
if (urlOrigin) {
return loadFromURL(urlOrigin);
} else if (typeof origin === 'string') {
const file = path.resolve(root, origin);
return loadFromSDL({ file, assumeValid: true });
} else {
throw new Error(`Configuration contains an invalid "schema" option`);
}
}
44 changes: 41 additions & 3 deletions packages/internal/src/loaders/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,41 @@
export type { SchemaLoader } from './types';
export { loadFromSDL } from './sdl';
export { loadFromURL } from './url';
export type * from './types';

import path from 'node:path';
import type { SchemaLoader, SchemaOrigin } from './types';
import { loadFromSDL } from './sdl';
import { loadFromURL } from './url';

export { loadFromSDL, loadFromURL };

const getURLConfig = (origin: SchemaOrigin | null) => {
try {
return (
origin && {
url: new URL(typeof origin === 'object' ? origin.url : origin),
headers: typeof origin === 'object' ? origin.headers : undefined,
}
);
} catch (_error) {
throw new Error(`Configuration contains an invalid "schema" option`);
}
};

export interface LoadConfig {
origin: SchemaOrigin;
rootPath?: string;
fetchInterval?: number;
assumeValid?: boolean;
}

export function load(config: LoadConfig): SchemaLoader {
const urlOrigin = getURLConfig(origin);
if (urlOrigin) {
return loadFromURL({ ...urlOrigin, interval: config.fetchInterval });
} else if (typeof origin === 'string') {
const file = config.rootPath ? path.resolve(config.rootPath, origin) : origin;
const assumeValid = config.assumeValid != null ? config.assumeValid : true;
return loadFromSDL({ file, assumeValid });
} else {
throw new Error(`Configuration contains an invalid "schema" option`);
}
}
98 changes: 50 additions & 48 deletions packages/internal/src/loaders/sdl.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { IntrospectionQuery, GraphQLSchema } from 'graphql';
import type { IntrospectionQuery } from 'graphql';
import { buildSchema, buildClientSchema, executeSync } from 'graphql';
import { CombinedError } from '@urql/core';
import fs from 'node:fs/promises';
import path from 'node:path';

import type { SupportedFeatures } from './query';
import { makeIntrospectionQuery } from './query';
import type { SupportedFeatures } from './query';

import type { SchemaLoader } from './types';
import type { SchemaLoader, SchemaLoaderResult, OnSchemaUpdate } from './types';

interface LoadFromSDLConfig {
assumeValid?: boolean;
Expand All @@ -21,68 +21,62 @@ const ALL_SUPPORTED_FEATURES: SupportedFeatures = {
};

export function loadFromSDL(config: LoadFromSDLConfig): SchemaLoader {
const subscriptions = new Set<() => void>();
const subscriptions = new Set<OnSchemaUpdate>();

let controller: AbortController | null = null;
let introspection: IntrospectionQuery | null = null;
let schema: GraphQLSchema | null = null;
let result: SchemaLoaderResult | null = null;

const load = async (): Promise<typeof result> => {
const ext = path.extname(config.file);
const data = await fs.readFile(config.file, { encoding: 'utf8' });
if (ext === '.json') {
let introspection: IntrospectionQuery | null = null;
try {
introspection = JSON.parse(data);
} catch (_error) {}
return (
introspection && {
introspection,
schema: buildClientSchema(introspection, { assumeValid: !!config.assumeValid }),
}
);
} else {
const schema = buildSchema(data, { assumeValidSDL: !!config.assumeValid });
const query = makeIntrospectionQuery(ALL_SUPPORTED_FEATURES);
const queryResult = executeSync({ schema, document: query });
if (queryResult.errors) {
throw new CombinedError({ graphQLErrors: queryResult.errors as any[] });
} else if (queryResult.data) {
const introspection = queryResult.data as unknown as IntrospectionQuery;
return { introspection, schema };
} else {
return null;
}
}
};

const watch = async () => {
controller = new AbortController();
const watcher = fs.watch(config.file, {
persistent: false,
signal: controller.signal,
persistent: false,
});

try {
for await (const event of watcher) {
if (event.eventType === 'rename' || !subscriptions.size) break;
for (const subscriber of subscriptions) subscriber();
for await (const _event of watcher) {
if ((result = await load())) {
for (const subscriber of subscriptions) subscriber(result);
}
}
} catch (error: any) {
if (error.name === 'AbortError') {
return;
} else {
throw error;
}
if (error.name !== 'AbortError') throw error;
} finally {
controller = null;
}
};

const introspect = async () => {
const ext = path.extname(config.file);
const data = await fs.readFile(config.file, { encoding: 'utf8' });
if (ext === '.json') {
introspection = JSON.parse(data) || null;
schema =
introspection && buildClientSchema(introspection, { assumeValid: !!config.assumeValid });
return introspection;
} else {
schema = buildSchema(data, { assumeValidSDL: !!config.assumeValid });
const query = makeIntrospectionQuery(ALL_SUPPORTED_FEATURES);
const result = executeSync({ schema, document: query });
if (result.errors) {
throw new CombinedError({ graphQLErrors: result.errors as any[] });
} else if (result.data) {
return (introspection = (result.data as any) || null);
} else {
return (introspection = null);
}
}
};

return {
async loadIntrospection() {
return introspect();
},
async loadSchema() {
if (schema) {
return schema;
} else {
await this.loadIntrospection();
return schema;
}
async load(reload?: boolean) {
return reload || !result ? (result = await load()) : result;
},
notifyOnUpdate(onUpdate) {
if (!subscriptions.size) watch();
Expand All @@ -94,5 +88,13 @@ export function loadFromSDL(config: LoadFromSDLConfig): SchemaLoader {
}
};
},
async loadIntrospection() {
const result = await this.load();
return result && result.introspection;
},
async loadSchema() {
const result = await this.load();
return result && result.schema;
},
};
}
20 changes: 19 additions & 1 deletion packages/internal/src/loaders/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
import type { IntrospectionQuery, GraphQLSchema } from 'graphql';

export interface SchemaLoaderResult {
introspection: IntrospectionQuery;
schema: GraphQLSchema;
}

export type OnSchemaUpdate = (result: SchemaLoaderResult) => void;

export interface SchemaLoader {
load(reload?: boolean): Promise<SchemaLoaderResult | null>;
notifyOnUpdate(onUpdate: OnSchemaUpdate): () => void;

/** @internal */
loadIntrospection(): Promise<IntrospectionQuery | null>;
/** @internal */
loadSchema(): Promise<GraphQLSchema | null>;
notifyOnUpdate(onUpdate: () => void): () => void;
}

export type SchemaOrigin =
| string
| {
url: string;
headers?: HeadersInit;
};
Loading

0 comments on commit 6d04115

Please sign in to comment.