Mirror: TypeScript LSP plugin that finds GraphQL documents in your code and provides diagnostics, auto-complete and hover-information.

refactor: Switch to schema loaders from `@gql.tada/internal` (#277)

Changed files
+96 -162
.changeset
packages
graphqlsp
+5
.changeset/sixty-walls-mix.md
···
+
---
+
'@0no-co/graphqlsp': patch
+
---
+
+
Switch to loading the schema with `@gql.tada/internal` utilities
+2 -2
packages/graphqlsp/package.json
···
"@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": {
+54 -141
packages/graphqlsp/src/graphql/getSchema.ts
···
-
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,
···
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 {
···
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);
-
}
-
} catch (e) {}
+
(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);
-
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;
-
}
+
logger('Got root-directory to resolve schema from: ' + rootPath);
+
logger('Resolving schema from "schema" config: ' + JSON.stringify(origin));
-
ref.prev = currentStringified;
-
try {
-
if (tadaOutputLocation) {
-
saveTadaIntrospection(
-
root,
-
introspection,
-
tadaOutputLocation,
-
info.config.tadaDisablePreprocessing ?? false,
-
logger
-
);
-
}
+
const loader = load({ origin, rootPath });
-
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)}`);
-
}
-
});
+
try {
+
logger(`Loading schema from "${origin}"`);
+
loaderResult = await loader.load();
+
} catch (error) {
+
logger(`Failed to load schema: ${error}`);
+
}
-
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;
};
+7 -13
packages/graphqlsp/src/index.ts
···
+
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';
···
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) =>
···
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 =
+28 -6
pnpm-lock.yaml
···
packages/graphqlsp:
dependencies:
'@gql.tada/internal':
-
specifier: ^0.1.0
-
version: 0.1.0
+
specifier: ^0.1.2
+
version: 0.1.2(graphql@16.8.1)(typescript@5.3.3)
+
graphql:
+
specifier: ^16.8.1
+
version: 16.8.1
node-fetch:
specifier: ^2.0.0
version: 2.6.7
···
'@types/node-fetch':
specifier: ^2.6.3
version: 2.6.3
-
graphql:
-
specifier: ^16.8.1
-
version: 16.8.1
graphql-language-service:
specifier: ^5.2.0
version: 5.2.0(graphql@16.8.1)
···
/@0no-co/graphql.web@1.0.4(graphql@16.8.1):
resolution: {integrity: sha512-W3ezhHGfO0MS1PtGloaTpg0PbaT8aZSmmaerL7idtU5F7oCI+uu25k+MsMS31BVFlp4aMkHSrNRxiD72IlK8TA==}
+
peerDependencies:
+
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0
+
peerDependenciesMeta:
+
graphql:
+
optional: true
+
dependencies:
+
graphql: 16.8.1
+
+
/@0no-co/graphql.web@1.0.6(graphql@16.8.1):
+
resolution: {integrity: sha512-KZ7TnwMcQJcFgzjoY623AVxtlDQonkqp3rSz0wb15/jHPyU1v5gynUibEpuutDeoyGJ5Tp+FwxjGyDGDwq3vIw==}
peerDependencies:
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0
peerDependenciesMeta:
···
/@gql.tada/internal@0.1.0:
resolution: {integrity: sha512-FTvBVXVvt0xUo8hvRlwFoyeNXpUDqc+e20MzFkF8ozbsa5PoYb/gksmmnHMjUphsIq1H3Hq8o4RGstFN5LKH4w==}
dependencies:
+
graphql: 16.8.1
+
typescript: 5.3.3
+
dev: false
+
+
/@gql.tada/internal@0.1.2(graphql@16.8.1)(typescript@5.3.3):
+
resolution: {integrity: sha512-8I4Z1zxYYGK66FWdB3yIZBn3cITLPnciEgjChp3K2+Ha1e/AEBGtZv9AUlodraO/RZafDMkpFhoi+tMpluBjeg==}
+
peerDependencies:
+
graphql: ^16.8.1
+
typescript: ^5.3.3
+
dependencies:
+
'@0no-co/graphql.web': 1.0.6(graphql@16.8.1)
graphql: 16.8.1
typescript: 5.3.3
···
peerDependencies:
typescript: ^5.0.0
dependencies:
-
'@gql.tada/internal': 0.1.0
+
'@gql.tada/internal': 0.1.2(graphql@16.8.1)(typescript@5.3.3)
+
graphql: 16.8.1
node-fetch: 2.6.7
typescript: 5.3.3
transitivePeerDependencies: