Last active
August 26, 2025 17:14
-
-
Save gaving/63fa6a2f3694ad7454e292caa627290f to your computer and use it in GitHub Desktop.
A codemod that converts gql to graphql and fixes related imports
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Usage: | |
* npx jscodeshift -t transform-gql-to-graphql.js src \ | |
* --extensions=ts,tsx,js,jsx --gitignore \ | |
* --graphqlImportPath=@/gql | |
* | |
* Options: | |
* --graphqlImportPath=../gql Path to your generated helper (default ../gql) | |
* | |
* What it does: | |
* - Converts: const Q = gql`query {...}` | |
* to: const Q = graphql(`query {...}`) | |
* - Removes imports of `gql` from 'graphql-tag' or '@apollo/client' | |
* - Adds (if missing): import { graphql } from '<graphqlImportPath>' | |
* - Drops `${FragmentDoc}` interpolations (client-preset doesn’t need them) | |
*/ | |
module.exports = function transformer(file, api, options) { | |
const j = api.jscodeshift; | |
const root = j(file.source); | |
const graphqlImportPath = options.graphqlImportPath || '../gql'; | |
// Collect local identifiers used for `gql` | |
const gqlLocalNames = new Set(); | |
// 1) Remove gql imports and record aliases | |
root.find(j.ImportDeclaration).forEach(path => { | |
const src = path.value.source.value; | |
if (src === 'graphql-tag' || src === '@apollo/client') { | |
const before = path.value.specifiers?.length || 0; | |
path.value.specifiers = (path.value.specifiers || []).filter(s => { | |
// named: import { gql as X } from ... | |
if (s.type === 'ImportSpecifier' && s.imported.name === 'gql') { | |
gqlLocalNames.add(s.local ? s.local.name : 'gql'); | |
return false; | |
} | |
// default: import gql from 'graphql-tag' | |
if (s.type === 'ImportDefaultSpecifier' && s.local.name === 'gql') { | |
gqlLocalNames.add('gql'); | |
return false; | |
} | |
return true; | |
}); | |
if (before && path.value.specifiers.length === 0) j(path).remove(); | |
} | |
}); | |
// If we didn’t find an explicit import, still watch for bare `gql` usage | |
if (gqlLocalNames.size === 0) gqlLocalNames.add('gql'); | |
// 2) Ensure an import for { graphql } exists | |
const hasGraphqlImport = root | |
.find(j.ImportDeclaration, { source: { value: graphqlImportPath } }) | |
.some(p => | |
(p.value.specifiers || []).some( | |
s => s.type === 'ImportSpecifier' && s.imported.name === 'graphql' | |
) | |
); | |
if (!hasGraphqlImport) { | |
const graphqlImport = j.importDeclaration( | |
[j.importSpecifier(j.identifier('graphql'))], | |
j.literal(graphqlImportPath) | |
); | |
const firstImport = root.find(j.ImportDeclaration).at(0); | |
if (firstImport.size()) { | |
firstImport.insertBefore(graphqlImport); | |
} else { | |
root.get().node.program.body.unshift(graphqlImport); | |
} | |
} | |
// 3) Replace tagged templates: gql`...` -> graphql(`...`) | |
root.find(j.TaggedTemplateExpression).forEach(path => { | |
const { tag, quasi } = path.value; | |
if (tag.type !== 'Identifier' || !gqlLocalNames.has(tag.name)) return; | |
// Reconstruct the template but DROP all ${...} interpolations | |
// (with client-preset, fragments only need to be named inside the string) | |
const cooked = quasi.quasis.map(q => q.value.cooked).join(''); | |
const newCall = j.callExpression(j.identifier('graphql'), [ | |
j.templateLiteral([j.templateElement({ raw: cooked, cooked }, true)], []), | |
]); | |
j(path).replaceWith(newCall); | |
}); | |
return root.toSource({ quote: 'single', trailingComma: true }); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment