Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add tag customize pattern #1226

Merged
merged 4 commits into from Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
80 changes: 74 additions & 6 deletions README.md
Expand Up @@ -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 = `<div></div>`; // 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 = `<div></div>`;
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`

Expand Down
21 changes: 9 additions & 12 deletions src/analyzer/analyzer.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -79,25 +80,21 @@ 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)) {
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;
}
}).map(node => getSanitizedTemplateText(node, sourceFile)),
findAllNodes(sourceFile, node => getTemplateNodeUnder(node, tag)).map(node =>
getSanitizedTemplateText(node, sourceFile),
),
);
}
},
Expand All @@ -112,15 +109,15 @@ 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;
}

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;
}

Expand Down
29 changes: 15 additions & 14 deletions 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', () => {
Expand All @@ -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();
});

Expand All @@ -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();
});

Expand All @@ -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();
});

Expand All @@ -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();
Expand All @@ -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();
});

Expand All @@ -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', () => {
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand Down Expand Up @@ -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');
Expand Down
21 changes: 11 additions & 10 deletions src/analyzer/extractor.ts
Expand Up @@ -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 {
Expand Down Expand Up @@ -84,19 +90,14 @@ 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 => getTemplateNodeUnder(node, tag));
nodes.forEach(node => {
const { resolvedInfo, resolveErrors } = this._helper.resolveTemplateLiteral(fileName, node);
if (!resolvedInfo) {
Expand Down Expand Up @@ -287,7 +288,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 => {
Expand All @@ -299,7 +300,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),
Expand Down
11 changes: 9 additions & 2 deletions 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', () => {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
6 changes: 3 additions & 3 deletions src/analyzer/type-generator.test.ts
Expand Up @@ -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 = [],
Expand All @@ -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: () => {},
Expand All @@ -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,
Expand Down