Skip to content

Instantly share code, notes, and snippets.

@kassens kassens/fix-graphql.js
Last active Jul 13, 2019

Embed
What would you like to do?
Updates graphql tags that are not within an object literal and not standalone expressions to be wrapped in an object similar to what the Relay babel plugin used to do before simplifying the logic there.
'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];
}
@Liinkiing

This comment has been minimized.

Copy link

commented Apr 15, 2019

If we use it like it is, we have some errors, like source is not defined and for example j(source) should be j(file.source), I think it is just a typo, but here a version with fixed errors. And thanks you for this codemod, it worked just great!

// To be used with jscodeshift: https://github.com/facebook/jscodeshift


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];
}


export default function transformer(file, api) {
  const j = api.jscodeshift;

  if (!file.source.includes('graphql`')) {
    return;
  }

  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;
      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();
}
@sibelius

This comment has been minimized.

Copy link

commented Apr 29, 2019

@Liinkiing your code is doing a wrong transform if the fragment is already an object

query: graphql``

will be:

query: {
   query: graphql 
@janicduplessis

This comment has been minimized.

Copy link

commented May 1, 2019

@sibelius Changing L7 property.node.type === 'Property' -> property.node.type === 'ObjectProperty' fixes it for me.

@sibelius

This comment has been minimized.

Copy link

commented May 1, 2019

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.

@brunorafael8

This comment has been minimized.

Copy link

commented May 6, 2019

I had the same problem, @sibelius

@VaclavSir

This comment has been minimized.

Copy link

commented May 13, 2019

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:

  1. 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 {
        }
      `,
    }
    
  2. 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`;
    
  3. step3: Add missing _data to fragment spreads:
    // Input:
    fragment Foo_data {
      ...Bar
    }
    // Output:
    fragment Foo_data {
      ...Bar_data
    }
    
  4. 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;
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.