├── .husky ├── .gitignore └── pre-commit ├── project-fixtures ├── no-config-prj │ ├── main.ts │ ├── schema.graphql │ ├── graphql-tag.d.ts │ └── local-extension.graphql ├── simple-prj │ ├── main.ts │ ├── fragments.ts │ ├── tsconfig.invalid.json │ ├── schema.graphql │ ├── graphql-tag.d.ts │ ├── local-extension.graphql │ ├── tsconfig.noplugin.json │ ├── tsconfig.notsgqlplugin.json │ ├── addon.js │ ├── tsconfig.invalid-addon.json │ └── tsconfig.json ├── transformation-prj │ ├── .gitignore │ ├── schema.graphql │ ├── tag.ts │ ├── fragment-leaf.ts │ ├── fragment-node.ts │ ├── query.ts │ ├── tsconfig.json │ └── webpack.config.js ├── transformation-global-frag-prj │ ├── .gitignore │ ├── schema.graphql │ ├── tag.ts │ ├── fragment-leaf.ts │ ├── fragment-node.ts │ ├── query.ts │ ├── tsconfig.json │ └── webpack.config.js ├── typegen-addon-prj │ ├── types.ts │ ├── graphql-tag.d.ts │ ├── schema.graphql │ ├── __generated__ │ │ ├── post-fragment.ts │ │ └── my-query.ts │ ├── tsconfig.json │ ├── index.ts │ └── addon.ts ├── react-apollo-prj │ ├── local-extension.graphql │ ├── .vscode │ │ └── settings.json │ ├── src │ │ ├── __generated__ │ │ │ ├── repository-item-repository.ts │ │ │ ├── update-my-repository.ts │ │ │ └── app-query.ts │ │ ├── index.tsx │ │ ├── RepositoryItem.tsx │ │ └── App.tsx │ ├── README.md │ ├── package.json │ ├── tsconfig.json │ └── GRAPHQL_OPERATIONS.md ├── graphql-codegen-prj │ ├── src │ │ ├── gql │ │ │ ├── index.ts │ │ │ ├── gql.ts │ │ │ └── fragment-masking.ts │ │ ├── App.tsx │ │ ├── UserAvatar.tsx │ │ ├── PopularPosts.tsx │ │ └── PostSummary.tsx │ ├── .vscode │ │ └── settings.json │ ├── README.md │ ├── codegen.ts │ ├── schema.graphql │ ├── tsconfig.json │ ├── package.json │ └── GRAPHQL_OPERATIONS.md └── gql-errors-prj │ ├── graphql-tag.d.ts │ ├── local-extension.graphql │ ├── schema.graphql │ ├── tsconfig.json │ ├── package.json │ ├── main.ts │ └── package-lock.json ├── src ├── cache │ ├── index.ts │ ├── lru-cache.ts │ └── lru-cache.test.ts ├── language-service-plugin │ ├── index.ts │ ├── ts-server-module.ts │ ├── plugin-module-factory.ts │ └── language-service-proxy-builder.ts ├── register-hooks │ ├── index.ts │ └── register-typescript.ts ├── schema-manager │ ├── testing │ │ ├── resources │ │ │ ├── invalid_syntax.graphql │ │ │ ├── invalid_extension.graphql │ │ │ └── normal.graphql │ │ ├── testing-schema-object.ts │ │ └── testing-schema-manager-host.ts │ ├── index.ts │ ├── types.ts │ ├── __snapshots__ │ │ └── extension-manager.test.ts.snap │ ├── http-schema-manager.ts │ ├── schema-manager-host.ts │ ├── request-introspection-query.test.ts │ ├── request-introspection-query.ts │ ├── schema-manager-factory.ts │ ├── file-schema-manager.ts │ ├── schema-manager.ts │ ├── scripted-http-schema-manager.ts │ ├── file-schema-manager.test.ts │ ├── extension-manager.test.ts │ ├── schema-manager-factory.test.ts │ └── extension-manager.ts ├── gql-ast-util │ ├── index.ts │ ├── __snapshots__ │ │ └── utility-functions.test.ts.snap │ ├── utility-functions.test.ts │ └── utility-functions.ts ├── transformer │ ├── index.ts │ └── transformer.ts ├── typegen-addons │ ├── index.ts │ ├── __snapshots__ │ │ └── typed-query-document.test.ts.snap │ ├── typed-query-document.test.ts │ ├── typed-query-document.ts │ └── testing │ │ └── addon-tester.ts ├── ts-ast-util │ ├── ast-factory-alias.ts │ ├── index.ts │ ├── testing │ │ ├── print-node.ts │ │ └── testing-language-service.ts │ ├── file-name-filter.ts │ ├── __snapshots__ │ │ ├── output-source.test.ts.snap │ │ └── template-expression-resolver.test.ts.snap │ ├── file-name-filter.test.ts │ ├── script-host.ts │ ├── script-source-helper.ts │ └── register-document-change-event.ts ├── tsmodule.d.ts ├── tsmodule.js ├── string-util │ ├── index.ts │ ├── padding.ts │ ├── case-converter.ts │ ├── color.ts │ ├── glob-to-regexp.ts │ ├── position-converter.ts │ └── glob-to-regexp.test.ts ├── typegen │ ├── index.ts │ └── addon │ │ └── merge-addons.ts ├── graphql-language-service-adapter │ ├── index.ts │ ├── __snapshots__ │ │ ├── get-semantic-diagnostics.test.ts.snap │ │ └── get-quick-info-at-position.test.ts.snap │ ├── testing │ │ ├── simple-schema.ts │ │ └── adapter-fixture.ts │ ├── get-definition-at-position.ts │ ├── simple-position.ts │ ├── simple-position.test.ts │ ├── get-quick-info-at-position.ts │ ├── get-quick-info-at-position.test.ts │ ├── get-definition-at-position.test.ts │ ├── types.ts │ ├── get-definition-and-bound-span.ts │ └── get-completion-at-position.ts ├── analyzer │ ├── index.ts │ ├── __snapshots__ │ │ ├── type-generator.test.ts.snap │ │ ├── markdown-reporter.test.ts.snap │ │ └── extractor.test.ts.snap │ ├── types.ts │ ├── testing │ │ └── testing-extractor.ts │ ├── markdown-reporter.test.ts │ ├── markdown-reporter.ts │ └── analyzer-factory.test.ts ├── types.ts ├── index.ts ├── cli │ ├── __snapshots__ │ │ └── parser.test.ts.snap │ ├── logger.ts │ ├── commands │ │ ├── extract.ts │ │ ├── typegen.ts │ │ ├── validate.ts │ │ └── report.ts │ └── cli.ts ├── errors │ ├── __snapshots__ │ │ └── error-reporter.test.ts.snap │ └── index.ts └── webpack │ └── plugin.ts ├── renovate.json ├── webpack.js ├── capture_v4.gif ├── .eslintignore ├── addons └── typed-query-document.js ├── .prettierrc.yml ├── .gitignore ├── .prettierignore ├── codecov.yml ├── jest.config.mjs ├── e2e ├── cli-specs │ ├── misc.js │ ├── validate.js │ ├── extract.js │ ├── report.js │ └── typegen.js ├── fixtures │ ├── cli.js │ └── lang-server.js ├── lang-server-specs │ ├── diagnostics-syntax.js │ ├── diagnostics.js │ ├── completions.js │ ├── quickinfo.js │ ├── diagnostics-complex-template.js │ ├── diagnostics-with-update.js │ └── definition.js ├── webpack-specs │ ├── watch.js │ └── transform.js └── run.js ├── tools └── add-toc.ts ├── tsconfig.json ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ ├── publish.yml │ └── build.yml ├── .eslintrc.yml ├── LICENSE.txt ├── CONTRIBUTING.md └── package.json /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /project-fixtures/no-config-prj/main.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /project-fixtures/simple-prj/main.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /project-fixtures/simple-prj/fragments.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/cache/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lru-cache'; 2 | -------------------------------------------------------------------------------- /project-fixtures/transformation-prj/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"] 3 | } 4 | -------------------------------------------------------------------------------- /project-fixtures/transformation-global-frag-prj/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /project-fixtures/simple-prj/tsconfig.invalid.json: -------------------------------------------------------------------------------- 1 | INVALID_JSON_FILE 2 | -------------------------------------------------------------------------------- /project-fixtures/typegen-addon-prj/types.ts: -------------------------------------------------------------------------------- 1 | export type GqlURL = string; 2 | -------------------------------------------------------------------------------- /webpack.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/webpack/plugin').WebpackPlugin; 2 | -------------------------------------------------------------------------------- /src/language-service-plugin/index.ts: -------------------------------------------------------------------------------- 1 | export * from './plugin-module-factory'; 2 | -------------------------------------------------------------------------------- /capture_v4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quramy/ts-graphql-plugin/HEAD/capture_v4.gif -------------------------------------------------------------------------------- /project-fixtures/simple-prj/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | hello: String! 3 | } 4 | -------------------------------------------------------------------------------- /project-fixtures/no-config-prj/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | hello: String! 3 | } 4 | -------------------------------------------------------------------------------- /src/register-hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { registerTypeScript } from './register-typescript'; 2 | -------------------------------------------------------------------------------- /src/schema-manager/testing/resources/invalid_syntax.graphql: -------------------------------------------------------------------------------- 1 | directive @hoge() on FIEELD 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx pretty-quick --staged 5 | -------------------------------------------------------------------------------- /project-fixtures/react-apollo-prj/local-extension.graphql: -------------------------------------------------------------------------------- 1 | directive @nonreactive on FRAGMENT_SPREAD 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | __generated__/ 2 | project-fixtures/ 3 | lib/ 4 | e2e/ 5 | coverage/ 6 | e2e_coverage/ 7 | -------------------------------------------------------------------------------- /src/gql-ast-util/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fragment-registry'; 2 | export * from './utility-functions'; 3 | -------------------------------------------------------------------------------- /src/transformer/index.ts: -------------------------------------------------------------------------------- 1 | export { TransformerHost, GetTransformerOptions } from './transformer-host'; 2 | -------------------------------------------------------------------------------- /src/typegen-addons/index.ts: -------------------------------------------------------------------------------- 1 | export { TypedQueryDocumentAddonFactory } from './typed-query-document'; 2 | -------------------------------------------------------------------------------- /project-fixtures/transformation-prj/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | hello: String! 3 | bye: String! 4 | } 5 | -------------------------------------------------------------------------------- /src/ts-ast-util/ast-factory-alias.ts: -------------------------------------------------------------------------------- 1 | import ts from '../tsmodule'; 2 | 3 | export const astf = ts.factory; 4 | -------------------------------------------------------------------------------- /addons/typed-query-document.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../lib/typegen-addons').TypedQueryDocumentAddonFactory; 2 | -------------------------------------------------------------------------------- /project-fixtures/graphql-codegen-prj/src/gql/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./fragment-masking"; 2 | export * from "./gql"; -------------------------------------------------------------------------------- /project-fixtures/react-apollo-prj/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /src/schema-manager/testing/resources/invalid_extension.graphql: -------------------------------------------------------------------------------- 1 | extend type NoExisitingType { 2 | name: String! 3 | } 4 | -------------------------------------------------------------------------------- /project-fixtures/transformation-global-frag-prj/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | hello: String! 3 | bye: String! 4 | } 5 | -------------------------------------------------------------------------------- /project-fixtures/simple-prj/graphql-tag.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'graphql-tag' { 2 | const gql: any; 3 | export default gql; 4 | } 5 | -------------------------------------------------------------------------------- /project-fixtures/gql-errors-prj/graphql-tag.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'graphql-tag' { 2 | const gql: any; 3 | export default gql; 4 | } 5 | -------------------------------------------------------------------------------- /project-fixtures/no-config-prj/graphql-tag.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'graphql-tag' { 2 | const gql: any; 3 | export default gql; 4 | } 5 | -------------------------------------------------------------------------------- /project-fixtures/simple-prj/local-extension.graphql: -------------------------------------------------------------------------------- 1 | directive @myDirective on FIELD 2 | 3 | extend type Query { 4 | bye: String! 5 | } 6 | -------------------------------------------------------------------------------- /project-fixtures/gql-errors-prj/local-extension.graphql: -------------------------------------------------------------------------------- 1 | directive @myDirective on FIELD 2 | 3 | extend type Query { 4 | bye: String! 5 | } 6 | -------------------------------------------------------------------------------- /project-fixtures/no-config-prj/local-extension.graphql: -------------------------------------------------------------------------------- 1 | directive @myDirective on FIELD 2 | 3 | extend type Query { 4 | bye: String! 5 | } 6 | -------------------------------------------------------------------------------- /src/schema-manager/testing/resources/normal.graphql: -------------------------------------------------------------------------------- 1 | type User { 2 | name: String! 3 | } 4 | 5 | extend type Query { 6 | me: User! 7 | } 8 | -------------------------------------------------------------------------------- /src/tsmodule.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-restricted-imports 2 | import * as ts from 'typescript'; 3 | export = ts; 4 | -------------------------------------------------------------------------------- /project-fixtures/simple-prj/tsconfig.noplugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | trailingComma: all 2 | tabWidth: 2 3 | semi: true 4 | singleQuote: true 5 | bracketSpacing: true 6 | printWidth: 120 7 | arrowParens: avoid 8 | -------------------------------------------------------------------------------- /project-fixtures/typegen-addon-prj/graphql-tag.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'graphql-tag' { 2 | function gql(...args: any[]): any; 3 | export default gql; 4 | } 5 | -------------------------------------------------------------------------------- /src/tsmodule.js: -------------------------------------------------------------------------------- 1 | const { getModule } = require('./language-service-plugin/ts-server-module'); 2 | 3 | module.exports = getModule() ?? require('typescript'); 4 | -------------------------------------------------------------------------------- /project-fixtures/gql-errors-prj/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | hello: String! 3 | helloWorld: String! @deprecated(reason: "Use 'hello' instead of this.") 4 | } 5 | -------------------------------------------------------------------------------- /src/string-util/index.ts: -------------------------------------------------------------------------------- 1 | export * from './padding'; 2 | export * from './case-converter'; 3 | export * from './color'; 4 | export * from './position-converter'; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | types/ 4 | *.log 5 | *.swp 6 | *.swo 7 | *.tsbuildinfo 8 | .DS_Store 9 | manifest.json 10 | coverage/ 11 | e2e_coverage/ 12 | -------------------------------------------------------------------------------- /project-fixtures/transformation-prj/tag.ts: -------------------------------------------------------------------------------- 1 | export default function gql(literals: TemplateStringsArray, ...args: unknown[]) { 2 | // dummy impl 3 | return ''; 4 | } 5 | -------------------------------------------------------------------------------- /project-fixtures/graphql-codegen-prj/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { PopularPosts } from './PopularPosts'; 2 | 3 | export default function App() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /project-fixtures/graphql-codegen-prj/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } 5 | -------------------------------------------------------------------------------- /project-fixtures/simple-prj/tsconfig.notsgqlplugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "plugins": [] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /project-fixtures/transformation-global-frag-prj/tag.ts: -------------------------------------------------------------------------------- 1 | export default function gql(literals: TemplateStringsArray, ...args: unknown[]) { 2 | // dummy impl 3 | return ''; 4 | } 5 | -------------------------------------------------------------------------------- /src/typegen/index.ts: -------------------------------------------------------------------------------- 1 | export * from './addon/types'; 2 | export { mergeAddons } from './addon/merge-addons'; 3 | export { TypeGenVisitor, TypeGenError } from './type-gen-visitor'; 4 | -------------------------------------------------------------------------------- /project-fixtures/simple-prj/addon.js: -------------------------------------------------------------------------------- 1 | module.exports = ctx => { 2 | return { 3 | document: () => { 4 | ctx.source.writeLeadingComment('Hello, Addon!'); 5 | }, 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /project-fixtures/transformation-prj/fragment-leaf.ts: -------------------------------------------------------------------------------- 1 | import gql from './tag'; 2 | 3 | export const fragmentLeaf = gql` 4 | fragment FragmentLeaf on Query { 5 | hello 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /project-fixtures/transformation-global-frag-prj/fragment-leaf.ts: -------------------------------------------------------------------------------- 1 | import gql from './tag'; 2 | 3 | const fragmentLeaf = gql` 4 | fragment FragmentLeaf on Query { 5 | hello 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/graphql-language-service-adapter/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | GraphQLLanguageServiceAdapter, 3 | GraphQLLanguageServiceAdapterCreateOptions, 4 | } from './graphql-language-service-adapter'; 5 | -------------------------------------------------------------------------------- /project-fixtures/transformation-global-frag-prj/fragment-node.ts: -------------------------------------------------------------------------------- 1 | import gql from './tag'; 2 | 3 | const fragmentNode = gql` 4 | fragment FragmentNode on Query { 5 | bye 6 | ...FragmentLeaf 7 | } 8 | `; 9 | -------------------------------------------------------------------------------- /src/string-util/padding.ts: -------------------------------------------------------------------------------- 1 | export function pad(letter: string, length: number) { 2 | const outs: string[] = []; 3 | for (let i = 0; i < length; i++) { 4 | outs.push(letter); 5 | } 6 | return outs.join(''); 7 | } 8 | -------------------------------------------------------------------------------- /project-fixtures/react-apollo-prj/src/__generated__/repository-item-repository.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* This is an autogenerated file. Do not edit this file directly! */ 3 | export type RepositoryItem_Repository = { 4 | name: string; 5 | description: string | null; 6 | }; 7 | -------------------------------------------------------------------------------- /project-fixtures/transformation-prj/fragment-node.ts: -------------------------------------------------------------------------------- 1 | import gql from './tag'; 2 | import { fragmentLeaf } from './fragment-leaf'; 3 | 4 | export const fragmentNode = gql` 5 | ${fragmentLeaf} 6 | fragment FragmentNode on Query { 7 | bye 8 | ...FragmentLeaf 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /project-fixtures/transformation-global-frag-prj/query.ts: -------------------------------------------------------------------------------- 1 | import gql from './tag'; 2 | import { fragmentNode } from './fragment-node'; 3 | 4 | const query = gql` 5 | query MyQuery { 6 | __typename 7 | ...FragmentNode 8 | } 9 | `; 10 | 11 | console.log(JSON.stringify(query, null, 2)); 12 | -------------------------------------------------------------------------------- /src/language-service-plugin/ts-server-module.ts: -------------------------------------------------------------------------------- 1 | import type ts from 'typescript/lib/tsserverlibrary'; 2 | 3 | let _module: typeof ts; 4 | 5 | export function setModule(typescript: typeof ts) { 6 | _module = typescript; 7 | } 8 | 9 | export function getModule() { 10 | return _module; 11 | } 12 | -------------------------------------------------------------------------------- /src/gql-ast-util/__snapshots__/utility-functions.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`detectDuplicatedFragments should detect duplicated fragments info 1`] = ` 4 | [ 5 | { 6 | "end": 149, 7 | "name": "Hoge", 8 | "start": 106, 9 | }, 10 | ] 11 | `; 12 | -------------------------------------------------------------------------------- /src/graphql-language-service-adapter/__snapshots__/get-semantic-diagnostics.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`getSemanticDiagnostics should report errors coused by schema building 1`] = `"[ts-graphql-plugin] Schema build error: 'some schema build error' Check schema.graphql[1:1]"`; 4 | -------------------------------------------------------------------------------- /project-fixtures/transformation-prj/query.ts: -------------------------------------------------------------------------------- 1 | import gql from './tag'; 2 | import { fragmentNode } from './fragment-node'; 3 | 4 | export const query = gql` 5 | ${fragmentNode} 6 | query MyQuery { 7 | __typename 8 | ...FragmentNode 9 | } 10 | `; 11 | 12 | console.log(JSON.stringify(query, null, 2)); 13 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | __snapshots__/ 4 | **/testing/resources/*.graphql 5 | coverage/ 6 | e2e_coverage/ 7 | .husky/ 8 | project-fixtures/**/*.invalid.json 9 | project-fixtures/**/__generated__/ 10 | project-fixtures/**/gql/ 11 | project-fixtures/**/manifest.json 12 | project-fixtures/**/GRAPHQL_OPERATIONS.md 13 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | ignore: 5 | - 'webpack.js' 6 | - 'addons/**/*' 7 | - 'project-fixtures/**/*' 8 | 9 | comment: 10 | layout: 'reach, diff, flags, files' 11 | behavior: default 12 | require_changes: no 13 | 14 | parsers: 15 | javascript: 16 | enable_partials: yes 17 | -------------------------------------------------------------------------------- /src/analyzer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export { Analyzer } from './analyzer'; 3 | export { AnalyzerFactory } from './analyzer-factory'; 4 | export type { 5 | ExtractFileResult, 6 | ExtractSucceededResult, 7 | ExtractGraphQLErrorResult, 8 | ExtractTemplateResolveErrorResult, 9 | } from './extractor'; 10 | -------------------------------------------------------------------------------- /project-fixtures/react-apollo-prj/README.md: -------------------------------------------------------------------------------- 1 | # React Apollo Example 2 | 3 | ## How to run this example 4 | 5 | - (At the root of ts-graphql-plugin project directory) 6 | - Execute `npm link` command. 7 | 8 | ```sh 9 | $ cd project-fixtures/react-apollo-prj 10 | $ npm install 11 | $ npm link ts-graphql-plugin 12 | $ code . 13 | ``` 14 | -------------------------------------------------------------------------------- /src/graphql-language-service-adapter/__snapshots__/get-quick-info-at-position.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`getQuickInfoAtPosition should return GraphQL quick info 1`] = `"Query.hello: String!"`; 4 | 5 | exports[`getQuickInfoAtPosition should return GraphQL quick info 2`] = `"Query.hello: String!"`; 6 | -------------------------------------------------------------------------------- /src/schema-manager/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | 3 | export { SchemaManager } from './schema-manager'; 4 | 5 | export { SchemaManagerFactory } from './schema-manager-factory'; 6 | 7 | export { 8 | createSchemaManagerHostFromLSPluginInfo, 9 | createSchemaManagerHostFromTSGqlPluginConfig, 10 | } from './schema-manager-host'; 11 | -------------------------------------------------------------------------------- /src/graphql-language-service-adapter/testing/simple-schema.ts: -------------------------------------------------------------------------------- 1 | import { buildSchema } from 'graphql'; 2 | 3 | const schemaSDL = ` 4 | type Query { 5 | hello: String! 6 | helloWorld: String! @deprecated(reason: "Don't use") 7 | } 8 | `; 9 | 10 | export function createSimpleSchema() { 11 | return buildSchema(schemaSDL); 12 | } 13 | -------------------------------------------------------------------------------- /project-fixtures/graphql-codegen-prj/README.md: -------------------------------------------------------------------------------- 1 | # graphql-codegen example 2 | 3 | ## How to run this example 4 | 5 | - (At the root of ts-graphql-plugin project directory) 6 | - Execute `npm link` command. 7 | 8 | ```sh 9 | $ cd project-fixtures/graphql-codegen-prj 10 | $ npm install 11 | $ npm link ts-graphql-plugin 12 | $ code . 13 | ``` 14 | -------------------------------------------------------------------------------- /project-fixtures/transformation-prj/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "plugins": [ 8 | { 9 | "name": "ts-graphql-plugin", 10 | "schema": "schema.graphql" 11 | } 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /project-fixtures/gql-errors-prj/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "plugins": [ 6 | { 7 | "name": "ts-graphql-plugin", 8 | "schema": "schema.graphql", 9 | "localSchemaExtensions": ["local-extension.graphql"] 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /project-fixtures/transformation-global-frag-prj/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "plugins": [ 8 | { 9 | "name": "ts-graphql-plugin", 10 | "schema": "schema.graphql" 11 | } 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /project-fixtures/graphql-codegen-prj/codegen.ts: -------------------------------------------------------------------------------- 1 | import type { CodegenConfig } from '@graphql-codegen/cli'; 2 | 3 | const config: CodegenConfig = { 4 | overwrite: true, 5 | schema: 'schema.graphql', 6 | documents: ['src/**/*.tsx'], 7 | generates: { 8 | 'src/gql/': { 9 | preset: 'client', 10 | }, 11 | }, 12 | }; 13 | 14 | export default config; 15 | -------------------------------------------------------------------------------- /project-fixtures/simple-prj/tsconfig.invalid-addon.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "plugins": [ 6 | { 7 | "name": "../../lib", 8 | "schema": "schema.graphql", 9 | "typegen": { 10 | "addons": ["invalid-module"] 11 | } 12 | } 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /project-fixtures/react-apollo-prj/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "react-apollo-project", 4 | "devDependencies": { 5 | "@apollo/client": "3.14.0", 6 | "@types/react": "19.2.7", 7 | "@types/react-dom": "19.2.3", 8 | "graphql": "16.12.0", 9 | "react": "19.2.3", 10 | "react-dom": "19.2.3", 11 | "typescript": "5.5.4" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /project-fixtures/typegen-addon-prj/schema.graphql: -------------------------------------------------------------------------------- 1 | scalar Date 2 | scalar URL 3 | 4 | type User { 5 | id: ID! 6 | posts: [Post!]! 7 | } 8 | 9 | type Post { 10 | id: ID! 11 | title: String! 12 | body: String! 13 | publishedAt: Date! 14 | entryURL: URL! 15 | ogImageURL: URL! 16 | author: User! 17 | } 18 | 19 | type Query { 20 | user(search: String!): [User!] 21 | } 22 | -------------------------------------------------------------------------------- /project-fixtures/graphql-codegen-prj/schema.graphql: -------------------------------------------------------------------------------- 1 | type Post { 2 | id: ID! 3 | title: String! 4 | body: String! 5 | author: User! 6 | createdAt: String! 7 | updatedAt: String! 8 | } 9 | 10 | type User { 11 | id: ID! 12 | name: String! 13 | avatarURL: String! 14 | createdAt: String! 15 | updatedAt: String! 16 | } 17 | 18 | type Query { 19 | popularPosts: [Post!]! 20 | } 21 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { SchemaConfig } from './schema-manager'; 2 | import type { TagConfig } from './ts-ast-util'; 3 | 4 | export type TsGraphQLPluginConfigOptions = SchemaConfig & { 5 | name: string; 6 | exclude?: string[]; 7 | enabledGlobalFragments?: boolean; 8 | removeDuplicatedFragments?: boolean; 9 | tag?: TagConfig; 10 | typegen?: { 11 | addons?: string[]; 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { pluginModuleFactory } from './language-service-plugin'; 2 | 3 | export type { 4 | TypeGenAddonFactory, 5 | TypeGenVisitorAddon, 6 | TypeGenVisitorAddonContext, 7 | CustomScalarInput, 8 | CustomScalarOutput, 9 | DocumentInput, 10 | FragmentDefinitionInput, 11 | OperationDefinionInput, 12 | } from './typegen/addon/types'; 13 | 14 | module.exports = pluginModuleFactory; 15 | -------------------------------------------------------------------------------- /src/string-util/case-converter.ts: -------------------------------------------------------------------------------- 1 | const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g; 2 | const STRING_DASHERIZE_REGEXP = /[ _]/g; 3 | 4 | export function decamelize(str: string): string { 5 | return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase(); 6 | } 7 | 8 | export function dasherize(str: string): string { 9 | return decamelize(str).replace(STRING_DASHERIZE_REGEXP, '-'); 10 | } 11 | -------------------------------------------------------------------------------- /src/language-service-plugin/plugin-module-factory.ts: -------------------------------------------------------------------------------- 1 | import type ts from 'typescript/lib/tsserverlibrary'; 2 | 3 | import { setModule } from './ts-server-module'; 4 | 5 | export const pluginModuleFactory: ts.server.PluginModuleFactory = ({ typescript }) => { 6 | setModule(typescript); 7 | const { create } = require('./create-plugin') as typeof import('./create-plugin'); 8 | return { create }; 9 | }; 10 | -------------------------------------------------------------------------------- /project-fixtures/typegen-addon-prj/__generated__/post-fragment.ts: -------------------------------------------------------------------------------- 1 | /* The following types are extracted from index.ts */ 2 | /* eslint-disable */ 3 | /* This is an autogenerated file. Do not edit this file directly! */ 4 | import { GqlURL } from "../types"; 5 | export type PostFragment = { 6 | title: string; 7 | body: string; 8 | publishedAt: Date; 9 | entryURL: GqlURL; 10 | ogImageURL: GqlURL; 11 | }; 12 | -------------------------------------------------------------------------------- /project-fixtures/simple-prj/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "plugins": [ 6 | { 7 | "name": "ts-graphql-plugin", 8 | "schema": "schema.graphql", 9 | "localSchemaExtensions": ["local-extension.graphql"], 10 | "typegen": { 11 | "addons": ["./addon"] 12 | } 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /project-fixtures/typegen-addon-prj/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "plugins": [ 7 | { 8 | "name": "../../lib", 9 | "_name": "ts-graphql-plugin", 10 | "schema": "schema.graphql", 11 | "typegen": { 12 | "addons": ["./addon"] 13 | } 14 | } 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /jest.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | transform: { 3 | '^.+\\.ts$': [ 4 | 'ts-jest', 5 | { 6 | diagnostics: false, 7 | }, 8 | ], 9 | }, 10 | testRegex: '(src/.*\\.test)\\.ts$', 11 | testPathIgnorePatterns: ['/node_modules/', '\\.d\\.ts$', 'lib/.*'], 12 | coverageProvider: 'v8', 13 | collectCoverageFrom: ['src/**/*.ts', '!**/testing/**'], 14 | moduleFileExtensions: ['js', 'ts', 'json'], 15 | }; 16 | -------------------------------------------------------------------------------- /project-fixtures/graphql-codegen-prj/src/UserAvatar.tsx: -------------------------------------------------------------------------------- 1 | import { graphql, useFragment, type FragmentType } from './gql'; 2 | 3 | const fragment = graphql(` 4 | fragment UserAvatar_User on User { 5 | name 6 | avatarURL 7 | } 8 | `); 9 | 10 | export function UserAvatar(props: { user: FragmentType }) { 11 | const user = useFragment(fragment, props.user); 12 | 13 | return {user.name}; 14 | } 15 | -------------------------------------------------------------------------------- /project-fixtures/graphql-codegen-prj/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "jsx": "preserve", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "moduleResolution": "node", 9 | "plugins": [ 10 | { 11 | "name": "ts-graphql-plugin", 12 | "schema": "schema.graphql", 13 | "exclude": ["codegen.ts", "src/gql"] 14 | } 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /project-fixtures/gql-errors-prj/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gql-syntax-error", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "graphql": "16.12.0", 14 | "ts-graphql-plugin": "file:../../", 15 | "typescript": "5.5.4" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /project-fixtures/typegen-addon-prj/index.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const PostFragment = gql` 4 | fragment PostFragment on Post { 5 | title 6 | body 7 | publishedAt 8 | entryURL 9 | ogImageURL 10 | } 11 | `; 12 | 13 | export const query = gql` 14 | ${PostFragment} 15 | query MyQuery($search: String!) { 16 | user(search: $search) { 17 | id 18 | posts { 19 | id 20 | ...PostFragment 21 | } 22 | } 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /project-fixtures/gql-errors-prj/main.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | const getField = () => 'hello'; 4 | 5 | const syntaxErrorQuery = gql` 6 | querry { 7 | name 8 | } 9 | `; 10 | 11 | const tooComplexExpressionQuery = gql` 12 | query { 13 | ${getField()} 14 | } 15 | `; 16 | const semanticErrorFragment = gql` 17 | fragment MyFragment on Query { 18 | hoge 19 | } 20 | `; 21 | 22 | const duplicatedFragment = gql` 23 | fragment MyFragment on Query { 24 | hoge 25 | } 26 | `; 27 | -------------------------------------------------------------------------------- /src/ts-ast-util/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './ast-factory-alias'; 3 | export * from './utilily-functions'; 4 | export * from './template-expression-resolver'; 5 | export * from './register-document-change-event'; 6 | export * from './tag-utils'; 7 | 8 | export { ScriptHost } from './script-host'; 9 | export { createScriptSourceHelper } from './script-source-helper'; 10 | export { createOutputSource } from './output-source'; 11 | export { createFileNameFilter } from './file-name-filter'; 12 | -------------------------------------------------------------------------------- /e2e/cli-specs/misc.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | async function run(cli) { 4 | const { code: versionCode } = await cli.run('--version'); 5 | const { code: helpCode } = await cli.run('--help'); 6 | const { code: commandHelpCode } = await cli.run('typegen', ['--help']); 7 | const { code: unknownCommandCode } = await cli.run('hogehoge'); 8 | assert.equal(versionCode, 0); 9 | assert.equal(helpCode, 0); 10 | assert.equal(commandHelpCode, 0); 11 | assert.equal(unknownCommandCode, 1); 12 | } 13 | 14 | module.exports = run; 15 | -------------------------------------------------------------------------------- /e2e/cli-specs/validate.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | async function run(cli) { 4 | for (const fixtureDir of ['project-fixtures/gql-errors-prj']) { 5 | const { code } = await cli.run('validate', ['-p', fixtureDir, '--verbose']); 6 | assert.equal(code, 1); 7 | } 8 | 9 | for (const fixtureDir of ['project-fixtures/react-apollo-prj', 'project-fixtures/graphql-codegen-prj']) { 10 | const { code } = await cli.run('validate', ['-p', fixtureDir, '--verbose']); 11 | assert.equal(code, 0); 12 | } 13 | } 14 | 15 | module.exports = run; 16 | -------------------------------------------------------------------------------- /src/cli/__snapshots__/parser.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CLI parser result should show command help 1`] = ` 4 | "Usage: tsgql typegen [options] 5 | 6 | Description: Generate type files. 7 | 8 | Options: 9 | -p, --project Path to tsconfig.json." 10 | `; 11 | 12 | exports[`CLI parser result should show global help 1`] = ` 13 | "Usage: tsgql [options] 14 | 15 | available commands are: 16 | typegen 17 | 18 | Options: 19 | --v, --version Print version" 20 | `; 21 | -------------------------------------------------------------------------------- /tools/add-toc.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | 4 | const toc = require('markdown-toc'); 5 | 6 | function addToc(markdownFilename: string) { 7 | const content = fs.readFileSync(path.join(__dirname, '..', markdownFilename), 'utf8'); 8 | const contentWithToc = toc.insert(content, { 9 | maxdepth: 4, 10 | bullets: ['-', '-', '-'], 11 | }); 12 | fs.writeFileSync(path.join(__dirname, '..', markdownFilename), contentWithToc, 'utf8'); 13 | } 14 | 15 | ['README.md', 'CONTRIBUTING.md', 'docs/CUSTOMIZE_TYPE_GEN.md'].forEach(addToc); 16 | -------------------------------------------------------------------------------- /project-fixtures/react-apollo-prj/src/__generated__/update-my-repository.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* This is an autogenerated file. Do not edit this file directly! */ 3 | import { TypedDocumentNode } from "@graphql-typed-document-node/core"; 4 | export type UpdateMyRepository = { 5 | updateRepository: ({ 6 | clientMutationId: string | null; 7 | }) | null; 8 | }; 9 | export type UpdateMyRepositoryVariables = { 10 | repositoryId: string; 11 | }; 12 | export type UpdateMyRepositoryDocument = TypedDocumentNode; 13 | -------------------------------------------------------------------------------- /src/graphql-language-service-adapter/get-definition-at-position.ts: -------------------------------------------------------------------------------- 1 | import type { AnalysisContext, GetDefinitionAtPosition } from './types'; 2 | import { getDefinitionAndBoundSpan } from './get-definition-and-bound-span'; 3 | 4 | export function getDefinitionAtPosition( 5 | ctx: AnalysisContext, 6 | delegate: GetDefinitionAtPosition, 7 | fileName: string, 8 | position: number, 9 | ) { 10 | const result = getDefinitionAndBoundSpan(ctx, () => undefined, fileName, position); 11 | if (!result) return delegate(fileName, position); 12 | return result.definitions; 13 | } 14 | -------------------------------------------------------------------------------- /src/typegen-addons/__snapshots__/typed-query-document.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TypedQueryDocumentAddonFactory should add export statement using TypedQueryDocumentNode 1`] = ` 4 | [ 5 | "/* eslint-disable */ 6 | /* This is an autogenerated file. Do not edit this file directly! */ 7 | import { TypedDocumentNode } from "@graphql-typed-document-node/core"; 8 | export type MyQuery = { 9 | hello: string; 10 | }; 11 | export type MyQueryVariables = {}; 12 | export type MyQueryDocument = TypedDocumentNode; 13 | ", 14 | ] 15 | `; 16 | -------------------------------------------------------------------------------- /src/ts-ast-util/testing/print-node.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | 3 | let printer: ts.Printer | undefined = undefined; 4 | 5 | export function printNode(node: ts.Node | undefined, hint?: ts.EmitHint) { 6 | if (!node) return ''; 7 | if (!printer) { 8 | printer = ts.createPrinter({ 9 | newLine: ts.NewLineKind.LineFeed, 10 | removeComments: false, 11 | }); 12 | } 13 | const outSource = ts.createSourceFile('out.ts', '', ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); 14 | return printer.printNode(hint ?? ts.EmitHint.Unspecified, node, outSource).trim(); 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "skipLibCheck": true, 4 | "incremental": true, 5 | "module": "commonjs", 6 | "declaration": true, 7 | "target": "es2019", 8 | "sourceMap": true, 9 | "outDir": "lib", 10 | "rootDir": "src", 11 | "strict": true, 12 | "stripInternal": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "esModuleInterop": true, 16 | "plugins": [ 17 | { 18 | "name": "typescript-eslint-language-service" 19 | } 20 | ] 21 | }, 22 | "exclude": ["lib", "e2e", "types", "typedef", "node_modules", "project-fixtures", "tools"] 23 | } 24 | -------------------------------------------------------------------------------- /project-fixtures/graphql-codegen-prj/src/PopularPosts.tsx: -------------------------------------------------------------------------------- 1 | import { useSuspenseQuery } from '@apollo/client'; 2 | import { graphql } from './gql'; 3 | 4 | import { PostSummary } from './PostSummary'; 5 | 6 | const query = graphql(` 7 | query PopularPosts_Query { 8 | popularPosts { 9 | id 10 | ...PostSummary_Post 11 | } 12 | } 13 | `); 14 | 15 | export function PopularPosts() { 16 | const { data } = useSuspenseQuery(query); 17 | return ( 18 |
    19 | {data.popularPosts.map(post => ( 20 |
  • 21 | 22 |
  • 23 | ))} 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /project-fixtures/react-apollo-prj/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "jsx": "react-jsx", 6 | "strict": true, 7 | "rootDir": "./src", 8 | "esModuleInterop": true, 9 | "moduleResolution": "node", 10 | "plugins": [ 11 | { 12 | "name": "ts-graphql-plugin", 13 | "schema": "schema.graphql", 14 | "exclude": ["src/__generated__"], 15 | "localSchemaExtensions": ["local-extension.graphql"], 16 | "typegen": { 17 | "addons": ["ts-graphql-plugin/addons/typed-query-document"] 18 | } 19 | } 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /project-fixtures/typegen-addon-prj/__generated__/my-query.ts: -------------------------------------------------------------------------------- 1 | /* The following types are extracted from index.ts */ 2 | /* eslint-disable */ 3 | /* This is an autogenerated file. Do not edit this file directly! */ 4 | import { GqlURL } from "../types"; 5 | export type PostFragment = { 6 | title: string; 7 | body: string; 8 | publishedAt: Date; 9 | entryURL: GqlURL; 10 | ogImageURL: GqlURL; 11 | }; 12 | export type MyQuery = { 13 | user: ({ 14 | id: string; 15 | posts: ({ 16 | id: string; 17 | } & PostFragment)[]; 18 | })[] | null; 19 | }; 20 | export type MyQueryVariables = { 21 | search: string; 22 | }; 23 | -------------------------------------------------------------------------------- /e2e/cli-specs/extract.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const assert = require('assert'); 4 | const rimraf = require('rimraf'); 5 | 6 | async function run(cli) { 7 | const manifestPath = 'project-fixtures/react-apollo-prj/manifest.json'; 8 | rimraf.sync(path.resolve(__dirname, '../..', manifestPath)); 9 | const { code } = await cli.run('extract', [ 10 | '-p', 11 | 'project-fixtures/react-apollo-prj', 12 | '--verbose', 13 | '-o', 14 | manifestPath, 15 | ]); 16 | assert.equal(fs.existsSync(path.resolve(__dirname, '../../', manifestPath)), true); 17 | assert.equal(code, 0); 18 | } 19 | 20 | module.exports = run; 21 | -------------------------------------------------------------------------------- /src/analyzer/__snapshots__/type-generator.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TypeGenerator generateTypes should create type files 1`] = ` 4 | "/* eslint-disable */ 5 | /* This is an autogenerated file. Do not edit this file directly! */ 6 | export type MyQuery = { 7 | hello: string; 8 | }; 9 | export type MyQueryVariables = {}; 10 | " 11 | `; 12 | 13 | exports[`TypeGenerator generateTypes should ignore complex operations document 1`] = `"This document node has complex operations."`; 14 | 15 | exports[`TypeGenerator generateTypes should report errors occuring in typegen visitor 1`] = `"Type "Query" does not have field "goodBye"."`; 16 | -------------------------------------------------------------------------------- /e2e/fixtures/cli.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { fork } = require('child_process'); 3 | 4 | class CLI { 5 | async run(commandName, options = []) { 6 | console.log(''); 7 | console.log(`*** run: ts-graphql-plugin ${commandName} ${options.join(' ')} ***`); 8 | const process = fork(path.resolve(__dirname, '../../lib/cli/cli'), [commandName, ...options], { 9 | cwd: path.join(__dirname, '../..'), 10 | }); 11 | return new Promise(res => { 12 | console.log(''); 13 | process.on('exit', code => res({ code })); 14 | }); 15 | } 16 | } 17 | 18 | function createCLI() { 19 | return new CLI(); 20 | } 21 | 22 | module.exports = createCLI; 23 | -------------------------------------------------------------------------------- /project-fixtures/graphql-codegen-prj/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "graphql-codegen-prj", 4 | "scripts": { 5 | "codegen": "graphql-codegen --config codegen.ts" 6 | }, 7 | "devDependencies": { 8 | "@apollo/client": "3.14.0", 9 | "@graphql-codegen/cli": "6.1.0", 10 | "@graphql-codegen/client-preset": "5.2.2", 11 | "@graphql-codegen/typescript": "5.0.7", 12 | "@graphql-codegen/typescript-document-nodes": "5.0.7", 13 | "@types/react": "19.2.7", 14 | "@types/react-dom": "19.2.3", 15 | "graphql": "16.12.0", 16 | "prettier": "3.7.3", 17 | "react": "19.2.3", 18 | "react-dom": "19.2.3", 19 | "typescript": "5.5.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /project-fixtures/graphql-codegen-prj/src/PostSummary.tsx: -------------------------------------------------------------------------------- 1 | import { graphql, useFragment, type FragmentType } from './gql'; 2 | 3 | import { UserAvatar } from './UserAvatar'; 4 | 5 | const fragment = graphql(` 6 | fragment PostSummary_Post on Post { 7 | id 8 | title 9 | author { 10 | name 11 | ...UserAvatar_User 12 | } 13 | } 14 | `); 15 | 16 | export function PostSummary(props: { post: FragmentType }) { 17 | const post = useFragment(fragment, props.post); 18 | return ( 19 | <> 20 | {post.title} 21 | written by {post.author.name} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /project-fixtures/react-apollo-prj/src/__generated__/app-query.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* This is an autogenerated file. Do not edit this file directly! */ 3 | import { TypedDocumentNode } from "@graphql-typed-document-node/core"; 4 | export type AppQuery = { 5 | viewer: { 6 | repositories: { 7 | nodes: (({ 8 | id: string; 9 | } & RepositoryItem_Repository) | null)[] | null; 10 | }; 11 | }; 12 | }; 13 | export type AppQueryVariables = { 14 | first: number; 15 | }; 16 | export type AppQueryDocument = TypedDocumentNode; 17 | export type RepositoryItem_Repository = { 18 | name: string; 19 | description: string | null; 20 | }; 21 | -------------------------------------------------------------------------------- /src/graphql-language-service-adapter/simple-position.ts: -------------------------------------------------------------------------------- 1 | import type { LineAndCharacter } from '../tsmodule'; 2 | import type { IPosition } from 'graphql-language-service'; 3 | 4 | export class SimplePosition implements IPosition { 5 | line: number; 6 | character: number; 7 | 8 | setLine(v: number) { 9 | this.line = v; 10 | } 11 | 12 | setCharacter(v: number) { 13 | this.character = v; 14 | } 15 | 16 | constructor(lc: LineAndCharacter) { 17 | this.line = lc.line; 18 | this.character = lc.character; 19 | } 20 | 21 | lessThanOrEqualTo(p: IPosition) { 22 | if (this.line < p.line) return true; 23 | if (this.line > p.line) return false; 24 | return this.character <= p.character; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /project-fixtures/react-apollo-prj/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'; 3 | import { createFragmentRegistry } from '@apollo/client/cache'; 4 | 5 | import { App } from './App'; 6 | 7 | import { repositoryItemRepositoryDocument } from './RepositoryItem'; 8 | 9 | function Page() { 10 | const client = new ApolloClient({ 11 | cache: new InMemoryCache({ 12 | fragments: createFragmentRegistry(repositoryItemRepositoryDocument), 13 | }), 14 | }); 15 | return ( 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | createRoot(document.getElementById('root')!).render(); 23 | -------------------------------------------------------------------------------- /src/schema-manager/testing/testing-schema-object.ts: -------------------------------------------------------------------------------- 1 | import { graphqlSync, GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql'; 2 | 3 | const testingSchema = new GraphQLSchema({ 4 | query: new GraphQLObjectType({ 5 | name: 'RootQueryType', 6 | fields: { 7 | hello: { 8 | type: GraphQLString, 9 | resolve() { 10 | return 'world'; 11 | }, 12 | }, 13 | }, 14 | }), 15 | }); 16 | 17 | export function executeTestingSchema({ 18 | query, 19 | variables, 20 | }: { 21 | readonly query: string; 22 | readonly variables: Record; 23 | }) { 24 | return graphqlSync({ 25 | schema: testingSchema, 26 | source: query, 27 | variableValues: variables, 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /e2e/cli-specs/report.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const assert = require('assert'); 4 | const rimraf = require('rimraf'); 5 | 6 | async function run(cli) { 7 | const fixtureDirs = ['project-fixtures/graphql-codegen-prj', 'project-fixtures/react-apollo-prj']; 8 | for (const fixtureDir of fixtureDirs) { 9 | const reportPath = `${fixtureDir}/GRAPHQL_OPERATIONS.md`; 10 | rimraf.sync(path.resolve(__dirname, '../..', reportPath)); 11 | const { code } = await cli.run('report', ['-p', fixtureDir, '--verbose', '-o', reportPath, '--includeFragments']); 12 | assert.equal(fs.existsSync(path.resolve(__dirname, '../../', reportPath)), true); 13 | assert.equal(code, 0); 14 | } 15 | } 16 | 17 | module.exports = run; 18 | -------------------------------------------------------------------------------- /src/typegen-addons/typed-query-document.test.ts: -------------------------------------------------------------------------------- 1 | import { TypedQueryDocumentAddonFactory } from './typed-query-document'; 2 | import { createAddonTester } from './testing/addon-tester'; 3 | 4 | describe(TypedQueryDocumentAddonFactory, () => { 5 | const addonTester = createAddonTester(TypedQueryDocumentAddonFactory); 6 | 7 | const sdl = ` 8 | type Query { 9 | hello: String! 10 | } 11 | `; 12 | 13 | it('should add export statement using TypedQueryDocumentNode', () => { 14 | const { outputSourceFiles } = addonTester.generateTypes({ 15 | files: [{ fileName: 'main.ts', content: 'const query = gql`query MyQuery { hello }`' }], 16 | schemaSDL: sdl, 17 | }); 18 | expect(outputSourceFiles.map(f => f.content)).toMatchSnapshot(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/cache/lru-cache.ts: -------------------------------------------------------------------------------- 1 | export class LRUCache { 2 | private _cacheMap = new Map(); 3 | 4 | constructor(private _maxSize: number = 100) {} 5 | 6 | set(key: TKey, value: TValue) { 7 | this._cacheMap.set(key, value); 8 | if (this._cacheMap.size > this._maxSize) { 9 | const lru = this._cacheMap.keys().next(); 10 | this._cacheMap.delete(lru.value); 11 | } 12 | } 13 | 14 | get(key: TKey) { 15 | const result = this._cacheMap.get(key); 16 | if (!result) return; 17 | this._cacheMap.delete(key); 18 | this._cacheMap.set(key, result); 19 | return result; 20 | } 21 | 22 | has(key: TKey) { 23 | return this._cacheMap.has(key); 24 | } 25 | 26 | delete(key: TKey) { 27 | this._cacheMap.delete(key); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /project-fixtures/react-apollo-prj/src/RepositoryItem.tsx: -------------------------------------------------------------------------------- 1 | import { gql, useFragment } from '@apollo/client'; 2 | import type { RepositoryItem_Repository } from './__generated__/repository-item-repository'; 3 | 4 | export const repositoryItemRepositoryDocument = gql` 5 | fragment RepositoryItem_Repository on Repository { 6 | name 7 | description 8 | } 9 | `; 10 | 11 | export function RepositoryItem({ id }: { id: string }) { 12 | const { complete, data } = useFragment({ 13 | fragment: repositoryItemRepositoryDocument, 14 | from: { 15 | __typename: 'Repository', 16 | id, 17 | }, 18 | }); 19 | if (!complete) return; 20 | return ( 21 |
22 |
{data.name}
23 | {data.description} 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/cli/logger.ts: -------------------------------------------------------------------------------- 1 | export interface Logger { 2 | debug(...args: string[]): void; 3 | info(...args: string[]): void; 4 | error(...args: any[]): void; 5 | } 6 | 7 | export type LogLevel = 'silent' | 'error' | 'info' | 'debug'; 8 | 9 | export class ConsoleLogger implements Logger { 10 | constructor(public logLevel: LogLevel = 'info') {} 11 | 12 | /* eslint-disable no-console */ 13 | error(...args: any[]): void { 14 | if (this.logLevel !== 'silent') { 15 | console.error(...args); 16 | } 17 | } 18 | info(...args: string[]): void { 19 | if (this.logLevel !== 'silent' && this.logLevel !== 'error') { 20 | console.log(...args); 21 | } 22 | } 23 | debug(...args: string[]): void { 24 | if (this.logLevel === 'debug') { 25 | console.log(...args); 26 | } 27 | } 28 | /* eslint-enable no-console */ 29 | } 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | ### Describe the bug 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | ### To Reproduce 14 | 15 | Steps to reproduce the behavior: 16 | 17 | ### Expected behavior 18 | 19 | A clear and concise description of what you expected to happen. 20 | 21 | ### Debug log 22 | 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | If the bug occurs on editor/IDE, turn on verbose log with the following and paste tssserver.log of from the replay. 26 | 27 | ```sh 28 | $ export TSS_LOG="-file `pwd`/tsserver.log -level verbose" 29 | ``` 30 | 31 | If the bug occurs on CLI, paste log with `--verbose` option. 32 | 33 | ### Additional context 34 | 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /src/schema-manager/types.ts: -------------------------------------------------------------------------------- 1 | export type SchemaConfig = { 2 | schema: 3 | | string 4 | | { 5 | file: { 6 | path: string; 7 | }; 8 | } 9 | | { 10 | http: { 11 | url: string; 12 | method?: string; 13 | headers?: { [key: string]: string }; 14 | }; 15 | } 16 | | { 17 | http: { 18 | fromScript: string; 19 | }; 20 | }; 21 | localSchemaExtensions?: string[]; 22 | }; 23 | 24 | export interface SchemaManagerHost { 25 | getProjectRootPath(): string; 26 | 27 | getConfig(): SchemaConfig; 28 | 29 | fileExists(path: string): boolean; 30 | readFile(path: string, encoding?: string): string | undefined; 31 | watchFile( 32 | path: string, 33 | cb: (fileName: string) => void, 34 | interval: number, 35 | ): { 36 | close(): void; 37 | }; 38 | 39 | log(msg: string): void; 40 | } 41 | -------------------------------------------------------------------------------- /project-fixtures/transformation-prj/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const TsGraphQLPlugin = require('../../webpack'); 4 | 5 | const tsgqlPlugin = new TsGraphQLPlugin({ tsconfigPath: __dirname }); 6 | 7 | module.exports = { 8 | resolve: { 9 | extensions: ['.ts', '.js'], 10 | }, 11 | entry: { 12 | main: path.resolve(__dirname, 'query.ts'), 13 | }, 14 | output: { 15 | path: path.resolve(__dirname, 'dist'), 16 | filename: '[name].js', 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.ts$/, 22 | exclude: /node_modules/, 23 | loader: 'ts-loader', 24 | options: { 25 | transpileOnly: true, 26 | getCustomTransformers: () => ({ 27 | before: [tsgqlPlugin.getTransformer()], 28 | }), 29 | }, 30 | }, 31 | ], 32 | }, 33 | plugins: [tsgqlPlugin], 34 | devtool: false, 35 | }; 36 | -------------------------------------------------------------------------------- /e2e/cli-specs/typegen.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const fs = require('fs'); 3 | const rimraf = require('rimraf'); 4 | 5 | async function run(cli) { 6 | const { code: code0 } = await cli.run('typegen', ['-p', 'project-fixtures/react-apollo-prj', '--verbose']); 7 | assert.equal(code0, 0); 8 | 9 | const { code: code1 } = await cli.run('typegen', ['-p', 'project-fixtures/gql-errors-prj', '--verbose']); 10 | assert.equal(code1, 1); 11 | 12 | rimraf.sync('project-fixtures/typegen-addon-prj/__generated__/**'); 13 | const { code: code2 } = await cli.run('typegen', ['-p', 'project-fixtures/typegen-addon-prj', '--verbose']); 14 | assert.equal(code2, 0); 15 | assert(fs.existsSync(__dirname + '/../../project-fixtures/typegen-addon-prj/__generated__/my-query.ts')); 16 | assert(fs.existsSync(__dirname + '/../../project-fixtures/typegen-addon-prj/__generated__/post-fragment.ts')); 17 | } 18 | 19 | module.exports = run; 20 | -------------------------------------------------------------------------------- /project-fixtures/transformation-global-frag-prj/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const TsGraphQLPlugin = require('../../webpack'); 4 | 5 | const tsgqlPlugin = new TsGraphQLPlugin({ tsconfigPath: __dirname }); 6 | 7 | module.exports = { 8 | resolve: { 9 | extensions: ['.ts', '.js'], 10 | }, 11 | entry: { 12 | main: path.resolve(__dirname, 'query.ts'), 13 | }, 14 | output: { 15 | path: path.resolve(__dirname, 'dist'), 16 | filename: '[name].js', 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.ts$/, 22 | exclude: /node_modules/, 23 | loader: 'ts-loader', 24 | options: { 25 | transpileOnly: true, 26 | getCustomTransformers: () => ({ 27 | before: [tsgqlPlugin.getTransformer()], 28 | }), 29 | }, 30 | }, 31 | ], 32 | }, 33 | plugins: [tsgqlPlugin], 34 | devtool: false, 35 | }; 36 | -------------------------------------------------------------------------------- /src/register-hooks/register-typescript.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import type { CompilerOptions } from 'typescript'; 3 | 4 | export function registerTypeScript() { 5 | let defaultCompileOptions: CompilerOptions; 6 | require.extensions['.ts'] = (module, fileName) => { 7 | const ts = require('typescript') as typeof import('typescript'); 8 | if (!defaultCompileOptions) { 9 | defaultCompileOptions = ts.getDefaultCompilerOptions(); 10 | } 11 | const content = fs.readFileSync(fileName, 'utf8'); 12 | const { outputText } = ts.transpileModule(content, { 13 | fileName, 14 | compilerOptions: { 15 | ...defaultCompileOptions, 16 | noEmit: true, 17 | esModuleInterop: true, 18 | target: ts.ScriptTarget.ES2019, 19 | module: ts.ModuleKind.CommonJS, 20 | }, 21 | reportDiagnostics: false, 22 | }); 23 | (module as any)._compile(outputText, fileName); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/analyzer/types.ts: -------------------------------------------------------------------------------- 1 | import { TsGraphQLPluginConfigOptions } from '../types'; 2 | import { TypeGenAddonFactory } from '../typegen'; 3 | 4 | export type OperationType = 'query' | 'mutation' | 'subscription' | 'fragment' | 'complex' | 'other'; 5 | 6 | export type TsGraphQLPluginConfig = Omit & { 7 | typegen: { 8 | addonFactories: TypeGenAddonFactory[]; 9 | }; 10 | }; 11 | 12 | export interface ManifestDocumentEntry { 13 | fileName: string; 14 | type: OperationType; 15 | operationName?: string; 16 | fragmentName?: string; 17 | body: string; 18 | tag?: string; 19 | documentStart: { line: number; character: number }; 20 | documentEnd: { line: number; character: number }; 21 | templateLiteralNodeStart: { line: number; character: number }; 22 | templateLiteralNodeEnd: { line: number; character: number }; 23 | } 24 | 25 | export interface ManifestOutput { 26 | documents: ManifestDocumentEntry[]; 27 | } 28 | -------------------------------------------------------------------------------- /src/analyzer/testing/testing-extractor.ts: -------------------------------------------------------------------------------- 1 | import { Extractor } from '../extractor'; 2 | import { FragmentRegistry } from '../../gql-ast-util'; 3 | import { createTestingLanguageServiceAndHost } from '../../ts-ast-util/testing/testing-language-service'; 4 | import { createScriptSourceHelper } from '../../ts-ast-util'; 5 | 6 | export function createTesintExtractor( 7 | files: { fileName: string; content: string }[], 8 | removeDuplicatedFragments = false, 9 | ) { 10 | const { languageService, languageServiceHost } = createTestingLanguageServiceAndHost({ files }); 11 | const extractor = new Extractor({ 12 | removeDuplicatedFragments, 13 | scriptSourceHelper: createScriptSourceHelper( 14 | { 15 | languageService, 16 | languageServiceHost, 17 | project: { getProjectName: () => '' }, 18 | }, 19 | { exclude: [] }, 20 | ), 21 | fragmentRegistry: new FragmentRegistry(), 22 | debug: () => {}, 23 | }); 24 | return extractor; 25 | } 26 | -------------------------------------------------------------------------------- /src/string-util/color.ts: -------------------------------------------------------------------------------- 1 | const resetCode = '\u001b[0m'; 2 | const colorCode = { 3 | thin: '\u001b[2m', 4 | invert: '\u001b[7m', 5 | black: '\u001b[30m', 6 | red: '\u001b[31m', 7 | green: '\u001b[32m', 8 | yellow: '\u001b[33m', 9 | blue: '\u001b[34m', 10 | magenta: '\u001b[35m', 11 | cyan: '\u001b[36m', 12 | white: '\u001b[37m', 13 | }; 14 | 15 | export const color = Object.entries(colorCode).reduce((acc: any, [name, code]) => { 16 | return { 17 | ...acc, 18 | [name]: (msg: string) => code + msg + resetCode, 19 | }; 20 | }, {}) as { 21 | [P in keyof typeof colorCode]: (msg: string) => string; 22 | }; 23 | 24 | export function clearColor(msg: string) { 25 | const outs: string[] = []; 26 | let i = 0; 27 | while (i < msg.length) { 28 | const charactor = msg[i++]; 29 | if (charactor === '\u001b') { 30 | while (msg[i++] !== 'm') continue; 31 | } else { 32 | outs.push(charactor); 33 | } 34 | } 35 | return outs.join(''); 36 | } 37 | -------------------------------------------------------------------------------- /src/language-service-plugin/language-service-proxy-builder.ts: -------------------------------------------------------------------------------- 1 | import type ts from 'typescript/lib/tsserverlibrary'; 2 | 3 | export type LanguageServiceMethodWrapper = ( 4 | delegate: ts.LanguageService[K], 5 | info?: { languageService: ts.LanguageService }, 6 | ) => ts.LanguageService[K]; 7 | 8 | export class LanguageServiceProxyBuilder { 9 | private _wrappers: any[] = []; 10 | 11 | constructor(private _info: { languageService: ts.LanguageService }) {} 12 | 13 | wrap>(name: K, wrapper: Q) { 14 | this._wrappers.push({ name, wrapper }); 15 | return this; 16 | } 17 | 18 | build(): ts.LanguageService { 19 | const ret = this._info.languageService; 20 | this._wrappers.forEach(({ name, wrapper }) => { 21 | (ret as any)[name] = wrapper(this._info.languageService[name as keyof ts.LanguageService], this._info); 22 | }); 23 | return ret; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | parser: '@typescript-eslint/parser' 3 | parserOptions: 4 | sourceType: 'module' 5 | project: './tsconfig.json' 6 | ecmaFeatures: 7 | jsx: true 8 | useJSXTextNode: true 9 | extends: 10 | - prettier 11 | plugins: 12 | - '@typescript-eslint' 13 | rules: 14 | no-eval: error 15 | no-debugger: error 16 | no-console: error 17 | no-duplicate-imports: error 18 | no-var: error 19 | no-unsafe-finally: error 20 | no-restricted-imports: off 21 | prefer-const: error 22 | prefer-rest-params: error 23 | no-trailing-spaces: 24 | - error 25 | - ignoreComments: true 26 | '@typescript-eslint/no-use-before-define': error 27 | '@typescript-eslint/no-namespace': error 28 | overrides: 29 | - files: '**/*.ts' 30 | excludedFiles: ['*.test.ts', '**/testing/**'] 31 | rules: 32 | '@typescript-eslint/no-restricted-imports': 33 | - error 34 | - paths: 35 | - name: typescript 36 | message: "Use 'tsmodule' instead" 37 | allowTypeImports: true 38 | -------------------------------------------------------------------------------- /src/schema-manager/__snapshots__/extension-manager.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ExtensionManager should parse and extend base schema 1`] = ` 4 | "type Query { 5 | hello: String! 6 | me: User! 7 | } 8 | 9 | type User { 10 | name: String! 11 | }" 12 | `; 13 | 14 | exports[`ExtensionManager should store parser errors with invalid extension 1`] = ` 15 | [ 16 | { 17 | "fileContent": "extend type NoExisitingType { 18 | name: String! 19 | } 20 | ", 21 | "fileName": "/testing/resources/invalid_extension.graphql", 22 | "message": "Cannot extend type "NoExisitingType" because it is not defined.", 23 | }, 24 | ] 25 | `; 26 | 27 | exports[`ExtensionManager should store parser errors with invalid syntax file 1`] = ` 28 | [ 29 | { 30 | "fileContent": "directive @hoge() on FIEELD 31 | ", 32 | "fileName": "/testing/resources/invalid_syntax.graphql", 33 | "locations": [ 34 | { 35 | "character": 16, 36 | "line": 0, 37 | }, 38 | ], 39 | "message": "Syntax Error: Expected Name, found ")".", 40 | }, 41 | ] 42 | `; 43 | -------------------------------------------------------------------------------- /src/typegen-addons/typed-query-document.ts: -------------------------------------------------------------------------------- 1 | import ts from '../tsmodule'; 2 | import type { TypeGenAddonFactory } from '../typegen'; 3 | import { astf } from '../ts-ast-util'; 4 | 5 | export const TypedQueryDocumentAddonFactory: TypeGenAddonFactory = ({ source }) => ({ 6 | operationDefinition({ tsResultNode, tsVariableNode }) { 7 | const lhs = astf.createIdentifier(`${tsResultNode.name.text}Document`); 8 | const rhs = astf.createTypeReferenceNode(astf.createIdentifier('TypedDocumentNode'), [ 9 | astf.createTypeReferenceNode(astf.createIdentifier(tsResultNode.name.text)), 10 | astf.createTypeReferenceNode(astf.createIdentifier(tsVariableNode.name.text)), 11 | ]); 12 | const modifiers = [astf.createModifier(ts.SyntaxKind.ExportKeyword)]; 13 | const typeAliasDeclaration = astf.createTypeAliasDeclaration(modifiers, lhs, undefined, rhs); 14 | // source.pushNamedImportIfNeeded('TypedQueryDocumentNode', 'graphql'); 15 | source.pushNamedImportIfNeeded('TypedDocumentNode', '@graphql-typed-document-node/core'); 16 | source.pushStatement(typeAliasDeclaration); 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /src/typegen/addon/merge-addons.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TypeGenVisitorAddon, 3 | StrictAddon, 4 | CustomScalarInput, 5 | CustomScalarOutput, 6 | DocumentInput, 7 | OperationDefinionInput, 8 | FragmentDefinitionInput, 9 | } from './types'; 10 | 11 | export function mergeAddons(addonList: (TypeGenVisitorAddon | undefined)[]) { 12 | const addon: StrictAddon = { 13 | customScalar: (input: CustomScalarInput) => { 14 | return addonList.reduce((acc: CustomScalarOutput | undefined, addon) => { 15 | return addon?.customScalar ? acc || addon?.customScalar(input) : acc; 16 | }, undefined); 17 | }, 18 | 19 | document(input: DocumentInput) { 20 | return addonList.forEach(addon => addon?.document?.(input)); 21 | }, 22 | 23 | operationDefinition(input: OperationDefinionInput) { 24 | return addonList.forEach(addon => addon?.operationDefinition?.(input)); 25 | }, 26 | 27 | fragmentDefinition(input: FragmentDefinitionInput) { 28 | return addonList.forEach(addon => addon?.fragmentDefinition?.(input)); 29 | }, 30 | }; 31 | 32 | return addon; 33 | } 34 | -------------------------------------------------------------------------------- /e2e/lang-server-specs/diagnostics-syntax.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const path = require('path'); 3 | 4 | function findResponse(responses, eventName) { 5 | return responses.find(response => response.event === eventName); 6 | } 7 | 8 | const fileContent = ` 9 | import gql from 'graphql-tag'; 10 | const q = gql\`{\`; 11 | `; 12 | 13 | async function run(server) { 14 | const file = path.resolve(__dirname, '../../project-fixtures/simple-prj/main.ts'); 15 | server.send({ command: 'open', arguments: { file, fileContent, scriptKindName: 'TS' } }); 16 | await server.waitEvent('projectLoadingFinish'); 17 | server.send({ command: 'geterr', arguments: { files: [file], delay: 0 } }); 18 | await server.waitEvent('semanticDiag'); 19 | return server.close().then(() => { 20 | const semanticDiagEvent = findResponse(server.responses, 'semanticDiag'); 21 | assert(!!semanticDiagEvent); 22 | assert.strictEqual(semanticDiagEvent.body.diagnostics.length, 1); 23 | assert.strictEqual(semanticDiagEvent.body.diagnostics[0].text, 'Syntax Error: Expected Name, found .'); 24 | }); 25 | } 26 | 27 | module.exports = run; 28 | -------------------------------------------------------------------------------- /e2e/lang-server-specs/diagnostics.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const path = require('path'); 3 | 4 | function findResponse(responses, eventName) { 5 | return responses.find(response => response.event === eventName); 6 | } 7 | 8 | const fileContent = ` 9 | import gql from 'graphql-tag'; 10 | const q = gql\`query { goodbye }\`; 11 | `; 12 | 13 | async function run(server) { 14 | const file = path.resolve(__dirname, '../../project-fixtures/simple-prj/main.ts'); 15 | server.send({ command: 'open', arguments: { file, fileContent, scriptKindName: 'TS' } }); 16 | await server.waitEvent('projectLoadingFinish'); 17 | server.send({ command: 'geterr', arguments: { files: [file], delay: 0 } }); 18 | await server.waitEvent('semanticDiag'); 19 | return server.close().then(() => { 20 | const semanticDiagEvent = findResponse(server.responses, 'semanticDiag'); 21 | assert(!!semanticDiagEvent); 22 | assert.equal(semanticDiagEvent.body.diagnostics.length, 1); 23 | assert.equal(semanticDiagEvent.body.diagnostics[0].text, 'Cannot query field "goodbye" on type "Query".'); 24 | }); 25 | } 26 | 27 | module.exports = run; 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) [2017] [Quramy] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | node-version: [18.x] 15 | 16 | steps: 17 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 18 | 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v6 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: npm 24 | 25 | - id: dist-tag 26 | uses: actions/github-script@v8 27 | with: 28 | result-encoding: string 29 | script: | 30 | return /^refs\/tags\/v\d+\.\d+\.\d+$/.test(context.ref) ? "latest" : "next" 31 | 32 | - name: npm publish 33 | run: | 34 | echo "//registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}" > ~/.npmrc 35 | npm whoami 36 | npm ci 37 | npm run build 38 | npm publish --tag ${{ steps.dist-tag.outputs.result }} 39 | env: 40 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 41 | CI: true 42 | -------------------------------------------------------------------------------- /project-fixtures/react-apollo-prj/GRAPHQL_OPERATIONS.md: -------------------------------------------------------------------------------- 1 | # Extracted GraphQL Operations 2 | ## Queries 3 | 4 | ### AppQuery 5 | 6 | ```graphql 7 | query AppQuery($first: Int!) { 8 | viewer { 9 | repositories(first: $first) { 10 | nodes { 11 | id 12 | ...RepositoryItem_Repository @nonreactive 13 | } 14 | } 15 | } 16 | } 17 | 18 | fragment RepositoryItem_Repository on Repository { 19 | name 20 | description 21 | } 22 | ``` 23 | 24 | From [src/App.tsx:6:19](src/App.tsx#L6-L17) 25 | 26 | ## Mutations 27 | 28 | ### UpdateMyRepository 29 | 30 | ```graphql 31 | mutation UpdateMyRepository($repositoryId: ID!) { 32 | updateRepository(input: {repositoryId: $repositoryId}) { 33 | clientMutationId 34 | } 35 | } 36 | ``` 37 | 38 | From [src/App.tsx:19:22](src/App.tsx#L19-L25) 39 | 40 | ## Fragments 41 | 42 | ### RepositoryItem_Repository 43 | 44 | ```graphql 45 | fragment RepositoryItem_Repository on Repository { 46 | name 47 | description 48 | } 49 | ``` 50 | 51 | From [src/RepositoryItem.tsx:4:53](src/RepositoryItem.tsx#L4-L9) 52 | 53 | --- 54 | Extracted by [ts-graphql-plugin](https://github.com/Quramy/ts-graphql-plugin) -------------------------------------------------------------------------------- /src/ts-ast-util/file-name-filter.ts: -------------------------------------------------------------------------------- 1 | import _path from 'node:path'; 2 | 3 | import { globToRegExp } from '../string-util/glob-to-regexp'; 4 | 5 | export function createFileNameFilter({ 6 | specs, 7 | projectName, 8 | _forceWin32 = false, 9 | }: { 10 | specs: string[] | undefined; 11 | projectName: string; 12 | _forceWin32?: boolean; 13 | }) { 14 | const path = _forceWin32 ? _path.win32 : _path; 15 | 16 | const testers = (specs ?? []).map(pattern => { 17 | if (pattern.includes('*') || pattern.includes('?')) { 18 | const regexp = globToRegExp(pattern); 19 | return (normalized: string) => regexp.test(normalized); 20 | } else { 21 | const dirOrFileName = pattern[pattern.length - 1] === '/' ? pattern.slice(0, pattern.length - 1) : pattern; 22 | return (normalized: string) => normalized === dirOrFileName || normalized.startsWith(dirOrFileName + '/'); 23 | } 24 | }); 25 | 26 | const projectRootDirName = path.dirname(projectName); 27 | 28 | const match = (fileName: string) => { 29 | const normalized = path.relative(projectRootDirName, fileName).replace(/\\/g, '/'); 30 | return testers.some(tester => tester(normalized)); 31 | }; 32 | 33 | return match; 34 | } 35 | -------------------------------------------------------------------------------- /e2e/lang-server-specs/completions.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const path = require('path'); 3 | const { extract } = require('fretted-strings'); 4 | 5 | function findResponse(responses, commandName) { 6 | return responses.find(response => response.command === commandName); 7 | } 8 | 9 | async function run(server) { 10 | const file = path.resolve(__dirname, '../../project-fixtures/simple-prj/main.ts'); 11 | const [fileContent, frets] = extract( 12 | ` 13 | const q = gql\`query { 14 | %%% \\ ^ %%% 15 | %%% \\ p %%% 16 | `, 17 | ); 18 | server.send({ command: 'open', arguments: { file, fileContent, scriptKindName: 'TS' } }); 19 | await server.waitEvent('projectLoadingFinish'); 20 | server.send({ 21 | command: 'completions', 22 | arguments: { file, offset: frets.p.character + 1, line: frets.p.line + 1, prefix: '' }, 23 | }); 24 | await server.waitResponse('completions'); 25 | return server.close().then(() => { 26 | const completionsResponse = findResponse(server.responses, 'completions'); 27 | assert(!!completionsResponse); 28 | assert(completionsResponse.body.some(item => item.name === 'hello')); 29 | }); 30 | } 31 | 32 | module.exports = run; 33 | -------------------------------------------------------------------------------- /src/graphql-language-service-adapter/simple-position.test.ts: -------------------------------------------------------------------------------- 1 | import { SimplePosition } from './simple-position'; 2 | 3 | describe(SimplePosition, () => { 4 | describe('setter methods', () => { 5 | test(SimplePosition.prototype.setLine.name, () => { 6 | const p = new SimplePosition({ line: 0, character: 0 }); 7 | p.setLine(1); 8 | expect(p.line).toBe(1); 9 | }); 10 | 11 | test(SimplePosition.prototype.setCharacter.name, () => { 12 | const p = new SimplePosition({ line: 0, character: 0 }); 13 | p.setCharacter(1); 14 | expect(p.character).toBe(1); 15 | }); 16 | }); 17 | 18 | describe(SimplePosition.prototype.lessThanOrEqualTo, () => { 19 | it('should return compared result to another position', () => { 20 | const p1 = new SimplePosition({ line: 1, character: 10 }); 21 | const p2 = new SimplePosition({ line: 0, character: 10 }); 22 | const p3 = new SimplePosition({ line: 1, character: 11 }); 23 | const p4 = new SimplePosition({ line: 1, character: 10 }); 24 | expect(p1.lessThanOrEqualTo(p2)).toBeFalsy(); 25 | expect(p1.lessThanOrEqualTo(p3)).toBeTruthy(); 26 | expect(p1.lessThanOrEqualTo(p4)).toBeTruthy(); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /project-fixtures/react-apollo-prj/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { gql, useSuspenseQuery, useFragment } from '@apollo/client'; 2 | 3 | import { RepositoryItem } from './RepositoryItem'; 4 | import type { AppQueryDocument } from './__generated__/app-query'; 5 | 6 | const query = gql` 7 | query AppQuery($first: Int!) { 8 | viewer { 9 | repositories(first: $first) { 10 | nodes { 11 | id 12 | ...RepositoryItem_Repository @nonreactive 13 | } 14 | } 15 | } 16 | } 17 | `; 18 | 19 | const mutation = gql` 20 | mutation UpdateMyRepository($repositoryId: ID!) { 21 | updateRepository(input: { repositoryId: $repositoryId }) { 22 | clientMutationId 23 | } 24 | } 25 | `; 26 | 27 | export function App() { 28 | const { data } = useSuspenseQuery(query as AppQueryDocument, { variables: { first: 100 } }); 29 | if (!data.viewer || !data.viewer.repositories.nodes) return null; 30 | return ( 31 |
    32 | {data.viewer.repositories.nodes.map( 33 | repository => 34 | repository && ( 35 |
  • 36 | 37 |
  • 38 | ), 39 | )} 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /e2e/lang-server-specs/quickinfo.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const path = require('path'); 3 | const { extract } = require('fretted-strings'); 4 | 5 | function findResponse(responses, commandName) { 6 | return responses.find(response => response.command === commandName); 7 | } 8 | 9 | async function run(server) { 10 | const file = path.resolve(__dirname, '../../project-fixtures/simple-prj/main.ts'); 11 | const [fileContent, frets] = extract( 12 | ` 13 | const q = gql\`query { 14 | hello 15 | %%% ^ %%% 16 | %%% p %%% 17 | } 18 | `, 19 | ); 20 | server.send({ 21 | command: 'open', 22 | arguments: { file, fileContent, scriptKindName: 'TS' }, 23 | }); 24 | await server.waitEvent('projectLoadingFinish'); 25 | server.send({ command: 'quickinfo', arguments: { file, offset: frets.p.character + 1, line: frets.p.line + 1 } }); 26 | await server.waitResponse('quickinfo'); 27 | return server.close().then(() => { 28 | const quickinfoResponse = findResponse(server.responses, 'quickinfo'); 29 | assert(!!quickinfoResponse); 30 | assert(quickinfoResponse.body.displayString === 'Query.hello: String!'); 31 | }); 32 | } 33 | 34 | module.exports = run; 35 | -------------------------------------------------------------------------------- /project-fixtures/graphql-codegen-prj/GRAPHQL_OPERATIONS.md: -------------------------------------------------------------------------------- 1 | # Extracted GraphQL Operations 2 | ## Queries 3 | 4 | ### PopularPosts_Query 5 | 6 | ```graphql 7 | query PopularPosts_Query { 8 | popularPosts { 9 | id 10 | ...PostSummary_Post 11 | } 12 | } 13 | 14 | fragment PostSummary_Post on Post { 15 | id 16 | title 17 | author { 18 | name 19 | ...UserAvatar_User 20 | } 21 | } 22 | 23 | fragment UserAvatar_User on User { 24 | name 25 | avatarURL 26 | } 27 | ``` 28 | 29 | From [src/PopularPosts.tsx:6:24](src/PopularPosts.tsx#L6-L13) 30 | 31 | ## Fragments 32 | 33 | ### PostSummary_Post 34 | 35 | ```graphql 36 | fragment PostSummary_Post on Post { 37 | id 38 | title 39 | author { 40 | name 41 | ...UserAvatar_User 42 | } 43 | } 44 | 45 | fragment UserAvatar_User on User { 46 | name 47 | avatarURL 48 | } 49 | ``` 50 | 51 | From [src/PostSummary.tsx:5:27](src/PostSummary.tsx#L5-L14) 52 | 53 | 54 | ### UserAvatar_User 55 | 56 | ```graphql 57 | fragment UserAvatar_User on User { 58 | name 59 | avatarURL 60 | } 61 | ``` 62 | 63 | From [src/UserAvatar.tsx:3:27](src/UserAvatar.tsx#L3-L8) 64 | 65 | --- 66 | Extracted by [ts-graphql-plugin](https://github.com/Quramy/ts-graphql-plugin) -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18.x] 16 | 17 | steps: 18 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v6 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: npm 24 | - name: Install dependencies 25 | run: | 26 | npm ci 27 | - name: Build 28 | run: | 29 | npm run build 30 | git diff --name-only --exit-code 31 | - name: Test 32 | run: | 33 | npm link 34 | npm link ts-graphql-plugin 35 | npm test 36 | env: 37 | CI: true 38 | - uses: codecov/codecov-action@v5 39 | with: 40 | name: jest 41 | token: ${{ secrets.CODECOV_TOKEN }} 42 | files: ./coverage/coverage-final.json 43 | - uses: codecov/codecov-action@v5 44 | with: 45 | name: e2e 46 | token: ${{ secrets.CODECOV_TOKEN }} 47 | files: ./e2e_coverage/coverage-final.json 48 | -------------------------------------------------------------------------------- /src/cache/lru-cache.test.ts: -------------------------------------------------------------------------------- 1 | import { LRUCache } from './lru-cache'; 2 | 3 | describe(LRUCache, () => { 4 | test('should return cached value', () => { 5 | const cache = new LRUCache(1); 6 | 7 | cache.set('a', 'a'); 8 | 9 | expect(cache.get('a')).toBe('a'); 10 | }); 11 | 12 | test('should release entry via delete', () => { 13 | const cache = new LRUCache(1); 14 | 15 | cache.set('a', 'a'); 16 | cache.delete('a'); 17 | 18 | expect(cache.get('a')).toBe(undefined); 19 | }); 20 | 21 | it('should store entries whose size is specified length via maxLength', () => { 22 | const cache = new LRUCache(2); 23 | 24 | cache.set('a', 'a'); 25 | cache.set('b', 'b'); 26 | cache.set('c', 'c'); 27 | 28 | expect(cache.has('a')).toBeFalsy(); 29 | expect(cache.has('b')).toBeTruthy(); 30 | expect(cache.has('c')).toBeTruthy(); 31 | }); 32 | 33 | it('should hold entries last recently used', () => { 34 | const cache = new LRUCache(2); 35 | 36 | cache.set('a', 'a'); 37 | cache.set('b', 'b'); 38 | cache.get('a'); 39 | cache.set('c', 'c'); 40 | 41 | expect(cache.has('a')).toBeTruthy(); 42 | expect(cache.has('b')).toBeFalsy(); 43 | expect(cache.has('c')).toBeTruthy(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/string-util/glob-to-regexp.ts: -------------------------------------------------------------------------------- 1 | export function globToRegExp(pattern: string) { 2 | let reStr = ''; 3 | for (let i = 0; i < pattern.length; i++) { 4 | const char = pattern[i]; 5 | 6 | switch (char) { 7 | case '/': 8 | case '$': 9 | case '^': 10 | case '+': 11 | case '.': 12 | case '(': 13 | case ')': 14 | case '=': 15 | case '!': 16 | case '|': 17 | case '[': 18 | case ']': 19 | case '{': 20 | case '}': 21 | reStr += '\\' + char; 22 | break; 23 | 24 | case '?': 25 | reStr += '.'; 26 | break; 27 | 28 | case '*': { 29 | const prevChar = pattern[i - 1]; 30 | let starCount = 1; 31 | while (pattern[i + 1] === '*') { 32 | starCount++; 33 | i++; 34 | } 35 | const nextChar = pattern[i + 1]; 36 | const isGlobstar = 37 | starCount > 1 && (prevChar === '/' || prevChar == null) && (nextChar === '/' || nextChar == null); 38 | 39 | if (isGlobstar) { 40 | reStr += '((?:[^/]*(?:/|$))*)'; 41 | i++; 42 | } else { 43 | reStr += '([^/]*)'; 44 | } 45 | break; 46 | } 47 | 48 | default: 49 | reStr += char; 50 | } 51 | } 52 | 53 | reStr = '^' + reStr + '$'; 54 | return new RegExp(reStr); 55 | } 56 | -------------------------------------------------------------------------------- /e2e/lang-server-specs/diagnostics-complex-template.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const path = require('path'); 3 | 4 | const { ERROR_CODES } = require('../../lib/errors'); 5 | 6 | function findResponse(responses, eventName) { 7 | return responses.find(response => response.event === eventName); 8 | } 9 | 10 | const fileContent = ` 11 | import gql from 'graphql-tag'; 12 | const fn = (msg: string) => msg; 13 | const f = gql\` 14 | fragment MyFragment on Query { 15 | hello 16 | } 17 | \`; 18 | const q = gql\` 19 | \${fn(f)} 20 | query { 21 | ...MyFragment 22 | } 23 | \`; 24 | `; 25 | 26 | async function run(server) { 27 | const file = path.resolve(__dirname, '../../project-fixtures/simple-prj/main.ts'); 28 | server.send({ command: 'open', arguments: { file, fileContent, scriptKindName: 'TS' } }); 29 | await server.waitEvent('projectLoadingFinish'); 30 | server.send({ command: 'geterr', arguments: { files: [file], delay: 0 } }); 31 | await server.waitEvent('semanticDiag'); 32 | return server.close().then(() => { 33 | const semanticDiagEvent = findResponse(server.responses, 'semanticDiag'); 34 | assert(!!semanticDiagEvent); 35 | assert.equal(semanticDiagEvent.body.diagnostics.length, 1); 36 | assert.equal(semanticDiagEvent.body.diagnostics[0].text, ERROR_CODES.templateIsTooComplex.message); 37 | }); 38 | } 39 | 40 | module.exports = run; 41 | -------------------------------------------------------------------------------- /src/errors/__snapshots__/error-reporter.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ErrorReporter outputError ErrorWithLocation should output location of errors in human readable format 1`] = ` 4 | "main.ts:2:27 - some error 5 | 6 | 1 7 | 2 const query = invalidQuery; 8 | ~~~~~~~~~~~~ 9 | " 10 | `; 11 | 12 | exports[`ErrorReporter outputError ErrorWithLocation should output location of errors in human readable format with 2 lines 1`] = ` 13 | "main.ts:3:21 - some error 14 | 15 | 2 const query = gql\`; 16 | 3 query MyQuery { 17 | ~~~~~~~~~ 18 | 4 name 19 | ~~~~~~~~~~~~~~~~~~~~ 20 | 5 } 21 | " 22 | `; 23 | 24 | exports[`ErrorReporter outputError ErrorWithLocation should output location of errors in human readable format with 3 or more lines 1`] = ` 25 | "main.ts:3:21 - some error 26 | 27 | 2 const query = gql\`; 28 | 3 query MyQuery { 29 | ~~~~~~~~~ 30 | 4 id 31 | ~~~~~~~~~~~~~~~~~~ 32 | 5 name 33 | ~~~~~~~~~~~~~~~~~~~~ 34 | 6 } 35 | " 36 | `; 37 | 38 | exports[`ErrorReporter outputError ErrorWithoutLocation should output error message 1`] = `"error: hoge"`; 39 | 40 | exports[`ErrorReporter outputError ErrorWithoutLocation should output warn message 1`] = `"warn: hoge"`; 41 | -------------------------------------------------------------------------------- /src/ts-ast-util/testing/testing-language-service.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | import path from 'path'; 3 | 4 | import { ScriptHost } from '../script-host'; 5 | 6 | type Options = { 7 | files?: { 8 | fileName: string; 9 | content: string; 10 | }[]; 11 | }; 12 | 13 | export class TestingLanguageServiceHost extends ScriptHost implements ts.LanguageServiceHost { 14 | constructor(options: Options) { 15 | super(path.resolve(__dirname, '../../'), ts.getDefaultCompilerOptions()); 16 | (options.files || []).forEach(({ fileName, content }) => this._updateFile(fileName, content)); 17 | } 18 | 19 | getFile(fileName: string) { 20 | const content = this.readFile(fileName); 21 | if (!content) return undefined; 22 | return { fileName, content }; 23 | } 24 | 25 | loadFromFileSystem(_fileName: string) { 26 | return undefined; 27 | } 28 | 29 | fileExists(_path: string) { 30 | return true; 31 | } 32 | 33 | updateFile(fileName: string, content: string) { 34 | this._updateFile(fileName, content); 35 | } 36 | } 37 | 38 | export function createTestingLanguageServiceAndHost(options: Options) { 39 | const host = new TestingLanguageServiceHost(options); 40 | return { 41 | languageService: ts.createLanguageService(host), 42 | languageServiceHost: host, 43 | }; 44 | } 45 | 46 | export function createTestingLanguageService(options: Options) { 47 | return createTestingLanguageServiceAndHost(options).languageService; 48 | } 49 | -------------------------------------------------------------------------------- /src/gql-ast-util/utility-functions.test.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'graphql'; 2 | import { detectDuplicatedFragments } from './utility-functions'; 3 | 4 | describe(detectDuplicatedFragments, () => { 5 | it('should detect duplicated fragments info', () => { 6 | const documentContent = ` 7 | fragment Hoge on Query { 8 | id 9 | } 10 | fragment Foo on Query { 11 | id 12 | } 13 | fragment Hoge on Query { 14 | id 15 | } 16 | `; 17 | expect(detectDuplicatedFragments(parse(documentContent))).toMatchSnapshot(); 18 | }); 19 | 20 | it('should return empty array when no duplication', () => { 21 | const documentContent = ` 22 | fragment Hoge on Query { 23 | id 24 | } 25 | fragment Foo on Query { 26 | id 27 | } 28 | `; 29 | expect(detectDuplicatedFragments(parse(documentContent))).toStrictEqual([]); 30 | }); 31 | 32 | it('should return duplicated fragments order by location range desc', () => { 33 | const documentContent = ` 34 | fragment Hoge on Query { 35 | id 36 | } 37 | fragment Bar on Query { 38 | id 39 | } 40 | fragment Foo on Query { 41 | id 42 | } 43 | fragment Hoge on Query { 44 | id 45 | } 46 | fragment Bar on Query { 47 | id 48 | } 49 | `; 50 | const actual = detectDuplicatedFragments(parse(documentContent)); 51 | expect(actual[0].start >= actual[1].end).toBeTruthy(); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /e2e/webpack-specs/watch.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const { execSync } = require('child_process'); 5 | const webpack = require('webpack'); 6 | const { print } = require('graphql/language'); 7 | 8 | async function run() { 9 | const config = require('../../project-fixtures/transformation-prj/webpack.config.js'); 10 | const fileToChange = path.resolve(__dirname, '../../project-fixtures/transformation-prj/fragment-node.ts'); 11 | const originalContent = fs.readFileSync(fileToChange, 'utf8'); 12 | const compiler = webpack({ ...config, mode: 'production' }); 13 | let watching; 14 | let called = 0; 15 | const stats = await new Promise((res, rej) => { 16 | watching = compiler.watch( 17 | { 18 | aggregateTimeout: 300, 19 | poll: undefined, 20 | }, 21 | (err, stats) => { 22 | if (err) return rej(err); 23 | if (!called) { 24 | called++; 25 | fs.writeFileSync(fileToChange, originalContent.replace('bye', 'goodBye'), 'utf8'); 26 | } else { 27 | res(stats); 28 | } 29 | }, 30 | ); 31 | }); 32 | watching.close(); 33 | fs.writeFileSync(fileToChange, originalContent, 'utf8'); 34 | assert(!stats.hasErrors()); 35 | const distFilePath = path.resolve(stats.toJson().outputPath, 'main.js'); 36 | const result = execSync(`node ${distFilePath}`); 37 | assert(print(JSON.parse(result.toString())).indexOf('goodBye') !== -1); 38 | } 39 | 40 | module.exports = run; 41 | -------------------------------------------------------------------------------- /src/graphql-language-service-adapter/get-quick-info-at-position.ts: -------------------------------------------------------------------------------- 1 | import { getHoverInformation } from 'graphql-language-service'; 2 | 3 | import ts from '../tsmodule'; 4 | import type { AnalysisContext, GetQuickInfoAtPosition } from './types'; 5 | import { SimplePosition } from './simple-position'; 6 | 7 | export function getQuickInfoAtPosition( 8 | ctx: AnalysisContext, 9 | delegate: GetQuickInfoAtPosition, 10 | fileName: string, 11 | position: number, 12 | ) { 13 | if (ctx.getScriptSourceHelper().isExcluded(fileName)) return delegate(fileName, position); 14 | const schema = ctx.getSchema(); 15 | if (!schema) return delegate(fileName, position); 16 | const node = ctx.findAscendantTemplateNode(fileName, position); 17 | if (!node) return delegate(fileName, position); 18 | const { resolvedInfo } = ctx.resolveTemplateInfo(fileName, node); 19 | if (!resolvedInfo) return delegate(fileName, position); 20 | const { combinedText, getInnerPosition, convertInnerPosition2InnerLocation } = resolvedInfo; 21 | const cursor = new SimplePosition(convertInnerPosition2InnerLocation(getInnerPosition(position).pos + 1)); 22 | const result = getHoverInformation(schema, combinedText, cursor); 23 | if (typeof result !== 'string' || !result.length) return delegate(fileName, position); 24 | return { 25 | kind: ts.ScriptElementKind.string, 26 | textSpan: { 27 | start: position, 28 | length: 1, 29 | }, 30 | kindModifiers: '', 31 | displayParts: [{ text: result, kind: '' }], 32 | } as ts.QuickInfo; 33 | } 34 | -------------------------------------------------------------------------------- /src/ts-ast-util/__snapshots__/output-source.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`createOutputSource pushDefaultImportIfNeeded should add import specifier if named import statement for same module already exists 1`] = ` 4 | "import Hoge, { Foo } from "./foo"; 5 | " 6 | `; 7 | 8 | exports[`createOutputSource pushDefaultImportIfNeeded should not add import statement when the same statement exists 1`] = ` 9 | "import Hoge from "./foo"; 10 | " 11 | `; 12 | 13 | exports[`createOutputSource pushImportDeclaration should add statement at first when the helpper has no import statement 1`] = ` 14 | "import "typescript"; 15 | hoge; 16 | " 17 | `; 18 | 19 | exports[`createOutputSource pushImportDeclaration should add statement at next the last import declaration 1`] = ` 20 | "import "graphql"; 21 | import "typescript"; 22 | hoge; 23 | " 24 | `; 25 | 26 | exports[`createOutputSource pushNamedImportIfNeeded should add import specifier if named import statement for same module already exists 1`] = ` 27 | "import { Foo, Hoge } from "./foo"; 28 | " 29 | `; 30 | 31 | exports[`createOutputSource pushNamedImportIfNeeded should not add import statement when the same statement exists 1`] = ` 32 | "import { Hoge } from "./foo"; 33 | " 34 | `; 35 | 36 | exports[`createOutputSource writeLeadingComment should comment at the top of file 1`] = ` 37 | "/* foo */ 38 | /* bar */ 39 | hoge; 40 | " 41 | `; 42 | 43 | exports[`createOutputSource writeLeadingComment should comment when outputSource has no statement 1`] = ` 44 | "/* foo */ 45 | hoge; 46 | " 47 | `; 48 | -------------------------------------------------------------------------------- /src/string-util/position-converter.ts: -------------------------------------------------------------------------------- 1 | export class OutOfRangeError extends Error { 2 | constructor() { 3 | super('Out of range'); 4 | } 5 | } 6 | 7 | export function pos2location(content: string, pos: number, throwErrorIfOutOfRange = false) { 8 | if (throwErrorIfOutOfRange) { 9 | if (pos < 0 || content.length <= pos) { 10 | throw new OutOfRangeError(); 11 | } 12 | } 13 | let l = 0, 14 | c = 0; 15 | for (let i = 0; i < content.length && i < pos; i++) { 16 | const cc = content[i]; 17 | if (cc === '\n') { 18 | c = 0; 19 | l++; 20 | } else { 21 | c++; 22 | } 23 | } 24 | return { line: l, character: c }; 25 | } 26 | 27 | export function location2pos( 28 | content: string, 29 | location: { line: number; character: number }, 30 | throwErrorIfOutOfRange = false, 31 | ) { 32 | let il = 0, 33 | ic = 0; 34 | if (throwErrorIfOutOfRange) { 35 | if (location.line < 0 || location.character < 0) { 36 | throw new OutOfRangeError(); 37 | } 38 | } 39 | for (let i = 0; i < content.length; i++) { 40 | const cc = content[i]; 41 | if (il === location.line) { 42 | if (throwErrorIfOutOfRange && (cc === '\n' || (cc === '\r' && content[i + 1] === '\n'))) { 43 | throw new OutOfRangeError(); 44 | } 45 | if (ic === location.character) { 46 | return i; 47 | } 48 | } 49 | if (cc === '\n') { 50 | ic = 0; 51 | il++; 52 | } else { 53 | ic++; 54 | } 55 | } 56 | if (throwErrorIfOutOfRange) { 57 | throw new OutOfRangeError(); 58 | } 59 | return content.length; 60 | } 61 | -------------------------------------------------------------------------------- /src/errors/index.ts: -------------------------------------------------------------------------------- 1 | export { ErrorReporter } from './error-reporter'; 2 | 3 | export type ErrorRange = { 4 | fileName: string; 5 | start: number; 6 | end: number; 7 | }; 8 | 9 | export type Severity = 'Error' | 'Warn'; 10 | 11 | export type ErrorContent = ErrorRange & { 12 | severity?: Severity; 13 | content: string; 14 | }; 15 | 16 | export class ErrorWithLocation extends Error { 17 | readonly name = 'ErrorWithLocation'; 18 | readonly severity: Severity = 'Error'; 19 | 20 | constructor( 21 | public readonly message: string, 22 | public readonly errorContent: ErrorContent, 23 | ) { 24 | super(message); 25 | if (errorContent.severity) { 26 | this.severity = errorContent.severity; 27 | } 28 | } 29 | } 30 | 31 | export class ErrorWithoutLocation extends Error { 32 | readonly name = 'ErrorWithoutLocation'; 33 | constructor( 34 | public readonly message: string, 35 | public readonly severity: Severity = 'Error', 36 | ) { 37 | super(message); 38 | } 39 | } 40 | 41 | export type TsGqlError = ErrorWithLocation | ErrorWithoutLocation; 42 | 43 | export const ERROR_CODES = { 44 | graphqlLangServiceError: { 45 | code: 51001, 46 | }, 47 | duplicatedFragmentDefinitions: { 48 | code: 51002, 49 | message: 'All fragments must have an unique name.', 50 | }, 51 | templateIsTooComplex: { 52 | code: 51010, 53 | message: 'This operation or fragment has too complex interpolation to analyze.', 54 | }, 55 | errorInOtherInterpolation: { 56 | code: 51011, 57 | message: 'This expression has some GraphQL errors.', 58 | }, 59 | schemaBuildError: { 60 | code: 51020, 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /src/analyzer/__snapshots__/markdown-reporter.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`MarkdownReporter should convert from manifest to markdown content 1`] = ` 4 | "# Extracted GraphQL Operations 5 | ## Queries 6 | 7 | ### MyQuery 8 | 9 | \`\`\`graphql 10 | fragment MyFragment on Query { 11 | hello 12 | } 13 | 14 | query MyQuery { 15 | ...MyFragment 16 | } 17 | \`\`\` 18 | 19 | From [src/main.ts:8:27](../src/main.ts#L8-L13) 20 | 21 | ## Mutations 22 | 23 | ### Greeting 24 | 25 | \`\`\`graphql 26 | mutation Greeting { 27 | greeting { 28 | reply 29 | } 30 | } 31 | \`\`\` 32 | 33 | From [src/main.ts:14:30](../src/main.ts#L14-L20) 34 | 35 | --- 36 | Extracted by [ts-graphql-plugin](https://github.com/Quramy/ts-graphql-plugin)" 37 | `; 38 | 39 | exports[`MarkdownReporter should convert from manifest to markdown content with ignoreFragments: false 1`] = ` 40 | "# Extracted GraphQL Operations 41 | ## Queries 42 | 43 | ### MyQuery 44 | 45 | \`\`\`graphql 46 | fragment MyFragment on Query { 47 | hello 48 | } 49 | 50 | query MyQuery { 51 | ...MyFragment 52 | } 53 | \`\`\` 54 | 55 | From [src/main.ts:8:27](../src/main.ts#L8-L13) 56 | 57 | ## Mutations 58 | 59 | ### Greeting 60 | 61 | \`\`\`graphql 62 | mutation Greeting { 63 | greeting { 64 | reply 65 | } 66 | } 67 | \`\`\` 68 | 69 | From [src/main.ts:14:30](../src/main.ts#L14-L20) 70 | 71 | ## Fragments 72 | 73 | ### MyFragment 74 | 75 | \`\`\`graphql 76 | fragment MyFragment on Query { 77 | hello 78 | } 79 | \`\`\` 80 | 81 | From [src/main.ts:3:30](../src/main.ts#L3-L7) 82 | 83 | --- 84 | Extracted by [ts-graphql-plugin](https://github.com/Quramy/ts-graphql-plugin)" 85 | `; 86 | -------------------------------------------------------------------------------- /src/typegen-addons/testing/addon-tester.ts: -------------------------------------------------------------------------------- 1 | import { buildSchema } from 'graphql'; 2 | 3 | import { TypeGenAddonFactory } from '../../typegen'; 4 | import { createTesintExtractor } from '../../analyzer/testing/testing-extractor'; 5 | import { TypeGenerator } from '../../analyzer/type-generator'; 6 | import { parseTagConfig, type TagConfig } from '../../ts-ast-util'; 7 | 8 | function createTestingTypeGenerator({ 9 | files = [], 10 | tag = '', 11 | addonFactories = [], 12 | }: { 13 | files?: { fileName: string; content: string }[]; 14 | tag?: TagConfig; 15 | addonFactories?: TypeGenAddonFactory[]; 16 | }) { 17 | const extractor = createTesintExtractor(files, true); 18 | const generator = new TypeGenerator({ 19 | prjRootPath: '', 20 | tag: parseTagConfig(tag), 21 | addonFactories, 22 | extractor, 23 | debug: () => {}, 24 | }); 25 | return generator; 26 | } 27 | 28 | type InputFile = { fileName: string; content: string }; 29 | 30 | class AddonTester { 31 | constructor( 32 | private readonly facory: TypeGenAddonFactory, 33 | private readonly options: { tag?: string }, 34 | ) {} 35 | 36 | generateTypes({ files, schemaSDL }: { files: InputFile[]; schemaSDL: string }) { 37 | const schema = buildSchema(schemaSDL); 38 | const generator = createTestingTypeGenerator({ files, addonFactories: [this.facory], tag: this.options.tag }); 39 | const { errors, outputSourceFiles } = generator.generateTypes({ files: files.map(f => f.fileName), schema }); 40 | return { errors, outputSourceFiles }; 41 | } 42 | } 43 | 44 | export function createAddonTester(factory: TypeGenAddonFactory, options: { tag?: string } = {}) { 45 | return new AddonTester(factory, options); 46 | } 47 | -------------------------------------------------------------------------------- /src/graphql-language-service-adapter/get-quick-info-at-position.test.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | import { GraphQLSchema } from 'graphql'; 3 | import extract from 'fretted-strings'; 4 | import { createSimpleSchema } from './testing/simple-schema'; 5 | import { AdapterFixture } from './testing/adapter-fixture'; 6 | 7 | function delegateFn(): ts.QuickInfo { 8 | return { 9 | kind: ts.ScriptElementKind.string, 10 | kindModifiers: '', 11 | textSpan: { 12 | start: 0, 13 | length: 0, 14 | }, 15 | displayParts: [], 16 | }; 17 | } 18 | 19 | function createFixture(name: string, schema?: GraphQLSchema) { 20 | return new AdapterFixture(name, schema); 21 | } 22 | 23 | describe('getQuickInfoAtPosition', () => { 24 | it('should return GraphQL quick info', () => { 25 | const fixture = createFixture('main.ts', createSimpleSchema()); 26 | const quickInfoFn = fixture.adapter.getQuickInfoAtPosition.bind(fixture.adapter, delegateFn, 'main.ts'); 27 | const [content, frets] = extract( 28 | ` 29 | const query = \` 30 | query { 31 | hello 32 | %%% ^ ^ %%% 33 | %%% a1 a2 %%% 34 | } 35 | \`; 36 | `, 37 | ); 38 | fixture.source = content; 39 | expect(quickInfoFn(frets.a1.pos - 1)!.displayParts).toEqual([]); 40 | expect( 41 | quickInfoFn(frets.a1.pos)! 42 | .displayParts!.map(dp => dp.text) 43 | .join(''), 44 | ).toMatchSnapshot(); 45 | expect( 46 | quickInfoFn(frets.a2.pos)! 47 | .displayParts!.map(dp => dp.text) 48 | .join(''), 49 | ).toMatchSnapshot(); 50 | expect(quickInfoFn(frets.a2.pos + 1)!.displayParts).toEqual([]); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/webpack/plugin.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import type { Compiler } from 'webpack'; 3 | import { TransformerHost, type GetTransformerOptions } from '../transformer'; 4 | 5 | type WatchFileSystemCompiler = Compiler & { 6 | watchFileSystem: { 7 | watcher: { 8 | mtimes?: { [name: string]: number }; 9 | }; 10 | wfs: { 11 | watcher: { 12 | mtimes?: { [name: string]: number }; 13 | }; 14 | }; 15 | }; 16 | }; 17 | 18 | const PLUGIN_NAME = 'ts-graphql-plugin'; 19 | 20 | export class WebpackPlugin { 21 | private readonly _host: TransformerHost; 22 | private _disabled = false; 23 | 24 | constructor({ tsconfigPath = process.cwd() }: { tsconfigPath?: string } = {}) { 25 | this._host = new TransformerHost({ projectPath: tsconfigPath }); 26 | } 27 | 28 | getTransformer(options?: GetTransformerOptions) { 29 | return this._host.getTransformer({ ...options, getEnabled: () => !this._disabled }); 30 | } 31 | 32 | apply(compiler: WatchFileSystemCompiler) { 33 | compiler.hooks.afterPlugins.tap(PLUGIN_NAME, () => this._host.loadProject()); 34 | compiler.hooks.watchRun.tap(PLUGIN_NAME, () => { 35 | this._disabled = compiler.options.mode === 'development'; 36 | const watcher = compiler.watchFileSystem.watcher || compiler.watchFileSystem.wfs.watcher; 37 | const changedFiles = compiler.modifiedFiles 38 | ? [...compiler.modifiedFiles.keys()] 39 | : Object.keys(watcher.mtimes ?? []); // webpack v4 does not expose modifiedFiles. So we access to changed files with some hacks. 40 | const changedSourceFileNames = changedFiles.filter(f => path.extname(f) === '.ts' || path.extname(f) === '.tsx'); 41 | this._host.updateFiles(changedSourceFileNames); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/schema-manager/testing/testing-schema-manager-host.ts: -------------------------------------------------------------------------------- 1 | import { SchemaManagerHost, SchemaConfig } from '../types'; 2 | 3 | export type CreateTestingSchemaManagerHostOptions = SchemaConfig & { 4 | prjRootPath?: string; 5 | files?: { 6 | fileName: string; 7 | content: string; 8 | }[]; 9 | log?: (msg: string) => void; 10 | }; 11 | 12 | class TestingSchemaManagerHost implements SchemaManagerHost { 13 | private _files: { fileName: string; content: string }[]; 14 | 15 | private _watchers: { path: string; cb: (fileName: string) => void }[] = []; 16 | 17 | constructor(private _config: CreateTestingSchemaManagerHostOptions) { 18 | this._files = _config.files || []; 19 | } 20 | 21 | getConfig() { 22 | return this._config; 23 | } 24 | 25 | getProjectRootPath() { 26 | return this._config.prjRootPath || '/'; 27 | } 28 | 29 | readFile(path: string) { 30 | const found = this._files.find(f => f.fileName === path); 31 | if (found) return found.content; 32 | } 33 | 34 | fileExists(path: string) { 35 | return !!this._files.find(f => f.fileName === path); 36 | } 37 | 38 | watchFile(path: string, cb: (fileName: string) => void) { 39 | this._watchers.push({ path, cb }); 40 | return { close() {} }; 41 | } 42 | 43 | log(msg: string) { 44 | if (this._config.log) { 45 | this._config.log(msg); 46 | } 47 | } 48 | 49 | updateFile(path: string, content: string) { 50 | this._files = this._files.map(f => (f.fileName === path ? { ...f, content } : f)); 51 | this._watchers.filter(w => w.path === path).forEach(w => w.cb(w.path)); 52 | } 53 | } 54 | 55 | export function createTestingSchemaManagerHost(config: CreateTestingSchemaManagerHostOptions) { 56 | return new TestingSchemaManagerHost(config); 57 | } 58 | -------------------------------------------------------------------------------- /src/graphql-language-service-adapter/get-definition-at-position.test.ts: -------------------------------------------------------------------------------- 1 | import extract from 'fretted-strings'; 2 | import { AdapterFixture } from './testing/adapter-fixture'; 3 | 4 | function createFixture(name: string) { 5 | return new AdapterFixture(name); 6 | } 7 | 8 | describe('getDefinitionAtPosition', () => { 9 | const delegateFn = jest.fn(() => []); 10 | 11 | it('should not return definition info when the cursor does not point fragment spread', () => { 12 | const fixture = createFixture('input.ts'); 13 | const [content, frets] = extract( 14 | ` 15 | const query = \` 16 | query MyQuery { 17 | %%% ^ %%% 18 | %%% cur %%% 19 | ...MyFragment 20 | } 21 | 22 | fragment MyFragment on Query { 23 | __typename 24 | } 25 | \`; 26 | `, 27 | ); 28 | fixture.source = content; 29 | const actual = fixture.adapter.getDefinitionAtPosition(delegateFn, 'input.ts', frets.cur.pos); 30 | expect(actual?.length).toBe(0); 31 | }); 32 | 33 | it('should return definition of fragment spread under cursor', () => { 34 | const fixture = createFixture('input.ts'); 35 | const [content, frets] = extract( 36 | ` 37 | const query = \` 38 | query MyQuery { 39 | ...MyFragment 40 | %%% ^ %%% 41 | %%% cur %%% 42 | } 43 | 44 | fragment MyFragment on Query { 45 | __typename 46 | } 47 | \`; 48 | `, 49 | ); 50 | fixture.source = content; 51 | const actual = fixture.adapter.getDefinitionAtPosition(delegateFn, 'input.ts', frets.cur.pos); 52 | expect(actual?.length).toBe(1); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /project-fixtures/typegen-addon-prj/addon.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { template } from 'talt'; 3 | import { TypeGenAddonFactory, TypeGenVisitorAddon } from '../../lib'; 4 | 5 | // `addonFactory` function is called for each output ts file 6 | // and should return an object which implements `TypeGenVisitorAddon` interface. 7 | const addonFactory: TypeGenAddonFactory = typegenContext => { 8 | const { source, extractedInfo } = typegenContext; 9 | 10 | const typesModuleRelativePath = path.relative(source.outputDirName, path.join(__dirname, 'types')); 11 | 12 | const addon: TypeGenVisitorAddon = { 13 | // `document` callback reacts GraphQL Document (Root) AST node. 14 | document() { 15 | source.writeLeadingComment( 16 | `The following types are extracted from ${path.relative(__dirname, extractedInfo.fileName)}`, 17 | ); 18 | }, 19 | 20 | // `customScalar` is called back when processing GraphQL Scalar field. 21 | // And it can return corresponding TypeScript TypeNode such as: 22 | // `ts.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)`, `ts.createTypeReferenceNode('SomeType')` 23 | customScalar({ scalarType }) { 24 | switch (scalarType.name) { 25 | case 'URL': { 26 | // Write `import { GqlURL } from '../../types'; 27 | source.pushNamedImportIfNeeded('GqlURL', typesModuleRelativePath); 28 | 29 | // Set this field as TypeScript `GqlURL` type 30 | return template.typeNode`GqlURL`(); 31 | } 32 | case 'Date': { 33 | return template.typeNode`Date`(); 34 | } 35 | default: 36 | // If return undefined, this scalar field type is determined by the core type generator. 37 | return; 38 | } 39 | }, 40 | }; 41 | 42 | return addon; 43 | }; 44 | 45 | module.exports = addonFactory; 46 | -------------------------------------------------------------------------------- /src/ts-ast-util/file-name-filter.test.ts: -------------------------------------------------------------------------------- 1 | import { createFileNameFilter } from './file-name-filter'; 2 | 3 | describe(createFileNameFilter, () => { 4 | it('should return macher function for dirname', () => { 5 | const match = createFileNameFilter({ 6 | specs: ['__generated__'], 7 | projectName: '/a/b/tsconfig.json', 8 | }); 9 | expect(match('/a/b/__generated__/x.ts')).toBeTruthy(); 10 | expect(match('/a/b/x.ts')).toBeFalsy(); 11 | }); 12 | 13 | it('should return macher function for dirname with trailing slash', () => { 14 | const match = createFileNameFilter({ 15 | specs: ['__generated__/'], 16 | projectName: '/a/b/tsconfig.json', 17 | }); 18 | expect(match('/a/b/__generated__/x.ts')).toBeTruthy(); 19 | expect(match('/a/b/x.ts')).toBeFalsy(); 20 | }); 21 | 22 | it('should return macher function for filename', () => { 23 | const match = createFileNameFilter({ 24 | specs: ['__generated__/x.ts'], 25 | projectName: '/a/b/tsconfig.json', 26 | }); 27 | expect(match('/a/b/__generated__/x.ts')).toBeTruthy(); 28 | expect(match('/a/b/x.ts')).toBeFalsy(); 29 | }); 30 | 31 | it('should return macher function for wildcard', () => { 32 | const match = createFileNameFilter({ 33 | specs: ['**/__generated__/**/*'], 34 | projectName: '/a/b/tsconfig.json', 35 | }); 36 | expect(match('/a/b/__generated__/x.ts')).toBeTruthy(); 37 | expect(match('/a/b/c/__generated__/x.ts')).toBeTruthy(); 38 | expect(match('/a/b/x.ts')).toBeFalsy(); 39 | }); 40 | 41 | it('should work for win32', () => { 42 | const match = createFileNameFilter({ 43 | specs: ['__generated__'], 44 | projectName: '\\a\\b\\tsconfig.json', 45 | _forceWin32: true, 46 | }); 47 | expect(match('\\a\\b\\__generated__\\x.ts')).toBeTruthy(); 48 | expect(match('\\a\\b\\x.ts')).toBeFalsy(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/ts-ast-util/script-host.ts: -------------------------------------------------------------------------------- 1 | import ts from '../tsmodule'; 2 | 3 | export class ScriptHost implements ts.LanguageServiceHost { 4 | private readonly _fileMap = new Map(); 5 | private readonly _fileVersionMap = new Map(); 6 | 7 | constructor( 8 | private readonly _currentDirectory: string, 9 | private readonly _compilerOptions: ts.CompilerOptions, 10 | ) {} 11 | 12 | readFile(fileName: string) { 13 | const hit = this._fileMap.get(fileName); 14 | if (hit != null) return hit; 15 | return this.loadFromFileSystem(fileName); 16 | } 17 | 18 | loadFromFileSystem(fileName: string) { 19 | const content = ts.sys.readFile(fileName, 'uts8'); 20 | this._updateFile(fileName, content); 21 | return content; 22 | } 23 | 24 | getCurrentDirectory() { 25 | return this._currentDirectory; 26 | } 27 | 28 | getScriptSnapshot(fileName: string) { 29 | const file = this._fileMap.get(fileName); 30 | if (file == null) return; 31 | return ts.ScriptSnapshot.fromString(file); 32 | } 33 | 34 | getScriptVersion(fileName: string) { 35 | const version = this._fileVersionMap.get(fileName); 36 | if (!version) return '0'; 37 | return version + ''; 38 | } 39 | 40 | getScriptFileNames() { 41 | return [...this._fileMap.keys()]; 42 | } 43 | 44 | getCompilationSettings() { 45 | return this._compilerOptions; 46 | } 47 | 48 | getDefaultLibFileName(opt: ts.CompilerOptions) { 49 | return ts.getDefaultLibFileName(opt); 50 | } 51 | 52 | fileExists(path: string) { 53 | return ts.sys.fileExists(path); 54 | } 55 | 56 | protected _updateFile(fileName: string, content: string | undefined) { 57 | this._fileMap.set(fileName, content); 58 | const currentVersion = this._fileVersionMap.get(fileName) || 0; 59 | this._fileVersionMap.set(fileName, currentVersion + 1); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/graphql-language-service-adapter/types.ts: -------------------------------------------------------------------------------- 1 | import type ts from 'typescript'; 2 | import type { GraphQLSchema, DocumentNode, FragmentDefinitionNode } from 'graphql'; 3 | import type { ScriptSourceHelper, ResolveResult } from '../ts-ast-util'; 4 | import type { SchemaBuildErrorInfo } from '../schema-manager/schema-manager'; 5 | 6 | export type GetCompletionAtPosition = ts.LanguageService['getCompletionsAtPosition']; 7 | export type GetSemanticDiagnostics = ts.LanguageService['getSemanticDiagnostics']; 8 | export type GetQuickInfoAtPosition = ts.LanguageService['getQuickInfoAtPosition']; 9 | export type GetDefinitionAndBoundSpan = ts.LanguageService['getDefinitionAndBoundSpan']; 10 | export type GetDefinitionAtPosition = ts.LanguageService['getDefinitionAtPosition']; 11 | 12 | export interface AnalysisContext { 13 | debug(msg: string): void; 14 | getScriptSourceHelper(): ScriptSourceHelper; 15 | getSchema(): GraphQLSchema | null | undefined; 16 | getSchemaOrSchemaErrors(): [GraphQLSchema, null] | [null, SchemaBuildErrorInfo[]]; 17 | getGlobalFragmentDefinitions(): FragmentDefinitionNode[]; 18 | getGlobalFragmentDefinitionEntry( 19 | name: string, 20 | ): { node: FragmentDefinitionNode; fileName: string; position: number } | undefined; 21 | getExternalFragmentDefinitions( 22 | documentStr: string, 23 | fileName: string, 24 | sourcePosition: number, 25 | ): FragmentDefinitionNode[]; 26 | getDuplicaterdFragmentDefinitions(): Set; 27 | getGraphQLDocumentNode(text: string): DocumentNode | undefined; 28 | findAscendantTemplateNode( 29 | fileName: string, 30 | position: number, 31 | ): ts.NoSubstitutionTemplateLiteral | ts.TemplateExpression | undefined; 32 | findTemplateNodes(fileName: string): (ts.NoSubstitutionTemplateLiteral | ts.TemplateExpression)[]; 33 | resolveTemplateInfo(fileName: string, node: ts.TemplateExpression | ts.NoSubstitutionTemplateLiteral): ResolveResult; 34 | } 35 | -------------------------------------------------------------------------------- /e2e/webpack-specs/transform.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const path = require('path'); 3 | const { execSync } = require('child_process'); 4 | const webpack = require('webpack'); 5 | const { print } = require('graphql/language'); 6 | 7 | async function specWithoutGlobalFragments() { 8 | const config = require('../../project-fixtures/transformation-prj/webpack.config.js'); 9 | const compiler = webpack({ ...config, mode: 'production' }); 10 | const stats = await new Promise((res, rej) => { 11 | compiler.run((err, stats) => { 12 | if (err) return rej(err); 13 | return res(stats); 14 | }); 15 | }); 16 | assert(!stats.hasErrors()); 17 | const distFilePath = path.resolve(stats.toJson().outputPath, 'main.js'); 18 | const result = execSync(`node ${distFilePath}`); 19 | assert.equal(typeof print(JSON.parse(result.toString())), 'string'); 20 | assert(print(JSON.parse(result.toString())).indexOf('MyQuery') !== -1); 21 | assert(print(JSON.parse(result.toString())).indexOf('fragment FragmentLeaf') !== -1); 22 | } 23 | 24 | async function specWithGlobalFragments() { 25 | const config = require('../../project-fixtures/transformation-global-frag-prj/webpack.config.js'); 26 | const compiler = webpack({ ...config, mode: 'production' }); 27 | const stats = await new Promise((res, rej) => { 28 | compiler.run((err, stats) => { 29 | if (err) return rej(err); 30 | return res(stats); 31 | }); 32 | }); 33 | assert(!stats.hasErrors()); 34 | const distFilePath = path.resolve(stats.toJson().outputPath, 'main.js'); 35 | const result = execSync(`node ${distFilePath}`); 36 | assert(print(JSON.parse(result.toString())).indexOf('MyQuery') !== -1); 37 | assert(print(JSON.parse(result.toString())).indexOf('fragment FragmentLeaf') !== -1); 38 | } 39 | 40 | async function run() { 41 | await specWithoutGlobalFragments(); 42 | await specWithGlobalFragments(); 43 | } 44 | 45 | module.exports = run; 46 | -------------------------------------------------------------------------------- /src/gql-ast-util/utility-functions.ts: -------------------------------------------------------------------------------- 1 | import type { DocumentNode, FragmentDefinitionNode } from 'graphql'; 2 | 3 | export function getFragmentsInDocument(...documentNodes: (DocumentNode | undefined)[]) { 4 | const fragmentDefs = new Map(); 5 | for (const documentNode of documentNodes) { 6 | if (!documentNode) return []; 7 | for (const def of documentNode.definitions) { 8 | if (def.kind === 'FragmentDefinition') { 9 | fragmentDefs.set(def.name.value, def); 10 | } 11 | } 12 | } 13 | return [...fragmentDefs.values()]; 14 | } 15 | 16 | export function getFragmentNamesInDocument(...documentNodes: (DocumentNode | undefined)[]) { 17 | const nameSet = new Set(); 18 | for (const documentNode of documentNodes) { 19 | if (!documentNode) return []; 20 | for (const def of documentNode.definitions) { 21 | if (def.kind === 'FragmentDefinition') { 22 | nameSet.add(def.name.value); 23 | } 24 | } 25 | } 26 | return [...nameSet]; 27 | } 28 | 29 | export function cloneFragmentMap(from: Map, namesToBeExcluded: string[] = []) { 30 | const map = new Map(from); 31 | for (const name in namesToBeExcluded) { 32 | map.delete(name); 33 | } 34 | return map; 35 | } 36 | 37 | export function detectDuplicatedFragments(documentNode: DocumentNode) { 38 | const fragments: FragmentDefinitionNode[] = []; 39 | const duplicatedFragments: FragmentDefinitionNode[] = []; 40 | documentNode.definitions.forEach(def => { 41 | if (def.kind === 'FragmentDefinition') { 42 | if (fragments.some(f => f.name.value === def.name.value)) { 43 | duplicatedFragments.push(def); 44 | } else { 45 | fragments.push(def); 46 | } 47 | } 48 | }); 49 | return duplicatedFragments 50 | .map(def => { 51 | return { 52 | name: def.name.value, 53 | start: def.loc!.start, 54 | end: def.loc!.end, 55 | }; 56 | }) 57 | .sort((a, b) => b.start - a.start); 58 | } 59 | -------------------------------------------------------------------------------- /src/cli/commands/extract.ts: -------------------------------------------------------------------------------- 1 | import type { CommandOptions, CommandCliSetting } from '../parser'; 2 | import { ConsoleLogger } from '../logger'; 3 | 4 | export const cliDefinition = { 5 | description: 'Extract GraphQL documents from TypeScript sources.', 6 | options: { 7 | project: { 8 | alias: 'p', 9 | description: 10 | "Analyze the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.", 11 | defaultValue: '.', 12 | type: 'string', 13 | }, 14 | outFile: { 15 | alias: 'o', 16 | description: 'Output file name of manifest.', 17 | defaultValue: 'manifest.json', 18 | type: 'string', 19 | }, 20 | verbose: { 21 | description: 'Show debug messages.', 22 | type: 'boolean', 23 | }, 24 | }, 25 | } as const satisfies CommandCliSetting; 26 | 27 | export async function extractCommand({ options }: CommandOptions) { 28 | const ts = require('typescript') as typeof import('typescript'); 29 | const { AnalyzerFactory } = require('../../analyzer') as typeof import('../../analyzer'); 30 | const { ErrorReporter } = require('../../errors/error-reporter') as typeof import('../../errors'); 31 | const { color } = require('../../string-util') as typeof import('../../string-util'); 32 | 33 | const logger = new ConsoleLogger(options.verbose ? 'debug' : 'info'); 34 | const errorReporter = new ErrorReporter(process.cwd(), logger.error.bind(logger)); 35 | 36 | const { project, outFile } = options; 37 | const analyzer = new AnalyzerFactory().createAnalyzerFromProjectPath(project, logger.debug.bind(logger)); 38 | const [errors, manifest] = analyzer.extractToManifest(); 39 | 40 | if (errors.length) { 41 | logger.error(color.magenta('Found some errors extracting operations.\n')); 42 | errors.forEach(error => errorReporter.outputError(error)); 43 | } 44 | ts.sys.writeFile(outFile, JSON.stringify(manifest, null, 2)); 45 | logger.info(`Write manifest file to '${color.green(outFile)}'.`); 46 | return true; 47 | } 48 | -------------------------------------------------------------------------------- /src/string-util/glob-to-regexp.test.ts: -------------------------------------------------------------------------------- 1 | import { globToRegExp } from './glob-to-regexp'; 2 | 3 | describe(globToRegExp, () => { 4 | describe('wildcard character', () => { 5 | test.each([ 6 | { pattern: '**/*', fileName: 'index.ts' }, 7 | { pattern: '**/*', fileName: 'a/index.ts' }, 8 | { pattern: '**/*', fileName: 'a/b/index.ts' }, 9 | { pattern: '**/*.test.ts', fileName: 'index.test.ts' }, 10 | { pattern: 'a/b/*.ts', fileName: 'a/b/index.ts' }, 11 | { pattern: '**/b/*.ts', fileName: 'b/index.ts' }, 12 | { pattern: '**/b/*.ts', fileName: 'a/b/index.ts' }, 13 | { pattern: '**/b/**/*', fileName: 'a/b/index.ts' }, 14 | { pattern: 'index.?ts', fileName: 'index.mts' }, 15 | { pattern: 'index.?ts', fileName: 'index.cts' }, 16 | ])("'$pattern' matches '$fileName'", ({ pattern, fileName }) => { 17 | expect(globToRegExp(pattern).test(fileName)).toBeTruthy(); 18 | }); 19 | 20 | test.each([ 21 | { pattern: '**/*.test.ts', fileName: 'index.ts' }, 22 | { pattern: 'a/b/*.ts', fileName: 'a/index.ts' }, 23 | { pattern: '**/b/*.ts', fileName: 'a/bb/index.ts' }, 24 | { pattern: 'index.?ts', fileName: 'index.ts' }, 25 | ])("'$pattern' does not match '$fileName'", ({ pattern, fileName }) => { 26 | expect(globToRegExp(pattern).test(fileName)).toBeFalsy(); 27 | }); 28 | }); 29 | 30 | describe('escape', () => { 31 | test.each([ 32 | { pattern: '../.', fileName: '../.' }, 33 | { pattern: '$.ts', fileName: '$.ts' }, 34 | { pattern: '^.ts', fileName: '^.ts' }, 35 | { pattern: '+.ts', fileName: '+.ts' }, 36 | { pattern: '=.ts', fileName: '=.ts' }, 37 | { pattern: '!.ts', fileName: '!.ts' }, 38 | { pattern: '(a)/b.ts', fileName: '(a)/b.ts' }, 39 | { pattern: '[a]/b.ts', fileName: '[a]/b.ts' }, 40 | { pattern: '{a}/b.ts', fileName: '{a}/b.ts' }, 41 | ])("'$pattern' matches '$fileName'", ({ pattern, fileName }) => { 42 | expect(globToRegExp(pattern).test(fileName)).toBeTruthy(); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/cli/commands/typegen.ts: -------------------------------------------------------------------------------- 1 | import type { CommandOptions, CommandCliSetting } from '../parser'; 2 | import { ConsoleLogger } from '../logger'; 3 | 4 | export const cliDefinition = { 5 | description: 'Generate TypeScript types from GraphQL operations or fragments in your .ts source files.', 6 | options: { 7 | project: { 8 | alias: 'p', 9 | description: 10 | "Analyze the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.", 11 | defaultValue: '.', 12 | type: 'string', 13 | }, 14 | verbose: { 15 | description: 'Show debug messages.', 16 | type: 'boolean', 17 | }, 18 | }, 19 | } as const satisfies CommandCliSetting; 20 | 21 | export async function typegenCommand({ options }: CommandOptions) { 22 | const ts = require('typescript') as typeof import('typescript'); 23 | const { AnalyzerFactory } = require('../../analyzer') as typeof import('../../analyzer'); 24 | const { ErrorReporter } = require('../../errors/error-reporter') as typeof import('../../errors'); 25 | const { color } = require('../../string-util') as typeof import('../../string-util'); 26 | 27 | const logger = new ConsoleLogger(options.verbose ? 'debug' : 'info'); 28 | const { project } = options; 29 | const errorReporter = new ErrorReporter(process.cwd(), logger.error.bind(logger)); 30 | const analyzer = new AnalyzerFactory().createAnalyzerFromProjectPath(project, logger.debug.bind(logger)); 31 | const { errors, outputSourceFiles } = await analyzer.typegen(); 32 | if (errors.length) { 33 | logger.error(`Found ${color.red(errors.length + '')} errors generating type files.\n`); 34 | errors.forEach(error => errorReporter.outputError(error)); 35 | } 36 | if (!outputSourceFiles || outputSourceFiles.length === 0) { 37 | logger.error('No type files to generate.'); 38 | return false; 39 | } 40 | outputSourceFiles.forEach(source => ts.sys.writeFile(source.fileName, source.content)); 41 | logger.info(`Write ${color.green(outputSourceFiles.length + ' type files')}.`); 42 | return true; 43 | } 44 | -------------------------------------------------------------------------------- /src/schema-manager/http-schema-manager.ts: -------------------------------------------------------------------------------- 1 | import { SchemaManager } from './schema-manager'; 2 | import type { SchemaManagerHost } from './types'; 3 | import { requestIntrospectionQuery, type RequestSetup } from './request-introspection-query'; 4 | 5 | export class HttpSchemaManager extends SchemaManager { 6 | private _schema: any = null; 7 | 8 | constructor( 9 | _host: SchemaManagerHost, 10 | protected _options: RequestSetup | null = null, 11 | ) { 12 | super(_host); 13 | } 14 | 15 | protected async _getOptions(): Promise { 16 | return this._options; 17 | } 18 | 19 | protected _fetchErrorOcurred(): void {} 20 | 21 | getBaseSchema() { 22 | return this._schema; 23 | } 24 | 25 | async waitBaseSchema() { 26 | try { 27 | const options = await this._getOptions(); 28 | 29 | if (options === null) { 30 | return null; 31 | } 32 | 33 | return await requestIntrospectionQuery(options); 34 | } catch (error) { 35 | return null; 36 | } 37 | } 38 | 39 | startWatch(interval: number = 1000) { 40 | const makeRequest = async (backoff = interval) => { 41 | let options; 42 | 43 | try { 44 | options = await this._getOptions(); 45 | } catch (error) { 46 | setTimeout(makeRequest, backoff * 2.0); 47 | return; 48 | } 49 | 50 | if (options === null) { 51 | this.log(`Options cannot be null`); 52 | setTimeout(makeRequest, backoff * 2.0); 53 | return; 54 | } 55 | 56 | try { 57 | const query = await requestIntrospectionQuery(options); 58 | 59 | this.log(`Fetch schema data from ${options.url}.`); 60 | 61 | if (query) { 62 | this._schema = query; 63 | this.emitChange(); 64 | } 65 | 66 | setTimeout(makeRequest, interval); 67 | } catch (reason) { 68 | this.log(`Fail to fetch schema data from ${options.url} via:`); 69 | this.log(`${JSON.stringify(reason, null, 2)}`); 70 | 71 | this._fetchErrorOcurred(); 72 | setTimeout(makeRequest, backoff * 2.0); 73 | } 74 | }; 75 | 76 | makeRequest(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /e2e/fixtures/lang-server.js: -------------------------------------------------------------------------------- 1 | const { fork } = require('child_process'); 2 | const path = require('path'); 3 | const { EventEmitter } = require('events'); 4 | 5 | class TSServer { 6 | constructor() { 7 | this._responseEventEmitter = new EventEmitter(); 8 | this._responseCommandEmitter = new EventEmitter(); 9 | const tsserverPath = require.resolve('typescript/lib/tsserver'); 10 | const server = fork(tsserverPath, { 11 | cwd: path.join(__dirname, '../../project-fixtures/simple-prj'), 12 | stdio: ['pipe', 'pipe', 'pipe', 'ipc'], 13 | }); 14 | this._exitPromise = new Promise((resolve, reject) => { 15 | server.on('exit', code => resolve(code)); 16 | server.on('error', reason => reject(reason)); 17 | }); 18 | server.stdout.setEncoding('utf-8'); 19 | server.stdout.on('data', data => { 20 | const [, , res] = data.split('\n'); 21 | const obj = JSON.parse(res); 22 | if (obj.type === 'event') { 23 | this._responseEventEmitter.emit(obj.event, obj); 24 | } else if (obj.type === 'response') { 25 | this._responseCommandEmitter.emit(obj.command, obj); 26 | } 27 | this.responses.push(obj); 28 | }); 29 | this._isClosed = false; 30 | this._server = server; 31 | this._seq = 0; 32 | this.responses = []; 33 | } 34 | 35 | send(command) { 36 | const seq = ++this._seq; 37 | const req = JSON.stringify(Object.assign({ seq: seq, type: 'request' }, command)) + '\n'; 38 | this._server.stdin.write(req); 39 | } 40 | 41 | close() { 42 | if (!this._isClosed) { 43 | this._isClosed = true; 44 | this._server.stdin.end(); 45 | } 46 | return this._exitPromise; 47 | } 48 | 49 | wait(time = 0) { 50 | return new Promise(res => setTimeout(() => res(), time)); 51 | } 52 | 53 | waitEvent(eventName) { 54 | return new Promise(res => this._responseEventEmitter.once(eventName, () => res())); 55 | } 56 | 57 | waitResponse(commandName) { 58 | return new Promise(res => this._responseCommandEmitter.once(commandName, () => res())); 59 | } 60 | } 61 | 62 | function createServer() { 63 | return new TSServer(); 64 | } 65 | 66 | module.exports = createServer; 67 | -------------------------------------------------------------------------------- /src/schema-manager/schema-manager-host.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import ts from '../tsmodule'; 3 | import type { SchemaManagerHost, SchemaConfig } from './types'; 4 | import type { TsGraphQLPluginConfigOptions } from '../types'; 5 | 6 | class SystemSchemaManagerHost implements SchemaManagerHost { 7 | constructor( 8 | private readonly _pluginConfig: TsGraphQLPluginConfigOptions, 9 | private readonly _prjRootPath: string, 10 | private readonly _debug: (msg: string) => void, 11 | ) {} 12 | 13 | log(msg: string): void { 14 | return this._debug(msg); 15 | } 16 | watchFile(path: string, cb: (fileName: string) => void, interval: number): { close(): void } { 17 | return ts.sys.watchFile!(path, cb, interval); 18 | } 19 | readFile(path: string, encoding?: string | undefined): string | undefined { 20 | return ts.sys.readFile(path, encoding); 21 | } 22 | fileExists(path: string): boolean { 23 | return ts.sys.fileExists(path); 24 | } 25 | getConfig(): SchemaConfig { 26 | return this._pluginConfig; 27 | } 28 | getProjectRootPath(): string { 29 | return this._prjRootPath; 30 | } 31 | } 32 | 33 | export function createSchemaManagerHostFromTSGqlPluginConfig( 34 | pluginConfig: Omit, 35 | prjRootPath: string, 36 | debug: (msg: string) => void = () => {}, 37 | ) { 38 | return new SystemSchemaManagerHost(pluginConfig, prjRootPath, debug); 39 | } 40 | 41 | export function createSchemaManagerHostFromLSPluginInfo(info: ts.server.PluginCreateInfo): SchemaManagerHost { 42 | return { 43 | getConfig() { 44 | return info.config as SchemaConfig; 45 | }, 46 | fileExists(path) { 47 | return info.serverHost.fileExists(path); 48 | }, 49 | readFile(path, encoding) { 50 | return info.serverHost.readFile(path, encoding); 51 | }, 52 | watchFile(path, cb, interval) { 53 | return info.serverHost.watchFile(path, cb, interval); 54 | }, 55 | getProjectRootPath() { 56 | return path.dirname(info.project.getProjectName()); 57 | }, 58 | log(msg) { 59 | info.project.projectService.logger.info(`[ts-graphql-plugin] ${msg}`); 60 | }, 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /e2e/run.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const glob = require('glob'); 3 | const createServer = require('./fixtures/lang-server'); 4 | const createCLI = require('./fixtures/cli'); 5 | 6 | async function runLangServerSpecs() { 7 | const langServerSpecFiles = glob.sync('lang-server-specs/*.js', { cwd: __dirname }); 8 | console.log('Start lang server e2e testing.'); 9 | let server; 10 | await langServerSpecFiles.reduce( 11 | (queue, file) => 12 | queue.then(() => require(path.join(__dirname, file))((server = createServer())).then(() => server.close())), 13 | Promise.resolve(null), 14 | ); 15 | console.log(`🌟 ${langServerSpecFiles.length} lang server specs were passed.`); 16 | console.log(''); 17 | } 18 | 19 | async function runCliSpecs() { 20 | const cliSpecFiles = glob.sync('cli-specs/*.js', { cwd: __dirname }); 21 | console.log('Start CLI e2e testing.'); 22 | await cliSpecFiles.reduce( 23 | (queue, file) => queue.then(() => require(path.resolve(__dirname, file))(createCLI())), 24 | Promise.resolve(), 25 | ); 26 | console.log(`🌟 ${cliSpecFiles.length} CLI specs were passed.`); 27 | console.log(''); 28 | } 29 | 30 | async function runWebpackSpecs() { 31 | const webpackSpecFiles = glob.sync('webpack-specs/*.js', { cwd: __dirname }); 32 | console.log('Start webpack e2e testing.'); 33 | await webpackSpecFiles.reduce( 34 | (queue, file) => queue.then(() => require(path.resolve(__dirname, file))()), 35 | Promise.resolve(), 36 | ); 37 | console.log(`🌟 ${webpackSpecFiles.length} webpack specs were passed.`); 38 | console.log(''); 39 | } 40 | 41 | const suitesMap = { 42 | 'lang-server': [runLangServerSpecs], 43 | cli: [runCliSpecs], 44 | webpack: [runWebpackSpecs], 45 | all: [runLangServerSpecs, runCliSpecs, runWebpackSpecs], 46 | }; 47 | 48 | async function run(suiteName) { 49 | try { 50 | const suites = suitesMap[suiteName] || suitesMap.all; 51 | await suites.reduce((queue, suite) => queue.then(() => suite()), Promise.resolve()); 52 | } catch (reason) { 53 | console.log('😢 some specs were failed...'); 54 | console.error(reason); 55 | process.exit(1); 56 | } 57 | } 58 | 59 | const suiteName = process.argv.slice(2)[0]; 60 | 61 | run(suiteName); 62 | -------------------------------------------------------------------------------- /src/cli/commands/validate.ts: -------------------------------------------------------------------------------- 1 | import type { CommandOptions, CommandCliSetting } from '../parser'; 2 | import { ConsoleLogger } from '../logger'; 3 | 4 | export const cliDefinition = { 5 | description: 'Validate GraphQL documents in your TypeScript sources.', 6 | options: { 7 | project: { 8 | alias: 'p', 9 | description: 10 | "Analyze the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.", 11 | defaultValue: '.', 12 | type: 'string', 13 | }, 14 | verbose: { 15 | description: 'Show debug messages.', 16 | type: 'boolean', 17 | }, 18 | exitOnWarn: { 19 | description: 'Exit with code 0 even when warnings are found.', 20 | type: 'boolean', 21 | }, 22 | }, 23 | } as const satisfies CommandCliSetting; 24 | 25 | export async function validateCommand({ options }: CommandOptions) { 26 | const { AnalyzerFactory } = require('../../analyzer') as typeof import('../../analyzer'); 27 | const { ErrorReporter } = require('../../errors/error-reporter') as typeof import('../../errors'); 28 | const { color } = require('../../string-util') as typeof import('../../string-util'); 29 | 30 | const logger = new ConsoleLogger(options.verbose ? 'debug' : 'info'); 31 | const errorReporter = new ErrorReporter(process.cwd(), logger.error.bind(logger)); 32 | const analyzer = new AnalyzerFactory().createAnalyzerFromProjectPath(options.project, logger.debug.bind(logger)); 33 | const { errors } = await analyzer.validate(); 34 | const errorErrors = errors.filter(e => e.severity === 'Error'); 35 | const warnErrors = errors.filter(e => e.severity === 'Warn'); 36 | if (errorErrors.length) { 37 | logger.error(`Found ${color.red(errorErrors.length + '')} errors:`); 38 | errorErrors.forEach(errorReporter.outputError.bind(errorReporter)); 39 | } 40 | if (warnErrors.length) { 41 | logger.error(`Found ${color.yellow(warnErrors.length + '')} warnings:`); 42 | warnErrors.forEach(errorReporter.outputError.bind(errorReporter)); 43 | } 44 | if (errorErrors.length) { 45 | return false; 46 | } else if (warnErrors.length) { 47 | return options.exitOnWarn; 48 | } 49 | logger.info(color.green('No GraphQL validation errors.')); 50 | return true; 51 | } 52 | -------------------------------------------------------------------------------- /e2e/lang-server-specs/diagnostics-with-update.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const path = require('path'); 3 | const { extract } = require('fretted-strings'); 4 | 5 | function findResponse(responses, eventName) { 6 | return responses.find(response => response.event === eventName); 7 | } 8 | 9 | async function run(server) { 10 | const fileFragments = path.resolve(__dirname, '../../project-fixtures/simple-prj/fragments.ts'); 11 | const fileFragmentsContent = ` 12 | import gql from 'graphql-tag'; 13 | const f = gql\`fragment MyFragment on Query { hello }\`; 14 | `; 15 | 16 | const fileMain = path.resolve(__dirname, '../../project-fixtures/simple-prj/main.ts'); 17 | const [fileMainContent, frets] = extract( 18 | ` 19 | import gql from 'graphql-tag'; 20 | const q = gql\`query MyQuery { }\`; 21 | %%% \\ ^ %%% 22 | %%% \\ p %%% 23 | `, 24 | ); 25 | 26 | server.send({ 27 | command: 'open', 28 | arguments: { file: fileFragments, fileContent: fileFragmentsContent, scriptKindName: 'TS' }, 29 | }); 30 | server.send({ command: 'open', arguments: { file: fileMain, fileContent: fileMainContent, scriptKindName: 'TS' } }); 31 | 32 | await server.waitEvent('projectLoadingFinish'); 33 | 34 | server.send({ 35 | command: 'updateOpen', 36 | arguments: { 37 | changedFiles: [ 38 | { 39 | fileName: fileMain, 40 | textChanges: [ 41 | { 42 | newText: '...MyFragment', 43 | start: { 44 | line: frets.p.line + 1, 45 | offset: frets.p.character + 1, 46 | }, 47 | end: { 48 | line: frets.p.line + 1, 49 | offset: frets.p.character + 1, 50 | }, 51 | }, 52 | ], 53 | }, 54 | ], 55 | }, 56 | }); 57 | await server.waitResponse('updateOpen'); 58 | server.send({ command: 'geterr', arguments: { files: [fileMain], delay: 0 } }); 59 | await server.waitEvent('semanticDiag'); 60 | return server.close().then(() => { 61 | const semanticDiagEvent = findResponse(server.responses, 'semanticDiag'); 62 | assert(!!semanticDiagEvent); 63 | assert.equal(semanticDiagEvent.body.diagnostics.length, 0); 64 | }); 65 | } 66 | 67 | module.exports = run; 68 | -------------------------------------------------------------------------------- /e2e/lang-server-specs/definition.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const path = require('path'); 3 | const { extract } = require('fretted-strings'); 4 | 5 | function findResponse(responses, commandName) { 6 | return responses.find(response => response.command === commandName); 7 | } 8 | 9 | async function run(server) { 10 | const fileFragments = path.resolve(__dirname, '../../project-fixtures/simple-prj/fragments.ts'); 11 | const fileMain = path.resolve(__dirname, '../../project-fixtures/simple-prj/main.ts'); 12 | const fileFragmentsContent = ` 13 | const fragment = gql\` 14 | fragment MyFragment on Query { 15 | hello 16 | } 17 | \`; 18 | `; 19 | const [fileMainContent, frets] = extract( 20 | ` 21 | const query = gql\` 22 | query MyQuery { 23 | ...MyFragment 24 | %%% ^ %%% 25 | %%% p %%% 26 | } 27 | \`; 28 | `, 29 | ); 30 | server.send({ 31 | command: 'open', 32 | arguments: { file: fileFragments, fileContent: fileFragmentsContent, scriptKindName: 'TS' }, 33 | }); 34 | server.send({ command: 'open', arguments: { file: fileMain, fileContent: fileMainContent, scriptKindName: 'TS' } }); 35 | 36 | await server.waitEvent('projectLoadingFinish'); 37 | 38 | server.send({ 39 | command: 'definition', 40 | arguments: { file: fileMain, offset: frets.p.character + 1, line: frets.p.line + 1, prefix: '' }, 41 | }); 42 | 43 | await server.waitResponse('definition'); 44 | 45 | server.send({ 46 | command: 'definitionAndBoundSpan', 47 | arguments: { file: fileMain, offset: frets.p.character + 1, line: frets.p.line + 1, prefix: '' }, 48 | }); 49 | 50 | await server.waitResponse('definitionAndBoundSpan'); 51 | 52 | await server.close(); 53 | 54 | const definitionResponse = findResponse(server.responses, 'definition'); 55 | assert(!!definitionResponse); 56 | assert(definitionResponse.body.length === 1); 57 | assert(definitionResponse.body[0].file === fileFragments); 58 | 59 | const definitionAndBoundSpanResponse = findResponse(server.responses, 'definitionAndBoundSpan'); 60 | assert(!!definitionAndBoundSpanResponse); 61 | assert(definitionAndBoundSpanResponse.body.definitions.length === 1); 62 | assert(definitionAndBoundSpanResponse.body.definitions[0].file === fileFragments); 63 | } 64 | 65 | module.exports = run; 66 | -------------------------------------------------------------------------------- /src/schema-manager/request-introspection-query.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse, graphql } from 'msw'; 2 | import { setupServer } from 'msw/node'; 3 | import { GraphQLSchema, ExecutionResult } from 'graphql'; 4 | import { requestIntrospectionQuery } from './request-introspection-query'; 5 | import { executeTestingSchema } from './testing/testing-schema-object'; 6 | 7 | describe(requestIntrospectionQuery, () => { 8 | const server = setupServer(); 9 | 10 | beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' })); 11 | afterEach(() => server.resetHandlers()); 12 | afterAll(() => server.close()); 13 | 14 | it('should reject if HTTP fail', async () => { 15 | await expect(requestIntrospectionQuery({ url: 'http://localhost/graphql' })).rejects.toMatchObject({}); 16 | }); 17 | 18 | it('should reject if HTTP returns bad status', async () => { 19 | server.use(http.post('http://localhost/graphql', () => new Response(null, { status: 404 }))); 20 | await expect(requestIntrospectionQuery({ url: 'http://localhost/graphql' })).rejects.toMatchObject({ 21 | statusCode: 404, 22 | }); 23 | }); 24 | 25 | it('should reject if server returns not json', async () => { 26 | server.use(http.post('http://localhost/graphql', () => new Response('', { status: 200 }))); 27 | await expect(requestIntrospectionQuery({ url: 'http://localhost/graphql' })).rejects.toBeInstanceOf(SyntaxError); 28 | }); 29 | 30 | it('should reject if server returns invalid introspection data', async () => { 31 | server.use( 32 | http.post('http://localhost/graphql', () => 33 | HttpResponse.json({ 34 | data: 'hoge', 35 | errors: null, 36 | }), 37 | ), 38 | ); 39 | await expect(requestIntrospectionQuery({ url: 'http://localhost/graphql' })).rejects.toBeInstanceOf(Error); 40 | }); 41 | 42 | describe('when server returns valid introspection result', () => { 43 | beforeEach(() => { 44 | server.use( 45 | graphql.operation(({ query, variables }) => 46 | HttpResponse.json(executeTestingSchema({ query, variables })), 47 | ), 48 | ); 49 | }); 50 | 51 | it.each(['http://localhost/graphql', 'https://localhost/graphql'])('should resolve schema for %s', async url => { 52 | await expect(requestIntrospectionQuery({ url })).resolves.toBeInstanceOf(GraphQLSchema); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/graphql-language-service-adapter/get-definition-and-bound-span.ts: -------------------------------------------------------------------------------- 1 | import { visit, type FragmentSpreadNode } from 'graphql'; 2 | import ts from '../tsmodule'; 3 | import { getSanitizedTemplateText } from '../ts-ast-util'; 4 | import type { AnalysisContext, GetDefinitionAndBoundSpan } from './types'; 5 | 6 | export function getDefinitionAndBoundSpan( 7 | ctx: AnalysisContext, 8 | delegate: GetDefinitionAndBoundSpan, 9 | fileName: string, 10 | position: number, 11 | ) { 12 | if (ctx.getScriptSourceHelper().isExcluded(fileName)) return delegate(fileName, position); 13 | const node = ctx.findAscendantTemplateNode(fileName, position); 14 | if (!node) return delegate(fileName, position); 15 | const { text, sourcePosition } = getSanitizedTemplateText(node); 16 | const documentNode = ctx.getGraphQLDocumentNode(text); 17 | if (!documentNode) return delegate(fileName, position); 18 | const innerPosition = position - sourcePosition; 19 | let fragmentSpreadNodeUnderCursor: FragmentSpreadNode | undefined; 20 | visit(documentNode, { 21 | FragmentSpread: node => { 22 | if (node.name.loc!.start <= innerPosition && innerPosition < node.name.loc!.end) { 23 | fragmentSpreadNodeUnderCursor = node; 24 | } 25 | }, 26 | }); 27 | if (!fragmentSpreadNodeUnderCursor) return delegate(fileName, position); 28 | const foundDefinitionDetail = ctx.getGlobalFragmentDefinitionEntry(fragmentSpreadNodeUnderCursor.name.value); 29 | if (!foundDefinitionDetail) return delegate(fileName, position); 30 | const definitionSourcePosition = foundDefinitionDetail.position + foundDefinitionDetail.node.name.loc!.start; 31 | return { 32 | textSpan: { 33 | start: sourcePosition + fragmentSpreadNodeUnderCursor.name.loc!.start, 34 | length: fragmentSpreadNodeUnderCursor.name.loc!.end - fragmentSpreadNodeUnderCursor.name.loc!.start, 35 | }, 36 | definitions: [ 37 | { 38 | fileName: foundDefinitionDetail.fileName, 39 | name: foundDefinitionDetail.node.name.value, 40 | textSpan: { 41 | start: definitionSourcePosition, 42 | length: foundDefinitionDetail.node.name.loc!.end - foundDefinitionDetail.node.name.loc!.start, 43 | }, 44 | kind: ts.ScriptElementKind.unknown, 45 | containerKind: ts.ScriptElementKind.unknown, 46 | containerName: '', 47 | }, 48 | ], 49 | } satisfies ts.DefinitionInfoAndBoundSpan; 50 | } 51 | -------------------------------------------------------------------------------- /src/schema-manager/request-introspection-query.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'node:url'; 2 | import Http from 'node:http'; 3 | import Https from 'node:https'; 4 | import { buildClientSchema, getIntrospectionQuery, type GraphQLSchema } from 'graphql'; 5 | 6 | const INTROSPECTION_QUERY_BODY = JSON.stringify({ 7 | query: getIntrospectionQuery(), 8 | }); 9 | 10 | const INTROSPECTION_QUERY_LENGTH = Buffer.byteLength(INTROSPECTION_QUERY_BODY); 11 | 12 | export interface RequestSetup { 13 | url: string; 14 | method?: string; 15 | headers?: { [key: string]: string }; 16 | } 17 | 18 | export function isRequestSetup(requestSetup: RequestSetup | any): requestSetup is RequestSetup { 19 | const availablePropertyNames = ['url', 'method', 'headers']; 20 | 21 | for (const property in requestSetup) { 22 | if (!availablePropertyNames.includes(property)) { 23 | return false; 24 | } 25 | } 26 | 27 | return !!requestSetup.url; 28 | } 29 | 30 | export function requestIntrospectionQuery(options: RequestSetup) { 31 | const headers: { [key: string]: string | number } = { 32 | 'Content-Type': 'application/json', 33 | 'Content-Length': INTROSPECTION_QUERY_LENGTH, 34 | 'User-Agent': 'ts-graphql-plugin', 35 | ...options.headers, 36 | }; 37 | 38 | return new Promise((resolve, reject) => { 39 | const uri = parse(options.url); 40 | 41 | const { method = 'POST' } = options; 42 | const { hostname, protocol, path } = uri; 43 | const port = uri.port && Number.parseInt(uri.port, 10); 44 | const reqParam = { hostname, protocol, path, port, headers, method }; 45 | 46 | const requester = protocol === 'https:' ? Https.request : Http.request; 47 | let body = ''; 48 | 49 | const req = requester(reqParam, res => { 50 | res.on('data', chunk => (body += chunk)); 51 | res.on('end', () => { 52 | if (!res.statusCode || res.statusCode < 200 || res.statusCode > 300) { 53 | reject({ 54 | statusCode: res.statusCode, 55 | body, 56 | }); 57 | } else { 58 | let result: any; 59 | try { 60 | result = JSON.parse(body); 61 | resolve(buildClientSchema(result.data)); 62 | } catch (e) { 63 | reject(e); 64 | } 65 | } 66 | }); 67 | }); 68 | 69 | req.on('error', reason => reject(reason)); 70 | req.write(INTROSPECTION_QUERY_BODY); 71 | req.end(); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /src/schema-manager/schema-manager-factory.ts: -------------------------------------------------------------------------------- 1 | import type { SchemaManagerHost } from './types'; 2 | import { SchemaManager, NoopSchemaManager } from './schema-manager'; 3 | import { FileSchemaManagerOptions, FileSchemaManager } from './file-schema-manager'; 4 | import { HttpSchemaManager } from './http-schema-manager'; 5 | import { ScriptedHttpSchemaManager } from './scripted-http-schema-manager'; 6 | import { RequestSetup } from './request-introspection-query'; 7 | 8 | interface FileSchemaConfigOptions { 9 | file: FileSchemaManagerOptions; 10 | } 11 | 12 | interface HttpSchemaConfigOptions { 13 | http: RequestSetup; 14 | } 15 | 16 | interface ScriptedHttpSchemaManagerOptions { 17 | http: { 18 | fromScript: string; 19 | }; 20 | } 21 | 22 | type SchemaConfigOptions = FileSchemaConfigOptions | HttpSchemaConfigOptions | ScriptedHttpSchemaManagerOptions; 23 | 24 | function isFileType(conf: SchemaConfigOptions): conf is FileSchemaConfigOptions { 25 | return !!(conf as any).file; 26 | } 27 | 28 | function isHttpType(conf: SchemaConfigOptions): conf is HttpSchemaConfigOptions { 29 | return !!(conf as any).http?.url; 30 | } 31 | 32 | function isScriptedHttpType(conf: SchemaConfigOptions): conf is ScriptedHttpSchemaManagerOptions { 33 | return !!(conf as any).http?.fromScript; 34 | } 35 | 36 | export class SchemaManagerFactory { 37 | constructor(private _host: SchemaManagerHost) {} 38 | 39 | create(): SchemaManager { 40 | const schemaConfig = this._host.getConfig().schema; 41 | let options: SchemaConfigOptions; 42 | 43 | if (typeof schemaConfig === 'string') { 44 | options = this._convertOptionsFromString(schemaConfig); 45 | } else { 46 | options = schemaConfig as SchemaConfigOptions; 47 | } 48 | 49 | if (isFileType(options)) { 50 | return new FileSchemaManager(this._host, options.file); 51 | } else if (isHttpType(options)) { 52 | return new HttpSchemaManager(this._host, options.http); 53 | } else if (isScriptedHttpType(options)) { 54 | return new ScriptedHttpSchemaManager(this._host, options.http); 55 | } 56 | 57 | return new NoopSchemaManager(this._host); 58 | } 59 | 60 | _convertOptionsFromString(path: string): SchemaConfigOptions { 61 | if (/https?/.test(path)) { 62 | return { 63 | http: { 64 | url: path, 65 | } as RequestSetup, 66 | }; 67 | } else { 68 | return { 69 | file: { 70 | path, 71 | } as FileSchemaManagerOptions, 72 | }; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/ts-ast-util/script-source-helper.ts: -------------------------------------------------------------------------------- 1 | import ts from '../tsmodule'; 2 | import { findAllNodes, findNode } from './utilily-functions'; 3 | import { TemplateExpressionResolver } from './template-expression-resolver'; 4 | import { createFileNameFilter } from './file-name-filter'; 5 | import type { ScriptSourceHelper } from './types'; 6 | 7 | export function createScriptSourceHelper( 8 | { 9 | languageService, 10 | languageServiceHost, 11 | project, 12 | }: { 13 | languageService: ts.LanguageService; 14 | languageServiceHost: ts.LanguageServiceHost; 15 | project: { getProjectName: () => string }; 16 | }, 17 | { 18 | exclude, 19 | reuseProgram, 20 | }: { 21 | exclude: string[] | undefined; 22 | reuseProgram?: boolean; 23 | }, 24 | ): ScriptSourceHelper { 25 | let cachedProgram: ts.Program | undefined; 26 | 27 | const getSourceFile = (fileName: string) => { 28 | // Note: 29 | // Reuse program in batching procedure(e.g. CLI) because getProgram() is "heavy" function. 30 | const program = cachedProgram ?? languageService.getProgram(); 31 | if (!program) { 32 | throw new Error('language service host does not have program!'); 33 | } 34 | if (reuseProgram) cachedProgram = program; 35 | const s = program.getSourceFile(fileName); 36 | if (!s) { 37 | throw new Error('No source file: ' + fileName); 38 | } 39 | return s; 40 | }; 41 | 42 | const isExcluded = createFileNameFilter({ specs: exclude, projectName: project.getProjectName() }); 43 | const getNode = (fileName: string, position: number) => { 44 | return findNode(getSourceFile(fileName), position); 45 | }; 46 | const getAllNodes = (fileName: string, cond: (n: ts.Node) => undefined | boolean | S) => { 47 | const s = getSourceFile(fileName); 48 | return findAllNodes(s, cond); 49 | }; 50 | const getLineAndChar = (fileName: string, position: number) => { 51 | const s = getSourceFile(fileName); 52 | return ts.getLineAndCharacterOfPosition(s, position); 53 | }; 54 | const resolver = new TemplateExpressionResolver( 55 | languageService, 56 | (fileName: string) => languageServiceHost.getScriptVersion(fileName), 57 | isExcluded, 58 | ); 59 | const resolveTemplateLiteral = resolver.resolve.bind(resolver); 60 | const updateTemplateLiteralInfo = resolver.update.bind(resolver); 61 | return { 62 | isExcluded, 63 | getNode, 64 | getAllNodes, 65 | getLineAndChar, 66 | resolveTemplateLiteral, 67 | updateTemplateLiteralInfo, 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /src/analyzer/markdown-reporter.test.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownReporter } from './markdown-reporter'; 2 | import { createTesintExtractor } from './testing/testing-extractor'; 3 | import { parseTagConfig } from '../ts-ast-util'; 4 | 5 | describe(MarkdownReporter, () => { 6 | it('should convert from manifest to markdown content', () => { 7 | const extractor = createTesintExtractor([ 8 | { 9 | fileName: '/prj-root/src/main.ts', 10 | content: ` 11 | import gql from 'graphql-tag'; 12 | const fragment = gql\` 13 | fragment MyFragment on Query { 14 | hello 15 | } 16 | \`; 17 | const query = gql\` 18 | \${fragment} 19 | query MyQuery { 20 | ...MyFragment 21 | } 22 | \`; 23 | const mutation = gql\` 24 | mutation Greeting { 25 | greeting { 26 | reply 27 | } 28 | } 29 | \`; 30 | `, 31 | }, 32 | ]); 33 | const manifest = extractor.toManifest( 34 | extractor.extract(['/prj-root/src/main.ts'], parseTagConfig('gql')), 35 | parseTagConfig('gql'), 36 | ); 37 | const content = new MarkdownReporter().toMarkdownConntent(manifest, { 38 | baseDir: '/prj-root', 39 | outputDir: '/prj-root/dist', 40 | }); 41 | expect(content).toMatchSnapshot(); 42 | }); 43 | 44 | it('should convert from manifest to markdown content with ignoreFragments: false', () => { 45 | const extractor = createTesintExtractor([ 46 | { 47 | fileName: '/prj-root/src/main.ts', 48 | content: ` 49 | import gql from 'graphql-tag'; 50 | const fragment = gql\` 51 | fragment MyFragment on Query { 52 | hello 53 | } 54 | \`; 55 | const query = gql\` 56 | \${fragment} 57 | query MyQuery { 58 | ...MyFragment 59 | } 60 | \`; 61 | const mutation = gql\` 62 | mutation Greeting { 63 | greeting { 64 | reply 65 | } 66 | } 67 | \`; 68 | `, 69 | }, 70 | ]); 71 | const manifest = extractor.toManifest( 72 | extractor.extract(['/prj-root/src/main.ts'], parseTagConfig('gql')), 73 | parseTagConfig('gql'), 74 | ); 75 | const content = new MarkdownReporter().toMarkdownConntent(manifest, { 76 | ignoreFragments: false, 77 | baseDir: '/prj-root', 78 | outputDir: '/prj-root/dist', 79 | }); 80 | expect(content).toMatchSnapshot(); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/cli/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { registerTypeScript } from '../register-hooks'; 4 | import { createParser } from './parser'; 5 | import { cliDefinition as typegenOptions, typegenCommand } from './commands/typegen'; 6 | import { cliDefinition as extractOptions, extractCommand } from './commands/extract'; 7 | import { cliDefinition as validateOptions, validateCommand } from './commands/validate'; 8 | import { cliDefinition as reportOptions, reportCommand } from './commands/report'; 9 | import { ConsoleLogger } from './logger'; 10 | 11 | async function main() { 12 | const logger = new ConsoleLogger(); 13 | const parser = createParser({ 14 | options: { 15 | help: { 16 | alias: 'h', 17 | description: 'Print this message.', 18 | type: 'boolean', 19 | }, 20 | version: { 21 | alias: 'v', 22 | description: 'Print version.', 23 | type: 'boolean', 24 | }, 25 | }, 26 | commands: { 27 | typegen: typegenOptions, 28 | extract: extractOptions, 29 | validate: validateOptions, 30 | report: reportOptions, 31 | }, 32 | logger, 33 | }); 34 | 35 | const cli = parser.parse(); 36 | 37 | if (cli.errors) { 38 | if (cli.errors.unknownCommand) { 39 | logger.error( 40 | `Unknown command name: ${cli.errors.unknownCommand}. Available commands are: ${cli 41 | .availableCommandNames() 42 | .join(', ')} .`, 43 | ); 44 | } 45 | process.exit(1); 46 | } 47 | 48 | if (!cli.command) { 49 | if (cli.options.help) { 50 | cli.showHelp(); 51 | process.exit(0); 52 | } 53 | if (cli.options.version) { 54 | logger.info(require('../../package.json').version); 55 | process.exit(0); 56 | } 57 | cli.showHelp(); 58 | process.exit(1); 59 | } else { 60 | if (cli.options.help) { 61 | cli.showCommandHelp(Object.keys(cli.command)[0]); 62 | process.exit(0); 63 | } 64 | } 65 | 66 | let result: boolean = false; 67 | try { 68 | registerTypeScript(); 69 | if (cli.command.typegen) { 70 | result = await typegenCommand(cli.command.typegen); 71 | } else if (cli.command.extract) { 72 | result = await extractCommand(cli.command.extract); 73 | } else if (cli.command.validate) { 74 | result = await validateCommand(cli.command.validate); 75 | } else if (cli.command.report) { 76 | result = await reportCommand(cli.command.report); 77 | } 78 | process.exit(result ? 0 : 1); 79 | } catch (e) { 80 | logger.error(e); 81 | process.exit(1); 82 | } 83 | } 84 | 85 | main(); 86 | -------------------------------------------------------------------------------- /src/analyzer/__snapshots__/extractor.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Extractor should convert results to manifest JSON 1`] = ` 4 | { 5 | "documents": [ 6 | { 7 | "body": "query MyQuery { 8 | hello 9 | }", 10 | "documentEnd": { 11 | "character": 8, 12 | "line": 6, 13 | }, 14 | "documentStart": { 15 | "character": 26, 16 | "line": 2, 17 | }, 18 | "fileName": "main.ts", 19 | "fragmentName": undefined, 20 | "operationName": "MyQuery", 21 | "tag": "", 22 | "templateLiteralNodeEnd": { 23 | "character": 9, 24 | "line": 6, 25 | }, 26 | "templateLiteralNodeStart": { 27 | "character": 25, 28 | "line": 2, 29 | }, 30 | "type": "query", 31 | }, 32 | { 33 | "body": "mutation Greeting { 34 | greeting { 35 | reply 36 | } 37 | }", 38 | "documentEnd": { 39 | "character": 8, 40 | "line": 13, 41 | }, 42 | "documentStart": { 43 | "character": 29, 44 | "line": 7, 45 | }, 46 | "fileName": "main.ts", 47 | "fragmentName": undefined, 48 | "operationName": "Greeting", 49 | "tag": "", 50 | "templateLiteralNodeEnd": { 51 | "character": 9, 52 | "line": 13, 53 | }, 54 | "templateLiteralNodeStart": { 55 | "character": 28, 56 | "line": 7, 57 | }, 58 | "type": "mutation", 59 | }, 60 | ], 61 | } 62 | `; 63 | 64 | exports[`Extractor should extract GraphQL documents 1`] = ` 65 | [ 66 | "query MyQuery { 67 | hello 68 | }", 69 | "mutation Greeting { 70 | greeting { 71 | reply 72 | } 73 | }", 74 | ] 75 | `; 76 | 77 | exports[`Extractor should extract GraphQL documents and shrink duplicated fragments when removeDuplicatedFragments: false 1`] = ` 78 | [ 79 | "fragment A on Query { 80 | hello 81 | } 82 | 83 | fragment A on Query { 84 | hello 85 | } 86 | 87 | query MyQuery { 88 | ...A 89 | }", 90 | ] 91 | `; 92 | 93 | exports[`Extractor should extract GraphQL documents and shrink duplicated fragments when removeDuplicatedFragments: true 1`] = ` 94 | [ 95 | "fragment A on Query { 96 | hello 97 | } 98 | 99 | query MyQuery { 100 | ...A 101 | }", 102 | ] 103 | `; 104 | 105 | exports[`Extractor should store template resolve errors with too complex interpolation 1`] = ` 106 | { 107 | "end": 219, 108 | "message": "This operation or fragment has too complex interpolation to analyze.", 109 | "start": 207, 110 | } 111 | `; 112 | -------------------------------------------------------------------------------- /src/graphql-language-service-adapter/get-completion-at-position.ts: -------------------------------------------------------------------------------- 1 | import type ts from '../tsmodule'; 2 | import { getAutocompleteSuggestions, type CompletionItem } from 'graphql-language-service'; 3 | import type { AnalysisContext, GetCompletionAtPosition } from './types'; 4 | import { SimplePosition } from './simple-position'; 5 | 6 | function translateCompletionItems(items: CompletionItem[]): ts.CompletionInfo { 7 | const result: ts.CompletionInfo = { 8 | isGlobalCompletion: false, 9 | isMemberCompletion: false, 10 | isNewIdentifierLocation: false, 11 | entries: items.map(r => { 12 | // FIXME use ts.ScriptElementKind 13 | const kind = r.kind ? r.kind + '' : ('unknown' as any); 14 | return { 15 | name: r.label, 16 | kindModifiers: 'declare', 17 | kind, 18 | sortText: '0', 19 | }; 20 | }), 21 | }; 22 | return result; 23 | } 24 | 25 | export function getCompletionAtPosition( 26 | ctx: AnalysisContext, 27 | delegate: GetCompletionAtPosition, 28 | fileName: string, 29 | position: number, 30 | options: ts.GetCompletionsAtPositionOptions | undefined, 31 | formattingSettings?: ts.FormatCodeSettings | undefined, 32 | ) { 33 | if (ctx.getScriptSourceHelper().isExcluded(fileName)) return delegate(fileName, position, options); 34 | const schema = ctx.getSchema(); 35 | if (!schema) return delegate(fileName, position, options); 36 | const node = ctx.findAscendantTemplateNode(fileName, position); 37 | if (!node) return delegate(fileName, position, options); 38 | const { resolvedInfo } = ctx.resolveTemplateInfo(fileName, node); 39 | if (!resolvedInfo) { 40 | return delegate(fileName, position, options, formattingSettings); 41 | } 42 | const { combinedText, getInnerPosition, convertInnerPosition2InnerLocation } = resolvedInfo; 43 | // NOTE: The getAutocompleteSuggestions function does not return if missing '+1' shift 44 | const innerPositionToSearch = getInnerPosition(position).pos + 1; 45 | const innerLocation = convertInnerPosition2InnerLocation(innerPositionToSearch); 46 | ctx.debug( 47 | 'Get GraphQL complete suggestions. documentText: "' + combinedText + '", position: ' + innerPositionToSearch, 48 | ); 49 | const positionForSeach = new SimplePosition({ 50 | line: innerLocation.line, 51 | character: innerLocation.character, 52 | }); 53 | const gqlCompletionItems = getAutocompleteSuggestions( 54 | schema, 55 | combinedText, 56 | positionForSeach, 57 | undefined, 58 | ctx.getGlobalFragmentDefinitions(), 59 | ); 60 | ctx.debug(JSON.stringify(gqlCompletionItems)); 61 | return translateCompletionItems(gqlCompletionItems); 62 | } 63 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution 2 | 3 | We're always welcome to issues / PRs :smile: 4 | 5 | 6 | 7 | - [Setup](#setup) 8 | - [Code format](#code-format) 9 | - [Testing](#testing) 10 | - [Unit testing](#unit-testing) 11 | - [E2E testing](#e2e-testing) 12 | - [Manual testing](#manual-testing) 13 | - [Language service plugin](#language-service-plugin) 14 | - [CLI](#cli) 15 | - [Adding New Dependencies](#adding-new-dependencies) 16 | 17 | 18 | 19 | ## Setup 20 | 21 | 1. Clone this repository 22 | 23 | ```sh 24 | git clone https://github.com/Quramy/ts-graphql-plugin.git 25 | cd ts-graphql-plugin 26 | ``` 27 | 28 | 2. Install dependencies 29 | 30 | ```sh 31 | npm install --no-save 32 | ``` 33 | 34 | 3. Compile TypeScript sources 35 | 36 | ```sh 37 | npm run build 38 | ``` 39 | 40 | ## Code format 41 | 42 | We use Prettier and configure to format sources automatically when they're git staged. 43 | 44 | And we use ESLint. 45 | 46 | ```sh 47 | npm run lint 48 | ``` 49 | 50 | ## Testing 51 | 52 | ### Unit testing 53 | 54 | If you add / modify some functions, write unit testing code about them. 55 | 56 | Execute the following to run all unit testing codes: 57 | 58 | ```sh 59 | npm run test 60 | ``` 61 | 62 | ### E2E testing 63 | 64 | In some cases, it's difficult to cover entire functions by unit testing. For example, we should assert "Our language service extension should react when text editor/IDE send a request". We should make sure the whole feature works together correctly. 65 | 66 | In such cases, consider adding E2E test specs. 67 | 68 | ```sh 69 | npm run build 70 | npm link 71 | npm link ts-graphql-plugin 72 | npm run e2e all 73 | ``` 74 | 75 | You can specify test suite name via: 76 | 77 | ```sh 78 | npm run e2e cli # Execute only specs under e2e/cli-specs 79 | ``` 80 | 81 | ### Manual testing 82 | 83 | #### Language service plugin 84 | 85 | You can check manually language service plugin features with our example project. 86 | 87 | ```sh 88 | npm run bulid 89 | npm link 90 | cd project-fixtures/react-apollo-prj 91 | npm install 92 | npm link ts-graphql-plugin 93 | code . # Or launch editor/IDE what you like 94 | ``` 95 | 96 | Of course, you can use other editor which communicates with tsserver . 97 | 98 | #### CLI 99 | 100 | You can run CLI using compiled `cli.js`. For example: 101 | 102 | ``` 103 | node lib/cli/cli.js validate -p project-fixtures/gql-errors-prj 104 | ``` 105 | 106 | ## Adding New Dependencies 107 | 108 | Not add new dependencies. ts-graphql-plugin is implemented for the purpose of being able to be installed by users in a short installation time. 109 | -------------------------------------------------------------------------------- /src/schema-manager/file-schema-manager.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { buildSchema, buildClientSchema } from 'graphql'; 3 | 4 | import type ts from '../tsmodule'; 5 | 6 | import { SchemaManager } from './schema-manager'; 7 | import type { SchemaManagerHost } from './types'; 8 | 9 | function extractIntrospectionContentFromJson(jsonObject: any) { 10 | if (jsonObject.data) { 11 | return jsonObject.data; 12 | } else { 13 | return jsonObject; 14 | } 15 | } 16 | 17 | export interface FileSchemaManagerOptions { 18 | path: string; 19 | } 20 | 21 | export class FileSchemaManager extends SchemaManager { 22 | private _schemaPath: string; 23 | private _watcher?: ts.FileWatcher; 24 | 25 | constructor( 26 | protected _host: SchemaManagerHost, 27 | options: FileSchemaManagerOptions, 28 | ) { 29 | super(_host); 30 | this._schemaPath = options.path; 31 | } 32 | 33 | getBaseSchema() { 34 | if (!this._schemaPath || typeof this._schemaPath !== 'string') return null; 35 | try { 36 | const resolvedSchmaPath = this.getAbsoluteSchemaPath(this._host.getProjectRootPath(), this._schemaPath); 37 | this.log('Read schema from ' + resolvedSchmaPath); 38 | const isExists = this._host.fileExists(resolvedSchmaPath); 39 | if (!isExists) return null; 40 | if (this._schemaPath.endsWith('.graphql') || this._schemaPath.endsWith('.gql')) { 41 | const sdl = this._host.readFile(resolvedSchmaPath, 'utf-8'); 42 | return sdl ? buildSchema(sdl) : null; 43 | } else { 44 | const introspectionContents = this._host.readFile(resolvedSchmaPath, 'utf-8'); 45 | return introspectionContents 46 | ? buildClientSchema(extractIntrospectionContentFromJson(JSON.parse(introspectionContents))) 47 | : null; 48 | } 49 | } catch (err) { 50 | this.log('Fail to read schema file...'); 51 | this.log(err instanceof Error ? err.message : `Unknown error: ${err}`); 52 | return null; 53 | } 54 | } 55 | 56 | async waitBaseSchema() { 57 | return this.getBaseSchema(); 58 | } 59 | 60 | getAbsoluteSchemaPath(projectRootPath: string, schemaPath: string) { 61 | if (path.isAbsolute(schemaPath)) return schemaPath; 62 | return path.resolve(projectRootPath, schemaPath); 63 | } 64 | 65 | startWatch(interval: number = 100) { 66 | const resolvedSchmaPath = this.getAbsoluteSchemaPath(this._host.getProjectRootPath(), this._schemaPath); 67 | this._watcher = this._host.watchFile( 68 | resolvedSchmaPath, 69 | () => { 70 | this.log('Change schema file.'); 71 | this.emitChange(); 72 | }, 73 | interval, 74 | ); 75 | } 76 | 77 | closeWatch() { 78 | if (this._watcher) this._watcher.close(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/schema-manager/schema-manager.ts: -------------------------------------------------------------------------------- 1 | import type { GraphQLSchema } from 'graphql'; 2 | import { ExtensionManager } from './extension-manager'; 3 | import type { SchemaManagerHost } from './types'; 4 | 5 | export type SchemaBuildErrorInfo = { 6 | message: string; 7 | fileName: string; 8 | fileContent: string; 9 | locations?: ReadonlyArray<{ line: number; character: number }>; 10 | }; 11 | 12 | export type OnChangeCallback = (errors: SchemaBuildErrorInfo[] | null, schema: GraphQLSchema | null) => void; 13 | 14 | export abstract class SchemaManager { 15 | private _onChanges: OnChangeCallback[]; 16 | private _extensionManager: ExtensionManager; 17 | 18 | constructor(protected _host: SchemaManagerHost) { 19 | this._onChanges = []; 20 | this._extensionManager = new ExtensionManager(_host); 21 | this._extensionManager.readExtensions(); 22 | } 23 | 24 | abstract getBaseSchema(): GraphQLSchema | null; 25 | protected abstract waitBaseSchema(): Promise; 26 | protected abstract startWatch(interval?: number): void; 27 | 28 | start(interval?: number) { 29 | this._extensionManager.startWatch(() => this.emitChange(), interval); 30 | this.startWatch(interval); 31 | } 32 | 33 | getSchema(): { schema: GraphQLSchema | null; errors: SchemaBuildErrorInfo[] | null } { 34 | const baseSchema = this.getBaseSchema(); 35 | const schema = baseSchema && this._extensionManager.extendSchema(baseSchema); 36 | if (schema) { 37 | return { schema, errors: null }; 38 | } else { 39 | return { schema: null, errors: this._extensionManager.getSchemaErrors() }; 40 | } 41 | } 42 | 43 | async waitSchema(): Promise<{ schema: GraphQLSchema | null; errors: SchemaBuildErrorInfo[] | null }> { 44 | const baseSchema = await this.waitBaseSchema(); 45 | if (!baseSchema) return { schema: null, errors: null }; 46 | const schema = this._extensionManager.extendSchema(baseSchema); 47 | if (schema) { 48 | return { schema, errors: null }; 49 | } else { 50 | return { schema: null, errors: this._extensionManager.getSchemaErrors() }; 51 | } 52 | } 53 | 54 | registerOnChange(cb: OnChangeCallback) { 55 | this._onChanges.push(cb); 56 | return () => { 57 | this._onChanges = this._onChanges.filter(x => x !== cb); 58 | }; 59 | } 60 | 61 | protected emitChange() { 62 | const { errors, schema } = this.getSchema(); 63 | this._onChanges.forEach(cb => cb(errors, schema)); 64 | } 65 | 66 | protected log(msg: string) { 67 | this._host.log(msg); 68 | } 69 | } 70 | 71 | export class NoopSchemaManager extends SchemaManager { 72 | startWatch() {} 73 | 74 | async waitBaseSchema() { 75 | return null; 76 | } 77 | 78 | getBaseSchema() { 79 | return null; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/ts-ast-util/__snapshots__/template-expression-resolver.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`resolve string combinination pattern should return combined string in TemplateExpression with StringLiteral 1`] = `"query { foo }"`; 4 | 5 | exports[`resolve string combinination pattern should return combined string with class static property interpolation 1`] = ` 6 | " 7 | 8 | fragment Foo on Hoge { 9 | name 10 | } 11 | 12 | query { 13 | ...Foo 14 | }" 15 | `; 16 | 17 | exports[`resolve string combinination pattern should return combined string with hopping reference 1`] = ` 18 | " 19 | 20 | fragment Foo on Hoge { 21 | name 22 | } 23 | query { 24 | ...Foo 25 | }" 26 | `; 27 | 28 | exports[`resolve string combinination pattern should return combined string with property access interpolation 1`] = ` 29 | " 30 | 31 | fragment Foo on Hoge { 32 | name 33 | } 34 | query { 35 | ...Foo 36 | }" 37 | `; 38 | 39 | exports[`resolve string combinination pattern should return combined string with reference between multiple files 1`] = ` 40 | " 41 | 42 | fragment Foo on Hoge { 43 | name 44 | } 45 | query { 46 | ...Foo 47 | }" 48 | `; 49 | 50 | exports[`resolve string combinination pattern should return combined string with reference to other literal 1`] = ` 51 | " 52 | 53 | fragment Foo on Hoge { 54 | name 55 | } 56 | query { 57 | ...Foo 58 | }" 59 | `; 60 | 61 | exports[`resolve string combinination pattern should return combined string with shorthand property access interpolation 1`] = ` 62 | " 63 | 64 | fragment Foo on Hoge { 65 | name 66 | } 67 | query { 68 | ...Foo 69 | }" 70 | `; 71 | 72 | exports[`resolve string combinination pattern should return combined string with template in call expression 1`] = ` 73 | " 74 | 75 | 76 | fragment Foo on Hoge { 77 | name 78 | } 79 | 80 | fragment Piyo on Hoge { 81 | ...Foo 82 | } 83 | 84 | query { 85 | ...Piyo 86 | }" 87 | `; 88 | -------------------------------------------------------------------------------- /src/schema-manager/scripted-http-schema-manager.ts: -------------------------------------------------------------------------------- 1 | import { join, isAbsolute } from 'node:path'; 2 | import type { SchemaManagerHost } from './types'; 3 | import { RequestSetup, isRequestSetup } from './request-introspection-query'; 4 | import { HttpSchemaManager } from './http-schema-manager'; 5 | 6 | interface ScriptedHttpSchemaManagerOptions { 7 | fromScript: string; 8 | } 9 | 10 | export class ScriptedHttpSchemaManager extends HttpSchemaManager { 11 | private _scriptFileName: string; 12 | 13 | constructor(_host: SchemaManagerHost, options: ScriptedHttpSchemaManagerOptions) { 14 | super(_host); 15 | this._scriptFileName = options.fromScript; 16 | this._host.watchFile(this._getScriptFilePath(), this._configurationScriptChanged.bind(this), 100); 17 | } 18 | 19 | private _getScriptFilePath() { 20 | const rootPath = isAbsolute(this._host.getProjectRootPath()) ? this._host.getProjectRootPath() : process.cwd(); 21 | return join(rootPath, this._scriptFileName); 22 | } 23 | 24 | private _requireScript(path: string) { 25 | delete require.cache[path]; 26 | return require(path); 27 | } 28 | 29 | private _configurationScriptChanged() { 30 | this._options = null; 31 | } 32 | 33 | protected _fetchErrorOcurred() { 34 | this._options = null; 35 | } 36 | 37 | protected async _getOptions(): Promise { 38 | if (this._options !== null) { 39 | return this._options; 40 | } 41 | 42 | const configurationScriptPath = this._getScriptFilePath(); 43 | 44 | if (!this._host.fileExists(configurationScriptPath)) { 45 | const errorMessage = `ScriptedHttpSchemaManager configuration script '${configurationScriptPath}' does not exist`; 46 | this.log(errorMessage); 47 | throw new Error(errorMessage); 48 | } 49 | 50 | const configurationScript = this._requireScript(configurationScriptPath); 51 | 52 | let setup = null; 53 | 54 | try { 55 | setup = await configurationScript(this._host.getProjectRootPath()); 56 | } catch (error) { 57 | const errorMessage = `ScriptedHttpSchemaManager configuration script '${this._scriptFileName}' execution failed due to: ${error}`; 58 | this.log(errorMessage); 59 | throw new Error(errorMessage); 60 | } 61 | 62 | if (!isRequestSetup(setup)) { 63 | const errorMessage = `RequestSetup object is wrong: ${JSON.stringify(setup, null, 2)}`; 64 | this.log(errorMessage); 65 | throw new Error(errorMessage); 66 | } 67 | 68 | if (!/https?:/.test(setup.url)) { 69 | const errorMessage = `RequestSetup.url have to be valid url: ${setup.url}`; 70 | this.log(errorMessage); 71 | throw new Error(errorMessage); 72 | } 73 | 74 | setup.method = setup.method || 'POST'; 75 | this._options = setup; 76 | return setup; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/ts-ast-util/register-document-change-event.ts: -------------------------------------------------------------------------------- 1 | import type ts from '../tsmodule'; 2 | 3 | type DocumentChangeEventListener = { 4 | onAcquire: (fileName: string, sourceFile: ts.SourceFile, version: string) => void; 5 | onUpdate?: (fileName: string, sourceFile: ts.SourceFile, version: string) => void; 6 | onRelease?: (fileName: string) => void; 7 | }; 8 | 9 | export function registerDocumentChangeEvent( 10 | target: ts.DocumentRegistry, 11 | documentChangeEventListener: DocumentChangeEventListener, 12 | ) { 13 | target.acquireDocument = new Proxy(target.acquireDocument, { 14 | apply: (delegate, thisArg, args: Parameters) => { 15 | const [fileName, , , version] = args; 16 | const sourceFile = delegate.apply(thisArg, args); 17 | documentChangeEventListener.onAcquire(fileName, sourceFile, version); 18 | return sourceFile; 19 | }, 20 | }); 21 | 22 | target.acquireDocumentWithKey = new Proxy(target.acquireDocumentWithKey, { 23 | apply: (delegate, thisArg, args: Parameters) => { 24 | const [fileName, , , , , version] = args; 25 | const sourceFile = delegate.apply(thisArg, args); 26 | documentChangeEventListener.onAcquire(fileName, sourceFile, version); 27 | return sourceFile; 28 | }, 29 | }); 30 | 31 | target.updateDocument = new Proxy(target.updateDocument, { 32 | apply: (delegate, thisArg, args: Parameters) => { 33 | const [fileName, , , version] = args; 34 | const sourceFile = delegate.apply(thisArg, args); 35 | documentChangeEventListener.onUpdate?.(fileName, sourceFile, version); 36 | return sourceFile; 37 | }, 38 | }); 39 | 40 | target.updateDocumentWithKey = new Proxy(target.updateDocumentWithKey, { 41 | apply: (delegate, thisArg, args: Parameters) => { 42 | const [fileName, , , , , version] = args; 43 | const sourceFile = delegate.apply(thisArg, args); 44 | documentChangeEventListener.onUpdate?.(fileName, sourceFile, version); 45 | return sourceFile; 46 | }, 47 | }); 48 | 49 | target.releaseDocument = new Proxy(target.releaseDocument, { 50 | apply: (delegate, thisArg, args: Parameters) => { 51 | const [fileName] = args; 52 | delegate.apply(thisArg, args); 53 | documentChangeEventListener.onRelease?.(fileName); 54 | }, 55 | }); 56 | 57 | target.releaseDocumentWithKey = new Proxy(target.releaseDocumentWithKey, { 58 | apply: (delegate, thisArg, args: Parameters) => { 59 | const [fileName] = args; 60 | delegate.apply(thisArg, args); 61 | documentChangeEventListener.onRelease?.(fileName); 62 | }, 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /src/analyzer/markdown-reporter.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import type { ManifestOutput, ManifestDocumentEntry } from './types'; 3 | 4 | export type ToMarkdownContentOptions = { 5 | baseDir: string; 6 | outputDir: string; 7 | ignoreFragments?: boolean; 8 | }; 9 | 10 | function createLinkPath(fileName: string, baseDir: string, outputDir: string) { 11 | const displayPath = path.isAbsolute(fileName) ? path.relative(baseDir, fileName) : fileName; 12 | const linkPath = path.isAbsolute(fileName) ? path.relative(outputDir, fileName) : fileName; 13 | return { displayPath, linkPath }; 14 | } 15 | 16 | function createSection( 17 | sectionName: string, 18 | operationDocs: ManifestDocumentEntry[], 19 | baseDir: string, 20 | outputDir: string, 21 | ) { 22 | const h2 = '## ' + sectionName[0].toUpperCase() + sectionName.slice(1); 23 | return ( 24 | h2 + 25 | '\n' + 26 | operationDocs 27 | .map(doc => { 28 | const { displayPath, linkPath } = createLinkPath(doc.fileName, baseDir, outputDir); 29 | return ` 30 | ### ${doc.type !== 'fragment' ? doc.operationName || 'anonymous' : doc.fragmentName} 31 | 32 | \`\`\`graphql 33 | ${doc.body.trim()} 34 | \`\`\` 35 | 36 | From [${displayPath}:${doc.documentStart.line + 1}:${doc.documentStart.character + 1}](${linkPath}#L${ 37 | doc.documentStart.line + 1 38 | }-L${doc.documentEnd.line + 1}) 39 | `; 40 | }) 41 | .join('\n') 42 | ); 43 | } 44 | 45 | export class MarkdownReporter { 46 | toMarkdownConntent( 47 | manifest: ManifestOutput, 48 | { ignoreFragments = true, baseDir, outputDir }: ToMarkdownContentOptions, 49 | ) { 50 | if (!manifest.documents.length) return null; 51 | const outs = ['# Extracted GraphQL Operations']; 52 | const groupedDocs = { 53 | queries: [], 54 | mutations: [], 55 | subscriptions: [], 56 | fragments: [], 57 | } as { [key: string]: ManifestDocumentEntry[] }; 58 | manifest.documents.forEach(doc => { 59 | switch (doc.type) { 60 | case 'query': 61 | groupedDocs.queries.push(doc); 62 | break; 63 | case 'mutation': 64 | groupedDocs.mutations.push(doc); 65 | break; 66 | case 'subscription': 67 | groupedDocs.subscriptions.push(doc); 68 | break; 69 | case 'fragment': 70 | if (!ignoreFragments) groupedDocs.fragments.push(doc); 71 | break; 72 | default: 73 | break; 74 | } 75 | }); 76 | Object.entries(groupedDocs).forEach(([name, docs]) => { 77 | if (docs.length) { 78 | outs.push(createSection(name, docs, baseDir, outputDir)); 79 | } 80 | }); 81 | outs.push('---'); 82 | outs.push('Extracted by [ts-graphql-plugin](https://github.com/Quramy/ts-graphql-plugin)'); 83 | return outs.join('\n'); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/analyzer/analyzer-factory.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { AnalyzerFactory } from './analyzer-factory'; 3 | import { Analyzer } from './analyzer'; 4 | 5 | describe(AnalyzerFactory, () => { 6 | describe(AnalyzerFactory.prototype.createAnalyzerFromProjectPath, () => { 7 | it('should create analyzer instance from existing typescript project directory path', () => { 8 | const analyzer = new AnalyzerFactory().createAnalyzerFromProjectPath( 9 | path.resolve(__dirname, '../../project-fixtures/react-apollo-prj'), 10 | ); 11 | expect(analyzer instanceof Analyzer).toBeTruthy(); 12 | }); 13 | 14 | it('should create analyzer instance from existing typescript project config file name', () => { 15 | const analyzer = new AnalyzerFactory().createAnalyzerFromProjectPath( 16 | path.resolve(__dirname, '../../project-fixtures/react-apollo-prj/tsconfig.json'), 17 | ); 18 | expect(analyzer instanceof Analyzer).toBeTruthy(); 19 | }); 20 | 21 | it('should throw an error when not exsisting config path', () => { 22 | expect(() => new AnalyzerFactory().createAnalyzerFromProjectPath('NOT_EXISTING_PRJ')).toThrowError(); 23 | }); 24 | 25 | it('should throw an error when project dir does not have tsconfig.json', () => { 26 | expect(() => 27 | new AnalyzerFactory().createAnalyzerFromProjectPath( 28 | path.resolve(__dirname, '../../project-fixtures/no-config-prj'), 29 | ), 30 | ).toThrowError(); 31 | }); 32 | 33 | it('should throw an error when config is written in invalid format', () => { 34 | expect(() => 35 | new AnalyzerFactory().createAnalyzerFromProjectPath( 36 | path.resolve(__dirname, '../../project-fixtures/simple-prj/tsconfig.invalid.json'), 37 | ), 38 | ).toThrowError(); 39 | }); 40 | 41 | it('should throw an error when config has no plugins field', () => { 42 | expect(() => 43 | new AnalyzerFactory().createAnalyzerFromProjectPath( 44 | path.resolve(__dirname, '../../project-fixtures/simple-prj/tsconfig.noplugin.json'), 45 | ), 46 | ).toThrowError(); 47 | }); 48 | 49 | it('should throw an error when config.plugins has no ts-graphql-plugin object', () => { 50 | expect(() => 51 | new AnalyzerFactory().createAnalyzerFromProjectPath( 52 | path.resolve(__dirname, '../../project-fixtures/simple-prj/tsconfig.notsgqlplugin.json'), 53 | ), 54 | ).toThrowError(); 55 | }); 56 | 57 | it('should throw an error when config.plugins.typegen.addons includes invalid modules', () => { 58 | expect(() => 59 | new AnalyzerFactory().createAnalyzerFromProjectPath( 60 | path.resolve(__dirname, '../../project-fixtures/simple-prj/tsconfig.invalid-addon.json'), 61 | ), 62 | ).toThrowError(); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-graphql-plugin", 3 | "version": "4.0.3", 4 | "description": "TypeScript Language Service Plugin for GraphQL", 5 | "keywords": [ 6 | "typescript", 7 | "graphql", 8 | "language service" 9 | ], 10 | "engines": { 11 | "node": ">=18" 12 | }, 13 | "main": "lib/index.js", 14 | "bin": { 15 | "tsgql": "lib/cli/cli.js", 16 | "ts-graphql-plugin": "lib/cli/cli.js" 17 | }, 18 | "types": "lib/index.d.ts", 19 | "files": [ 20 | "webpack.js", 21 | "addons/**/*.js", 22 | "lib/**/*.js", 23 | "lib/**/*.d.ts", 24 | "!lib/**/*.test.*" 25 | ], 26 | "scripts": { 27 | "prepare": "husky install", 28 | "clean": "rimraf -g lib \"e2e/*.log\" \"*.tsbuildinfo\"", 29 | "build": "run-s build:ts build:doc", 30 | "build:ts": "tsc -p . && cp src/tsmodule.js lib && cp src/tsmodule.d.ts lib", 31 | "build:doc": "npm run doc:toc", 32 | "lint": "eslint \"src/**/*.{ts,tsx}\"", 33 | "jest": "jest", 34 | "jest:ci": "jest --coverage --maxWorkers=4", 35 | "e2e": "node e2e/run.js", 36 | "e2e:ci": "c8 -o e2e_coverage -x e2e -r json -i \"src/**/*\" node e2e/run.js", 37 | "test": "npm run format:check && npm run lint && npm run jest:ci && npm run e2e:ci", 38 | "prettier": "prettier .", 39 | "format": "npm run prettier -- --write", 40 | "format:check": "npm run prettier -- --check", 41 | "doc:toc": "ts-node -P tools/tsconfig.json tools/add-toc.ts", 42 | "watch:compile": "tsc --watch -p .", 43 | "watch:jest": "jest --watch", 44 | "watch": "npm run run clean && run-p watch:*" 45 | }, 46 | "author": "Quramy", 47 | "license": "MIT", 48 | "repository": { 49 | "type": "git", 50 | "url": "https://github.com/Quramy/ts-graphql-plugin.git" 51 | }, 52 | "dependencies": { 53 | "graphql-language-service": "^5.2.1" 54 | }, 55 | "devDependencies": { 56 | "@types/jest": "29.5.14", 57 | "@types/node": "24.10.4", 58 | "@types/node-fetch": "3.0.2", 59 | "@typescript-eslint/eslint-plugin": "7.18.0", 60 | "@typescript-eslint/parser": "7.18.0", 61 | "c8": "10.1.3", 62 | "eslint": "8.57.1", 63 | "eslint-config-prettier": "10.1.8", 64 | "fretted-strings": "2.0.0", 65 | "glob": "11.1.0", 66 | "graphql": "16.12.0", 67 | "graphql-config": "5.1.5", 68 | "husky": "9.1.7", 69 | "jest": "29.7.0", 70 | "markdown-toc": "1.2.0", 71 | "msw": "2.10.3", 72 | "npm-run-all2": "8.0.4", 73 | "prettier": "^3.2.5", 74 | "pretty-quick": "4.2.2", 75 | "rimraf": "6.1.2", 76 | "talt": "2.4.4", 77 | "ts-jest": "29.4.6", 78 | "ts-loader": "9.5.4", 79 | "ts-node": "10.9.2", 80 | "typescript": "5.5.4", 81 | "typescript-eslint-language-service": "5.0.5", 82 | "webpack": "5.104.1", 83 | "webpack-cli": "6.0.1" 84 | }, 85 | "peerDependencies": { 86 | "graphql": "^15.0.0 || ^16.0.0", 87 | "typescript": "^4.8.0 || ^5.0.0" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/schema-manager/file-schema-manager.test.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, graphql, buildSchema } from 'graphql'; 2 | import { getIntrospectionQuery } from 'graphql/utilities'; 3 | import { FileSchemaManager } from './file-schema-manager'; 4 | import { createTestingSchemaManagerHost } from './testing/testing-schema-manager-host'; 5 | 6 | function createManagerWithHost(path: string, content: string) { 7 | const host = createTestingSchemaManagerHost({ 8 | schema: '', 9 | prjRootPath: '/', 10 | files: [ 11 | { 12 | fileName: '/' + path, 13 | content, 14 | }, 15 | ], 16 | }); 17 | const manager = new FileSchemaManager(host, { path }); 18 | return { manager, host }; 19 | } 20 | 21 | describe(FileSchemaManager, () => { 22 | it('should provide base schema from SDL file', async () => { 23 | const sdl = ` 24 | type Query { 25 | hello: String! 26 | } 27 | `; 28 | const { manager } = createManagerWithHost('schema.graphql', sdl); 29 | expect(await manager.waitBaseSchema()).toBeInstanceOf(GraphQLSchema); 30 | }); 31 | 32 | it('should provide base schema from introspection query result', async () => { 33 | const sdl = ` 34 | type Query { 35 | hello: String! 36 | } 37 | `; 38 | const introspectionResult = await graphql({ schema: buildSchema(sdl), source: getIntrospectionQuery() }); 39 | const { manager } = createManagerWithHost('schema.json', JSON.stringify(introspectionResult.data)); 40 | expect(await manager.waitBaseSchema()).toBeInstanceOf(GraphQLSchema); 41 | }); 42 | 43 | it('should provide base schema from JSON object whose data is introspection query result', async () => { 44 | const sdl = ` 45 | type Query { 46 | hello: String! 47 | } 48 | `; 49 | const introspectionResult = await graphql({ schema: buildSchema(sdl), source: getIntrospectionQuery() }); 50 | const { manager } = createManagerWithHost('schema.json', JSON.stringify(introspectionResult)); 51 | expect(await manager.waitBaseSchema()).toBeInstanceOf(GraphQLSchema); 52 | }); 53 | 54 | it('should return null as getBaseSchema() when invalid JSON', async () => { 55 | const { manager } = createManagerWithHost('schema.json', '{ '); 56 | expect(await manager.waitBaseSchema()).toBeNull(); 57 | }); 58 | 59 | it('should update when schema changes', async () => { 60 | const sdl = ` 61 | type Query { 62 | hello: String! 63 | } 64 | `; 65 | const { manager, host } = createManagerWithHost('schema.graphql', sdl); 66 | const lazySchema = new Promise(res => manager.registerOnChange(() => res(manager.getBaseSchema()))); 67 | manager.startWatch(); 68 | const newContent = ` 69 | type Query { 70 | hello: Int! 71 | } 72 | `; 73 | host.updateFile('/schema.graphql', newContent); 74 | expect(await lazySchema).toBeInstanceOf(GraphQLSchema); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/cli/commands/report.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import type { CommandOptions, CommandCliSetting } from '../parser'; 3 | import { ConsoleLogger } from '../logger'; 4 | 5 | export const cliDefinition = { 6 | description: 'Output GraphQL operations in your TypeScript sources to markdown file.', 7 | options: { 8 | project: { 9 | alias: 'p', 10 | description: 11 | "Analyze the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.", 12 | defaultValue: '.', 13 | type: 'string', 14 | }, 15 | outFile: { 16 | alias: 'o', 17 | description: 'Output Markdown file name.', 18 | defaultValue: 'GRAPHQL_OPERATIONS.md', 19 | type: 'string', 20 | }, 21 | fromManifest: { 22 | alias: 'M', 23 | description: 'Path to manifest.json file.', 24 | type: 'string', 25 | }, 26 | includeFragments: { 27 | description: 'If set, report including fragment informations.', 28 | type: 'boolean', 29 | }, 30 | verbose: { 31 | description: 'Show debug messages.', 32 | type: 'boolean', 33 | }, 34 | }, 35 | } as const satisfies CommandCliSetting; 36 | 37 | export async function reportCommand({ options }: CommandOptions) { 38 | const ts = require('typescript') as typeof import('typescript'); 39 | const { AnalyzerFactory } = require('../../analyzer') as typeof import('../../analyzer'); 40 | const { ErrorReporter } = require('../../errors/error-reporter') as typeof import('../../errors'); 41 | const { color } = require('../../string-util') as typeof import('../../string-util'); 42 | 43 | const logger = new ConsoleLogger(options.verbose ? 'debug' : 'info'); 44 | const { fromManifest, outFile, project, includeFragments } = options; 45 | const errorReporter = new ErrorReporter(process.cwd(), logger.error.bind(logger)); 46 | const analyzer = new AnalyzerFactory().createAnalyzerFromProjectPath(project, logger.debug.bind(logger)); 47 | const manifest = fromManifest ? JSON.parse(ts.sys.readFile(fromManifest, 'utf8') || '') : undefined; 48 | let outFileName = path.isAbsolute(outFile) ? outFile : path.resolve(process.cwd(), outFile); 49 | outFileName = ts.sys.directoryExists(outFileName) ? path.join(outFileName, 'GRAPHQL_OPERATIONS.md') : outFileName; 50 | const [errors, markdown] = await analyzer.report(outFileName, manifest, !includeFragments); 51 | errors.forEach(errorReporter.outputError.bind(errorReporter)); 52 | if (errors.length) { 53 | logger.error(color.magenta('Found some errors extracting operations.\n')); 54 | errors.forEach(error => errorReporter.outputError(error)); 55 | } 56 | if (!markdown) { 57 | logger.error('No GraphQL operations.'); 58 | return false; 59 | } 60 | ts.sys.writeFile(outFileName, markdown); 61 | logger.info(`Write report file to '${color.green(path.relative(process.cwd(), outFileName))}'.`); 62 | return true; 63 | } 64 | -------------------------------------------------------------------------------- /project-fixtures/graphql-codegen-prj/src/gql/gql.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import * as types from './graphql'; 3 | import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; 4 | 5 | /** 6 | * Map of all GraphQL operations in the project. 7 | * 8 | * This map has several performance disadvantages: 9 | * 1. It is not tree-shakeable, so it will include all operations in the project. 10 | * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. 11 | * 3. It does not support dead code elimination, so it will add unused operations. 12 | * 13 | * Therefore it is highly recommended to use the babel or swc plugin for production. 14 | */ 15 | const documents = { 16 | "\n query PopularPosts_Query {\n popularPosts {\n id\n ...PostSummary_Post\n }\n }\n": types.PopularPosts_QueryDocument, 17 | "\n fragment PostSummary_Post on Post {\n id\n title\n author {\n name\n ...UserAvatar_User\n }\n }\n": types.PostSummary_PostFragmentDoc, 18 | "\n fragment UserAvatar_User on User {\n name\n avatarURL\n }\n": types.UserAvatar_UserFragmentDoc, 19 | }; 20 | 21 | /** 22 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 23 | * 24 | * 25 | * @example 26 | * ```ts 27 | * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`); 28 | * ``` 29 | * 30 | * The query argument is unknown! 31 | * Please regenerate the types. 32 | */ 33 | export function graphql(source: string): unknown; 34 | 35 | /** 36 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 37 | */ 38 | export function graphql(source: "\n query PopularPosts_Query {\n popularPosts {\n id\n ...PostSummary_Post\n }\n }\n"): (typeof documents)["\n query PopularPosts_Query {\n popularPosts {\n id\n ...PostSummary_Post\n }\n }\n"]; 39 | /** 40 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 41 | */ 42 | export function graphql(source: "\n fragment PostSummary_Post on Post {\n id\n title\n author {\n name\n ...UserAvatar_User\n }\n }\n"): (typeof documents)["\n fragment PostSummary_Post on Post {\n id\n title\n author {\n name\n ...UserAvatar_User\n }\n }\n"]; 43 | /** 44 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. 45 | */ 46 | export function graphql(source: "\n fragment UserAvatar_User on User {\n name\n avatarURL\n }\n"): (typeof documents)["\n fragment UserAvatar_User on User {\n name\n avatarURL\n }\n"]; 47 | 48 | export function graphql(source: string) { 49 | return (documents as any)[source] ?? {}; 50 | } 51 | 52 | export type DocumentType> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never; -------------------------------------------------------------------------------- /src/schema-manager/extension-manager.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { buildSchema, printSchema } from 'graphql'; 4 | import { ExtensionManager } from './extension-manager'; 5 | import { createTestingSchemaManagerHost } from './testing/testing-schema-manager-host'; 6 | 7 | function createManagerWithHost(config: { localSchemaExtensions: string[] }) { 8 | const host = createTestingSchemaManagerHost({ 9 | schema: '', 10 | localSchemaExtensions: config.localSchemaExtensions, 11 | files: config.localSchemaExtensions.map(name => ({ 12 | fileName: path.join(__dirname, name), 13 | content: fs.readFileSync(path.join(__dirname, name), 'utf-8'), 14 | })), 15 | prjRootPath: __dirname, 16 | }); 17 | return { extensionManager: new ExtensionManager(host), host }; 18 | } 19 | 20 | function createManager(config: { localSchemaExtensions: string[] }) { 21 | return createManagerWithHost(config).extensionManager; 22 | } 23 | 24 | const baseSdl = ` 25 | type Query { 26 | hello: String! 27 | } 28 | `; 29 | 30 | const baseSchema = buildSchema(baseSdl); 31 | 32 | describe(ExtensionManager, () => { 33 | it('should parse and extend base schema', () => { 34 | const extensionManager = createManager({ localSchemaExtensions: ['./testing/resources/normal.graphql'] }); 35 | extensionManager.readExtensions(); 36 | const schema = extensionManager.extendSchema(baseSchema); 37 | expect(printSchema(schema!)).toMatchSnapshot(); 38 | expect(extensionManager.getSchemaErrors()).toStrictEqual([]); 39 | }); 40 | 41 | it('should store parser errors with invalid syntax file', () => { 42 | const extensionManager = createManager({ localSchemaExtensions: ['./testing/resources/invalid_syntax.graphql'] }); 43 | extensionManager.readExtensions(); 44 | const schema = extensionManager.extendSchema(baseSchema); 45 | expect(schema).toBeNull(); 46 | const errors = extensionManager 47 | .getSchemaErrors()! 48 | .map(e => ({ ...e, fileName: e.fileName!.replace(__dirname, '') })); 49 | expect(errors).toMatchSnapshot(); 50 | }); 51 | 52 | it('should store parser errors with invalid extension', () => { 53 | const extensionManager = createManager({ 54 | localSchemaExtensions: ['./testing/resources/invalid_extension.graphql'], 55 | }); 56 | extensionManager.readExtensions(); 57 | const schema = extensionManager.extendSchema(baseSchema); 58 | expect(schema).toBeNull(); 59 | const errors = extensionManager 60 | .getSchemaErrors()! 61 | .map(e => ({ ...e, fileName: e.fileName!.replace(__dirname, '') })); 62 | expect(errors).toMatchSnapshot(); 63 | }); 64 | 65 | it('should execute call back when files change', async () => { 66 | const { extensionManager, host } = createManagerWithHost({ 67 | localSchemaExtensions: ['./testing/resources/normal.graphql'], 68 | }); 69 | const called = new Promise(res => extensionManager.startWatch(res)); 70 | host.updateFile(path.join(__dirname, 'testing/resources/normal.graphql'), ''); 71 | await called; 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/graphql-language-service-adapter/testing/adapter-fixture.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | import { GraphQLSchema } from 'graphql'; 3 | import { 4 | createScriptSourceHelper, 5 | getTemplateNodeUnder, 6 | getSanitizedTemplateText, 7 | ScriptSourceHelper, 8 | } from '../../ts-ast-util'; 9 | import { FragmentRegistry } from '../../gql-ast-util'; 10 | import { GraphQLLanguageServiceAdapter } from '../graphql-language-service-adapter'; 11 | import { 12 | createTestingLanguageServiceAndHost, 13 | TestingLanguageServiceHost, 14 | } from '../../ts-ast-util/testing/testing-language-service'; 15 | 16 | export class AdapterFixture { 17 | readonly adapter: GraphQLLanguageServiceAdapter; 18 | readonly langService: ts.LanguageService; 19 | readonly scriptSourceHelper: ScriptSourceHelper; 20 | private readonly _sourceFileName: string; 21 | private readonly _langServiceHost: TestingLanguageServiceHost; 22 | private readonly _fragmentRegistry: FragmentRegistry; 23 | 24 | constructor(sourceFileName: string, schema?: GraphQLSchema) { 25 | const { languageService, languageServiceHost } = createTestingLanguageServiceAndHost({ 26 | files: [{ fileName: sourceFileName, content: '' }], 27 | }); 28 | this._sourceFileName = sourceFileName; 29 | this._langServiceHost = languageServiceHost; 30 | this._fragmentRegistry = new FragmentRegistry(); 31 | this.langService = languageService; 32 | (this.scriptSourceHelper = createScriptSourceHelper( 33 | { languageService, languageServiceHost, project: { getProjectName: () => 'tsconfig.json' } }, 34 | { exclude: [] }, 35 | )), 36 | (this.adapter = new GraphQLLanguageServiceAdapter(this.scriptSourceHelper, { 37 | schema: schema || null, 38 | removeDuplicatedFragments: true, 39 | fragmentRegistry: this._fragmentRegistry, 40 | tag: { 41 | names: [], 42 | allowNotTaggedTemplate: true, 43 | allowTaggedTemplateExpression: true, 44 | allowFunctionCallExpression: true, 45 | }, 46 | })); 47 | } 48 | 49 | get source() { 50 | return this._langServiceHost.getFile(this._sourceFileName)!.content; 51 | } 52 | 53 | set source(content: string) { 54 | this._langServiceHost.updateFile(this._sourceFileName, content); 55 | const documents = this.scriptSourceHelper 56 | .getAllNodes(this._sourceFileName, node => 57 | getTemplateNodeUnder(node, { 58 | names: [], 59 | allowNotTaggedTemplate: true, 60 | allowTaggedTemplateExpression: true, 61 | allowFunctionCallExpression: true, 62 | }), 63 | ) 64 | .map(node => getSanitizedTemplateText(node)); 65 | this._fragmentRegistry.registerDocuments(this._sourceFileName, content, documents); 66 | } 67 | 68 | registerFragment(sourceFileName: string, fragmentDefDoc: string) { 69 | if (sourceFileName === this._sourceFileName) return this; 70 | this._fragmentRegistry.registerDocuments(sourceFileName, fragmentDefDoc, [ 71 | { sourcePosition: 0, text: fragmentDefDoc }, 72 | ]); 73 | return this; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /project-fixtures/graphql-codegen-prj/src/gql/fragment-masking.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; 3 | import { FragmentDefinitionNode } from 'graphql'; 4 | import { Incremental } from './graphql'; 5 | 6 | 7 | export type FragmentType> = TDocumentType extends DocumentTypeDecoration< 8 | infer TType, 9 | any 10 | > 11 | ? [TType] extends [{ ' $fragmentName'?: infer TKey }] 12 | ? TKey extends string 13 | ? { ' $fragmentRefs'?: { [key in TKey]: TType } } 14 | : never 15 | : never 16 | : never; 17 | 18 | // return non-nullable if `fragmentType` is non-nullable 19 | export function useFragment( 20 | _documentNode: DocumentTypeDecoration, 21 | fragmentType: FragmentType> 22 | ): TType; 23 | // return nullable if `fragmentType` is nullable 24 | export function useFragment( 25 | _documentNode: DocumentTypeDecoration, 26 | fragmentType: FragmentType> | null | undefined 27 | ): TType | null | undefined; 28 | // return array of non-nullable if `fragmentType` is array of non-nullable 29 | export function useFragment( 30 | _documentNode: DocumentTypeDecoration, 31 | fragmentType: ReadonlyArray>> 32 | ): ReadonlyArray; 33 | // return array of nullable if `fragmentType` is array of nullable 34 | export function useFragment( 35 | _documentNode: DocumentTypeDecoration, 36 | fragmentType: ReadonlyArray>> | null | undefined 37 | ): ReadonlyArray | null | undefined; 38 | export function useFragment( 39 | _documentNode: DocumentTypeDecoration, 40 | fragmentType: FragmentType> | ReadonlyArray>> | null | undefined 41 | ): TType | ReadonlyArray | null | undefined { 42 | return fragmentType as any; 43 | } 44 | 45 | 46 | export function makeFragmentData< 47 | F extends DocumentTypeDecoration, 48 | FT extends ResultOf 49 | >(data: FT, _fragment: F): FragmentType { 50 | return data as FragmentType; 51 | } 52 | export function isFragmentReady( 53 | queryNode: DocumentTypeDecoration, 54 | fragmentNode: TypedDocumentNode, 55 | data: FragmentType, any>> | null | undefined 56 | ): data is FragmentType { 57 | const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ 58 | ?.deferredFields; 59 | 60 | if (!deferredFields) return true; 61 | 62 | const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; 63 | const fragName = fragDef?.name?.value; 64 | 65 | const fields = (fragName && deferredFields[fragName]) || []; 66 | return fields.length > 0 && fields.every(field => data && field in data); 67 | } 68 | -------------------------------------------------------------------------------- /project-fixtures/gql-errors-prj/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gql-syntax-error", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "gql-syntax-error", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "graphql": "16.12.0", 13 | "ts-graphql-plugin": "file:../../", 14 | "typescript": "5.5.4" 15 | } 16 | }, 17 | "../..": { 18 | "version": "4.0.3", 19 | "dev": true, 20 | "license": "MIT", 21 | "dependencies": { 22 | "graphql-language-service": "^5.2.1" 23 | }, 24 | "bin": { 25 | "ts-graphql-plugin": "lib/cli/cli.js", 26 | "tsgql": "lib/cli/cli.js" 27 | }, 28 | "devDependencies": { 29 | "@types/jest": "29.5.14", 30 | "@types/node": "22.19.1", 31 | "@types/node-fetch": "3.0.2", 32 | "@typescript-eslint/eslint-plugin": "7.18.0", 33 | "@typescript-eslint/parser": "7.18.0", 34 | "c8": "10.1.2", 35 | "eslint": "8.57.1", 36 | "eslint-config-prettier": "9.1.2", 37 | "fretted-strings": "2.0.0", 38 | "glob": "11.0.3", 39 | "graphql": "16.12.0", 40 | "graphql-config": "5.1.5", 41 | "husky": "9.1.7", 42 | "jest": "29.7.0", 43 | "markdown-toc": "1.2.0", 44 | "msw": "2.10.3", 45 | "npm-run-all2": "7.0.2", 46 | "prettier": "^3.2.5", 47 | "pretty-quick": "4.2.2", 48 | "rimraf": "6.1.0", 49 | "talt": "2.4.4", 50 | "ts-jest": "29.4.5", 51 | "ts-loader": "9.5.4", 52 | "ts-node": "10.9.2", 53 | "typescript": "5.5.4", 54 | "typescript-eslint-language-service": "5.0.5", 55 | "webpack": "5.99.9", 56 | "webpack-cli": "6.0.1" 57 | }, 58 | "engines": { 59 | "node": ">=18" 60 | }, 61 | "peerDependencies": { 62 | "graphql": "^15.0.0 || ^16.0.0", 63 | "typescript": "^4.8.0 || ^5.0.0" 64 | } 65 | }, 66 | "node_modules/graphql": { 67 | "version": "16.12.0", 68 | "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", 69 | "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", 70 | "dev": true, 71 | "license": "MIT", 72 | "engines": { 73 | "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" 74 | } 75 | }, 76 | "node_modules/ts-graphql-plugin": { 77 | "resolved": "../..", 78 | "link": true 79 | }, 80 | "node_modules/typescript": { 81 | "version": "5.5.4", 82 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", 83 | "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", 84 | "dev": true, 85 | "license": "Apache-2.0", 86 | "bin": { 87 | "tsc": "bin/tsc", 88 | "tsserver": "bin/tsserver" 89 | }, 90 | "engines": { 91 | "node": ">=14.17" 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/schema-manager/schema-manager-factory.test.ts: -------------------------------------------------------------------------------- 1 | import { SchemaManagerFactory } from './schema-manager-factory'; 2 | import { HttpSchemaManager } from './http-schema-manager'; 3 | import { FileSchemaManager } from './file-schema-manager'; 4 | import { createTestingSchemaManagerHost } from './testing/testing-schema-manager-host'; 5 | import { ScriptedHttpSchemaManager } from './scripted-http-schema-manager'; 6 | 7 | describe(SchemaManagerFactory, () => { 8 | it('should return HttpSchemaManager from http url string', () => { 9 | const factory = new SchemaManagerFactory( 10 | createTestingSchemaManagerHost({ 11 | schema: 'http://localhost', 12 | }), 13 | ); 14 | const actual = factory.create(); 15 | expect(actual instanceof HttpSchemaManager).toBeTruthy(); 16 | }); 17 | 18 | it('should return HttpSchemaManager from https url string', () => { 19 | const factory = new SchemaManagerFactory( 20 | createTestingSchemaManagerHost({ 21 | schema: 'https://localhost', 22 | }), 23 | ); 24 | const actual = factory.create(); 25 | expect(actual instanceof HttpSchemaManager).toBeTruthy(); 26 | }); 27 | 28 | it('should return FileSchemaManager from file schema string', () => { 29 | const factory = new SchemaManagerFactory( 30 | createTestingSchemaManagerHost({ 31 | schema: 'file:///tmp/s.json', 32 | }), 33 | ); 34 | const actual = factory.create(); 35 | expect(actual instanceof FileSchemaManager).toBeTruthy(); 36 | }); 37 | 38 | it('should return FileSchemaManager from no schema string', () => { 39 | const factory = new SchemaManagerFactory( 40 | createTestingSchemaManagerHost({ 41 | schema: '/tmp/s.json', 42 | }), 43 | ); 44 | const actual = factory.create(); 45 | expect(actual instanceof FileSchemaManager).toBeTruthy(); 46 | }); 47 | 48 | it('should return HttpSchemaManager from http object with url property', () => { 49 | const factory = new SchemaManagerFactory( 50 | createTestingSchemaManagerHost({ 51 | schema: { 52 | http: { 53 | url: 'http://localhost', 54 | }, 55 | }, 56 | }), 57 | ); 58 | const actual = factory.create(); 59 | expect(actual instanceof HttpSchemaManager).toBeTruthy(); 60 | }); 61 | 62 | it('should return FileSchemaManager from file object', () => { 63 | const factory = new SchemaManagerFactory( 64 | createTestingSchemaManagerHost({ 65 | schema: { 66 | file: { 67 | path: 'http://localhost', 68 | }, 69 | }, 70 | }), 71 | ); 72 | const actual = factory.create(); 73 | expect(actual instanceof FileSchemaManager).toBeTruthy(); 74 | }); 75 | 76 | it('should return ScriptedHttpSchemaManager from http object with fromScript property', () => { 77 | const factory = new SchemaManagerFactory( 78 | createTestingSchemaManagerHost({ 79 | schema: { 80 | http: { 81 | fromScript: 'graphql-config.js', 82 | }, 83 | }, 84 | }), 85 | ); 86 | const actual = factory.create(); 87 | expect(actual instanceof ScriptedHttpSchemaManager).toBeTruthy(); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/transformer/transformer.ts: -------------------------------------------------------------------------------- 1 | import { print, type DocumentNode } from 'graphql'; 2 | import ts from '../tsmodule'; 3 | import { astf, getTemplateNodeUnder, removeAliasFromImportDeclaration, type StrictTagCondition } from '../ts-ast-util'; 4 | 5 | export type DocumentTransformer = (documentNode: DocumentNode) => DocumentNode; 6 | 7 | export type TransformOptions = { 8 | tag: StrictTagCondition; 9 | documentTransformers: DocumentTransformer[]; 10 | removeFragmentDefinitions: boolean; 11 | target: 'text' | 'object'; 12 | getDocumentNode: (node: ts.NoSubstitutionTemplateLiteral | ts.TemplateExpression) => DocumentNode | undefined; 13 | getEnabled: () => boolean; 14 | }; 15 | 16 | function toObjectNode(field: any): ts.Expression { 17 | if (field === null) { 18 | return astf.createNull(); 19 | } else if (typeof field === 'boolean') { 20 | return field ? astf.createTrue() : astf.createFalse(); 21 | } else if (typeof field === 'number') { 22 | return astf.createNumericLiteral(field + ''); 23 | } else if (typeof field === 'string') { 24 | return astf.createStringLiteral(field); 25 | } else if (Array.isArray(field)) { 26 | return astf.createArrayLiteralExpression(field.map(item => toObjectNode(item))); 27 | } 28 | return astf.createObjectLiteralExpression( 29 | Object.entries(field) 30 | .filter(([k, v]) => k !== 'loc' && v !== undefined) 31 | .map(([k, v]) => astf.createPropertyAssignment(astf.createIdentifier(k), toObjectNode(v))), 32 | true, 33 | ); 34 | } 35 | 36 | export function getTransformer({ 37 | tag, 38 | target, 39 | getDocumentNode, 40 | removeFragmentDefinitions, 41 | documentTransformers, 42 | getEnabled, 43 | }: TransformOptions) { 44 | return (ctx: ts.TransformationContext) => { 45 | const visit = (node: ts.Node): ts.Node | undefined => { 46 | if (!getEnabled()) return node; 47 | let templateNode: ts.NoSubstitutionTemplateLiteral | ts.TemplateExpression | undefined = undefined; 48 | 49 | if (ts.isImportDeclaration(node) && tag.names.length > 0) { 50 | return removeAliasFromImportDeclaration(node, tag.names); 51 | } 52 | 53 | if (ts.isTaggedTemplateExpression(node) && (!tag.names.length || !!getTemplateNodeUnder(node, tag))) { 54 | templateNode = node.template; 55 | } else if (ts.isCallExpression(node) && !!getTemplateNodeUnder(node, tag)) { 56 | templateNode = node.arguments[0] as ts.TemplateLiteral; 57 | } 58 | 59 | if (!templateNode) return ts.visitEachChild(node, visit, ctx); 60 | 61 | const originalDocumentNode = getDocumentNode(templateNode); 62 | if (!originalDocumentNode) return ts.visitEachChild(node, visit, ctx); 63 | const documentNode = documentTransformers.reduce((doc, dt) => dt(doc), originalDocumentNode); 64 | if (!documentNode) return ts.visitEachChild(node, visit, ctx); 65 | const toBeRemoved = 66 | removeFragmentDefinitions && documentNode.definitions.every(def => def.kind === 'FragmentDefinition'); 67 | if (target === 'text') { 68 | if (toBeRemoved) return astf.createStringLiteral(''); 69 | return astf.createStringLiteral(print(documentNode)); 70 | } 71 | if (toBeRemoved) return astf.createNumericLiteral('0'); 72 | return toObjectNode(documentNode); 73 | }; 74 | return (sourceFile: ts.SourceFile) => ts.visitEachChild(sourceFile, visit, ctx); 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /src/schema-manager/extension-manager.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { parse, extendSchema, GraphQLError, type GraphQLSchema, type DocumentNode } from 'graphql'; 3 | import type { SchemaBuildErrorInfo } from './schema-manager'; 4 | import type { SchemaManagerHost } from './types'; 5 | 6 | export class ExtensionManager { 7 | private _targetSdlFileNames: string[]; 8 | private _parsedExtensionAstMap = new Map(); 9 | private _graphqlErrorMap = new Map(); 10 | 11 | constructor(private _host: SchemaManagerHost) { 12 | const { localSchemaExtensions } = this._host.getConfig(); 13 | this._targetSdlFileNames = (localSchemaExtensions || []).map(filePath => 14 | this._getAbsoluteSchemaPath(this._host.getProjectRootPath(), filePath), 15 | ); 16 | } 17 | 18 | readExtensions() { 19 | this._targetSdlFileNames.forEach(filePath => this._readExtension(filePath)); 20 | } 21 | 22 | extendSchema(baseSchema: GraphQLSchema) { 23 | if (this._graphqlErrorMap.size) return null; 24 | for (const [fileName, { node, sdlContent }] of this._parsedExtensionAstMap.entries()) { 25 | try { 26 | baseSchema = extendSchema(baseSchema, node); 27 | } catch (error) { 28 | if (error instanceof Error) { 29 | const { message } = error; 30 | this._graphqlErrorMap.set(fileName, { message, fileName, fileContent: sdlContent }); 31 | } 32 | return null; 33 | } 34 | } 35 | this._graphqlErrorMap.clear(); 36 | return baseSchema; 37 | } 38 | 39 | getSchemaErrors() { 40 | return [...this._graphqlErrorMap.values()]; 41 | } 42 | 43 | startWatch(cb: () => void, interval = 100) { 44 | this._targetSdlFileNames.forEach(fileName => { 45 | this._host.watchFile( 46 | fileName, 47 | () => { 48 | this._host.log('Changed local extension schema: ' + fileName); 49 | this._readExtension(fileName); 50 | cb(); 51 | }, 52 | interval, 53 | ); 54 | }); 55 | } 56 | 57 | private _readExtension(fileName: string) { 58 | if (!this._host.fileExists(fileName)) return null; 59 | const sdlContent = this._host.readFile(fileName, 'utf8'); 60 | if (!sdlContent) return null; 61 | this._host.log('Read local extension schema: ' + fileName); 62 | try { 63 | const node = parse(sdlContent); 64 | this._parsedExtensionAstMap.set(fileName, { node, sdlContent }); 65 | this._graphqlErrorMap.delete(fileName); 66 | } catch (error) { 67 | if (error instanceof GraphQLError) { 68 | const { message, locations } = error; 69 | this._host.log('Failed to parse: ' + fileName + ', ' + message); 70 | if (locations) { 71 | this._graphqlErrorMap.set(fileName, { 72 | message, 73 | fileName, 74 | fileContent: sdlContent, 75 | locations: locations.map(loc => ({ line: loc.line - 1, character: loc.column - 1 })), 76 | }); 77 | } else { 78 | this._graphqlErrorMap.set(fileName, { message, fileName, fileContent: sdlContent }); 79 | } 80 | } 81 | } 82 | } 83 | 84 | private _getAbsoluteSchemaPath(projectRootPath: string, schemaPath: string) { 85 | if (path.isAbsolute(schemaPath)) return schemaPath; 86 | return path.resolve(projectRootPath, schemaPath); 87 | } 88 | } 89 | --------------------------------------------------------------------------------