Skip to content

Instantly share code, notes, and snippets.

@gaving
Last active August 26, 2025 17:14
Show Gist options
  • Save gaving/63fa6a2f3694ad7454e292caa627290f to your computer and use it in GitHub Desktop.
Save gaving/63fa6a2f3694ad7454e292caa627290f to your computer and use it in GitHub Desktop.
A codemod that converts gql to graphql and fixes related imports
/**
* 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