Skip to content

Commit

Permalink
refactor: Switch to schema loaders from @gql.tada/internal (#277)
Browse files Browse the repository at this point in the history
  • Loading branch information
kitten authored Apr 4, 2024
1 parent e696b3a commit aa5b67d
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 164 deletions.
5 changes: 5 additions & 0 deletions .changeset/sixty-walls-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@0no-co/graphqlsp': patch
---

Switch to loading the schema with `@gql.tada/internal` utilities
4 changes: 2 additions & 2 deletions packages/graphqlsp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@
"@sindresorhus/fnv1a": "^2.0.0",
"@types/node": "^18.15.11",
"@types/node-fetch": "^2.6.3",
"graphql": "^16.8.1",
"graphql-language-service": "^5.2.0",
"lru-cache": "^10.0.1",
"typescript": "^5.3.3"
},
"dependencies": {
"@gql.tada/internal": "^0.1.0",
"@gql.tada/internal": "^0.1.2",
"graphql": "^16.8.1",
"node-fetch": "^2.0.0"
},
"peerDependencies": {
Expand Down
199 changes: 56 additions & 143 deletions packages/graphqlsp/src/graphql/getSchema.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import {
GraphQLSchema,
buildSchema,
buildClientSchema,
getIntrospectionQuery,
IntrospectionQuery,
introspectionFromSchema,
} from 'graphql';
import path from 'path';
import fetch from 'node-fetch';
import fs from 'fs';

import type { GraphQLSchema, IntrospectionQuery } from 'graphql';

import {
type SchemaOrigin,
type SchemaLoaderResult,
load,
resolveTypeScriptRootDir,
minifyIntrospection,
outputIntrospectionFile,
Expand All @@ -19,24 +16,18 @@ import { ts } from '../ts';
import { Logger } from '../index';

async function saveTadaIntrospection(
root: string,
schema: GraphQLSchema | IntrospectionQuery,
introspection: IntrospectionQuery,
tadaOutputLocation: string,
disablePreprocessing: boolean,
logger: Logger
) {
const introspection = !('__schema' in schema)
? introspectionFromSchema(schema, { descriptions: false })
: schema;

const minified = minifyIntrospection(introspection);

const contents = await outputIntrospectionFile(minified, {
const contents = outputIntrospectionFile(minified, {
fileType: tadaOutputLocation,
shouldPreprocess: !disablePreprocessing,
});

let output = path.resolve(root, tadaOutputLocation);
let output = tadaOutputLocation;
let stat: fs.Stats | undefined;

try {
Expand Down Expand Up @@ -67,147 +58,69 @@ async function saveTadaIntrospection(
logger(`Introspection saved to path @ ${output}`);
}

export type SchemaOrigin = {
url: string;
headers: Record<string, unknown>;
};
export interface SchemaRef {
current: GraphQLSchema | null;
version: number;
}

export const loadSchema = (
// TODO: abstract info away
info: ts.server.PluginCreateInfo,
schema: SchemaOrigin | string,
tadaOutputLocation: string | undefined,
origin: SchemaOrigin,
logger: Logger
): { current: GraphQLSchema | null; version: number } => {
const root =
resolveTypeScriptRootDir(
path => info.project.readFile(path),
info.project.getProjectName()
) || path.dirname(info.project.getProjectName());
logger('Got root-directory to resolve schema from: ' + root);
const ref: {
current: GraphQLSchema | null;
version: number;
prev: string | null;
} = {
current: null,
version: 0,
prev: null,
};
let url: URL | undefined;
let config: { headers: Record<string, unknown> } | undefined;
): SchemaRef => {
let loaderResult: SchemaLoaderResult | null = null;
const ref: SchemaRef = { current: null, version: 0 };

try {
if (typeof schema === 'object') {
url = new URL(schema.url);
config = { headers: schema.headers };
} else {
url = new URL(schema);
(async () => {
const rootPath =
(await resolveTypeScriptRootDir(info.project.getProjectName())) ||
path.dirname(info.project.getProjectName());
const tadaDisablePreprocessing =
info.config.tadaDisablePreprocessing ?? false;
const tadaOutputLocation =
info.config.tadaOutputLocation &&
path.resolve(rootPath, info.config.tadaOutputLocation);

logger('Got root-directory to resolve schema from: ' + rootPath);
logger('Resolving schema from "schema" config: ' + JSON.stringify(origin));

const loader = load({ origin, rootPath });

try {
logger(`Loading schema from "${origin}"`);
loaderResult = await loader.load();
} catch (error) {
logger(`Failed to load schema: ${error}`);
}
} catch (e) {}

if (url) {
const pollSchema = () => {
logger(`Fetching introspection from ${url!.toString()}`);
fetch(url!.toString(), {
method: 'POST',
headers: config
? {
...(config.headers || {}),
'Content-Type': 'application/json',
}
: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: getIntrospectionQuery({
descriptions: true,
schemaDescription: false,
inputValueDeprecation: false,
directiveIsRepeatable: false,
specifiedByUrl: false,
}),
}),
})
.then(response => {
logger(`Got response ${response.statusText} ${response.status}`);
if (response.ok) return response.json();
else return response.text();
})
.then(result => {
if (typeof result === 'string') {
logger(`Got error while fetching introspection ${result}`);
} else if (result.data) {
const introspection = (result as { data: IntrospectionQuery }).data;
const currentStringified = JSON.stringify(introspection);
if (ref.prev && ref.prev === currentStringified) {
return;
}

ref.prev = currentStringified;
try {
if (tadaOutputLocation) {
saveTadaIntrospection(
root,
introspection,
tadaOutputLocation,
info.config.tadaDisablePreprocessing ?? false,
logger
);
}

ref.current = buildClientSchema(introspection);
ref.version = ref.version + 1;
logger(`Got schema for ${url!.toString()}`);
} catch (e: any) {
logger(`Got schema error for ${e.message}`);
}
} else {
logger(`Got invalid response ${JSON.stringify(result)}`);
}
});

setTimeout(() => {
pollSchema();
}, 1000 * 60);
};

pollSchema();
} else if (typeof schema === 'string') {
const isJson = path.extname(schema) === '.json';
const resolvedPath = path.resolve(root, schema);
logger(`Getting schema from ${resolvedPath}`);

async function readSchema() {
const contents = fs.readFileSync(resolvedPath, 'utf-8');

const schemaOrIntrospection = isJson
? (JSON.parse(contents) as IntrospectionQuery)
: buildSchema(contents);

ref.version = ref.version + 1;
ref.current =
'__schema' in schemaOrIntrospection
? buildClientSchema(schemaOrIntrospection)
: schemaOrIntrospection;

if (loaderResult) {
ref.current = loaderResult && loaderResult.schema;
ref.version++;
if (tadaOutputLocation) {
saveTadaIntrospection(
root,
schemaOrIntrospection,
loaderResult.introspection,
tadaOutputLocation,
info.config.tadaDisablePreprocessing ?? false,
tadaDisablePreprocessing,
logger
);
}
}

readSchema();
fs.watchFile(resolvedPath, () => {
readSchema();
loader.notifyOnUpdate(result => {
logger(`Got schema for origin "${origin}"`);
ref.current = (loaderResult = result).schema;
ref.version++;
if (tadaOutputLocation) {
saveTadaIntrospection(
loaderResult.introspection,
tadaOutputLocation,
tadaDisablePreprocessing,
logger
);
}
});

logger(`Got schema and initialized watcher for ${schema}`);
}
})();

return ref;
};
20 changes: 7 additions & 13 deletions packages/graphqlsp/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { SchemaOrigin } from '@gql.tada/internal';

import { ts, init as initTypeScript } from './ts';
import { SchemaOrigin, loadSchema } from './graphql/getSchema';
import { loadSchema } from './graphql/getSchema';
import { getGraphQLCompletions } from './autoComplete';
import { getGraphQLQuickInfo } from './quickInfo';
import { ALL_DIAGNOSTICS, getGraphQLDiagnostics } from './diagnostics';
Expand All @@ -20,15 +22,15 @@ function createBasicDecorator(info: ts.server.PluginCreateInfo) {

export type Logger = (msg: string) => void;

type Config = {
schema: SchemaOrigin | string;
interface Config {
schema: SchemaOrigin;
tadaDisablePreprocessing?: boolean;
templateIsCallExpression?: boolean;
shouldCheckForColocatedFragments?: boolean;
template?: string;
trackFieldUsage?: boolean;
tadaOutputLocation?: string;
};
}

function create(info: ts.server.PluginCreateInfo) {
const logger: Logger = (msg: string) =>
Expand All @@ -49,15 +51,7 @@ function create(info: ts.server.PluginCreateInfo) {

const proxy = createBasicDecorator(info);

const schema = loadSchema(
info,
config.schema,
// TODO: either we check here for the client having a package.json
// with gql.tada and use a default file loc or we use a config
// option with a location
config.tadaOutputLocation,
logger
);
const schema = loadSchema(info, config.schema, logger);

proxy.getSemanticDiagnostics = (filename: string): ts.Diagnostic[] => {
const originalDiagnostics =
Expand Down
34 changes: 28 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit aa5b67d

Please sign in to comment.