-
-
Save kassens/3e2ef9af1e5e1128f8fba3362bb92f98 to your computer and use it in GitHub Desktop.
'use strict'; | |
// To be used with jscodeshift: https://github.com/facebook/jscodeshift | |
export default function transformer(file, api) { | |
const j = api.jscodeshift; | |
if (!file.source.includes('graphql`')) { | |
return; | |
} | |
source = j(source) | |
.find(j.TaggedTemplateExpression, { | |
tag: {type: 'Identifier', name: 'graphql'}, | |
}) | |
.filter(path => getAssignedObjectPropertyName(path) == null) | |
.filter(path => path.parentPath.node.type !== 'ExpressionStatement') | |
.forEach(path => { | |
const text = path.node.quasi.quasis[0].value.raw; | |
const fragmentNameMatch = text | |
.replace(/#.*/g, '') | |
.match(/fragment (\w+)/); | |
if (!fragmentNameMatch) { | |
return; | |
} | |
const fragmentName = fragmentNameMatch[1]; | |
const [, propName] = getFragmentNameParts(fragmentName); | |
j(path).replaceWith( | |
j.objectExpression([ | |
j.objectProperty(j.identifier(propName), path.node), | |
]), | |
); | |
}) | |
.toSource(); | |
return source; | |
} | |
function getAssignedObjectPropertyName(path) { | |
var property = path; | |
while (property) { | |
if (property.node.type === 'Property' && property.node.key.name) { | |
return property.node.key.name; | |
} | |
property = property.parentPath; | |
} | |
} | |
function getFragmentNameParts(fragmentName) { | |
const match = fragmentName.match( | |
/^([a-zA-Z][a-zA-Z0-9]*)(?:_([a-zA-Z][_a-zA-Z0-9]*))?$/, | |
); | |
if (!match) { | |
throw new Error( | |
'BabelPluginGraphQL: Fragments should be named ' + | |
'`ModuleName_fragmentName`, got `' + | |
fragmentName + | |
'`.', | |
); | |
} | |
const module = match[1]; | |
const propName = match[2] || '@nocommit'; | |
return [module, propName]; | |
} |
did the tricky, it solved most of the problems
there was another edge case
where you declare more than one fragment inside the graphql tag
BabelPluginRelay will catch this:
Error: BabelPluginRelay: Expected exactly one fragment in the graphql tag referenced by the property query.
I had the same problem, @sibelius
Thank you so much for sharing this codemod. 👍 It helped me to start with jscodeshift (never used it before) and migrate some components.
I extended it to solve a few more issues specific for the project I'm working on:
step1
: Transform graphql tag to an object, split multiple fragments, add missing_data
suffix to the fragment name:// Input: graphql` fragment Foo { } fragment Foo_bar { } ` // Output: { data: graphql` fragment Foo_data { } `, bar: graphql` fragment Foo_bar { } `, }
step2
: Add missing_data
to type imports:// Input: import type { Foo as Data } from './__generated__/Foo.graphql`; // Output: import type { Foo_data as Data } from './__generated__/Foo_data.graphql`;
step3
: Add missing_data
to fragment spreads:// Input: fragment Foo_data { ...Bar } // Output: fragment Foo_data { ...Bar_data }
step4
: Switch to@kiwicom/relay
:// Input: import { createFragmentContainer, graphql } from 'react-relay'; // Output: import { createFragmentContainer, graphql } from '@kiwicom/relay';
The last step is highly opinionated - we're switching to our @kiwicom/relay wrapper mainly for the better Flow type safety.
Here is the changed source:
// @flow
'use strict';
var { Source, parse, print } = require('graphql');
// To be used with jscodeshift: https://github.com/facebook/jscodeshift
export const parser = 'flow';
export default function transformer(file: any, api: any) {
const j = api.jscodeshift;
if (!file.source.includes('graphql')) {
return;
}
const step1 = j(file.source)
.find(j.TaggedTemplateExpression, {
tag: { type: 'Identifier', name: 'graphql' },
})
.filter(path => getAssignedObjectPropertyName(path) == null)
.filter(path => path.parentPath.node.type !== 'ExpressionStatement')
.forEach(path => {
const text = path.node.quasi.quasis[0].value.raw;
const document = parse(new Source(text));
if (document.definitions[0].kind !== 'FragmentDefinition') {
return;
}
j(path).replaceWith(
j.objectExpression(
document.definitions.map(definition => {
const originalFragmentName = definition.name.value;
const fragmentName = originalFragmentName.includes('_')
? originalFragmentName
: `${originalFragmentName}_data`;
definition.name.value = fragmentName;
const fixedDefinition = print(definition);
const propNameMatch = fragmentName.match(/([^_]+)$/);
const propName = propNameMatch[1];
return j.objectProperty(
j.identifier(propName),
j.taggedTemplateExpression(
j.identifier('graphql'),
j.templateLiteral(
[
j.templateElement(
{
cooked: fixedDefinition,
raw: fixedDefinition,
},
true,
),
],
[],
),
),
);
}),
),
);
})
.toSource();
const step2 = j(step1)
.find(j.ImportDeclaration)
.filter(path => {
let importPath = path.value.source.value;
if (!importPath.match(/\/__generated__\//)) {
// not a Relay import
return false;
}
if (importPath.match(/(Query|Mutation|Subscription).graphql$/)) {
// not a fragment
return false;
}
return true;
})
.forEach(path => {
path.value.specifiers.forEach(specifier => {
if (!specifier.imported.name.includes('_')) {
specifier.imported.name += '_data';
}
});
const fixedPath = path.value.source.value.replace(
/\/([a-zA-Z][a-zA-Z0-9]*)\.graphql$/,
"/$1_data.graphql",
);
path.value.source.value = fixedPath;
})
.toSource();
const step3 = j(step2)
.find(j.TaggedTemplateExpression, {
tag: { type: 'Identifier', name: 'graphql' },
})
.forEach(path => {
const text = path.node.quasi.quasis[0].value.raw;
const fixedText = text.replace(
/(\.\.\.[a-zA-Z][a-zA-Z0-9]*)(\s)/gm,
'$1_data$2',
);
path.node.quasi.quasis[0].value.raw = fixedText;
})
.toSource();
const step4 = j(step3)
.find(j.ImportDeclaration)
.filter(path => {
return path.value.source.value === 'react-relay';
})
.forEach(path => {
path.value.source.value = '@kiwicom/relay';
})
.toSource();
return step4;
}
function getAssignedObjectPropertyName(path) {
let property = path;
while (property) {
if (property.node.type === 'Property' && property.node.key.name) {
return property.node.key.name;
}
property = property.parentPath;
}
}
Thank you a lot but The codemod is not useful when we have multiple fragments inside one graphql tag.
I have modified it to support this situation:
// To be used with jscodeshift: https://github.com/facebook/jscodeshift
export default function transformer(file, api) {
const j = api.jscodeshift;
if (!file.source.includes('graphql`')) {
return;
}
function getAssignedObjectPropertyName(path) {
var property = path;
while (property) {
if (property.node.type === 'Property' && property.node.key.name) {
return property.node.key.name;
}
property = property.parentPath;
}
}
function getFragmentNameParts(fragmentName) {
const match = fragmentName.match(
/^([a-zA-Z][a-zA-Z0-9]*)(?:_([a-zA-Z][_a-zA-Z0-9]*))?$/,
);
if (!match) {
throw new Error(
'BabelPluginGraphQL: Fragments should be named ' +
'`ModuleName_fragmentName`, got `' +
fragmentName +
'`.',
);
}
const module = match[1];
const propName = match[2] || "data";
return [module, propName];
}
return j(file.source)
.find(j.TaggedTemplateExpression, { tag: { type: 'Identifier', name: 'graphql' } })
.filter(path => getAssignedObjectPropertyName(path) == null)
.filter(path => path.parentPath.node.type !== 'ExpressionStatement')
.forEach(path => {
const text = path.node.quasi.quasis[0].value.raw;
if (!text.includes('fragment ')) {
return;
}
const fragmentStrings = text.replace(/#.*/g, '')
.split('fragment ')
.map(item => item.trim())
.filter(item => item !== '' && !item.startsWith('query') && !item.startsWith('mutation'))
.map(item => `fragment ${item}`);
const expressions = fragmentStrings.map((fragmentString) => {
const fragmentNameMatch = fragmentString.match(/fragment\s+(\w+)/);
if (!fragmentNameMatch) {
return;
}
const fragmentName = fragmentNameMatch[1];
const [, propName] = getFragmentNameParts(fragmentName);
const key = j.identifier(propName);
const value = j.taggedTemplateExpression(
j.identifier('graphql'),
j.templateLiteral([
j.templateElement({ cooked: fragmentString, raw: fragmentString }, true ),
], []),
);
return j.objectProperty(key, value);
}).filter(Boolean)
if (expressions.length > 0) {
j(path).replaceWith(j.objectExpression(expressions));
}
})
.toSource();
}
@sibelius Changing L7
property.node.type === 'Property'
->property.node.type === 'ObjectProperty'
fixes it for me.