diff --git a/e2e/webpack-specs/transform.js b/e2e/webpack-specs/transform.js index a0884de79..b0e0ec4ba 100644 --- a/e2e/webpack-specs/transform.js +++ b/e2e/webpack-specs/transform.js @@ -4,7 +4,7 @@ const { execSync } = require('child_process'); const webpack = require('webpack'); const { print } = require('graphql/language'); -async function run() { +async function specWithoutGlobalFragments() { const config = require('../../project-fixtures/transformation-prj/webpack.config.js'); const compiler = webpack({ ...config, mode: 'production' }); const stats = await new Promise((res, rej) => { @@ -17,6 +17,29 @@ async function run() { const distFilePath = path.resolve(stats.toJson().outputPath, 'main.js'); const result = execSync(`node ${distFilePath}`); assert.equal(typeof print(JSON.parse(result.toString())), 'string'); + assert(print(JSON.parse(result.toString())).indexOf('MyQuery') !== -1); + assert(print(JSON.parse(result.toString())).indexOf('fragment FragmentLeaf') !== -1); +} + +async function specWithGlobalFragments() { + const config = require('../../project-fixtures/transformation-global-fag-prj/webpack.config.js'); + const compiler = webpack({ ...config, mode: 'production' }); + const stats = await new Promise((res, rej) => { + compiler.run((err, stats) => { + if (err) return rej(err); + return res(stats); + }); + }); + assert(!stats.hasErrors()); + const distFilePath = path.resolve(stats.toJson().outputPath, 'main.js'); + const result = execSync(`node ${distFilePath}`); + assert(print(JSON.parse(result.toString())).indexOf('MyQuery') !== -1); + assert(print(JSON.parse(result.toString())).indexOf('fragment FragmentLeaf') !== -1); +} + +async function run() { + await specWithoutGlobalFragments(); + await specWithGlobalFragments(); } module.exports = run; diff --git a/project-fixtures/transformation-global-fag-prj/.gitignore b/project-fixtures/transformation-global-fag-prj/.gitignore new file mode 100644 index 000000000..849ddff3b --- /dev/null +++ b/project-fixtures/transformation-global-fag-prj/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/project-fixtures/transformation-global-fag-prj/fragment-leaf.ts b/project-fixtures/transformation-global-fag-prj/fragment-leaf.ts new file mode 100644 index 000000000..75d620c15 --- /dev/null +++ b/project-fixtures/transformation-global-fag-prj/fragment-leaf.ts @@ -0,0 +1,7 @@ +import gql from './tag'; + +const fragmentLeaf = gql` + fragment FragmentLeaf on Query { + hello + } +`; diff --git a/project-fixtures/transformation-global-fag-prj/fragment-node.ts b/project-fixtures/transformation-global-fag-prj/fragment-node.ts new file mode 100644 index 000000000..8e347ecec --- /dev/null +++ b/project-fixtures/transformation-global-fag-prj/fragment-node.ts @@ -0,0 +1,8 @@ +import gql from './tag'; + +const fragmentNode = gql` + fragment FragmentNode on Query { + bye + ...FragmentLeaf + } +`; diff --git a/project-fixtures/transformation-global-fag-prj/query.ts b/project-fixtures/transformation-global-fag-prj/query.ts new file mode 100644 index 000000000..8744c954a --- /dev/null +++ b/project-fixtures/transformation-global-fag-prj/query.ts @@ -0,0 +1,11 @@ +import gql from './tag'; +import { fragmentNode } from './fragment-node'; + +const query = gql` + query MyQuery { + __typename + ...FragmentNode + } +`; + +console.log(JSON.stringify(query, null, 2)); diff --git a/project-fixtures/transformation-global-fag-prj/schema.graphql b/project-fixtures/transformation-global-fag-prj/schema.graphql new file mode 100644 index 000000000..9f96760be --- /dev/null +++ b/project-fixtures/transformation-global-fag-prj/schema.graphql @@ -0,0 +1,4 @@ +type Query { + hello: String! + bye: String! +} diff --git a/project-fixtures/transformation-global-fag-prj/tag.ts b/project-fixtures/transformation-global-fag-prj/tag.ts new file mode 100644 index 000000000..8317cd610 --- /dev/null +++ b/project-fixtures/transformation-global-fag-prj/tag.ts @@ -0,0 +1,4 @@ +export default function gql(literals: TemplateStringsArray, ...args: unknown[]) { + // dummy impl + return ''; +} diff --git a/project-fixtures/transformation-global-fag-prj/tsconfig.json b/project-fixtures/transformation-global-fag-prj/tsconfig.json new file mode 100644 index 000000000..16c226bd0 --- /dev/null +++ b/project-fixtures/transformation-global-fag-prj/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es2015", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "plugins": [ + { + "name": "ts-graphql-plugin", + "tag": "gql", + "schema": "schema.graphql", + "enabledGlobalFragments": true + } + ] + } +} diff --git a/project-fixtures/transformation-global-fag-prj/webpack.config.js b/project-fixtures/transformation-global-fag-prj/webpack.config.js new file mode 100644 index 000000000..454bdefe5 --- /dev/null +++ b/project-fixtures/transformation-global-fag-prj/webpack.config.js @@ -0,0 +1,35 @@ +const path = require('path'); + +const TsGraphQLPlugin = require('../../webpack'); + +const tsgqlPlugin = new TsGraphQLPlugin({ tsconfigPath: __dirname }); + +module.exports = { + resolve: { + extensions: ['.ts', '.js'], + }, + entry: { + main: path.resolve(__dirname, 'query.ts'), + }, + output: { + path: path.resolve(__dirname, 'dist'), + filename: '[name].js', + }, + module: { + rules: [ + { + test: /\.ts$/, + exclude: /node_modules/, + loader: 'ts-loader', + options: { + transpileOnly: true, + getCustomTransformers: () => ({ + before: [tsgqlPlugin.getTransformer()], + }), + }, + }, + ], + }, + plugins: [tsgqlPlugin], + devtool: false, +}; diff --git a/src/transformer/transformer-host.ts b/src/transformer/transformer-host.ts index 6b957854f..c93635d22 100644 --- a/src/transformer/transformer-host.ts +++ b/src/transformer/transformer-host.ts @@ -1,11 +1,14 @@ import ts from 'typescript'; -import { DocumentNode } from 'graphql'; +import { Kind, type DocumentNode, FragmentDefinitionNode } from 'graphql'; +import { getFragmentDependenciesForAST } from 'graphql-language-service'; import { Analyzer, AnalyzerFactory, ExtractFileResult } from '../analyzer'; import { getTransformer, DocumentTransformer } from './transformer'; import { parseTagConfig } from '../ts-ast-util'; +import { cloneFragmentMap, getFragmentNamesInDocument } from '../gql-ast-util'; class DocumentNodeRegistory { protected readonly _map = new Map>(); + private _externalFragmentMap = new Map(); constructor() {} @@ -16,10 +19,20 @@ class DocumentNodeRegistory { getDocumentNode(templateNode: ts.TemplateExpression | ts.NoSubstitutionTemplateLiteral) { const positionMap = this._map.get(templateNode.getSourceFile().fileName); if (!positionMap) return; - return positionMap.get(templateNode.getStart()); + const docNode = positionMap.get(templateNode.getStart()); + if (!docNode) return; + const externalFragments = getFragmentDependenciesForAST( + docNode, + cloneFragmentMap(this._externalFragmentMap, getFragmentNamesInDocument(docNode)), + ); + return { + kind: Kind.DOCUMENT, + definitions: [...docNode.definitions, ...externalFragments], + } satisfies DocumentNode; } - update(extractedResults: ExtractFileResult[]) { + update(extractedResults: ExtractFileResult[], externalFragmentMap: Map) { + this._externalFragmentMap = externalFragmentMap; extractedResults.forEach(result => { if (!result.documentNode) return; let positionMap = this._map.get(result.fileName); @@ -55,8 +68,8 @@ export class TransformerHost { } loadProject() { - const [, { fileEntries }] = this._analyzer.extract(); - this._documentNodeRegistory.update(fileEntries); + const [, { fileEntries, globalFragments }] = this._analyzer.extract(); + this._documentNodeRegistory.update(fileEntries, globalFragments.definitionMap); } updateFiles(fileNameList: string[]) { @@ -71,10 +84,10 @@ export class TransformerHost { // other-opened-file.ts: declare `query { ...X }` importing fragment from changed-file.ts // // In the above case, the transformed output of other-opened-file.ts should have GraphQL docuemnt corresponding to `fragment X on Query { fieldA } query { ...X }` - const [, { fileEntries }] = this._analyzer.extract([ + const [, { fileEntries, globalFragments }] = this._analyzer.extract([ ...new Set([...fileNameList, ...this._documentNodeRegistory.getFiles()]), ]); - this._documentNodeRegistory.update(fileEntries); + this._documentNodeRegistory.update(fileEntries, globalFragments.definitionMap); } getTransformer({