Skip to content

Commit

Permalink
Optionally output eager es modules (#2781)
Browse files Browse the repository at this point in the history
Summary:
In this PR I added `eagerESModules` option to `relay-runtime` and `babel-plugin-relay`. I have two problem which can be solved with this change

1. Rollup handles only es modules. To handle commonjs there is a plugin
which converts it into esm. Though it always skips module if
import/export statements are found. As a result I get runtime error:
"require is not defined".

To workaround this I wrote custom plugin. Though it would be cool to
have proper solution out of the box.

```js
{
  name: 'relay-generated',
  transform(code) {
    // convert __generated__ requires into imports
    // remove after relay-compiler will be able to emit esm
    if (code.includes('__generated__')) {
      let i = -1;
      const paths = [];
      const processed = code.replace(
        /require\((.+__generated__.+)\)/g,
        (req, pathString) => {
          i += 1;
          paths.push(pathString);
          return `__generated__${i}`;
        },
      );
      const imports = paths.map(
        (p, i) => `import __generated__${i} from ${p};\n`,
      );
      return {
        code: imports.join('') + processed,
      };
    }
  },
},
```

2. Another problem is flow bug (facebook/flow#7444) which treats all not existent types as any.
This leads to many type unsafe places.

```js
import type {NotExistentType} from './__generated__/MyQuery.graphql';

type Props = {|
  my: NotExistentType
|}
```
Pull Request resolved: #2781

Reviewed By: jstejada

Differential Revision: D17385417

Pulled By: tyao1

fbshipit-source-id: dfa031412666b8afcac4cbae4678f64abbbe5ec9
  • Loading branch information
TrySound authored and facebook-github-bot committed Feb 10, 2020
1 parent 8c51286 commit db7a661
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 36 deletions.
4 changes: 4 additions & 0 deletions packages/babel-plugin-relay/BabelPluginRelay.js
Expand Up @@ -27,6 +27,10 @@ export type RelayPluginOptions = {
haste?: boolean,
// Check this global variable before validation.
isDevVariable?: string,

// enable generating eager es modules for modern runtime
eagerESModules?: boolean,

// Directory as specified by artifactDirectory when running relay-compiler
artifactDirectory?: string,
...
Expand Down
51 changes: 51 additions & 0 deletions packages/babel-plugin-relay/__tests__/BabelPluginRelay-esm-test.js
@@ -0,0 +1,51 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @emails oncall+relay
*/

'use strict';

const transformerWithOptions = require('./transformerWithOptions');

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

it('tests the hash when `isDevVariable` is set', () => {
expect(
transformerWithOptions({eagerESModules: true, isDevVariable: 'IS_DEV'})(
'graphql`fragment TestFrag on Node { id }`',
),
).toMatchSnapshot();
});

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

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

exports[`\`development\` option does not test the hash when \`development\` is not set 1`] = `
"import _TestFrag from './__generated__/TestFrag.graphql';
_TestFrag;
"
`;

exports[`\`development\` option tests the hash when \`development\` is set 1`] = `
"import _TestFrag from './__generated__/TestFrag.graphql';
_TestFrag.hash &&
_TestFrag.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.\\",
),
_TestFrag;
"
`;

exports[`\`development\` option tests the hash when \`isDevVariable\` is set 1`] = `
"import _TestFrag from './__generated__/TestFrag.graphql';
IS_DEV
? (_TestFrag.hash &&
_TestFrag.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.\\",
),
_TestFrag)
: _TestFrag;
"
`;

exports[`\`development\` option uses a custom build command in message 1`] = `
"import _TestFrag from './__generated__/TestFrag.graphql';
_TestFrag.hash &&
_TestFrag.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.\\",
),
_TestFrag;
"
`;
90 changes: 62 additions & 28 deletions packages/babel-plugin-relay/compileGraphQLTag.js
Expand Up @@ -57,6 +57,7 @@ function compileGraphQLTag(
);
}

const eagerESModules = Boolean(state.opts && state.opts.eagerESModules);
const isHasteMode = Boolean(state.opts && state.opts.haste);
const isDevVariable = state.opts && state.opts.isDevVariable;
const artifactDirectory = state.opts && state.opts.artifactDirectory;
Expand All @@ -68,6 +69,7 @@ function compileGraphQLTag(

return createNode(t, state, path, definition, {
artifactDirectory,
eagerESModules,
buildCommand,
isDevelopment,
isHasteMode,
Expand All @@ -89,6 +91,8 @@ function createNode(
options: {|
// If an output directory is specified when running relay-compiler this should point to that directory
artifactDirectory: ?string,
// Generate eager es modules instead of lazy require
eagerESModules: boolean,
// The command to run to compile Relay files, used for error messages.
buildCommand: string,
// Generate extra validation, defaults to true.
Expand Down Expand Up @@ -120,10 +124,8 @@ function createNode(
topScope = topScope.parent;
}

const requireGraphQLModule = t.CallExpression(t.Identifier('require'), [
t.StringLiteral(requiredPath),
]);
const id = topScope.generateDeclaredUidIdentifier(definitionName);
const id = topScope.generateUidIdentifier(definitionName);

const expHash = t.MemberExpression(id, t.Identifier('hash'));
const expWarn = warnNeedsRebuild(t, definitionName, options.buildCommand);
const expWarnIfOutdated = t.LogicalExpression(
Expand All @@ -136,34 +138,66 @@ function createNode(
),
);

const expAssignProd = t.AssignmentExpression('=', id, requireGraphQLModule);
const expAssignAndCheck = t.SequenceExpression([
expAssignProd,
expWarnIfOutdated,
id,
]);

let expAssign;
if (options.isDevVariable != null) {
expAssign = t.ConditionalExpression(
t.Identifier(options.isDevVariable),
expAssignAndCheck,
expAssignProd,
if (options.eagerESModules) {
const importDeclaration = t.ImportDeclaration(
[t.ImportDefaultSpecifier(id)],
t.StringLiteral(requiredPath),
);
} else if (options.isDevelopment) {
expAssign = expAssignAndCheck;
const program = path.findParent(parent => parent.isProgram());
program.unshiftContainer('body', importDeclaration);

const expAssignAndCheck = t.SequenceExpression([expWarnIfOutdated, id]);

let expAssign;
if (options.isDevVariable != null) {
expAssign = t.ConditionalExpression(
t.Identifier(options.isDevVariable),
expAssignAndCheck,
id,
);
} else if (options.isDevelopment) {
expAssign = expAssignAndCheck;
} else {
expAssign = id;
}

path.replaceWith(expAssign);
} else {
expAssign = expAssignProd;
}
topScope.push({id});

const expVoid0 = t.UnaryExpression('void', t.NumericLiteral(0));
path.replaceWith(
t.ConditionalExpression(
t.BinaryExpression('!==', id, expVoid0),
const requireGraphQLModule = t.CallExpression(t.Identifier('require'), [
t.StringLiteral(requiredPath),
]);

const expAssignProd = t.AssignmentExpression('=', id, requireGraphQLModule);
const expAssignAndCheck = t.SequenceExpression([
expAssignProd,
expWarnIfOutdated,
id,
expAssign,
),
);
]);

let expAssign;
if (options.isDevVariable != null) {
expAssign = t.ConditionalExpression(
t.Identifier(options.isDevVariable),
expAssignAndCheck,
expAssignProd,
);
} else if (options.isDevelopment) {
expAssign = expAssignAndCheck;
} else {
expAssign = expAssignProd;
}

const expVoid0 = t.UnaryExpression('void', t.NumericLiteral(0));
path.replaceWith(
t.ConditionalExpression(
t.BinaryExpression('!==', id, expVoid0),
id,
expAssign,
),
);
}
}

function warnNeedsRebuild(
Expand Down
12 changes: 10 additions & 2 deletions packages/relay-compiler/bin/RelayCompilerMain.js
Expand Up @@ -57,6 +57,7 @@ export type Config = {|
quiet: boolean,
persistOutput?: ?string,
noFutureProofEnums: boolean,
eagerESModules?: boolean,
language: string | PluginInitializer,
persistFunction?: ?string | ?((text: string) => Promise<string>),
artifactDirectory?: ?string,
Expand Down Expand Up @@ -114,9 +115,14 @@ type LanguagePlugin = PluginInitializer | {default: PluginInitializer, ...};
*/
function getLanguagePlugin(
language: string | PluginInitializer,
options?: {|
eagerESModules: boolean,
|},
): PluginInterface {
if (language === 'javascript') {
return RelayLanguagePluginJavaScript();
return RelayLanguagePluginJavaScript({
eagerESModules: Boolean(options && options.eagerESModules),
});
} else {
let languagePlugin: LanguagePlugin;
if (typeof language === 'string') {
Expand Down Expand Up @@ -269,7 +275,9 @@ function getCodegenRunner(config: Config): CodegenRunner {
quiet: config.quiet,
});
const schema = getSchemaSource(config.schema);
const languagePlugin = getLanguagePlugin(config.language);
const languagePlugin = getLanguagePlugin(config.language, {
eagerESModules: config.eagerESModules === true,
});
const persistQueryFunction = getPersistQueryFunction(config);
const inputExtensions = config.extensions || languagePlugin.inputExtensions;
const outputExtension = languagePlugin.outputExtension;
Expand Down
4 changes: 3 additions & 1 deletion packages/relay-compiler/index.js
Expand Up @@ -52,7 +52,6 @@ const compileArtifacts = require('./runner/compileArtifacts');
const compileRelayArtifacts = require('./codegen/compileRelayArtifacts');
const extractAST = require('./runner/extractAST');
const filterContextForNode = require('./core/filterContextForNode');
const formatGeneratedModule = require('./language/javascript/formatGeneratedModule');
const getChangedNodeNames = require('./runner/getChangedNodeNames');
const getDefinitionNodeHash = require('./util/getDefinitionNodeHash');
const getIdentifierForArgumentValue = require('./core/getIdentifierForArgumentValue');
Expand All @@ -68,6 +67,9 @@ const {
getReaderSourceDefinitionName,
getSourceDefinitionName,
} = require('./core/GraphQLDerivedFromMetadata');
const {
formatGeneratedCommonjsModule: formatGeneratedModule,
} = require('./language/javascript/formatGeneratedModule');

export type {Filesystem} from './codegen/CodegenDirectory';
export type {
Expand Down
Expand Up @@ -14,16 +14,21 @@

const RelayFlowGenerator = require('./RelayFlowGenerator');

const formatGeneratedModule = require('./formatGeneratedModule');

const {find} = require('./FindGraphQLTags');
const {
formatGeneratedCommonjsModule,
formatGeneratedESModule,
} = require('./formatGeneratedModule');

import type {PluginInterface} from '../RelayLanguagePluginInterface';

module.exports = (): PluginInterface => ({
module.exports = (options?: {|eagerESModules: boolean|}): PluginInterface => ({
inputExtensions: ['js', 'jsx'],
outputExtension: 'js',
typeGenerator: RelayFlowGenerator,
formatModule: formatGeneratedModule,
formatModule:
options && options.eagerESModules
? formatGeneratedESModule
: formatGeneratedCommonjsModule,
findGraphQLTags: find,
});
Expand Up @@ -46,8 +46,20 @@ ${docTextComment}
const node/*: ${documentType || 'empty'}*/ = ${concreteText};
// prettier-ignore
(node/*: any*/).hash = '${sourceHash}';
`;
};

const formatGeneratedCommonjsModule: FormatModule = options => {
return `${formatGeneratedModule(options)}
module.exports = node;
`;
};

module.exports = formatGeneratedModule;
const formatGeneratedESModule: FormatModule = options => {
return `${formatGeneratedModule(options)}
export default node;
`;
};

exports.formatGeneratedCommonjsModule = formatGeneratedCommonjsModule;
exports.formatGeneratedESModule = formatGeneratedESModule;

0 comments on commit db7a661

Please sign in to comment.