From 0f9ce77e2883bd59b24d655496f0520074f3c338 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Fri, 14 Apr 2023 20:39:39 +0200 Subject: [PATCH] resolve introspection from url --- .changeset/five-dancers-refuse.md | 5 + example/src/index.generated.ts | 237 ++++++++++++++++++++++-------- example/tsconfig.json | 22 +-- example/yarn.lock | 10 +- package.json | 4 +- pnpm-lock.yaml | 50 +++++++ src/getSchema.ts | 78 ++++++++-- src/index.ts | 14 +- 8 files changed, 329 insertions(+), 91 deletions(-) create mode 100644 .changeset/five-dancers-refuse.md diff --git a/.changeset/five-dancers-refuse.md b/.changeset/five-dancers-refuse.md new file mode 100644 index 00000000..1b97278d --- /dev/null +++ b/.changeset/five-dancers-refuse.md @@ -0,0 +1,5 @@ +--- +'@0no-co/graphqlsp': minor +--- + +Add ability to specify a URL for your schema, GraphQLSP will then fetch the introspection from the specified URL diff --git a/example/src/index.generated.ts b/example/src/index.generated.ts index 89382ad4..311118eb 100644 --- a/example/src/index.generated.ts +++ b/example/src/index.generated.ts @@ -1,9 +1,15 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; 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 Exact = { + [K in keyof T]: T[K]; +}; +export type MakeOptional = Omit & { + [SubKey in K]?: Maybe; +}; +export type MakeMaybe = Omit & { + [SubKey in K]: Maybe; +}; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; @@ -13,18 +19,33 @@ export type Scalars = { Float: number; }; +/** Elemental property associated with either a Pokémon or one of their moves. */ +export type PokemonType = + | 'Grass' + | 'Poison' + | 'Fire' + | 'Flying' + | 'Water' + | 'Bug' + | 'Normal' + | 'Electric' + | 'Ground' + | 'Fairy' + | 'Fighting' + | 'Psychic' + | 'Rock' + | 'Steel' + | 'Ice' + | 'Ghost' + | 'Dragon' + | 'Dark'; + /** Move a Pokémon can perform with the associated damage and type. */ export type Attack = { __typename?: 'Attack'; - damage?: Maybe; name?: Maybe; type?: Maybe; -}; - -export type AttacksConnection = { - __typename?: 'AttacksConnection'; - fast?: Maybe>>; - special?: Maybe>>; + damage?: Maybe; }; /** Requirement that prevents an evolution through regular means of levelling up. */ @@ -34,88 +55,178 @@ export type EvolutionRequirement = { name?: Maybe; }; +export type PokemonDimension = { + __typename?: 'PokemonDimension'; + minimum?: Maybe; + maximum?: Maybe; +}; + +export type AttacksConnection = { + __typename?: 'AttacksConnection'; + fast?: Maybe>>; + special?: Maybe>>; +}; + export type Pokemon = { __typename?: 'Pokemon'; - attacks?: Maybe; - /** @deprecated And this is the reason why */ + id: Scalars['ID']; + name: Scalars['String']; classification?: Maybe; + types?: Maybe>>; + resistant?: Maybe>>; + weaknesses?: Maybe>>; evolutionRequirements?: Maybe>>; - evolutions?: Maybe>>; + weight?: Maybe; + height?: Maybe; + attacks?: Maybe; /** Likelihood of an attempt to catch a Pokémon to fail. */ fleeRate?: Maybe; - height?: Maybe; - id: Scalars['ID']; /** Maximum combat power a Pokémon may achieve at max level. */ maxCP?: Maybe; /** Maximum health points a Pokémon may achieve at max level. */ maxHP?: Maybe; - name: Scalars['String']; - resistant?: Maybe>>; - types?: Maybe>>; - weaknesses?: Maybe>>; - weight?: Maybe; -}; - -export type PokemonDimension = { - __typename?: 'PokemonDimension'; - maximum?: Maybe; - minimum?: Maybe; + evolutions?: Maybe>>; }; -/** Elemental property associated with either a Pokémon or one of their moves. */ -export type PokemonType = - | 'Bug' - | 'Dark' - | 'Dragon' - | 'Electric' - | 'Fairy' - | 'Fighting' - | 'Fire' - | 'Flying' - | 'Ghost' - | 'Grass' - | 'Ground' - | 'Ice' - | 'Normal' - | 'Poison' - | 'Psychic' - | 'Rock' - | 'Steel' - | 'Water'; - export type Query = { __typename?: 'Query'; - /** Get a single Pokémon by its ID, a three character long identifier padded with zeroes */ - pokemon?: Maybe; /** List out all Pokémon, optionally in pages */ pokemons?: Maybe>>; + /** Get a single Pokémon by its ID, a three character long identifier padded with zeroes */ + pokemon?: Maybe; }; - -export type QueryPokemonArgs = { - id: Scalars['ID']; -}; - - export type QueryPokemonsArgs = { limit?: InputMaybe; skip?: InputMaybe; }; -export type PokemonsQueryVariables = Exact<{ [key: string]: never; }>; +export type QueryPokemonArgs = { + id: Scalars['ID']; +}; +export type PokemonsQueryVariables = Exact<{ [key: string]: never }>; -export type PokemonsQuery = { __typename?: 'Query', pokemons?: Array<{ __typename: 'Pokemon', id: string, name: string } | null> | null }; +export type PokemonsQuery = { + __typename?: 'Query'; + pokemons?: Array<{ + __typename: 'Pokemon'; + id: string; + name: string; + } | null> | null; +}; -export type PokemonFieldsFragment = { __typename?: 'Pokemon', id: string, name: string }; +export type PokemonFieldsFragment = { + __typename?: 'Pokemon'; + id: string; + name: string; +}; export type PokemonQueryVariables = Exact<{ id: Scalars['ID']; }>; +export type PokemonQuery = { + __typename?: 'Query'; + pokemon?: { __typename: 'Pokemon'; id: string; name: string } | null; +}; -export type PokemonQuery = { __typename?: 'Query', pokemon?: { __typename: 'Pokemon', id: string, name: string } | null }; - -export const PokemonFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"pokemonFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Pokemon"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode; -export const PokemonsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Pokemons"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pokemons"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"pokemonFields"}},{"kind":"Field","name":{"kind":"Name","value":"__typename"}}]}}]}},...PokemonFieldsFragmentDoc.definitions]} as unknown as DocumentNode; -export const PokemonDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Pokemon"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pokemon"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"__typename"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const PokemonFieldsFragmentDoc = { + kind: 'Document', + definitions: [ + { + kind: 'FragmentDefinition', + name: { kind: 'Name', value: 'pokemonFields' }, + typeCondition: { + kind: 'NamedType', + name: { kind: 'Name', value: 'Pokemon' }, + }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + ], + }, + }, + ], +} as unknown as DocumentNode; +export const PokemonsDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + name: { kind: 'Name', value: 'Pokemons' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'pokemons' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + { + kind: 'FragmentSpread', + name: { kind: 'Name', value: 'pokemonFields' }, + }, + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, + ], + }, + }, + ], + }, + }, + ...PokemonFieldsFragmentDoc.definitions, + ], +} as unknown as DocumentNode; +export const PokemonDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + name: { kind: 'Name', value: 'Pokemon' }, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'id' } }, + type: { + kind: 'NonNullType', + type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'pokemon' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'id' }, + value: { + kind: 'Variable', + name: { kind: 'Name', value: 'id' }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'name' } }, + { kind: 'Field', name: { kind: 'Name', value: '__typename' } }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode; diff --git a/example/tsconfig.json b/example/tsconfig.json index 95d59f04..adfcde16 100644 --- a/example/tsconfig.json +++ b/example/tsconfig.json @@ -1,9 +1,11 @@ { "compilerOptions": { - "plugins": [{ - "name": "plugin", - "schema": "./schema.graphql" - }], + "plugins": [ + { + "name": "plugin", + "schema": "https://trygql.formidable.dev/graphql/basic-pokedex" + } + ], /* Visit https://aka.ms/tsconfig to read more about this file */ /* Projects */ @@ -15,7 +17,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ @@ -29,7 +31,7 @@ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ + "module": "commonjs" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ @@ -75,12 +77,12 @@ /* Interop Constraints */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ + "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ @@ -102,6 +104,6 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ } } diff --git a/example/yarn.lock b/example/yarn.lock index 338b087d..2b6e6a88 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -1209,6 +1209,13 @@ node-fetch@2.6.7: dependencies: whatwg-url "^5.0.0" +node-fetch@^2.0.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" + integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== + dependencies: + whatwg-url "^5.0.0" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -1316,13 +1323,14 @@ picocolors@^1.0.0: integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== "plugin@file:..": - version "1.0.0" + version "0.1.0" dependencies: "@graphql-codegen/core" "^2.6.8" "@graphql-codegen/typed-document-node" "^2.3.10" "@graphql-codegen/typescript" "^2.8.5" "@graphql-codegen/typescript-operations" "^2.5.10" graphql-language-service "^5.0.6" + node-fetch "^2.0.0" promise@^7.1.1: version "7.3.1" diff --git a/package.json b/package.json index b488eb8a..1bd00614 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@rollup/plugin-terser": "^0.4.1", "@rollup/plugin-typescript": "^11.1.0", "@types/node": "^18.15.11", + "@types/node-fetch": "^2.6.3", "dotenv": "^16.0.3", "graphql": "^16.5.0", "husky": "^8.0.3", @@ -51,6 +52,7 @@ "@graphql-codegen/typed-document-node": "^2.3.10", "@graphql-codegen/typescript": "^2.8.5", "@graphql-codegen/typescript-operations": "^2.5.10", - "graphql-language-service": "^5.0.6" + "graphql-language-service": "^5.0.6", + "node-fetch": "^2.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6355ba4f..b03f3236 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,6 +16,9 @@ dependencies: graphql-language-service: specifier: ^5.0.6 version: 5.0.6(graphql@16.5.0) + node-fetch: + specifier: ^2.0.0 + version: 2.6.7 devDependencies: '@changesets/cli': @@ -33,6 +36,9 @@ devDependencies: '@types/node': specifier: ^18.15.11 version: 18.15.11 + '@types/node-fetch': + specifier: ^2.6.3 + version: 2.6.3 dotenv: specifier: ^16.0.3 version: 16.0.3 @@ -1150,6 +1156,13 @@ packages: resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} dev: true + /@types/node-fetch@2.6.3: + resolution: {integrity: sha512-ETTL1mOEdq/sxUtgtOhKjyB2Irra4cjxksvcMUR5Zr4n+PxVhsCD9WS46oPbHL3et9Zde7CNRr+WUNlcHvsX+w==} + dependencies: + '@types/node': 18.15.11 + form-data: 3.0.1 + dev: true + /@types/node@12.20.55: resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} dev: true @@ -1260,6 +1273,10 @@ packages: engines: {node: '>=8'} dev: true + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: true + /auto-bind@4.0.0: resolution: {integrity: sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==} engines: {node: '>=8'} @@ -1533,6 +1550,13 @@ packages: resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} dev: true + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: true + /commander@10.0.0: resolution: {integrity: sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==} engines: {node: '>=14'} @@ -1651,6 +1675,11 @@ packages: object-keys: 1.1.1 dev: true + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: true + /dependency-graph@0.11.0: resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==} engines: {node: '>= 0.6.0'} @@ -1894,6 +1923,15 @@ packages: is-callable: 1.2.7 dev: true + /form-data@3.0.1: + resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: true + /fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -2582,6 +2620,18 @@ packages: picomatch: 2.3.1 dev: true + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: true + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: true + /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} diff --git a/src/getSchema.ts b/src/getSchema.ts index cfe71c7c..b7571a4d 100644 --- a/src/getSchema.ts +++ b/src/getSchema.ts @@ -1,26 +1,82 @@ -import { GraphQLSchema, buildSchema, buildClientSchema } from 'graphql'; +import { + GraphQLSchema, + buildSchema, + buildClientSchema, + getIntrospectionQuery, + IntrospectionQuery, +} from 'graphql'; +import fetch from 'node-fetch'; import path from 'path'; import fs from 'fs'; +import { Logger } from './index'; + export const loadSchema = ( root: string, - schema: string + schema: string, + logger: Logger ): { current: GraphQLSchema | null } => { const ref: { current: GraphQLSchema | null } = { current: null }; - const isJson = schema.endsWith('json'); - const resolvedPath = path.resolve(path.dirname(root), schema); - const contents = fs.readFileSync(resolvedPath, 'utf-8'); + let url: URL | undefined; + + try { + url = new URL(schema); + } catch (e) {} - fs.watchFile(resolvedPath, () => { + if (url) { + logger(`Fetching introspection from ${url.toString()}`); + fetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: getIntrospectionQuery({ + descriptions: true, + schemaDescription: true, + 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 { + try { + ref.current = buildClientSchema( + (result as { data: IntrospectionQuery }).data + ); + logger(`Got schema for ${url!.toString()}`); + } catch (e: any) { + logger(`Got schema error for ${e.message}`); + } + } + }); + } else { + const isJson = schema.endsWith('json'); + const resolvedPath = path.resolve(path.dirname(root), schema); + logger(`Getting schema from ${resolvedPath}`); const contents = fs.readFileSync(resolvedPath, 'utf-8'); + + fs.watchFile(resolvedPath, () => { + const contents = fs.readFileSync(resolvedPath, 'utf-8'); + ref.current = isJson + ? buildClientSchema(JSON.parse(contents)) + : buildSchema(contents); + }); + ref.current = isJson ? buildClientSchema(JSON.parse(contents)) : buildSchema(contents); - }); - - ref.current = isJson - ? buildClientSchema(JSON.parse(contents)) - : buildSchema(contents); + logger(`Got schema and initialized watcher for ${schema}`); + } return ref; }; diff --git a/src/index.ts b/src/index.ts index 2088c943..75d00049 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,24 +43,28 @@ function createBasicDecorator(info: ts.server.PluginCreateInfo) { return proxy; } +export type Logger = (msg: string) => void; + function create(info: ts.server.PluginCreateInfo) { - const logger = (msg: string) => + const logger: Logger = (msg: string) => info.project.projectService.logger.info(`[ts-graphql-plugin] ${msg}`); logger('config: ' + JSON.stringify(info.config)); if (!info.config.schema) { throw new Error('Please provide a GraphQL Schema!'); } - info.project.projectService.logger.info('Setting up the GraphQL Plugin'); + logger('Setting up the GraphQL Plugin'); const tagTemplate = info.config.template || 'gql'; const scalars = info.config.scalars || {}; const proxy = createBasicDecorator(info); - // TODO: check out interesting stuff on ts.factory - - const schema = loadSchema(info.project.getProjectName(), info.config.schema); + const schema = loadSchema( + info.project.getProjectName(), + info.config.schema, + logger + ); proxy.getSemanticDiagnostics = (filename: string): ts.Diagnostic[] => { const originalDiagnostics =