Skip to content

Commit

Permalink
check fragment hashes in babel-plugin-relay
Browse files Browse the repository at this point in the history
Summary: This validates that the product engineer has run the Relay build and is not using outdated GraphQL fragments. Optionally this can be turned off in the babel plugin settings.

Reviewed By: leebyron

Differential Revision: D6229469

fbshipit-source-id: 4615cf9db39017beb91e57d5089e88aedc79076e
  • Loading branch information
kassens authored and facebook-github-bot committed Nov 7, 2017
1 parent b45baa4 commit a628637
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 16 deletions.
11 changes: 9 additions & 2 deletions packages/babel-plugin-relay/BabelPluginRelay.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,18 @@ import type {Validator} from './RelayQLTransformer';
import typeof BabelTypes from 'babel-types';

export type RelayPluginOptions = {
schema?: string,
compat?: boolean,
// The command to run to compile Relay files, used for error messages.
buildCommand?: string,

// Use haste style global requires, defaults to false.
haste?: boolean,

// Enable compat mode compiling for modern and classic runtime.
compat?: boolean,

// Classic options
inputArgumentName?: string,
schema?: string,
snakeCase?: boolean,
substituteVariables?: boolean,
validator?: Validator<any>,
Expand Down
34 changes: 34 additions & 0 deletions packages/babel-plugin-relay/__tests__/BabelPluginRelay-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@ describe('BabelPluginRelay', () => {

function transformerWithOptions(
options: RelayPluginOptions,
environment: 'development' | 'production' = 'production',
): string => string {
return (text, filename) => {
const previousEnv = process.env.BABEL_ENV;
try {
process.env.BABEL_ENV = environment;
return babel.transform(text, {
compact: false,
filename,
Expand All @@ -42,6 +45,8 @@ describe('BabelPluginRelay', () => {
}).code;
} catch (e) {
return 'ERROR:\n\n' + e;
} finally {
process.env.BABEL_ENV = previousEnv;
}
};
}
Expand Down Expand Up @@ -87,4 +92,33 @@ describe('BabelPluginRelay', () => {
}),
);
});

describe('`development` option', () => {
it('tests the hash when `development` is set', () => {
expect(
transformerWithOptions({}, 'development')(
'graphql`fragment TestFrag on Node { id }`',
),
).toMatchSnapshot();
});

it('uses a custom build command in message', () => {
expect(
transformerWithOptions(
{
buildCommand: 'relay-build',
},
'development',
)('graphql`fragment TestFrag on Node { id }`'),
).toMatchSnapshot();
});

it('does not test the hash when `development` is not set', () => {
expect(
transformerWithOptions({}, 'production')(
'graphql`fragment TestFrag on Node { id }`',
),
).toMatchSnapshot();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`BabelPluginRelay \`development\` option does not test the hash when \`development\` is not set 1`] = `
"({
data: function () {
return require(\\"./__generated__/TestFrag.graphql\\");
}
});"
`;

exports[`BabelPluginRelay \`development\` option tests the hash when \`development\` is set 1`] = `
"({
data: function () {
const node = require(\\"./__generated__/TestFrag.graphql\\");
if (node.hash && node.hash !== \\"0bb6b7b29bc3e910921551c4ff5b6757\\") {
console.error(\\"The definition of 'TestFrag' appears to have changed. Run \`relay-compiler\` to update the generated files to receive the expected data.\\");
}
return node;
}
});"
`;

exports[`BabelPluginRelay \`development\` option uses a custom build command in message 1`] = `
"({
data: function () {
const node = require(\\"./__generated__/TestFrag.graphql\\");
if (node.hash && node.hash !== \\"0bb6b7b29bc3e910921551c4ff5b6757\\") {
console.error(\\"The definition of 'TestFrag' appears to have changed. Run \`relay-build\` to update the generated files to receive the expected data.\\");
}
return node;
}
});"
`;
13 changes: 11 additions & 2 deletions packages/babel-plugin-relay/compileGraphQLTag.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,17 @@ function compileGraphQLTag(
function createAST(t, state, path, graphqlDefinition) {
const isCompatMode = Boolean(state.opts && state.opts.compat);
const isHasteMode = Boolean(state.opts && state.opts.haste);

const modernNode = createModernNode(t, graphqlDefinition, isHasteMode);
// defaults to 'true'
const isDevelopment =
(process.env.BABEL_ENV || process.env.NODE_ENV) !== 'production';
const buildCommand =
(state.opts && state.opts.buildCommand) || 'relay-compiler';

const modernNode = createModernNode(t, graphqlDefinition, {
buildCommand,
isDevelopment,
isHasteMode,
});
if (isCompatMode) {
return createCompatNode(
t,
Expand Down
80 changes: 68 additions & 12 deletions packages/babel-plugin-relay/createModernNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@

'use strict';

const crypto = require('crypto');

const {print} = require('graphql');

const GENERATED = './__generated__/';

import typeof BabelTypes from 'babel-types';
Expand All @@ -23,24 +27,76 @@ import type {OperationDefinitionNode, FragmentDefinitionNode} from 'graphql';
function createModernNode(
t: BabelTypes,
graphqlDefinition: OperationDefinitionNode | FragmentDefinitionNode,
isHasteMode: boolean,
options: {
// The command to run to compile Relay files, used for error messages.
buildCommand: string,
// Generate extra validation, defaults to true.
isDevelopment: boolean,
// Use haste style global requires, defaults to false.
isHasteMode: boolean,
},
): Object {
const definitionName = graphqlDefinition.name;
const definitionName = graphqlDefinition.name && graphqlDefinition.name.value;
if (!definitionName) {
throw new Error('GraphQL operations and fragments must contain names');
}
const requiredFile = definitionName.value + '.graphql';
const requiredPath = isHasteMode ? requiredFile : GENERATED + requiredFile;
return t.functionExpression(
null,
[],
t.blockStatement([
t.returnStatement(
t.callExpression(t.identifier('require'), [
t.stringLiteral(requiredPath),
const requiredFile = definitionName + '.graphql';
const requiredPath = options.isHasteMode
? requiredFile
: GENERATED + requiredFile;

const hash = crypto
.createHash('md5')
.update(print(graphqlDefinition), 'utf8')
.digest('hex');

const requireGraphQLModule = t.callExpression(t.identifier('require'), [
t.stringLiteral(requiredPath),
]);

let bodyStatements;
if (options.isDevelopment) {
const nodeVariable = t.identifier('node');
const nodeDotHash = t.memberExpression(nodeVariable, t.identifier('hash'));
bodyStatements = [
t.variableDeclaration('const', [
t.variableDeclarator(nodeVariable, requireGraphQLModule),
]),
t.ifStatement(
t.logicalExpression(
'&&',
nodeDotHash,
t.binaryExpression('!==', nodeDotHash, t.stringLiteral(hash)),
),
t.blockStatement([
t.expressionStatement(
warnNeedsRebuild(t, definitionName, options.buildCommand),
),
]),
),
]),
t.returnStatement(nodeVariable),
];
} else {
bodyStatements = [t.returnStatement(requireGraphQLModule)];
}
return t.functionExpression(null, [], t.blockStatement(bodyStatements));
}

function warnNeedsRebuild(
t: BabelTypes,
definitionName: string,
buildCommand: string,
) {
return t.callExpression(
t.memberExpression(t.identifier('console'), t.identifier('error')),
[
t.stringLiteral(
`The definition of '${definitionName}' appears to have changed. Run ` +
'`' +
buildCommand +
'` to update the generated files to receive the expected data.',
),
],
);
}

Expand Down

0 comments on commit a628637

Please sign in to comment.