From 37219ef5fcfb55c36d273b10a120cb9b75449dcd Mon Sep 17 00:00:00 2001 From: Quramy Date: Fri, 15 Mar 2024 12:08:58 +0900 Subject: [PATCH 1/4] feat: Add `tag` customize pattern --- src/analyzer/analyzer.ts | 22 +- src/analyzer/extractor.test.ts | 29 +-- src/analyzer/extractor.ts | 27 ++- src/analyzer/markdown-reporter.test.ts | 11 +- src/analyzer/type-generator.test.ts | 6 +- src/analyzer/type-generator.ts | 6 +- .../graphql-language-service-adapter.ts | 38 ++-- .../testing/adapter-fixture.ts | 6 + .../plugin-module-factory.ts | 16 +- src/transformer/transformer-host.ts | 3 +- src/transformer/transformer.test.ts | 3 +- src/transformer/transformer.ts | 18 +- src/ts-ast-util/index.ts | 1 + src/ts-ast-util/script-source-helper.ts | 2 +- src/ts-ast-util/tag-utils.test.ts | 212 ++++++++++++++++++ src/ts-ast-util/tag-utils.ts | 106 +++++++++ src/ts-ast-util/types.ts | 18 +- src/ts-ast-util/utilily-functions.test.ts | 22 -- src/ts-ast-util/utilily-functions.ts | 14 -- src/typegen-addons/testing/addon-tester.ts | 5 +- src/types.ts | 5 +- 21 files changed, 456 insertions(+), 114 deletions(-) create mode 100644 src/ts-ast-util/tag-utils.test.ts create mode 100644 src/ts-ast-util/tag-utils.ts diff --git a/src/analyzer/analyzer.ts b/src/analyzer/analyzer.ts index ac15acd1c..1206ced3f 100644 --- a/src/analyzer/analyzer.ts +++ b/src/analyzer/analyzer.ts @@ -4,10 +4,11 @@ import { ScriptSourceHelper } from '../ts-ast-util/types'; import { Extractor } from './extractor'; import { createScriptSourceHelper, - hasTagged, + getTemplateNodeUnder, findAllNodes, registerDocumentChangeEvent, getSanitizedTemplateText, + parseTagConfig, } from '../ts-ast-util'; import { FragmentRegistry } from '../gql-ast-util'; import { SchemaManager, SchemaBuildErrorInfo } from '../schema-manager/schema-manager'; @@ -79,12 +80,12 @@ export class Analyzer { this._typeGenerator = new TypeGenerator({ prjRootPath: this._prjRootPath, extractor: this._extractor, - tag: this._pluginConfig.tag, + tag: parseTagConfig(this._pluginConfig.tag), addonFactories: this._pluginConfig.typegen.addonFactories, debug: this._debug, }); if (this._pluginConfig.enabledGlobalFragments === true) { - const tag = this._pluginConfig.tag; + const tag = parseTagConfig(this._pluginConfig.tag); registerDocumentChangeEvent(documentRegistry, { onAcquire: (fileName, sourceFile, version) => { if (!isExcluded(fileName) && this._languageServiceHost.getScriptFileNames().includes(fileName)) { @@ -92,11 +93,12 @@ export class Analyzer { fileName, version, findAllNodes(sourceFile, node => { - if (tag && ts.isTaggedTemplateExpression(node) && hasTagged(node, tag, sourceFile)) { - return node.template; - } else if (ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateExpression(node)) { - return node; - } + // if (tag && ts.isTaggedTemplateExpression(node) && hasTagged(node, tag, sourceFile)) { + // return node.template; + // } else if (ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateExpression(node)) { + // return node; + // } + return getTemplateNodeUnder(node, tag); }).map(node => getSanitizedTemplateText(node, sourceFile)), ); } @@ -112,7 +114,7 @@ export class Analyzer { extract(fileNameList?: string[]) { const results = this._extractor.extract( fileNameList || this._languageServiceHost.getScriptFileNames(), - this._pluginConfig.tag, + parseTagConfig(this._pluginConfig.tag), ); const errors = this._extractor.pickupErrors(results); return [errors, results] as const; @@ -120,7 +122,7 @@ export class Analyzer { extractToManifest() { const [errors, results] = this.extract(); - const manifest = this._extractor.toManifest(results, this._pluginConfig.tag); + const manifest = this._extractor.toManifest(results, parseTagConfig(this._pluginConfig.tag)); return [errors, manifest] as const; } diff --git a/src/analyzer/extractor.test.ts b/src/analyzer/extractor.test.ts index b159c4f91..bf68edef2 100644 --- a/src/analyzer/extractor.test.ts +++ b/src/analyzer/extractor.test.ts @@ -1,6 +1,7 @@ import { print } from 'graphql'; import { Extractor, ExtractSucceededResult } from './extractor'; import { createTesintExtractor } from './testing/testing-extractor'; +import { parseTagConfig } from '../ts-ast-util'; describe(Extractor, () => { it('should extract GraphQL documents', () => { @@ -24,7 +25,7 @@ describe(Extractor, () => { `, }, ]); - const result = extractor.extract(['main.ts'], 'gql'); + const result = extractor.extract(['main.ts'], parseTagConfig('gql')); expect(result.fileEntries.map(r => print(r.documentNode!))).toMatchSnapshot(); }); @@ -51,7 +52,7 @@ describe(Extractor, () => { ], true, ); - const result = extractor.extract(['main.ts'], 'gql'); + const result = extractor.extract(['main.ts'], parseTagConfig('gql')); expect(result.fileEntries.map(r => print(r.documentNode!))).toMatchSnapshot(); }); @@ -78,7 +79,7 @@ describe(Extractor, () => { ], false, ); - const result = extractor.extract(['main.ts'], 'gql'); + const result = extractor.extract(['main.ts'], parseTagConfig('gql')); expect(result.fileEntries.map(r => print(r.documentNode!))).toMatchSnapshot(); }); @@ -102,7 +103,7 @@ describe(Extractor, () => { `, }, ]); - const result = extractor.extract(['main.ts'], 'gql'); + const result = extractor.extract(['main.ts'], parseTagConfig('gql')); expect(result.fileEntries[0].resolevedTemplateInfo).toBeTruthy(); expect(result.fileEntries[1].resolevedTemplateInfo).toBeFalsy(); expect(result.fileEntries[1].resolveTemplateError).toMatchSnapshot(); @@ -121,7 +122,7 @@ describe(Extractor, () => { `, }, ]); - const result = extractor.extract(['main.ts'], 'gql'); + const result = extractor.extract(['main.ts'], parseTagConfig('gql')); expect(result.fileEntries[0].graphqlError).toBeTruthy(); }); @@ -146,8 +147,8 @@ describe(Extractor, () => { `, }, ]); - const result = extractor.extract(['main.ts'], 'gql'); - expect(extractor.toManifest(result)).toMatchSnapshot(); + const result = extractor.extract(['main.ts'], parseTagConfig('')); + expect(extractor.toManifest(result, parseTagConfig(''))).toMatchSnapshot(); }); describe('getDominantDefinition', () => { @@ -168,7 +169,7 @@ describe(Extractor, () => { `, }, ]); - const result = extractor.extract(['main.ts'], 'gql') as { fileEntries: ExtractSucceededResult[] }; + const result = extractor.extract(['main.ts'], parseTagConfig('gql')) as { fileEntries: ExtractSucceededResult[] }; const { type, operationName } = extractor.getDominantDefinition(result.fileEntries[0]); expect(type).toBe('query'); expect(operationName).toBe('MyQuery'); @@ -190,7 +191,7 @@ describe(Extractor, () => { `, }, ]); - const result = extractor.extract(['main.ts'], 'gql') as { fileEntries: ExtractSucceededResult[] }; + const result = extractor.extract(['main.ts'], parseTagConfig('gql')) as { fileEntries: ExtractSucceededResult[] }; const { type, operationName } = extractor.getDominantDefinition(result.fileEntries[0]); expect(type).toBe('mutation'); expect(operationName).toBe('MyMutation'); @@ -212,7 +213,7 @@ describe(Extractor, () => { `, }, ]); - const result = extractor.extract(['main.ts'], 'gql') as { fileEntries: ExtractSucceededResult[] }; + const result = extractor.extract(['main.ts'], parseTagConfig('gql')) as { fileEntries: ExtractSucceededResult[] }; const { type, operationName } = extractor.getDominantDefinition(result.fileEntries[0]); expect(type).toBe('subscription'); expect(operationName).toBe('MySubscription'); @@ -239,7 +240,7 @@ describe(Extractor, () => { `, }, ]); - const result = extractor.extract(['main.ts'], 'gql') as { fileEntries: ExtractSucceededResult[] }; + const result = extractor.extract(['main.ts'], parseTagConfig('gql')) as { fileEntries: ExtractSucceededResult[] }; const { type, operationName } = extractor.getDominantDefinition(result.fileEntries[0]); expect(type).toBe('complex'); expect(operationName).toBe('MULTIPLE_OPERATIONS'); @@ -259,7 +260,7 @@ describe(Extractor, () => { `, }, ]); - const result = extractor.extract(['main.ts'], 'gql') as { fileEntries: ExtractSucceededResult[] }; + const result = extractor.extract(['main.ts'], parseTagConfig('gql')) as { fileEntries: ExtractSucceededResult[] }; const { type, fragmentName } = extractor.getDominantDefinition(result.fileEntries[0]); expect(type).toBe('fragment'); expect(fragmentName).toBe('MyFragment'); @@ -283,7 +284,7 @@ describe(Extractor, () => { `, }, ]); - const result = extractor.extract(['main.ts'], 'gql') as { fileEntries: ExtractSucceededResult[] }; + const result = extractor.extract(['main.ts'], parseTagConfig('gql')) as { fileEntries: ExtractSucceededResult[] }; const { type, fragmentName } = extractor.getDominantDefinition(result.fileEntries[0]); expect(type).toBe('fragment'); expect(fragmentName).toBe('MyFragment'); @@ -311,7 +312,7 @@ describe(Extractor, () => { `, }, ]); - const result = extractor.extract(['main.ts'], 'gql') as { fileEntries: ExtractSucceededResult[] }; + const result = extractor.extract(['main.ts'], parseTagConfig('gql')) as { fileEntries: ExtractSucceededResult[] }; const { type, fragmentName } = extractor.getDominantDefinition(result.fileEntries[0]); expect(type).toBe('fragment'); expect(fragmentName).toBe('MyFragment2'); diff --git a/src/analyzer/extractor.ts b/src/analyzer/extractor.ts index 3a1b980b0..963412b7d 100644 --- a/src/analyzer/extractor.ts +++ b/src/analyzer/extractor.ts @@ -10,7 +10,13 @@ import { type FragmentDefinitionNode, } from 'graphql'; import { getFragmentDependenciesForAST } from 'graphql-language-service'; -import { isTagged, ScriptSourceHelper, ResolvedTemplateInfo } from '../ts-ast-util'; +import { + getTemplateNodeUnder, + getTagName, + ScriptSourceHelper, + ResolvedTemplateInfo, + StrictTagCondition, +} from '../ts-ast-util'; import { ManifestOutput, ManifestDocumentEntry, OperationType } from './types'; import { ErrorWithLocation, ERROR_CODES } from '../errors'; import { @@ -84,19 +90,20 @@ export class Extractor { this._debug = debug; } - extract(files: string[], tagName?: string): ExtractResult { + extract(files: string[], tag: StrictTagCondition): ExtractResult { const results: ExtractFileResult[] = []; const targetFiles = files.filter(fileName => !this._helper.isExcluded(fileName)); this._debug('Extract template literals from: '); this._debug(targetFiles.map(f => ' ' + f).join(',\n')); targetFiles.forEach(fileName => { if (this._helper.isExcluded(fileName)) return; - const nodes = this._helper - .getAllNodes(fileName, node => ts.isTemplateExpression(node) || ts.isNoSubstitutionTemplateLiteral(node)) - .filter(node => (tagName ? isTagged(node, tagName) : true)) as ( - | ts.TemplateExpression - | ts.NoSubstitutionTemplateLiteral - )[]; + // const nodes = this._helper + // .getAllNodes(fileName, node => ts.isTemplateExpression(node) || ts.isNoSubstitutionTemplateLiteral(node)) + // .filter(node => (tag ? isTagged(node, tag) : true)) as ( + // | ts.TemplateExpression + // | ts.NoSubstitutionTemplateLiteral + // )[]; + const nodes = this._helper.getAllNodes(fileName, node => getTemplateNodeUnder(node, tag)); nodes.forEach(node => { const { resolvedInfo, resolveErrors } = this._helper.resolveTemplateLiteral(fileName, node); if (!resolvedInfo) { @@ -287,7 +294,7 @@ export class Extractor { }; } - toManifest({ fileEntries: extractResults, globalFragments }: ExtractResult, tagName: string = ''): ManifestOutput { + toManifest({ fileEntries: extractResults, globalFragments }: ExtractResult, tag: StrictTagCondition): ManifestOutput { const documents = extractResults .filter(r => !!r.documentNode) .map(result => { @@ -299,7 +306,7 @@ export class Extractor { operationName, fragmentName, body: print(this.inflateDocument(r, { globalFragments }).inflatedDocumentNode), - tag: tagName, + tag: getTagName(r.templateNode, tag), templateLiteralNodeStart: this._helper.getLineAndChar(r.fileName, r.templateNode.getStart()), templateLiteralNodeEnd: this._helper.getLineAndChar(r.fileName, r.templateNode.getEnd()), documentStart: this._helper.getLineAndChar(r.fileName, r.templateNode.getStart() + 1), diff --git a/src/analyzer/markdown-reporter.test.ts b/src/analyzer/markdown-reporter.test.ts index f45d8a856..9e1659a74 100644 --- a/src/analyzer/markdown-reporter.test.ts +++ b/src/analyzer/markdown-reporter.test.ts @@ -1,5 +1,6 @@ import { MarkdownReporter } from './markdown-reporter'; import { createTesintExtractor } from './testing/testing-extractor'; +import { parseTagConfig } from '../ts-ast-util'; describe(MarkdownReporter, () => { it('should convert from manifest to markdown content', () => { @@ -29,7 +30,10 @@ describe(MarkdownReporter, () => { `, }, ]); - const manifest = extractor.toManifest(extractor.extract(['/prj-root/src/main.ts'], 'gql')); + const manifest = extractor.toManifest( + extractor.extract(['/prj-root/src/main.ts'], parseTagConfig('gql')), + parseTagConfig('gql'), + ); const content = new MarkdownReporter().toMarkdownConntent(manifest, { baseDir: '/prj-root', outputDir: '/prj-root/dist', @@ -64,7 +68,10 @@ describe(MarkdownReporter, () => { `, }, ]); - const manifest = extractor.toManifest(extractor.extract(['/prj-root/src/main.ts'], 'gql')); + const manifest = extractor.toManifest( + extractor.extract(['/prj-root/src/main.ts'], parseTagConfig('gql')), + parseTagConfig('gql'), + ); const content = new MarkdownReporter().toMarkdownConntent(manifest, { ignoreFragments: false, baseDir: '/prj-root', diff --git a/src/analyzer/type-generator.test.ts b/src/analyzer/type-generator.test.ts index 7113c2341..be385e4ac 100644 --- a/src/analyzer/type-generator.test.ts +++ b/src/analyzer/type-generator.test.ts @@ -3,7 +3,7 @@ import { TypeGenerator } from './type-generator'; import { createTesintExtractor } from './testing/testing-extractor'; import { ExtractSucceededResult } from './extractor'; import { TypeGenAddonFactory } from '../typegen/addon/types'; -import { createOutputSource } from '../ts-ast-util'; +import { createOutputSource, DEFAULT_TAG_CONDITION } from '../ts-ast-util'; function createTestingTypeGenerator({ files = [], @@ -15,7 +15,7 @@ function createTestingTypeGenerator({ const extractor = createTesintExtractor(files, true); const generator = new TypeGenerator({ prjRootPath: '', - tag: undefined, + tag: DEFAULT_TAG_CONDITION, addonFactories, extractor, debug: () => {}, @@ -37,7 +37,7 @@ describe(TypeGenerator, () => { }); const { fileEntries: [fileEntry], - } = extractor.extract(['main.ts']) as { fileEntries: ExtractSucceededResult[] }; + } = extractor.extract(['main.ts'], DEFAULT_TAG_CONDITION) as { fileEntries: ExtractSucceededResult[] }; const { addon, context } = generator.createAddon({ fileEntry, schema, diff --git a/src/analyzer/type-generator.ts b/src/analyzer/type-generator.ts index ed1c3f414..78fed41bf 100644 --- a/src/analyzer/type-generator.ts +++ b/src/analyzer/type-generator.ts @@ -5,21 +5,21 @@ import { GraphQLSchema } from 'graphql'; import { TsGqlError, ErrorWithLocation } from '../errors'; import { mergeAddons, TypeGenVisitor, TypeGenError, TypeGenAddonFactory, TypeGenVisitorAddonContext } from '../typegen'; import { dasherize } from '../string-util'; -import { OutputSource, createOutputSource } from '../ts-ast-util'; +import { OutputSource, createOutputSource, type StrictTagCondition } from '../ts-ast-util'; import { Extractor, ExtractSucceededResult } from './extractor'; export type TypeGeneratorOptions = { prjRootPath: string; extractor: Extractor; debug: (msg: string) => void; - tag: string | undefined; + tag: StrictTagCondition; addonFactories: TypeGenAddonFactory[]; }; export class TypeGenerator { private readonly _prjRootPath: string; private readonly _extractor: Extractor; - private readonly _tag: string | undefined; + private readonly _tag: StrictTagCondition; private readonly _addonFactories: TypeGenAddonFactory[]; private readonly _debug: (msg: string) => void; private readonly _printer: ts.Printer; diff --git a/src/graphql-language-service-adapter/graphql-language-service-adapter.ts b/src/graphql-language-service-adapter/graphql-language-service-adapter.ts index 64092a6ea..2e97c1a42 100644 --- a/src/graphql-language-service-adapter/graphql-language-service-adapter.ts +++ b/src/graphql-language-service-adapter/graphql-language-service-adapter.ts @@ -1,6 +1,12 @@ import ts from 'typescript'; import { GraphQLSchema, parse, type DocumentNode } from 'graphql'; -import { isTagged, ScriptSourceHelper, TagCondition, isTemplateLiteralTypeNode } from '../ts-ast-util'; +import { + getTemplateNodeUnder, + isTaggedTemplateNode, + ScriptSourceHelper, + StrictTagCondition, + isTemplateLiteralTypeNode, +} from '../ts-ast-util'; import { SchemaBuildErrorInfo } from '../schema-manager/schema-manager'; import { getFragmentNamesInDocument, detectDuplicatedFragments, type FragmentRegistry } from '../gql-ast-util'; import { AnalysisContext, GetCompletionAtPosition, GetSemanticDiagnostics, GetQuickInfoAtPosition } from './types'; @@ -13,7 +19,7 @@ export interface GraphQLLanguageServiceAdapterCreateOptions { schema?: GraphQLSchema | null; schemaErrors?: SchemaBuildErrorInfo[] | null; logger?: (msg: string) => void; - tag?: string; + tag: StrictTagCondition; removeDuplicatedFragments: boolean; fragmentRegistry: FragmentRegistry; } @@ -23,7 +29,7 @@ type Args = T extends (...args: infer A) => any ? A : never; export class GraphQLLanguageServiceAdapter { private _schemaErrors?: SchemaBuildErrorInfo[] | null; private _schema?: GraphQLSchema | null; - private readonly _tagCondition?: TagCondition; + private readonly _tagCondition: StrictTagCondition; private readonly _removeDuplicatedFragments: boolean; private readonly _analysisContext: AnalysisContext; private readonly _fragmentRegisry: FragmentRegistry; @@ -36,7 +42,7 @@ export class GraphQLLanguageServiceAdapter { if (opt.logger) this._logger = opt.logger; if (opt.schemaErrors) this.updateSchema(opt.schemaErrors, null); if (opt.schema) this.updateSchema(null, opt.schema); - if (opt.tag) this._tagCondition = opt.tag; + this._tagCondition = opt.tag; this._removeDuplicatedFragments = opt.removeDuplicatedFragments; this._analysisContext = this._createAnalysisContext(); this._fragmentRegisry = opt.fragmentRegistry; @@ -104,22 +110,26 @@ export class GraphQLLanguageServiceAdapter { } else { return; } - if (this._tagCondition && !isTagged(node, this._tagCondition)) { + // if (this._tagCondition && !isTagged(node, this._tagCondition)) { + // return; + // } + if (!isTaggedTemplateNode(node, this._tagCondition)) { return; } return node; } private _findTemplateNodes(fileName: string) { - const allTemplateStringNodes = this._helper.getAllNodes( - fileName, - (n: ts.Node) => ts.isNoSubstitutionTemplateLiteral(n) || ts.isTemplateExpression(n), - ); - const nodes = allTemplateStringNodes.filter(n => { - if (!this._tagCondition) return true; - return isTagged(n, this._tagCondition); - }) as (ts.NoSubstitutionTemplateLiteral | ts.TemplateExpression)[]; - return nodes; + return this._helper.getAllNodes(fileName, node => getTemplateNodeUnder(node, this._tagCondition)); + // const allTemplateStringNodes = this._helper.getAllNodes( + // fileName, + // (n: ts.Node) => ts.isNoSubstitutionTemplateLiteral(n) || ts.isTemplateExpression(n), + // ); + // const nodes = allTemplateStringNodes.filter(n => { + // if (!this._tagCondition) return true; + // return isTagged(n, this._tagCondition); + // }) as (ts.NoSubstitutionTemplateLiteral | ts.TemplateExpression)[]; + // return nodes; } private _parse(text: string) { diff --git a/src/graphql-language-service-adapter/testing/adapter-fixture.ts b/src/graphql-language-service-adapter/testing/adapter-fixture.ts index 8f2660403..f80e724d8 100644 --- a/src/graphql-language-service-adapter/testing/adapter-fixture.ts +++ b/src/graphql-language-service-adapter/testing/adapter-fixture.ts @@ -32,6 +32,12 @@ export class AdapterFixture { schema: schema || null, removeDuplicatedFragments: true, fragmentRegistry: this._fragmentRegistry, + tag: { + names: [], + allowNotTaggedTemplate: true, + allowTaggedTemplateExpression: true, + allowFunctionCallExpression: true, + }, }, ); } diff --git a/src/language-service-plugin/plugin-module-factory.ts b/src/language-service-plugin/plugin-module-factory.ts index 81fc3ac62..ec6ddbf32 100644 --- a/src/language-service-plugin/plugin-module-factory.ts +++ b/src/language-service-plugin/plugin-module-factory.ts @@ -6,10 +6,11 @@ import { FragmentRegistry } from '../gql-ast-util'; import { createScriptSourceHelper, registerDocumentChangeEvent, - hasTagged, + getTemplateNodeUnder, findAllNodes, getSanitizedTemplateText, createFileNameFilter, + parseTagConfig, } from '../ts-ast-util'; import { LanguageServiceProxyBuilder } from './language-service-proxy-builder'; @@ -19,7 +20,7 @@ function create(info: ts.server.PluginCreateInfo): ts.LanguageService { const schemaManager = new SchemaManagerFactory(createSchemaManagerHostFromLSPluginInfo(info)).create(); const { schema, errors: schemaErrors } = schemaManager.getSchema(); const config = info.config as TsGraphQLPluginConfigOptions; - const tag = config.tag; + const tag = parseTagConfig(config.tag); const removeDuplicatedFragments = config.removeDuplicatedFragments === false ? false : true; const enabledGlobalFragments = config.enabledGlobalFragments === true; const isExcluded = createFileNameFilter({ specs: config.exclude, projectName: info.project.getProjectName() }); @@ -31,11 +32,12 @@ function create(info: ts.server.PluginCreateInfo): ts.LanguageService { fileName, version, findAllNodes(sourceFile, node => { - if (tag && ts.isTaggedTemplateExpression(node) && hasTagged(node, tag, sourceFile)) { - return node.template; - } else if (ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateExpression(node)) { - return node; - } + // if (tag && ts.isTaggedTemplateExpression(node) && hasTagged(node, tag, sourceFile)) { + // return node.template; + // } else if (ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateExpression(node)) { + // return node; + // } + return getTemplateNodeUnder(node, tag); }).map(node => getSanitizedTemplateText(node, sourceFile)), ); }; diff --git a/src/transformer/transformer-host.ts b/src/transformer/transformer-host.ts index a39232d3b..6b957854f 100644 --- a/src/transformer/transformer-host.ts +++ b/src/transformer/transformer-host.ts @@ -2,6 +2,7 @@ import ts from 'typescript'; import { DocumentNode } from 'graphql'; import { Analyzer, AnalyzerFactory, ExtractFileResult } from '../analyzer'; import { getTransformer, DocumentTransformer } from './transformer'; +import { parseTagConfig } from '../ts-ast-util'; class DocumentNodeRegistory { protected readonly _map = new Map>(); @@ -92,7 +93,7 @@ export class TransformerHost { }); return getTransformer({ getEnabled, - tag, + tag: parseTagConfig(tag), target, removeFragmentDefinitions, getDocumentNode: node => this._documentNodeRegistory.getDocumentNode(node), diff --git a/src/transformer/transformer.test.ts b/src/transformer/transformer.test.ts index 44bdf83d2..a21ab7a23 100644 --- a/src/transformer/transformer.test.ts +++ b/src/transformer/transformer.test.ts @@ -1,6 +1,7 @@ import ts from 'typescript'; import { DocumentNode, parse, visit } from 'graphql'; +import { parseTagConfig } from '../ts-ast-util'; import { getTransformer } from './transformer'; function transformAndPrint({ @@ -23,7 +24,7 @@ function transformAndPrint({ const getDocumentNode = () => parse(docContent); const source = ts.createSourceFile('main.ts', tsContent, ts.ScriptTarget.Latest, true); const transformer = getTransformer({ - tag, + tag: parseTagConfig(tag), target, getDocumentNode, removeFragmentDefinitions, diff --git a/src/transformer/transformer.ts b/src/transformer/transformer.ts index cd30e6182..e0c3271c1 100644 --- a/src/transformer/transformer.ts +++ b/src/transformer/transformer.ts @@ -1,11 +1,11 @@ import ts from 'typescript'; import { DocumentNode, print } from 'graphql'; -import { astf, hasTagged, removeAliasFromImportDeclaration } from '../ts-ast-util'; +import { astf, getTemplateNodeUnder, removeAliasFromImportDeclaration, type StrictTagCondition } from '../ts-ast-util'; export type DocumentTransformer = (documentNode: DocumentNode) => DocumentNode; export type TransformOptions = { - tag?: string; + tag: StrictTagCondition; documentTransformers: DocumentTransformer[]; removeFragmentDefinitions: boolean; target: 'text' | 'object'; @@ -44,17 +44,21 @@ export function getTransformer({ return (ctx: ts.TransformationContext) => { const visit = (node: ts.Node): ts.Node | undefined => { if (!getEnabled()) return node; + if (tag.names.length > 1) return node; let templateNode: ts.NoSubstitutionTemplateLiteral | ts.TemplateExpression | undefined = undefined; - if (tag && ts.isImportDeclaration(node)) { - return removeAliasFromImportDeclaration(node, tag); + if (ts.isImportDeclaration(node) && tag.names[0]) { + return removeAliasFromImportDeclaration(node, tag.names[0]); } - if (ts.isTaggedTemplateExpression(node) && (!tag || hasTagged(node, tag))) { + if ( + ts.isTaggedTemplateExpression(node) && + (!tag.names.length || !!getTemplateNodeUnder(node, { ...tag, allowFunctionCallExpression: false })) + ) { templateNode = node.template; - } else if (!tag && ts.isNoSubstitutionTemplateLiteral(node)) { + } else if (tag.allowNotTaggedTemplate && ts.isNoSubstitutionTemplateLiteral(node)) { templateNode = node; - } else if (!tag && ts.isTemplateExpression(node)) { + } else if (tag.allowNotTaggedTemplate && ts.isTemplateExpression(node)) { templateNode = node; } diff --git a/src/ts-ast-util/index.ts b/src/ts-ast-util/index.ts index 66f2cc5d6..1435e2bfb 100644 --- a/src/ts-ast-util/index.ts +++ b/src/ts-ast-util/index.ts @@ -3,6 +3,7 @@ export * from './ast-factory-alias'; export * from './utilily-functions'; export * from './template-expression-resolver'; export * from './register-document-change-event'; +export * from './tag-utils'; export { ScriptHost } from './script-host'; export { createScriptSourceHelper } from './script-source-helper'; diff --git a/src/ts-ast-util/script-source-helper.ts b/src/ts-ast-util/script-source-helper.ts index 0beb8916e..7beb3fea6 100644 --- a/src/ts-ast-util/script-source-helper.ts +++ b/src/ts-ast-util/script-source-helper.ts @@ -35,7 +35,7 @@ export function createScriptSourceHelper( const getNode = (fileName: string, position: number) => { return findNode(getSourceFile(fileName), position); }; - const getAllNodes = (fileName: string, cond: (n: ts.Node) => boolean) => { + const getAllNodes = (fileName: string, cond: (n: ts.Node) => undefined | boolean | S) => { const s = getSourceFile(fileName); return findAllNodes(s, cond); }; diff --git a/src/ts-ast-util/tag-utils.test.ts b/src/ts-ast-util/tag-utils.test.ts new file mode 100644 index 000000000..b2a47367e --- /dev/null +++ b/src/ts-ast-util/tag-utils.test.ts @@ -0,0 +1,212 @@ +import ts from 'typescript'; +import { findAllNodes, findNode } from './utilily-functions'; +import type { TagConfig, StrictTagCondition } from './types'; +import { parseTagConfig, getTemplateNodeUnder, isTagged, getTagName } from './tag-utils'; + +describe(parseTagConfig, () => { + test.each([ + { + config: undefined, + expected: { + names: [], + allowNotTaggedTemplate: true, + allowTaggedTemplateExpression: true, + allowFunctionCallExpression: false, + } as StrictTagCondition, + }, + { + config: '', + expected: { + names: [], + allowNotTaggedTemplate: true, + allowTaggedTemplateExpression: true, + allowFunctionCallExpression: false, + } as StrictTagCondition, + }, + { + config: 'gql', + expected: { + names: ['gql'], + allowNotTaggedTemplate: false, + allowTaggedTemplateExpression: true, + allowFunctionCallExpression: false, + } as StrictTagCondition, + }, + { + config: ['gql', 'graphql'], + expected: { + names: ['gql', 'graphql'], + allowNotTaggedTemplate: false, + allowTaggedTemplateExpression: true, + allowFunctionCallExpression: false, + } as StrictTagCondition, + }, + { + config: {}, + expected: { + names: [], + allowNotTaggedTemplate: false, + allowTaggedTemplateExpression: true, + allowFunctionCallExpression: false, + } as StrictTagCondition, + }, + { + config: { + name: 'gql', + } satisfies TagConfig, + expected: { + names: ['gql'], + allowNotTaggedTemplate: false, + allowTaggedTemplateExpression: true, + allowFunctionCallExpression: false, + } as StrictTagCondition, + }, + { + config: { + name: ['gql', 'graphql'], + } satisfies TagConfig, + expected: { + names: ['gql', 'graphql'], + allowNotTaggedTemplate: false, + allowTaggedTemplateExpression: true, + allowFunctionCallExpression: false, + } as StrictTagCondition, + }, + { + config: { + name: ['graphql'], + ignoreFunctionCallExpression: true, + } satisfies TagConfig, + expected: { + names: ['graphql'], + allowNotTaggedTemplate: false, + allowTaggedTemplateExpression: true, + allowFunctionCallExpression: false, + } as StrictTagCondition, + }, + { + config: { + name: ['graphql'], + ignoreFunctionCallExpression: false, + } satisfies TagConfig, + expected: { + names: ['graphql'], + allowNotTaggedTemplate: false, + allowTaggedTemplateExpression: true, + allowFunctionCallExpression: true, + } as StrictTagCondition, + }, + ])('input: $config', ({ config, expected }) => { + expect(parseTagConfig(config)).toEqual(expected); + }); +}); + +describe(getTemplateNodeUnder, () => { + const createFixture = (sourceText: string, tagConfig: TagConfig) => { + const source = ts.createSourceFile('input.ts', sourceText, ts.ScriptTarget.Latest, false); + return { + exec: () => findAllNodes(source, node => getTemplateNodeUnder(node, parseTagConfig(tagConfig))), + }; + }; + + it.each([ + { source: '`query { }`', config: undefined }, + { source: 'gql`query { }`', config: 'gql' }, + { source: 'gql`query { }`', config: ['gql', 'graphql'] }, + { + source: 'graphql(`query { }`)', + config: { name: 'graphql', ignoreFunctionCallExpression: false } satisfies TagConfig, + }, + ])('should return TemplateLiteralNode from $source with tagCongig: $config', ({ source, config }) => { + const [found] = createFixture(source, config).exec(); + expect(found).toBeTruthy(); + }); + + test.each([ + { source: '`query { }`', config: 'gql' }, + { source: 'graphql`query { }`', config: 'gql' }, + { + source: 'fn(`query { }`)', + config: { name: 'graphql', ignoreFunctionCallExpression: false } satisfies TagConfig, + }, + { + source: 'graphql(`query { }`)', + config: { name: 'graphql', ignoreFunctionCallExpression: true } satisfies TagConfig, + }, + ])('should not return undefind from $source with tagCongig: $config', ({ source, config }) => { + const actual = createFixture(source, config).exec(); + expect(actual).toEqual([]); + }); +}); + +describe(getTagName, () => { + const createFixture = (sourceText: string, tagConfig: TagConfig) => { + const source = ts.createSourceFile('input.ts', sourceText, ts.ScriptTarget.Latest, true); + return { + exec: () => + findAllNodes(source, node => getTemplateNodeUnder(node, parseTagConfig(tagConfig))).map(n => + getTagName(n, parseTagConfig(tagConfig)), + ), + }; + }; + + it.each([ + { source: '`query { }`', expected: '', config: undefined }, + { source: 'gql`query { }`', expected: 'gql', config: 'gql' }, + { source: 'gql`query { }`', expected: 'gql', config: ['gql', 'graphql'] }, + { + source: 'graphql(`query { }`)', + expected: 'graphql', + config: { name: 'graphql', ignoreFunctionCallExpression: false } satisfies TagConfig, + }, + ])('should return TemplateLiteralNode from $source with tagCongig: $config', ({ source, expected, config }) => { + const actual = createFixture(source, config).exec(); + expect(actual).toEqual([expected]); + }); + + it('return empty string when node is parsed without parent', () => { + const sourceText = 'gql`query { }`'; + const source = ts.createSourceFile('input.ts', sourceText, ts.ScriptTarget.Latest, false); + const actual = findAllNodes(source, node => getTemplateNodeUnder(node, parseTagConfig('gql'))).map(n => + getTagName(n, parseTagConfig('gql')), + ); + expect(actual).toEqual([undefined]); + }); + + it('return empty string when node does not match condition', () => { + const sourceText = '`hoge`'; + const source = ts.createSourceFile('input.ts', sourceText, ts.ScriptTarget.Latest, true); + const actual = getTagName(source.statements[0] as unknown as ts.TemplateLiteral, parseTagConfig('gql')); + expect(actual).toBe(undefined); + }); + + it('return empty string when node parent is not TaggedTemplateExpression nor CallExpression', () => { + const sourceText = 'const obj = { x: `hoge` }'; + const source = ts.createSourceFile('input.ts', sourceText, ts.ScriptTarget.Latest, true); + const actual = getTagName( + findAllNodes(source, node => ts.isNoSubstitutionTemplateLiteral(node) && node)[0], + parseTagConfig('gql'), + ); + expect(actual).toBe(undefined); + }); +}); + +describe(isTagged, () => { + it('should return true when the tag condition is matched', () => { + // prettier-ignore + const text = 'function myTag(...args: any[]) { return "" }' + '\n' + + 'const x = myTag`query { }`'; + const s = ts.createSourceFile('input.ts', text, ts.ScriptTarget.Latest, true); + const node = findNode(s, text.length - 3) as ts.Node; + expect(isTagged(node, 'myTag')).toBeTruthy(); + }); + + it('should return true when the tag condition is not matched', () => { + // prettier-ignore + const text = 'function myTag(...args: any[]) { return "" }' + '\n' + + 'const x = myTag`query { }`'; + const s = ts.createSourceFile('input.ts', text, ts.ScriptTarget.Latest, true); + const node = findNode(s, text.length - 3) as ts.Node; + expect(isTagged(node, 'MyTag')).toBeFalsy(); + }); +}); diff --git a/src/ts-ast-util/tag-utils.ts b/src/ts-ast-util/tag-utils.ts new file mode 100644 index 000000000..8c8f4b416 --- /dev/null +++ b/src/ts-ast-util/tag-utils.ts @@ -0,0 +1,106 @@ +import ts from 'typescript'; +import type { TagConfig, StrictTagCondition } from './types'; + +// TODO Change default at v4 +export const DEFAULT_TAG_CONDITION = { + names: [], + allowNotTaggedTemplate: true, + allowTaggedTemplateExpression: true, + allowFunctionCallExpression: false, +} satisfies StrictTagCondition; + +export function parseTagConfig(tagConfig: TagConfig | undefined): StrictTagCondition { + const parseName = (name: unknown) => { + if (typeof name === 'string') { + return [name]; + } else if (Array.isArray(name)) { + return name.map(n => n.toString()); + } else { + return [] as string[]; + } + }; + + if (!tagConfig) { + return DEFAULT_TAG_CONDITION; + } + + if (typeof tagConfig === 'string' || Array.isArray(tagConfig)) { + return { + names: parseName(tagConfig), + allowNotTaggedTemplate: false, + allowTaggedTemplateExpression: true, + allowFunctionCallExpression: false, + }; + } + + const names = parseName(tagConfig.name); + return { + names, + allowNotTaggedTemplate: false, + allowTaggedTemplateExpression: true, + allowFunctionCallExpression: + tagConfig.ignoreFunctionCallExpression == null + ? false // Change behavior at v4 + : tagConfig.ignoreFunctionCallExpression === false + ? true + : false, + }; +} + +export function getTemplateNodeUnder( + node: ts.Node | undefined, + { allowFunctionCallExpression, allowNotTaggedTemplate, allowTaggedTemplateExpression, names }: StrictTagCondition, +) { + if (!node) return undefined; + if (allowTaggedTemplateExpression && ts.isTaggedTemplateExpression(node) && ts.isIdentifier(node.tag)) { + if (names.includes(node.tag.escapedText as string)) { + return node.template; + } + } + if (allowFunctionCallExpression && ts.isCallExpression(node) && ts.isIdentifier(node.expression)) { + const firstArg = node.arguments[0]; + if (!ts.isTemplateLiteral(firstArg)) return; + if (names.includes(node.expression.escapedText as string)) { + return firstArg; + } + } + if (allowNotTaggedTemplate && ts.isTemplateLiteral(node)) { + return node; + } +} + +export function isTaggedTemplateNode(node: ts.TemplateLiteral, tagCondition: StrictTagCondition) { + if (tagCondition.allowNotTaggedTemplate) return true; + return !!getTemplateNodeUnder(node.parent, tagCondition); +} + +export function getTagName( + node: ts.TemplateLiteral, + { allowNotTaggedTemplate, allowFunctionCallExpression, allowTaggedTemplateExpression, names }: StrictTagCondition, +) { + if (!node.parent) return undefined; + if (!ts.isCallExpression(node.parent) && !ts.isTaggedTemplateExpression(node.parent)) { + return allowNotTaggedTemplate ? '' : undefined; + } + if (allowFunctionCallExpression && ts.isCallExpression(node.parent) && ts.isIdentifier(node.parent.expression)) { + const name = node.parent.expression.escapedText as string; + return names.includes(name) ? name : allowNotTaggedTemplate ? '' : undefined; + } + if (allowTaggedTemplateExpression && ts.isTaggedTemplateExpression(node.parent) && ts.isIdentifier(node.parent.tag)) { + const name = node.parent.tag.escapedText as string; + return names.includes(name) ? name : allowNotTaggedTemplate ? '' : undefined; + } + return undefined; +} + +export function hasTagged(node: ts.Node | undefined, condition: string | undefined, source?: ts.SourceFile) { + if (!node) return; + if (!ts.isTaggedTemplateExpression(node)) return false; + const tagNode = node; + return tagNode.tag.getText(source) === condition; +} + +export function isTagged(node: ts.Node | undefined, condition: string | undefined, source?: ts.SourceFile) { + if (!node) return false; + return hasTagged(node.parent, condition, source); +} diff --git a/src/ts-ast-util/types.ts b/src/ts-ast-util/types.ts index fd3692464..45a8d5fc7 100644 --- a/src/ts-ast-util/types.ts +++ b/src/ts-ast-util/types.ts @@ -120,6 +120,22 @@ export type ComputePosition = (innerPosition: number) => { isInOtherExpression?: boolean; }; +export type TagConfig = + | undefined + | string + | string[] + | { + name?: string | string[]; + ignoreFunctionCallExpression?: boolean; + }; + +export type StrictTagCondition = { + names: string[]; + allowNotTaggedTemplate: boolean; + allowTaggedTemplateExpression: boolean; + allowFunctionCallExpression: boolean; +}; + /** * * Serves the following information. @@ -154,7 +170,7 @@ export interface ResolveResult { } export interface ScriptSourceHelper { - getAllNodes: (fileName: string, condition: (n: ts.Node) => boolean) => ts.Node[]; + getAllNodes: (fileName: string, condition: (n: ts.Node) => undefined | boolean | S) => S[]; getNode: (fileName: string, position: number) => ts.Node | undefined; getLineAndChar: (fileName: string, position: number) => ts.LineAndCharacter; isExcluded: (fileName: string) => boolean; diff --git a/src/ts-ast-util/utilily-functions.test.ts b/src/ts-ast-util/utilily-functions.test.ts index 893014f11..174cc1f4a 100644 --- a/src/ts-ast-util/utilily-functions.test.ts +++ b/src/ts-ast-util/utilily-functions.test.ts @@ -1,35 +1,13 @@ import ts from 'typescript'; import { findAllNodes, - findNode, getSanitizedTemplateText, - isTagged, isImportDeclarationWithCondition, mergeImportDeclarationsWithSameModules, removeAliasFromImportDeclaration, } from './utilily-functions'; import { printNode } from './testing/print-node'; -describe(isTagged, () => { - it('should return true when the tag condition is matched', () => { - // prettier-ignore - const text = 'function myTag(...args: any[]) { return "" }' + '\n' - + 'const x = myTag`query { }`'; - const s = ts.createSourceFile('input.ts', text, ts.ScriptTarget.Latest, true); - const node = findNode(s, text.length - 3) as ts.Node; - expect(isTagged(node, 'myTag')).toBeTruthy(); - }); - - it('should return true when the tag condition is not matched', () => { - // prettier-ignore - const text = 'function myTag(...args: any[]) { return "" }' + '\n' - + 'const x = myTag`query { }`'; - const s = ts.createSourceFile('input.ts', text, ts.ScriptTarget.Latest, true); - const node = findNode(s, text.length - 3) as ts.Node; - expect(isTagged(node, 'MyTag')).toBeFalsy(); - }); -}); - describe(findAllNodes, () => { it('should return nodes which match given condition', () => { // prettier-ignore diff --git a/src/ts-ast-util/utilily-functions.ts b/src/ts-ast-util/utilily-functions.ts index 34826b65b..68a284609 100644 --- a/src/ts-ast-util/utilily-functions.ts +++ b/src/ts-ast-util/utilily-functions.ts @@ -37,8 +37,6 @@ function removeFromImportClause(base: ts.ImportClause | undefined, name: string) return astf.updateImportClause(base, base.isTypeOnly, nameId, namedBindings); } -export type TagCondition = string; - export function findNode(sourceFile: ts.SourceFile, position: number): ts.Node | undefined { function find(node: ts.Node): ts.Node | undefined { if (position >= node.getStart() && position < node.getEnd()) { @@ -66,18 +64,6 @@ export function findAllNodes( return result as S[]; } -export function hasTagged(node: ts.Node | undefined, condition: TagCondition, source?: ts.SourceFile) { - if (!node) return; - if (!ts.isTaggedTemplateExpression(node)) return false; - const tagNode = node; - return tagNode.tag.getText(source) === condition; -} - -export function isTagged(node: ts.Node | undefined, condition: TagCondition, source?: ts.SourceFile) { - if (!node) return false; - return hasTagged(node.parent, condition, source); -} - export function getSanitizedTemplateText( node: ts.NoSubstitutionTemplateLiteral | ts.TemplateExpression, source?: ts.SourceFile, diff --git a/src/typegen-addons/testing/addon-tester.ts b/src/typegen-addons/testing/addon-tester.ts index acdc23a3f..81288de4f 100644 --- a/src/typegen-addons/testing/addon-tester.ts +++ b/src/typegen-addons/testing/addon-tester.ts @@ -3,6 +3,7 @@ import { buildSchema } from 'graphql'; import { TypeGenAddonFactory } from '../../typegen'; import { createTesintExtractor } from '../../analyzer/testing/testing-extractor'; import { TypeGenerator } from '../../analyzer/type-generator'; +import { parseTagConfig, type TagConfig } from '../../ts-ast-util'; function createTestingTypeGenerator({ files = [], @@ -10,13 +11,13 @@ function createTestingTypeGenerator({ addonFactories = [], }: { files?: { fileName: string; content: string }[]; - tag?: string; + tag?: TagConfig; addonFactories?: TypeGenAddonFactory[]; }) { const extractor = createTesintExtractor(files, true); const generator = new TypeGenerator({ prjRootPath: '', - tag, + tag: parseTagConfig(tag), addonFactories, extractor, debug: () => {}, diff --git a/src/types.ts b/src/types.ts index 5129f8ca7..690c8a666 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,11 +1,12 @@ -import { SchemaConfig } from './schema-manager'; +import type { SchemaConfig } from './schema-manager'; +import type { TagConfig } from './ts-ast-util'; export type TsGraphQLPluginConfigOptions = SchemaConfig & { name: string; exclude?: string[]; enabledGlobalFragments?: boolean; removeDuplicatedFragments?: boolean; - tag?: string; + tag?: TagConfig; typegen?: { addons?: string[]; }; From 513624952dd133624e3d9bbc5f21f16305856da2 Mon Sep 17 00:00:00 2001 From: Quramy Date: Fri, 15 Mar 2024 12:26:17 +0900 Subject: [PATCH 2/4] chore: Remove commented out lines --- src/analyzer/analyzer.ts | 11 ++--- src/analyzer/extractor.ts | 6 --- .../get-completion-at-position.ts | 2 +- .../get-quick-info-at-position.ts | 2 +- .../graphql-language-service-adapter.ts | 42 +++++++------------ src/graphql-language-service-adapter/types.ts | 2 +- .../plugin-module-factory.ts | 11 ++--- 7 files changed, 25 insertions(+), 51 deletions(-) diff --git a/src/analyzer/analyzer.ts b/src/analyzer/analyzer.ts index 1206ced3f..e3c811b84 100644 --- a/src/analyzer/analyzer.ts +++ b/src/analyzer/analyzer.ts @@ -92,14 +92,9 @@ export class Analyzer { fragmentRegistry.registerDocuments( fileName, version, - findAllNodes(sourceFile, node => { - // if (tag && ts.isTaggedTemplateExpression(node) && hasTagged(node, tag, sourceFile)) { - // return node.template; - // } else if (ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateExpression(node)) { - // return node; - // } - return getTemplateNodeUnder(node, tag); - }).map(node => getSanitizedTemplateText(node, sourceFile)), + findAllNodes(sourceFile, node => getTemplateNodeUnder(node, tag)).map(node => + getSanitizedTemplateText(node, sourceFile), + ), ); } }, diff --git a/src/analyzer/extractor.ts b/src/analyzer/extractor.ts index 963412b7d..fa3288827 100644 --- a/src/analyzer/extractor.ts +++ b/src/analyzer/extractor.ts @@ -97,12 +97,6 @@ export class Extractor { this._debug(targetFiles.map(f => ' ' + f).join(',\n')); targetFiles.forEach(fileName => { if (this._helper.isExcluded(fileName)) return; - // const nodes = this._helper - // .getAllNodes(fileName, node => ts.isTemplateExpression(node) || ts.isNoSubstitutionTemplateLiteral(node)) - // .filter(node => (tag ? isTagged(node, tag) : true)) as ( - // | ts.TemplateExpression - // | ts.NoSubstitutionTemplateLiteral - // )[]; const nodes = this._helper.getAllNodes(fileName, node => getTemplateNodeUnder(node, tag)); nodes.forEach(node => { const { resolvedInfo, resolveErrors } = this._helper.resolveTemplateLiteral(fileName, node); diff --git a/src/graphql-language-service-adapter/get-completion-at-position.ts b/src/graphql-language-service-adapter/get-completion-at-position.ts index 543f3a4a7..2c8aef521 100644 --- a/src/graphql-language-service-adapter/get-completion-at-position.ts +++ b/src/graphql-language-service-adapter/get-completion-at-position.ts @@ -33,7 +33,7 @@ export function getCompletionAtPosition( if (ctx.getScriptSourceHelper().isExcluded(fileName)) return delegate(fileName, position, options); const schema = ctx.getSchema(); if (!schema) return delegate(fileName, position, options); - const node = ctx.findTemplateNode(fileName, position); + const node = ctx.findAscendantTemplateNode(fileName, position); if (!node) return delegate(fileName, position, options); const { resolvedInfo } = ctx.resolveTemplateInfo(fileName, node); if (!resolvedInfo) { diff --git a/src/graphql-language-service-adapter/get-quick-info-at-position.ts b/src/graphql-language-service-adapter/get-quick-info-at-position.ts index 1aec9f3fd..7fb0bc297 100644 --- a/src/graphql-language-service-adapter/get-quick-info-at-position.ts +++ b/src/graphql-language-service-adapter/get-quick-info-at-position.ts @@ -12,7 +12,7 @@ export function getQuickInfoAtPosition( if (ctx.getScriptSourceHelper().isExcluded(fileName)) return delegate(fileName, position); const schema = ctx.getSchema(); if (!schema) return delegate(fileName, position); - const node = ctx.findTemplateNode(fileName, position); + const node = ctx.findAscendantTemplateNode(fileName, position); if (!node) return delegate(fileName, position); const { resolvedInfo } = ctx.resolveTemplateInfo(fileName, node); if (!resolvedInfo) return delegate(fileName, position); diff --git a/src/graphql-language-service-adapter/graphql-language-service-adapter.ts b/src/graphql-language-service-adapter/graphql-language-service-adapter.ts index 2e97c1a42..28c623bb3 100644 --- a/src/graphql-language-service-adapter/graphql-language-service-adapter.ts +++ b/src/graphql-language-service-adapter/graphql-language-service-adapter.ts @@ -87,49 +87,39 @@ export class GraphQLLanguageServiceAdapter { getExternalFragmentDefinitions: (documentStr, fileName, sourcePosition) => this._fragmentRegisry.getExternalFragments(documentStr, fileName, sourcePosition), getDuplicaterdFragmentDefinitions: () => this._fragmentRegisry.getDuplicaterdFragmentDefinitions(), - findTemplateNode: (fileName, position) => this._findTemplateNode(fileName, position), + findAscendantTemplateNode: (fileName, position) => this._findAscendantTemplateNode(fileName, position), findTemplateNodes: fileName => this._findTemplateNodes(fileName), resolveTemplateInfo: (fileName, node) => this._resolveTemplateInfo(fileName, node), }; return ctx; } - private _findTemplateNode(fileName: string, position: number) { - const foundNode = this._helper.getNode(fileName, position); - if (!foundNode) return; - let node: ts.NoSubstitutionTemplateLiteral | ts.TemplateExpression; - if (ts.isNoSubstitutionTemplateLiteral(foundNode)) { - node = foundNode; - } else if (ts.isTemplateHead(foundNode) && !isTemplateLiteralTypeNode(foundNode.parent)) { - node = foundNode.parent; + private _findAscendantTemplateNode(fileName: string, position: number) { + const nodeUnderCursor = this._helper.getNode(fileName, position); + if (!nodeUnderCursor) return; + + let templateNode: ts.NoSubstitutionTemplateLiteral | ts.TemplateExpression; + + if (ts.isNoSubstitutionTemplateLiteral(nodeUnderCursor)) { + templateNode = nodeUnderCursor; + } else if (ts.isTemplateHead(nodeUnderCursor) && !isTemplateLiteralTypeNode(nodeUnderCursor.parent)) { + templateNode = nodeUnderCursor.parent; } else if ( - (ts.isTemplateMiddle(foundNode) || ts.isTemplateTail(foundNode)) && - !isTemplateLiteralTypeNode(foundNode.parent.parent) + (ts.isTemplateMiddle(nodeUnderCursor) || ts.isTemplateTail(nodeUnderCursor)) && + !isTemplateLiteralTypeNode(nodeUnderCursor.parent.parent) ) { - node = foundNode.parent.parent; + templateNode = nodeUnderCursor.parent.parent; } else { return; } - // if (this._tagCondition && !isTagged(node, this._tagCondition)) { - // return; - // } - if (!isTaggedTemplateNode(node, this._tagCondition)) { + if (!isTaggedTemplateNode(templateNode, this._tagCondition)) { return; } - return node; + return templateNode; } private _findTemplateNodes(fileName: string) { return this._helper.getAllNodes(fileName, node => getTemplateNodeUnder(node, this._tagCondition)); - // const allTemplateStringNodes = this._helper.getAllNodes( - // fileName, - // (n: ts.Node) => ts.isNoSubstitutionTemplateLiteral(n) || ts.isTemplateExpression(n), - // ); - // const nodes = allTemplateStringNodes.filter(n => { - // if (!this._tagCondition) return true; - // return isTagged(n, this._tagCondition); - // }) as (ts.NoSubstitutionTemplateLiteral | ts.TemplateExpression)[]; - // return nodes; } private _parse(text: string) { diff --git a/src/graphql-language-service-adapter/types.ts b/src/graphql-language-service-adapter/types.ts index 7c1e54967..0586340f4 100644 --- a/src/graphql-language-service-adapter/types.ts +++ b/src/graphql-language-service-adapter/types.ts @@ -20,7 +20,7 @@ export interface AnalysisContext { ): FragmentDefinitionNode[]; getDuplicaterdFragmentDefinitions(): Set; getGraphQLDocumentNode(text: string): DocumentNode | undefined; - findTemplateNode( + findAscendantTemplateNode( fileName: string, position: number, ): ts.NoSubstitutionTemplateLiteral | ts.TemplateExpression | undefined; diff --git a/src/language-service-plugin/plugin-module-factory.ts b/src/language-service-plugin/plugin-module-factory.ts index ec6ddbf32..f845b6b6c 100644 --- a/src/language-service-plugin/plugin-module-factory.ts +++ b/src/language-service-plugin/plugin-module-factory.ts @@ -31,14 +31,9 @@ function create(info: ts.server.PluginCreateInfo): ts.LanguageService { fragmentRegistry.registerDocuments( fileName, version, - findAllNodes(sourceFile, node => { - // if (tag && ts.isTaggedTemplateExpression(node) && hasTagged(node, tag, sourceFile)) { - // return node.template; - // } else if (ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateExpression(node)) { - // return node; - // } - return getTemplateNodeUnder(node, tag); - }).map(node => getSanitizedTemplateText(node, sourceFile)), + findAllNodes(sourceFile, node => getTemplateNodeUnder(node, tag)).map(node => + getSanitizedTemplateText(node, sourceFile), + ), ); }; From b775c514e6296c9ba7d884036c4b872012e7f5ee Mon Sep 17 00:00:00 2001 From: Quramy Date: Fri, 15 Mar 2024 12:30:20 +0900 Subject: [PATCH 3/4] chore: Remove unused functions --- src/ts-ast-util/tag-utils.test.ts | 24 ++---------------------- src/ts-ast-util/tag-utils.ts | 12 ------------ 2 files changed, 2 insertions(+), 34 deletions(-) diff --git a/src/ts-ast-util/tag-utils.test.ts b/src/ts-ast-util/tag-utils.test.ts index b2a47367e..eeab862fc 100644 --- a/src/ts-ast-util/tag-utils.test.ts +++ b/src/ts-ast-util/tag-utils.test.ts @@ -1,7 +1,7 @@ import ts from 'typescript'; -import { findAllNodes, findNode } from './utilily-functions'; +import { findAllNodes } from './utilily-functions'; import type { TagConfig, StrictTagCondition } from './types'; -import { parseTagConfig, getTemplateNodeUnder, isTagged, getTagName } from './tag-utils'; +import { parseTagConfig, getTemplateNodeUnder, getTagName } from './tag-utils'; describe(parseTagConfig, () => { test.each([ @@ -190,23 +190,3 @@ describe(getTagName, () => { expect(actual).toBe(undefined); }); }); - -describe(isTagged, () => { - it('should return true when the tag condition is matched', () => { - // prettier-ignore - const text = 'function myTag(...args: any[]) { return "" }' + '\n' - + 'const x = myTag`query { }`'; - const s = ts.createSourceFile('input.ts', text, ts.ScriptTarget.Latest, true); - const node = findNode(s, text.length - 3) as ts.Node; - expect(isTagged(node, 'myTag')).toBeTruthy(); - }); - - it('should return true when the tag condition is not matched', () => { - // prettier-ignore - const text = 'function myTag(...args: any[]) { return "" }' + '\n' - + 'const x = myTag`query { }`'; - const s = ts.createSourceFile('input.ts', text, ts.ScriptTarget.Latest, true); - const node = findNode(s, text.length - 3) as ts.Node; - expect(isTagged(node, 'MyTag')).toBeFalsy(); - }); -}); diff --git a/src/ts-ast-util/tag-utils.ts b/src/ts-ast-util/tag-utils.ts index 8c8f4b416..573f92475 100644 --- a/src/ts-ast-util/tag-utils.ts +++ b/src/ts-ast-util/tag-utils.ts @@ -92,15 +92,3 @@ export function getTagName( } return undefined; } - -export function hasTagged(node: ts.Node | undefined, condition: string | undefined, source?: ts.SourceFile) { - if (!node) return; - if (!ts.isTaggedTemplateExpression(node)) return false; - const tagNode = node; - return tagNode.tag.getText(source) === condition; -} - -export function isTagged(node: ts.Node | undefined, condition: string | undefined, source?: ts.SourceFile) { - if (!node) return false; - return hasTagged(node.parent, condition, source); -} From 1ffe423b5d87e83eea0b3fe0b19090edcb880e88 Mon Sep 17 00:00:00 2001 From: Quramy Date: Fri, 15 Mar 2024 13:44:46 +0900 Subject: [PATCH 4/4] docs: Modify document --- README.md | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 56e2f8b92..f54ccaf75 100644 --- a/README.md +++ b/README.md @@ -262,16 +262,84 @@ If not set, this plugin treats all template strings in your .ts as GraphQL query For example: +```js +/* tsconfig.json */ + +{ + "compilerOptions": { + "plugins": [ + { + "name": "ts-graphql-plugin", + "tag": "gql", + } + ] + } +} +``` + ```ts -import gql from 'graphql-tag'; +/* yourApp.ts */ -// when tag paramter is 'gql' -const str1 = gql`query { }`; // work -const str2 = `
`; // don't work -const str3 = otherTagFn`foooo`; // don't work +import { gql } from '@apollo/client'; + +// Recognized as GraphQL document +const str1 = gql` + query AppQuery { + __typename + } +`; + +// Not recognized as GraphQL document +const str2 = `
`; +const str3 = otherTagFn`foooo`; ``` -It's useful to write multiple kinds template strings(e.g. one is Angular Component template, another is Apollo GraphQL query). +Sometimes you want to consider the arguments of a particular function calling as a GraphQL document, such as: + +```ts +import { graphql } from '@octokit/graphql'; + +const { viewer } = await graphql(` + query MyQuery { + viewer { + name + } + } +`); +``` + +Configure as the following: + +```js +/* tsconfig.json */ + +{ + "compilerOptions": { + "plugins": [ + { + "name": "ts-graphql-plugin", + "tag": { + "name": "graphql", + "ignoreFunctionCallExpression: false, + } + } + ] + } +} +``` + +The `tag` option accepts the following type: + +```ts +type TagConfig = + | undefined + | string + | string[] + | { + name?: string | string[]; + ignoreFunctionCallExpression?: boolean; + }; +``` ### `exclude`