@@ -7,7 +7,13 @@ import {
77} from '@graphql-codegen/plugin-helpers' ;
88import { pascalCase } from 'change-case-all' ;
99import { OperationTypeNode , visit } from 'graphql' ;
10- import { CallExpression , ImportSpecifier , Project , SyntaxKind } from 'ts-morph' ;
10+ import {
11+ CallExpression ,
12+ ImportDeclaration ,
13+ ImportSpecifier ,
14+ Project ,
15+ SyntaxKind ,
16+ } from 'ts-morph' ;
1117import type { TypedPresetConfig } from './config' ;
1218
1319export const preset : Types . OutputPreset < TypedPresetConfig > = {
@@ -18,22 +24,37 @@ export const preset: Types.OutputPreset<TypedPresetConfig> = {
1824 presetConfig,
1925 profiler = createNoopProfiler ( ) ,
2026 } ) => {
21- // lo
2227 const absoluteTsConfigFilePath = path . join (
2328 cwd ( ) ,
2429 presetConfig . tsConfigFilePath
2530 ) ;
2631 if ( ! fs . existsSync ( absoluteTsConfigFilePath ) ) {
2732 throw new Error ( 'Requires tsconfig' ) ;
2833 }
34+ const gqlTagPath = path . posix . join (
35+ baseOutputDir ,
36+ presetConfig . artifactDirectory
37+ ) ; // FIXME: handle absolute path
38+
2939 const tsProject = new Project ( {
3040 tsConfigFilePath : absoluteTsConfigFilePath ,
3141 } ) ;
3242 const tsSourceFiles = tsProject . getSourceFiles ( ) ;
3343
3444 const hooksToReplace : Record <
3545 string ,
36- { name : string ; importFrom : path . ParsedPath }
46+ {
47+ hookName : string ;
48+ hookType :
49+ | 'useQuery'
50+ | 'useLazyQuery'
51+ | 'useSuspenseQuery'
52+ | 'useMutation'
53+ | 'useSubscription' ;
54+ importFrom : path . ParsedPath ;
55+ operationName : string ;
56+ documentSDL : string ;
57+ }
3758 > = { } ;
3859
3960 documents . forEach ( ( documentFile ) => {
@@ -47,74 +68,77 @@ export const preset: Types.OutputPreset<TypedPresetConfig> = {
4768 visit ( documentFile . document , {
4869 OperationDefinition ( node ) {
4970 if ( ! node . name ) {
50- throw new Error ( 'Anonimous operation!' ) ; // FIXME: should not throw
71+ throw new Error ( 'Anonymous operation!' ) ; // FIXME: should not throw
5172 }
5273
74+ const operationName = pascalCase ( node . name . value ) ;
75+
5376 if ( node . operation === OperationTypeNode . QUERY ) {
54- const queryHookName = `use${ pascalCase ( node . name . value ) } Query` ;
77+ // Query
78+ const queryHookName = `use${ operationName } Query` ;
5579 hooksToReplace [ queryHookName ] = {
56- name : queryHookName ,
80+ hookName : queryHookName ,
81+ hookType : 'useQuery' ,
5782 importFrom : documentPath ,
83+ operationName,
84+ documentSDL,
5885 } ;
5986
6087 const lazyHookName = `use${ pascalCase ( node . name . value ) } LazyQuery` ;
6188 hooksToReplace [ lazyHookName ] = {
62- name : lazyHookName ,
89+ hookName : lazyHookName ,
90+ hookType : 'useLazyQuery' ,
6391 importFrom : documentPath ,
92+ operationName,
93+ documentSDL,
6494 } ;
6595
6696 const suspenseHookName = `use${ pascalCase (
6797 node . name . value
6898 ) } SuspenseQuery`;
6999 hooksToReplace [ suspenseHookName ] = {
70- name : suspenseHookName ,
100+ hookName : suspenseHookName ,
101+ hookType : 'useSuspenseQuery' ,
71102 importFrom : documentPath ,
103+ operationName,
104+ documentSDL,
72105 } ;
73106 } else {
107+ // Mutation & Subscription
74108 const hookName = `use${ pascalCase ( node . name . value ) } ${ pascalCase (
75109 node . operation
76110 ) } `;
77111 hooksToReplace [ hookName ] = {
78- name : hookName ,
112+ hookName : hookName ,
113+ hookType :
114+ node . operation === OperationTypeNode . MUTATION
115+ ? 'useMutation'
116+ : 'useSubscription' ,
79117 importFrom : documentPath ,
118+ operationName,
119+ documentSDL,
80120 } ;
81121 }
82122 } ,
83123 } ) ;
84-
85- const gqlTagPath = path . posix . join (
86- baseOutputDir ,
87- presetConfig . artifactDirectory
88- ) ; // FIXME: handle absolute path
89-
90- const newFilename = path . join (
91- documentPath . dir ,
92- `${ documentPath . name } .graphql.ts`
93- ) ;
94- const gqlTagImportPath = path . posix . relative (
95- documentPath . dir ,
96- gqlTagPath
97- ) ;
98124 } ) ;
99125
100126 tsSourceFiles . forEach ( ( tsSourceFile ) => {
101- console . log (
102- '***filepath: ' ,
103- path . basename ( tsSourceFile . getFilePath ( ) ) ,
104- 'START:'
105- ) ;
106127 // find imports
107128 const fileMetadata : {
108129 hasNodeToReplace : boolean ;
130+ needsToImport : string [ ] ;
109131 functionsToReplace : Record <
110132 string ,
111133 {
134+ importDeclarationNode : ImportDeclaration ;
112135 importSpecifierNode : ImportSpecifier ;
113136 callExpression : CallExpression | null ;
114137 }
115138 > ;
116139 } = {
117140 hasNodeToReplace : false ,
141+ needsToImport : [ ] ,
118142 functionsToReplace : { } ,
119143 } ;
120144
@@ -127,6 +151,7 @@ export const preset: Types.OutputPreset<TypedPresetConfig> = {
127151 if ( hooksToReplace [ importSpecifier . getName ( ) ] ) {
128152 // FIXME: we are assuming same name means must replace. Should check import path as well?
129153 fileMetadata . functionsToReplace [ importSpecifier . getName ( ) ] = {
154+ importDeclarationNode : node ,
130155 importSpecifierNode : importSpecifier ,
131156 callExpression : null ,
132157 } ;
@@ -148,21 +173,83 @@ export const preset: Types.OutputPreset<TypedPresetConfig> = {
148173 fileMetadata . hasNodeToReplace = true ;
149174 fileMetadata . functionsToReplace [ calledFunctionName ] . callExpression =
150175 callExpression ;
176+ fileMetadata . needsToImport . push (
177+ hooksToReplace [ calledFunctionName ] . hookType
178+ ) ;
151179 }
152180 } ) ;
153181
154182 if ( ! fileMetadata . hasNodeToReplace ) {
155183 return ;
156184 }
157185
158- console . log (
159- '***filepath: ' ,
160- path . basename ( tsSourceFile . getFilePath ( ) ) ,
161- 'END\n'
186+ // Codemod files to co-locate the document nodes
187+ tsSourceFile . addImportDeclaration ( {
188+ moduleSpecifier : '@apollo/client/react' , // FIXME: option to import from different module
189+ namedImports : fileMetadata . needsToImport . map ( ( importName ) => ( {
190+ name : importName ,
191+ } ) ) ,
192+ } ) ;
193+
194+ tsSourceFile . addImportDeclaration ( {
195+ moduleSpecifier : path . posix . relative (
196+ path . dirname ( tsSourceFile . getFilePath ( ) ) , // TODO: would this break in windows?
197+ gqlTagPath
198+ ) , // TODO: add relative path to codegen gql
199+ namedImports : [ { name : presetConfig . gqlTagName } ] ,
200+ } ) ;
201+
202+ Object . values ( fileMetadata . functionsToReplace ) . forEach (
203+ ( functionToReplace ) => {
204+ if ( functionToReplace . callExpression ) {
205+ const graphqlDocument =
206+ hooksToReplace [ functionToReplace . importSpecifierNode . getName ( ) ] ;
207+ const documentNodeName = `${ graphqlDocument . operationName } Doc` ;
208+
209+ functionToReplace . callExpression
210+ . getFirstDescendantByKindOrThrow ( SyntaxKind . Identifier )
211+ . replaceWithText ( graphqlDocument . hookType ) ;
212+ functionToReplace . callExpression . insertArgument (
213+ 0 ,
214+ documentNodeName
215+ ) ;
216+
217+ // Remove import specifier of hooks, and if no specifiers left, remove the whole import declaration
218+ functionToReplace . importSpecifierNode . remove ( ) ;
219+ if (
220+ functionToReplace . importDeclarationNode . getDescendantsOfKind (
221+ SyntaxKind . ImportSpecifier
222+ ) . length === 0
223+ ) {
224+ functionToReplace . importDeclarationNode . remove ( ) ;
225+ }
226+
227+ // Add documentNode to file
228+ const lastImportIndex = tsSourceFile . getDescendantsOfKind (
229+ SyntaxKind . ImportDeclaration
230+ ) . length ;
231+ tsSourceFile . insertStatements (
232+ lastImportIndex ,
233+ `const ${ documentNodeName } = graphql(\`${ graphqlDocument . documentSDL } \`)`
234+ ) ;
235+ }
236+ }
162237 ) ;
238+
239+ console . log ( '*** after update:' ) ;
240+ console . log ( tsSourceFile . getFullText ( ) ) ;
241+
242+ console . log ( 'END\n' ) ;
163243 } ) ;
164244
165- return [ ] ;
245+ return tsSourceFiles . map ( ( tsSourceFile ) => ( {
246+ filename : tsSourceFile . getFilePath ( ) ,
247+ pluginMap : { add : addPlugin } ,
248+ plugins : [ { add : { content : tsSourceFile . getFullText ( ) } } ] ,
249+ config : { } ,
250+ schema,
251+ documents : [ ] ,
252+ } ) ) ;
166253 } ,
167254} ;
168255
0 commit comments