diff --git a/packages/babel-plugin-relay/BabelPluginRelay.js b/packages/babel-plugin-relay/BabelPluginRelay.js index 2651f71a8477c..da51b650b9243 100644 --- a/packages/babel-plugin-relay/BabelPluginRelay.js +++ b/packages/babel-plugin-relay/BabelPluginRelay.js @@ -39,6 +39,8 @@ export type RelayPluginOptions = { snakeCase?: boolean, substituteVariables?: boolean, validator?: Validator, + // Directory as specified by outputDir when running relay-compiler + artifactDirectory?: string, }; export type BabelState = { diff --git a/packages/babel-plugin-relay/__tests__/BabelPluginRelay-test.js b/packages/babel-plugin-relay/__tests__/BabelPluginRelay-test.js index e16cd5e6afe74..5afb7696036ea 100644 --- a/packages/babel-plugin-relay/__tests__/BabelPluginRelay-test.js +++ b/packages/babel-plugin-relay/__tests__/BabelPluginRelay-test.js @@ -26,14 +26,15 @@ describe('BabelPluginRelay', () => { function transformerWithOptions( options: RelayPluginOptions, environment: 'development' | 'production' = 'production', + filename?: string, ): string => string { - return (text, filename) => { + return (text, providedFileName) => { const previousEnv = process.env.BABEL_ENV; try { process.env.BABEL_ENV = environment; return babel.transform(text, { compact: false, - filename, + filename: filename || providedFileName, parserOpts: {plugins: ['jsx']}, plugins: [[BabelPluginRelay, options]], }).code; @@ -84,6 +85,17 @@ describe('BabelPluginRelay', () => { }), ); + generateTestsFromFixtures( + `${__dirname}/fixtures-modern-artifact-directory`, + transformerWithOptions( + { + artifactDirectory: '/test/artifacts', + }, + 'production', + '/testing/Container.js', + ), + ); + describe('`development` option', () => { it('tests the hash when `development` is set', () => { expect( diff --git a/packages/babel-plugin-relay/__tests__/__snapshots__/BabelPluginRelay-test.js.snap b/packages/babel-plugin-relay/__tests__/__snapshots__/BabelPluginRelay-test.js.snap index 5879d8f75dba1..4a8f45b747d7d 100644 --- a/packages/babel-plugin-relay/__tests__/__snapshots__/BabelPluginRelay-test.js.snap +++ b/packages/babel-plugin-relay/__tests__/__snapshots__/BabelPluginRelay-test.js.snap @@ -1114,6 +1114,76 @@ module.exports = RelayCompatContainer.createContainer(CompatProfilePic, { }); `; +exports[`BabelPluginRelay matches expected output: arguments.txt 5`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfilePic = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfilePic, { + passing: graphql\` + fragment CompatProfile_passing on User { + ...ProfilePic_user @arguments(size: 40, scale: 1.5, title: "Photo") + ...ProfilePic_user @arguments(size: $pictureSize) + } + \`, + receiving: graphql\` + fragment CompatProfile_receiving on User @argumentDefinitions( + first: {type: "Int", defaultValue: 5} + named: {type: "String", defaultValue: "john"} + scale: {type: "Float", defaultValue: 1.5} + noDefault: {type: "Int"} + ) { + friends(first: $first, named: $named, scale: $scale) { + count + } + } + \`, + receivingGlobals: graphql\` + fragment CompatProfile_receivingGlobals on User { + friends(first: $friendsCount) { + count + } + } + \`, +}); + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfilePic = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfilePic, { + passing: function () { + return require('../test/artifacts/CompatProfile_passing.graphql'); + }, + receiving: function () { + return require('../test/artifacts/CompatProfile_receiving.graphql'); + }, + receivingGlobals: function () { + return require('../test/artifacts/CompatProfile_receivingGlobals.graphql'); + } +}); +`; + exports[`BabelPluginRelay matches expected output: arguments-listvalue.txt 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ /** @@ -1434,6 +1504,58 @@ module.exports = RelayClassic.createFragmentContainer(CompatProfile, { }); `; +exports[`BabelPluginRelay matches expected output: arguments-listvalue.txt 5`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayClassic = require('RelayClassic'); + +const {graphql} = RelayClassic; + +const CompatProfile = () => null; + +module.exports = RelayClassic.createFragmentContainer(CompatProfile, { + viewer: graphql\` + fragment CompatProfile_viewer on Viewer @argumentDefinitions( + browserContext: {type: "MarketplaceBrowseContext", defaultValue: BROWSE_FEED} + priceRange: {type: "[Float]", defaultValue: [0, 50]} + ) { + marketplace_explore( + marketplace_browse_context: $browserContext, + with_price_between: $priceRange, + ) { + count + } + } + \` +}); + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayClassic = require('RelayClassic'); + +const { graphql } = RelayClassic; + +const CompatProfile = () => null; + +module.exports = RelayClassic.createFragmentContainer(CompatProfile, { + viewer: function () { + return require('../test/artifacts/CompatProfile_viewer.graphql'); + } +}); +`; + exports[`BabelPluginRelay matches expected output: connectionPattern.txt 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ var Relay = require('react-relay'); @@ -3583,6 +3705,66 @@ module.exports = RelayCompatContainer.createContainer(CompatProfilePic, { }); `; +exports[`BabelPluginRelay matches expected output: duplicate-variables.txt 5`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfilePic = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfilePic, { + user: graphql\` + fragment CompatProfile_user on User @argumentDefinitions( + scale: {type: "Float"} + ) { + profile_picture(scale: $scale) { + uri + } + } + \`, + user2: graphql\` + fragment CompatProfile_user2 on User @argumentDefinitions( + scale: {type: "Float"} + ) { + profile_picture(scale: $scale) { + uri + } + } + \` +}); + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfilePic = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfilePic, { + user: function () { + return require('../test/artifacts/CompatProfile_user.graphql'); + }, + user2: function () { + return require('../test/artifacts/CompatProfile_user2.graphql'); + } +}); +`; + exports[`BabelPluginRelay matches expected output: error_confusing-fragment-name.txt 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ /** @@ -3687,6 +3869,33 @@ ERROR: Error: unknown: BabelPluginGraphQL: Fragment \`CompatProfile_data\` should not end in \`_data\` to avoid conflict with a fragment named \`CompatProfile\` which also provides resulting data via the React prop \`data\`. Either rename this fragment to \`CompatProfile\` or choose a different prop name. `; +exports[`BabelPluginRelay matches expected output: error_confusing-fragment-name.txt 5`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, graphql\` + fragment CompatProfile_data on User { + name + } +\`); + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +ERROR: + +Error: /testing/Container.js: BabelPluginGraphQL: Fragment \`CompatProfile_data\` should not end in \`_data\` to avoid conflict with a fragment named \`CompatProfile\` which also provides resulting data via the React prop \`data\`. Either rename this fragment to \`CompatProfile\` or choose a different prop name. +`; + exports[`BabelPluginRelay matches expected output: error_too-many-fragments.txt 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ /** @@ -3815,6 +4024,39 @@ ERROR: Error: unknown: BabelPluginRelay: Expected exactly one fragment in the graphql tag referenced by the property user. `; +exports[`BabelPluginRelay matches expected output: error_too-many-fragments.txt 5`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, { + user: graphql\` + fragment CompatProfile_user on User { + name + } + + fragment CompatProfile_viewer on User { + name + } + \`, +}); + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +ERROR: + +Error: /testing/Container.js: BabelPluginRelay: Expected exactly one fragment in the graphql tag referenced by the property user. +`; + exports[`BabelPluginRelay matches expected output: error_unexpected-fragment.txt 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ /** @@ -3955,6 +4197,42 @@ ERROR: Error: unknown: BabelPluginRelay: Expected exactly one operation (query, mutation, or subscription) per graphql tag. `; +exports[`BabelPluginRelay matches expected output: error_unexpected-fragment.txt 5`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule Compat + */ + +'use strict'; + +const graphql = require('graphql'); +const CompatProfilePic = require('CompatProfilePic'); + +const CompatCommentCreateMutation = graphql\` + mutation CompatCommentCreateMutation($input: CommentCreateInput!) { + commentCreate(input: $input) { + viewer { + actor { + id + ...CompatProfilePic_user + } + } + } + } + + fragment Whoopsie_key on User { + name + } +\`; + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +ERROR: + +Error: /testing/Container.js: BabelPluginRelay: Expected exactly one operation (query, mutation, or subscription) per graphql tag. +`; + exports[`BabelPluginRelay matches expected output: error_unexpected-operation.txt 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ /** @@ -4075,6 +4353,37 @@ ERROR: Error: unknown: BabelPluginRelay: Expected only fragments within this graphql tag. `; +exports[`BabelPluginRelay matches expected output: error_unexpected-operation.txt 5`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, graphql\` + fragment CompatProfile_user on User { + name + } + + query Whoopsie { + name + } +\`); + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +ERROR: + +Error: /testing/Container.js: BabelPluginRelay: Expected only fragments within this graphql tag. +`; + exports[`BabelPluginRelay matches expected output: export-refetch-container.txt 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ /** @@ -4481,6 +4790,66 @@ module.exports = createRefetchContainer(RefetchExample, { }); `; +exports[`BabelPluginRelay matches expected output: export-refetch-container.txt 5`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule RefetchExample + */ + +const React = require('React'); +const {createRefetchContainer, graphql} = require('RelayClassic'); + +class RefetchExample extends React.Component { + render() { + return
{this.props.user.name}
; + } +} + +module.exports = createRefetchContainer( + RefetchExample, + graphql\` + fragment RefetchExample_user on User { + name + } + \`, + graphql\` + query RefetchExampleRefetchQuery { + viewer { + actor { + ...RefetchExample_user + } + } + } + \`, +); + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule RefetchExample + */ + +const React = require('React'); +const { createRefetchContainer, graphql } = require('RelayClassic'); + +class RefetchExample extends React.Component { + render() { + return
{this.props.user.name}
; + } +} + +module.exports = createRefetchContainer(RefetchExample, { + user: function () { + return require('../test/artifacts/RefetchExample_user.graphql'); + } +}, function () { + return require('../test/artifacts/RefetchExampleRefetchQuery.graphql'); +}); +`; + exports[`BabelPluginRelay matches expected output: fieldForEnum.txt 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ var Relay = require('react-relay'); @@ -5742,6 +6111,53 @@ module.exports = RelayCompatContainer.createContainer(CompatProfile, { }); `; +exports[`BabelPluginRelay matches expected output: fragment-spread.txt 5`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const CompatProfilePic = require('CompatProfilePic'); +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, { + user: graphql\` + fragment CompatProfile_user on User { + name + ...CompatProfilePic_user + } + \`, +}); + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const CompatProfilePic = require('CompatProfilePic'); +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, { + user: function () { + return require('../test/artifacts/CompatProfile_user.graphql'); + } +}); +`; + exports[`BabelPluginRelay matches expected output: fragmentDirectives.txt 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ var Relay = require('react-relay'); @@ -6561,6 +6977,62 @@ function SomeTopLevelView() { } `; +exports[`BabelPluginRelay matches expected output: memoize-inner-scope.txt 5`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule SomeTopLevelView + */ + +'use strict'; + +const ProfilePic = require('ProfilePic'); + +function SomeTopLevelView() { + let _graphql = 'unrelated'; + + return ( + + + + ); +} + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule SomeTopLevelView + */ + +'use strict'; + +var _graphql2; + +const ProfilePic = require('ProfilePic'); + +function SomeTopLevelView() { + let _graphql = 'unrelated'; + + return + + ; +} +`; + exports[`BabelPluginRelay matches expected output: metadataConnection.txt 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ var Relay = require('react-relay'); @@ -7527,6 +7999,49 @@ const CompatCommentCreateMutation = { }; `; +exports[`BabelPluginRelay matches expected output: module-operation.txt 5`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatCommentCreateMutation + */ + +'use strict'; + +const graphql = require('graphql'); +const CompatProfilePic = require('CompatProfilePic'); + +const CompatCommentCreateMutation = graphql\` + mutation CompatCommentCreateMutation($input: CommentCreateInput!) { + commentCreate(input: $input) { + viewer { + actor { + id + ...CompatProfilePic_user + } + } + } + } +\`; + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatCommentCreateMutation + */ + +'use strict'; + +const graphql = require('graphql'); +const CompatProfilePic = require('CompatProfilePic'); + +const CompatCommentCreateMutation = function () { + return require('../test/artifacts/CompatCommentCreateMutation.graphql'); +}; +`; + exports[`BabelPluginRelay matches expected output: multiple-root-fields.txt 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ /** @@ -7935,6 +8450,53 @@ const CompatViewerQuery = { }; `; +exports[`BabelPluginRelay matches expected output: multiple-root-fields.txt 5`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule Compat + */ + +'use strict'; + +const graphql = require('graphql'); +const CompatProfilePic = require('CompatProfilePic'); + +const CompatViewerQuery = graphql\` + query CompatViewerQuery($id: ID!, $scale: Float = 1.5) { + viewer { + actor { + id + ...CompatProfilePic_user + } + } + user: node(id: $id) { + ... on User { + id + ...CompatProfilePic_user + } + } + } +\`; + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule Compat + */ + +'use strict'; + +const graphql = require('graphql'); +const CompatProfilePic = require('CompatProfilePic'); + +const CompatViewerQuery = function () { + return require('../test/artifacts/CompatViewerQuery.graphql'); +}; +`; + exports[`BabelPluginRelay matches expected output: mutation.txt 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ /** @@ -8323,6 +8885,49 @@ var x = function () { }(); `; +exports[`BabelPluginRelay matches expected output: mutation.txt 6`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule Compat + */ + +'use strict'; + +const graphql = require('graphql'); +const CompatProfilePic = require('CompatProfilePic'); + +const CompatCommentCreateMutation = graphql\` + mutation CompatCommentCreateMutation($input: CommentCreateInput!) { + commentCreate(input: $input) { + viewer { + actor { + id + ...CompatProfilePic_user + } + } + } + } +\`; + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule Compat + */ + +'use strict'; + +const graphql = require('graphql'); +const CompatProfilePic = require('CompatProfilePic'); + +const CompatCommentCreateMutation = function () { + return require('../test/artifacts/CompatCommentCreateMutation.graphql'); +}; +`; + exports[`BabelPluginRelay matches expected output: mutationBadSchemaMissingArgs.txt 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ var Relay = require('react-relay'); @@ -8687,6 +9292,50 @@ module.exports = RelayCompatContainer.createContainer(CompatProfile, { }); `; +exports[`BabelPluginRelay matches expected output: no-fragment-spread.txt 5`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, { + user: graphql\` + fragment CompatProfile_user on User { + name + } + \`, +}); + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, { + user: function () { + return require('../test/artifacts/CompatProfile_user.graphql'); + } +}); +`; + exports[`BabelPluginRelay matches expected output: no-object.txt 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ /** @@ -8909,6 +9558,48 @@ module.exports = RelayCompatContainer.createContainer(CompatProfile, { }); `; +exports[`BabelPluginRelay matches expected output: no-object.txt 5`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, graphql\` + fragment CompatProfile_user on User { + name + } +\`); + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, { + user: function () { + return require('../test/artifacts/CompatProfile_user.graphql'); + } +}); +`; + exports[`BabelPluginRelay matches expected output: no-object-many-fragments.txt 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ /** @@ -9221,6 +9912,55 @@ module.exports = RelayCompatContainer.createContainer(CompatProfile, { }); `; +exports[`BabelPluginRelay matches expected output: no-object-many-fragments.txt 5`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, graphql\` + fragment CompatProfile_user on User { + name + } + + fragment CompatProfile_viewer on User { + name + } +\`); + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, { + user: function () { + return require('../test/artifacts/CompatProfile_user.graphql'); + }, + viewer: function () { + return require('../test/artifacts/CompatProfile_viewer.graphql'); + } +}); +`; + exports[`BabelPluginRelay matches expected output: nonExistentMutation.txt 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ var Relay = require('react-relay'); @@ -9676,6 +10416,47 @@ const CompatViewerQuery = { }; `; +exports[`BabelPluginRelay matches expected output: query.txt 5`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule Compat + */ + +'use strict'; + +const graphql = require('graphql'); +const CompatProfilePic = require('CompatProfilePic'); + +const CompatViewerQuery = graphql\` + query CompatViewerQuery($id: ID!, $scale: Float = 1.5) { + node(id: $id) { + ... on User { + id + ...CompatProfilePic_user + } + } + } +\`; + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule Compat + */ + +'use strict'; + +const graphql = require('graphql'); +const CompatProfilePic = require('CompatProfilePic'); + +const CompatViewerQuery = function () { + return require('../test/artifacts/CompatViewerQuery.graphql'); +}; +`; + exports[`BabelPluginRelay matches expected output: queryWithArrayObjectArg.txt 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ const RelayClassic = require('react-relay/classic'); @@ -10762,6 +11543,49 @@ module.exports = RelayCompatContainer.createContainer(CompatProfile, { }); `; +exports[`BabelPluginRelay matches expected output: simple-named-fragment.txt 5`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, graphql\` + fragment CompatProfile on User { + name + ...SomeOtherContainer + } +\`); + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, { + data: function () { + return require('../test/artifacts/CompatProfile.graphql'); + } +}); +`; + exports[`BabelPluginRelay matches expected output: simple-named-with-many-fragments.txt 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ /** @@ -11074,6 +11898,55 @@ module.exports = RelayCompatContainer.createContainer(CompatProfile, { }); `; +exports[`BabelPluginRelay matches expected output: simple-named-with-many-fragments.txt 5`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, graphql\` + fragment CompatProfile on User { + name + } + + fragment CompatProfile_viewer on User { + name + } +\`); + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, { + data: function () { + return require('../test/artifacts/CompatProfile.graphql'); + }, + viewer: function () { + return require('../test/artifacts/CompatProfile_viewer.graphql'); + } +}); +`; + exports[`BabelPluginRelay matches expected output: subscription.txt 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ var Relay = require('react-relay'); @@ -12406,3 +13279,89 @@ module.exports = createFragmentContainer(CompatProfile, { } }); `; + +exports[`BabelPluginRelay matches expected output: within-class-reference.txt 5`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatStory + */ + +'use strict'; + +const {createFragmentContainer, graphql} = require('RelayClassic'); +const React = require('React'); +const CompatProfilePic = require('CompatProfilePic'); + +class CompatProfile extends React.Component { + render() { + return
+ + {this.props.data.name} + {this.props.data.subscribeStatus} +
; + } + + doSomething() { + commitMutation( + this.props.relay, + graphql\` + mutation ActorSubscribe($input: ActorSubscribeInput!) { + actorSubscribe(input: $input) { + subscribee { + ...CompatProfile + } + } + } + \`, + { input: { subscribeeId: 123 } } + ) + } +} + +module.exports = createFragmentContainer(CompatProfile, graphql\` + fragment CompatProfile on Actor { + name + subscribeStatus + ...CompatProfilePic_user + } +\`); + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatStory + */ + +'use strict'; + +var _graphql; + +const { createFragmentContainer, graphql } = require('RelayClassic'); +const React = require('React'); +const CompatProfilePic = require('CompatProfilePic'); + +class CompatProfile extends React.Component { + render() { + return
+ + {this.props.data.name} + {this.props.data.subscribeStatus} +
; + } + + doSomething() { + commitMutation(this.props.relay, _graphql || (_graphql = function () { + return require('../test/artifacts/ActorSubscribe.graphql'); + }), { input: { subscribeeId: 123 } }); + } +} + +module.exports = createFragmentContainer(CompatProfile, { + data: function () { + return require('../test/artifacts/CompatProfile.graphql'); + } +}); +`; diff --git a/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/arguments-listvalue.txt b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/arguments-listvalue.txt new file mode 100644 index 0000000000000..e0c689b46ce61 --- /dev/null +++ b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/arguments-listvalue.txt @@ -0,0 +1,28 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayClassic = require('RelayClassic'); + +const {graphql} = RelayClassic; + +const CompatProfile = () => null; + +module.exports = RelayClassic.createFragmentContainer(CompatProfile, { + viewer: graphql` + fragment CompatProfile_viewer on Viewer @argumentDefinitions( + browserContext: {type: "MarketplaceBrowseContext", defaultValue: BROWSE_FEED} + priceRange: {type: "[Float]", defaultValue: [0, 50]} + ) { + marketplace_explore( + marketplace_browse_context: $browserContext, + with_price_between: $priceRange, + ) { + count + } + } + ` +}); diff --git a/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/arguments.txt b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/arguments.txt new file mode 100644 index 0000000000000..d60ae2fdda771 --- /dev/null +++ b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/arguments.txt @@ -0,0 +1,40 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfilePic = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfilePic, { + passing: graphql` + fragment CompatProfile_passing on User { + ...ProfilePic_user @arguments(size: 40, scale: 1.5, title: "Photo") + ...ProfilePic_user @arguments(size: $pictureSize) + } + `, + receiving: graphql` + fragment CompatProfile_receiving on User @argumentDefinitions( + first: {type: "Int", defaultValue: 5} + named: {type: "String", defaultValue: "john"} + scale: {type: "Float", defaultValue: 1.5} + noDefault: {type: "Int"} + ) { + friends(first: $first, named: $named, scale: $scale) { + count + } + } + `, + receivingGlobals: graphql` + fragment CompatProfile_receivingGlobals on User { + friends(first: $friendsCount) { + count + } + } + `, +}); diff --git a/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/duplicate-variables.txt b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/duplicate-variables.txt new file mode 100644 index 0000000000000..a638046aeeaf5 --- /dev/null +++ b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/duplicate-variables.txt @@ -0,0 +1,33 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfilePic = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfilePic, { + user: graphql` + fragment CompatProfile_user on User @argumentDefinitions( + scale: {type: "Float"} + ) { + profile_picture(scale: $scale) { + uri + } + } + `, + user2: graphql` + fragment CompatProfile_user2 on User @argumentDefinitions( + scale: {type: "Float"} + ) { + profile_picture(scale: $scale) { + uri + } + } + ` +}); diff --git a/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/error_confusing-fragment-name.txt b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/error_confusing-fragment-name.txt new file mode 100644 index 0000000000000..fa6ed34cdddaa --- /dev/null +++ b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/error_confusing-fragment-name.txt @@ -0,0 +1,18 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, graphql` + fragment CompatProfile_data on User { + name + } +`); diff --git a/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/error_too-many-fragments.txt b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/error_too-many-fragments.txt new file mode 100644 index 0000000000000..b59d7a9d2574a --- /dev/null +++ b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/error_too-many-fragments.txt @@ -0,0 +1,24 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, { + user: graphql` + fragment CompatProfile_user on User { + name + } + + fragment CompatProfile_viewer on User { + name + } + `, +}); diff --git a/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/error_unexpected-fragment.txt b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/error_unexpected-fragment.txt new file mode 100644 index 0000000000000..5ead95bb138e7 --- /dev/null +++ b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/error_unexpected-fragment.txt @@ -0,0 +1,27 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule Compat + */ + +'use strict'; + +const graphql = require('graphql'); +const CompatProfilePic = require('CompatProfilePic'); + +const CompatCommentCreateMutation = graphql` + mutation CompatCommentCreateMutation($input: CommentCreateInput!) { + commentCreate(input: $input) { + viewer { + actor { + id + ...CompatProfilePic_user + } + } + } + } + + fragment Whoopsie_key on User { + name + } +`; diff --git a/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/error_unexpected-operation.txt b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/error_unexpected-operation.txt new file mode 100644 index 0000000000000..9248de683e8ca --- /dev/null +++ b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/error_unexpected-operation.txt @@ -0,0 +1,22 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, graphql` + fragment CompatProfile_user on User { + name + } + + query Whoopsie { + name + } +`); diff --git a/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/export-refetch-container.txt b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/export-refetch-container.txt new file mode 100644 index 0000000000000..8a66ea134259b --- /dev/null +++ b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/export-refetch-container.txt @@ -0,0 +1,32 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule RefetchExample + */ + +const React = require('React'); +const {createRefetchContainer, graphql} = require('RelayClassic'); + +class RefetchExample extends React.Component { + render() { + return
{this.props.user.name}
; + } +} + +module.exports = createRefetchContainer( + RefetchExample, + graphql` + fragment RefetchExample_user on User { + name + } + `, + graphql` + query RefetchExampleRefetchQuery { + viewer { + actor { + ...RefetchExample_user + } + } + } + `, +); diff --git a/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/fragment-spread.txt b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/fragment-spread.txt new file mode 100644 index 0000000000000..dd0e2b324285a --- /dev/null +++ b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/fragment-spread.txt @@ -0,0 +1,22 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const CompatProfilePic = require('CompatProfilePic'); +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, { + user: graphql` + fragment CompatProfile_user on User { + name + ...CompatProfilePic_user + } + `, +}); diff --git a/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/memoize-inner-scope.txt b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/memoize-inner-scope.txt new file mode 100644 index 0000000000000..77833221163c9 --- /dev/null +++ b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/memoize-inner-scope.txt @@ -0,0 +1,29 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule SomeTopLevelView + */ + +'use strict'; + +const ProfilePic = require('ProfilePic'); + +function SomeTopLevelView() { + let _graphql = 'unrelated'; + + return ( + + + + ); +} diff --git a/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/module-operation.txt b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/module-operation.txt new file mode 100644 index 0000000000000..0e4dc2c733267 --- /dev/null +++ b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/module-operation.txt @@ -0,0 +1,23 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatCommentCreateMutation + */ + +'use strict'; + +const graphql = require('graphql'); +const CompatProfilePic = require('CompatProfilePic'); + +const CompatCommentCreateMutation = graphql` + mutation CompatCommentCreateMutation($input: CommentCreateInput!) { + commentCreate(input: $input) { + viewer { + actor { + id + ...CompatProfilePic_user + } + } + } + } +`; diff --git a/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/multiple-root-fields.txt b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/multiple-root-fields.txt new file mode 100644 index 0000000000000..da5009619c6bc --- /dev/null +++ b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/multiple-root-fields.txt @@ -0,0 +1,27 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule Compat + */ + +'use strict'; + +const graphql = require('graphql'); +const CompatProfilePic = require('CompatProfilePic'); + +const CompatViewerQuery = graphql` + query CompatViewerQuery($id: ID!, $scale: Float = 1.5) { + viewer { + actor { + id + ...CompatProfilePic_user + } + } + user: node(id: $id) { + ... on User { + id + ...CompatProfilePic_user + } + } + } +`; diff --git a/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/mutation.txt b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/mutation.txt new file mode 100644 index 0000000000000..375d6376961d7 --- /dev/null +++ b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/mutation.txt @@ -0,0 +1,23 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule Compat + */ + +'use strict'; + +const graphql = require('graphql'); +const CompatProfilePic = require('CompatProfilePic'); + +const CompatCommentCreateMutation = graphql` + mutation CompatCommentCreateMutation($input: CommentCreateInput!) { + commentCreate(input: $input) { + viewer { + actor { + id + ...CompatProfilePic_user + } + } + } + } +`; diff --git a/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/no-fragment-spread.txt b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/no-fragment-spread.txt new file mode 100644 index 0000000000000..4afb18bd86b4d --- /dev/null +++ b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/no-fragment-spread.txt @@ -0,0 +1,20 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, { + user: graphql` + fragment CompatProfile_user on User { + name + } + `, +}); diff --git a/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/no-object-many-fragments.txt b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/no-object-many-fragments.txt new file mode 100644 index 0000000000000..0659758e8078a --- /dev/null +++ b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/no-object-many-fragments.txt @@ -0,0 +1,22 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, graphql` + fragment CompatProfile_user on User { + name + } + + fragment CompatProfile_viewer on User { + name + } +`); diff --git a/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/no-object.txt b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/no-object.txt new file mode 100644 index 0000000000000..915a37b16a77d --- /dev/null +++ b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/no-object.txt @@ -0,0 +1,18 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, graphql` + fragment CompatProfile_user on User { + name + } +`); diff --git a/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/query.txt b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/query.txt new file mode 100644 index 0000000000000..1ae3d3d83a21f --- /dev/null +++ b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/query.txt @@ -0,0 +1,21 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule Compat + */ + +'use strict'; + +const graphql = require('graphql'); +const CompatProfilePic = require('CompatProfilePic'); + +const CompatViewerQuery = graphql` + query CompatViewerQuery($id: ID!, $scale: Float = 1.5) { + node(id: $id) { + ... on User { + id + ...CompatProfilePic_user + } + } + } +`; diff --git a/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/simple-named-fragment.txt b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/simple-named-fragment.txt new file mode 100644 index 0000000000000..ef37f6cedc98f --- /dev/null +++ b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/simple-named-fragment.txt @@ -0,0 +1,19 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, graphql` + fragment CompatProfile on User { + name + ...SomeOtherContainer + } +`); diff --git a/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/simple-named-with-many-fragments.txt b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/simple-named-with-many-fragments.txt new file mode 100644 index 0000000000000..cc8084534506e --- /dev/null +++ b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/simple-named-with-many-fragments.txt @@ -0,0 +1,22 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatProfile + */ + +'use strict'; + +const RelayCompatContainer = require('RelayCompatContainer'); +const graphql = require('graphql'); + +const CompatProfile = () => null; + +module.exports = RelayCompatContainer.createContainer(CompatProfile, graphql` + fragment CompatProfile on User { + name + } + + fragment CompatProfile_viewer on User { + name + } +`); diff --git a/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/within-class-reference.txt b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/within-class-reference.txt new file mode 100644 index 0000000000000..f314c5e4335bf --- /dev/null +++ b/packages/babel-plugin-relay/__tests__/fixtures-modern-artifact-directory/within-class-reference.txt @@ -0,0 +1,45 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CompatStory + */ + +'use strict'; + +const {createFragmentContainer, graphql} = require('RelayClassic'); +const React = require('React'); +const CompatProfilePic = require('CompatProfilePic'); + +class CompatProfile extends React.Component { + render() { + return
+ + {this.props.data.name} + {this.props.data.subscribeStatus} +
; + } + + doSomething() { + commitMutation( + this.props.relay, + graphql` + mutation ActorSubscribe($input: ActorSubscribeInput!) { + actorSubscribe(input: $input) { + subscribee { + ...CompatProfile + } + } + } + `, + { input: { subscribeeId: 123 } } + ) + } +} + +module.exports = createFragmentContainer(CompatProfile, graphql` + fragment CompatProfile on Actor { + name + subscribeStatus + ...CompatProfilePic_user + } +`); diff --git a/packages/babel-plugin-relay/compileGraphQLTag.js b/packages/babel-plugin-relay/compileGraphQLTag.js index b2f377c83a268..49628841038ca 100644 --- a/packages/babel-plugin-relay/compileGraphQLTag.js +++ b/packages/babel-plugin-relay/compileGraphQLTag.js @@ -84,6 +84,7 @@ function createAST(t, state, path, graphqlDefinition) { const isCompatMode = Boolean(state.opts && state.opts.compat); const isHasteMode = Boolean(state.opts && state.opts.haste); const isDevVariable = state.opts && state.opts.isDevVariable; + const artifactDirectory = state.opts && state.opts.artifactDirectory; const buildCommand = (state.opts && state.opts.buildCommand) || 'relay-compiler'; @@ -91,7 +92,8 @@ function createAST(t, state, path, graphqlDefinition) { const isDevelopment = (process.env.BABEL_ENV || process.env.NODE_ENV) !== 'production'; - const modernNode = createModernNode(t, graphqlDefinition, { + const modernNode = createModernNode(t, graphqlDefinition, state, { + artifactDirectory, buildCommand, isDevelopment, isHasteMode, diff --git a/packages/babel-plugin-relay/createModernNode.js b/packages/babel-plugin-relay/createModernNode.js index 9d83dae920ab2..e995a7a61d0f8 100644 --- a/packages/babel-plugin-relay/createModernNode.js +++ b/packages/babel-plugin-relay/createModernNode.js @@ -11,13 +11,17 @@ 'use strict'; const crypto = require('crypto'); +const path = require('path'); const {print} = require('graphql'); +const invariant = require('./invariant'); + const GENERATED = './__generated__/'; import typeof BabelTypes from 'babel-types'; import type {OperationDefinitionNode, FragmentDefinitionNode} from 'graphql'; +import type {BabelState} from './BabelPluginRelay'; /** * Relay Modern creates separate generated files, so Babel transforms graphql @@ -26,7 +30,10 @@ import type {OperationDefinitionNode, FragmentDefinitionNode} from 'graphql'; function createModernNode( t: BabelTypes, graphqlDefinition: OperationDefinitionNode | FragmentDefinitionNode, + state: BabelState, options: { + // If an output directory is specified when running relay-compiler this should point to that directory + artifactDirectory: ?string, // The command to run to compile Relay files, used for error messages. buildCommand: string, // Generate extra validation, defaults to true. @@ -44,7 +51,9 @@ function createModernNode( const requiredFile = definitionName + '.graphql'; const requiredPath = options.isHasteMode ? requiredFile - : GENERATED + requiredFile; + : options.artifactDirectory + ? getRelativeImportPath(state, options.artifactDirectory, requiredFile) + : GENERATED + requiredFile; const hash = crypto .createHash('md5') @@ -107,4 +116,23 @@ function warnNeedsRebuild( ); } +function getRelativeImportPath( + state: BabelState, + artifactDirectory: string, + fileToRequire: string, +): string { + invariant(state.file != null, 'babel state file is null'); + const filename = state.file.opts.filename; + + const relative = path.relative( + path.dirname(filename), + path.resolve(artifactDirectory), + ); + + const relativeReference = + relative.length === 0 || !relative.startsWith('.') ? './' : ''; + + return relativeReference + path.join(relative, fileToRequire); +} + module.exports = createModernNode; diff --git a/packages/relay-compiler/RelayCompilerPublic.js b/packages/relay-compiler/RelayCompilerPublic.js index d579d01595504..ec407f6fea068 100644 --- a/packages/relay-compiler/RelayCompilerPublic.js +++ b/packages/relay-compiler/RelayCompilerPublic.js @@ -13,11 +13,12 @@ const RelayCodeGenerator = require('./codegen/RelayCodeGenerator'); const RelayFileWriter = require('./codegen/RelayFileWriter'); const RelayIRTransforms = require('./core/RelayIRTransforms'); -const RelayJSModuleParser = require('./core/RelayJSModuleParser'); +const RelaySourceModuleParser = require('./core/RelaySourceModuleParser'); const RelayParser = require('./core/RelayParser'); +const FindGraphQLTags = require('./language/javascript/FindGraphQLTags'); const compileRelayArtifacts = require('./codegen/compileRelayArtifacts'); -const formatGeneratedModule = require('./codegen/formatGeneratedModule'); +const formatGeneratedModule = require('./language/javascript/formatGeneratedModule'); const {CompilerContext: GraphQLCompilerContext} = require('graphql-compiler'); const { @@ -29,6 +30,8 @@ const { export type {CompileResult, ParserConfig, WriterConfig} from 'graphql-compiler'; +const RelayJSModuleParser = RelaySourceModuleParser(FindGraphQLTags.find); + module.exports = { ConsoleReporter, Parser: RelayParser, diff --git a/packages/relay-compiler/bin/RelayCompilerBin.js b/packages/relay-compiler/bin/RelayCompilerBin.js index 3b2df7c781be6..4952478e7fcef 100644 --- a/packages/relay-compiler/bin/RelayCompilerBin.js +++ b/packages/relay-compiler/bin/RelayCompilerBin.js @@ -19,11 +19,11 @@ const { DotGraphQLParser, } = require('graphql-compiler'); -const RelayJSModuleParser = require('../core/RelayJSModuleParser'); +const RelaySourceModuleParser = require('../core/RelaySourceModuleParser'); const RelayFileWriter = require('../codegen/RelayFileWriter'); const RelayIRTransforms = require('../core/RelayIRTransforms'); +const RelayLanguagePluginJavaScript = require('../language/javascript/RelayLanguagePluginJavaScript'); -const formatGeneratedModule = require('../codegen/formatGeneratedModule'); const fs = require('fs'); const path = require('path'); const yargs = require('yargs'); @@ -46,6 +46,10 @@ const { import type {GetWriterOptions} from 'graphql-compiler'; import type {GraphQLSchema} from 'graphql'; +import type { + PluginInitializer, + PluginInterface, +} from '../language/RelayLanguagePluginInterface'; function buildWatchExpression(options: { extensions: Array, @@ -82,6 +86,48 @@ function getFilepathsFromGlob( }); } +type LanguagePlugin = PluginInitializer | {default: PluginInitializer}; + +/** + * Unless the requested plugin is the builtin `javascript` one, import a + * language plugin as either a CommonJS or ES2015 module. + * + * When importing, first check if it’s a path to an existing file, otherwise + * assume it’s a package and prepend the plugin namespace prefix. + * + * Make sure to always use Node's `require` function, which otherwise would get + * replaced with `__webpack_require__` when bundled using webpack, by using + * `eval` to get it at runtime. + */ +function getLanguagePlugin(language: string): PluginInterface { + if (language === 'javascript') { + return RelayLanguagePluginJavaScript(); + } else { + const pluginPath = path.resolve(process.cwd(), language); + const requirePath = fs.existsSync(pluginPath) + ? pluginPath + : `relay-compiler-language-${language}`; + try { + // eslint-disable-next-line no-eval + let languagePlugin: LanguagePlugin = eval('require')(requirePath); + if (languagePlugin.default) { + languagePlugin = languagePlugin.default; + } + if (typeof languagePlugin === 'function') { + return languagePlugin(); + } else { + throw new Error('Expected plugin to export a function.'); + } + } catch (err) { + const e = new Error( + `Unable to load language plugin ${requirePath}: ${err.message}`, + ); + e.stack = err.stack; + throw e; + } + } +} + async function run(options: { schema: string, src: string, @@ -94,6 +140,8 @@ async function run(options: { validate: boolean, quiet: boolean, noFutureProofEnums: boolean, + language: string, + artifactDirectory: ?string, }) { const schemaPath = path.resolve(process.cwd(), options.schema); if (!fs.existsSync(schemaPath)) { @@ -133,6 +181,30 @@ Ensure that one such file exists in ${srcDir} or its parents. const schema = getSchema(schemaPath); + const languagePlugin = getLanguagePlugin(options.language); + + const inputExtensions = options.extensions || languagePlugin.inputExtensions; + const outputExtension = languagePlugin.outputExtension; + + const sourceParserName = inputExtensions.join('/'); + const sourceWriterName = outputExtension; + + const sourceModuleParser = RelaySourceModuleParser( + languagePlugin.findGraphQLTags, + ); + + const artifactDirectory = options.artifactDirectory + ? // $FlowFixMe artifactDirectory can’t be null/undefined at this point + path.resolve(process.cwd(), options.artifactDirectory) + : null; + + const generatedDirectoryName = artifactDirectory || '__generated__'; + + const sourceSearchOptions = { + extensions: inputExtensions, + include: options.include, + exclude: ['**/*.graphql.*', ...options.exclude], // Do not include artifacts + }; const graphqlSearchOptions = { extensions: ['graphql'], include: options.include, @@ -140,13 +212,17 @@ Ensure that one such file exists in ${srcDir} or its parents. }; const parserConfigs = { - js: { + [sourceParserName]: { baseDir: srcDir, - getFileFilter: RelayJSModuleParser.getFileFilter, - getParser: RelayJSModuleParser.getParser, + getFileFilter: sourceModuleParser.getFileFilter, + getParser: sourceModuleParser.getParser, getSchema: () => schema, - watchmanExpression: useWatchman ? buildWatchExpression(options) : null, - filepaths: useWatchman ? null : getFilepathsFromGlob(srcDir, options), + watchmanExpression: useWatchman + ? buildWatchExpression(sourceSearchOptions) + : null, + filepaths: useWatchman + ? null + : getFilepathsFromGlob(srcDir, sourceSearchOptions), }, graphql: { baseDir: srcDir, @@ -161,11 +237,17 @@ Ensure that one such file exists in ${srcDir} or its parents. }, }; const writerConfigs = { - js: { - getWriter: getRelayFileWriter(srcDir, options.noFutureProofEnums), + [sourceWriterName]: { + getWriter: getRelayFileWriter( + srcDir, + languagePlugin, + options.noFutureProofEnums, + artifactDirectory, + ), isGeneratedFile: (filePath: string) => - filePath.endsWith('.js') && filePath.includes('__generated__'), - parser: 'js', + filePath.endsWith('.graphql.' + outputExtension) && + filePath.includes(generatedDirectoryName), + parser: sourceParserName, baseParsers: ['graphql'], }, }; @@ -193,7 +275,12 @@ Ensure that one such file exists in ${srcDir} or its parents. } } -function getRelayFileWriter(baseDir: string, noFutureProofEnums: boolean) { +function getRelayFileWriter( + baseDir: string, + languagePlugin: PluginInterface, + noFutureProofEnums: boolean, + outputDir?: ?string, +) { return ({ onlyValidate, schema, @@ -213,11 +300,14 @@ function getRelayFileWriter(baseDir: string, noFutureProofEnums: boolean) { queryTransforms, }, customScalars: {}, - formatModule: formatGeneratedModule, + formatModule: languagePlugin.formatModule, inputFieldWhiteListForFlow: [], schemaExtensions, useHaste: false, noFutureProofEnums, + extension: languagePlugin.outputExtension, + typeGenerator: languagePlugin.typeGenerator, + outputDir, }, onlyValidate, schema, @@ -306,8 +396,9 @@ const argv = yargs }, extensions: { array: true, - default: ['js'], - describe: 'File extensions to compile (--extensions js jsx)', + describe: + 'File extensions to compile (defaults to extensions provided by the ' + + 'language plugin)', type: 'string', }, verbose: { @@ -342,6 +433,19 @@ const argv = yargs 'from breaking.', default: false, }, + language: { + describe: + 'The name of the language plugin used for input files and artifacts', + type: 'string', + default: 'javascript', + }, + artifactDirectory: { + describe: + 'A specific directory to output all artifacts to. When enabling this ' + + 'the babel plugin needs `artifactDirectory` set as well.', + type: 'string', + default: null, + }, }) .help().argv; diff --git a/packages/relay-compiler/codegen/RelayFileWriter.js b/packages/relay-compiler/codegen/RelayFileWriter.js index da6d9219c3674..ba5354b9c65e1 100644 --- a/packages/relay-compiler/codegen/RelayFileWriter.js +++ b/packages/relay-compiler/codegen/RelayFileWriter.js @@ -10,7 +10,6 @@ 'use strict'; -const RelayFlowGenerator = require('../core/RelayFlowGenerator'); const RelayParser = require('../core/RelayParser'); const RelayValidator = require('../core/RelayValidator'); @@ -30,9 +29,12 @@ const { } = require('graphql-compiler'); const {Map: ImmutableMap} = require('immutable'); -import type {ScalarTypeMapping} from '../core/RelayFlowTypeTransformers'; +import type {ScalarTypeMapping} from '../language/javascript/RelayFlowTypeTransformers'; import type {RelayCompilerTransforms} from './compileRelayArtifacts'; -import type {FormatModule} from './writeRelayGeneratedFile'; +import type { + FormatModule, + TypeGenerator, +} from '../language/RelayLanguagePluginInterface'; import type { FileWriterInterface, Reporter, @@ -57,7 +59,7 @@ export type WriterConfig = { formatModule: FormatModule, generateExtraFiles?: GenerateExtraFiles, inputFieldWhiteListForFlow: Array, - outputDir?: string, + outputDir?: ?string, generatedDirectories?: Array, persistQuery?: (text: string) => Promise, platform?: string, @@ -65,6 +67,8 @@ export type WriterConfig = { schemaExtensions: Array, noFutureProofEnums: boolean, useHaste: boolean, + extension: string, + typeGenerator: TypeGenerator, // Haste style module that exports flow types for GraphQL enums. // TODO(T22422153) support non-haste environments enumsHasteModule?: string, @@ -218,7 +222,7 @@ class RelayFileWriter implements FileWriterInterface { }; const transformedFlowContext = compilerContext.applyTransforms( - RelayFlowGenerator.flowTransforms, + this._config.typeGenerator.transforms, this._reporter, ); const transformedQueryContext = compilerContext.applyTransforms( @@ -271,20 +275,21 @@ class RelayFileWriter implements FileWriterInterface { const relayRuntimeModule = this._config.relayRuntimeModule || 'relay-runtime'; - const flowNode = transformedFlowContext.get(node.name); + const typeNode = transformedFlowContext.get(node.name); invariant( - flowNode, - 'RelayFileWriter: did not compile flow types for: %s', + typeNode, + 'RelayFileWriter: did not compile types for: %s', node.name, ); - const flowTypes = RelayFlowGenerator.generate(flowNode, { + const typeText = this._config.typeGenerator.generate(typeNode, { customScalars: this._config.customScalars, enumsHasteModule: this._config.enumsHasteModule, existingFragmentNames, inputFieldWhiteList: this._config.inputFieldWhiteListForFlow, relayRuntimeModule, useHaste: this._config.useHaste, + useSingleArtifactDirectory: !!this._config.outputDir, noFutureProofEnums: this._config.noFutureProofEnums, }); @@ -296,11 +301,12 @@ class RelayFileWriter implements FileWriterInterface { getGeneratedDirectory(node.name), node, formatModule, - flowTypes, + typeText, persistQuery, this._config.platform, relayRuntimeModule, sourceHash, + this._config.extension, ); }), ); diff --git a/packages/relay-compiler/codegen/writeRelayGeneratedFile.js b/packages/relay-compiler/codegen/writeRelayGeneratedFile.js index 14e291b6b3530..b03629b358215 100644 --- a/packages/relay-compiler/codegen/writeRelayGeneratedFile.js +++ b/packages/relay-compiler/codegen/writeRelayGeneratedFile.js @@ -19,49 +19,33 @@ const {RelayConcreteNode} = require('RelayRuntime'); const {Profiler} = require('graphql-compiler'); import type {GeneratedNode} from 'RelayRuntime'; +import type {FormatModule} from '../language/RelayLanguagePluginInterface'; import type {CodegenDirectory} from 'graphql-compiler'; -/** - * Generate a module for the given document name/text. - */ -export type FormatModule = ({| - moduleName: string, - documentType: - | typeof RelayConcreteNode.FRAGMENT - | typeof RelayConcreteNode.REQUEST - | typeof RelayConcreteNode.BATCH_REQUEST, - docText: ?string, - concreteText: string, - flowText: string, - hash: ?string, - devOnlyAssignments: ?string, - relayRuntimeModule: string, - sourceHash: string, -|}) => string; - async function writeRelayGeneratedFile( codegenDir: CodegenDirectory, generatedNode: GeneratedNode, formatModule: FormatModule, - flowText: string, + typeText: string, _persistQuery: ?(text: string) => Promise, platform: ?string, relayRuntimeModule: string, sourceHash: string, + extension: string, ): Promise { // Copy to const so Flow can refine. const persistQuery = _persistQuery; const moduleName = generatedNode.name + '.graphql'; const platformName = platform ? moduleName + '.' + platform : moduleName; - const filename = platformName + '.js'; - const flowTypeName = + const filename = platformName + '.' + extension; + const typeName = generatedNode.kind === RelayConcreteNode.FRAGMENT ? 'ConcreteFragment' : generatedNode.kind === RelayConcreteNode.REQUEST ? 'ConcreteRequest' : generatedNode.kind === RelayConcreteNode.BATCH_REQUEST ? 'ConcreteBatchRequest' - : 'empty'; + : null; const devOnlyProperties = {}; let docText; @@ -83,8 +67,8 @@ async function writeRelayGeneratedFile( hasher.update('cache-breaker-7'); hasher.update(JSON.stringify(generatedNode)); hasher.update(sourceHash); - if (flowText) { - hasher.update(flowText); + if (typeText) { + hasher.update(typeText); } if (persistQuery) { hasher.update('persisted'); @@ -138,9 +122,9 @@ async function writeRelayGeneratedFile( const moduleText = formatModule({ moduleName, - documentType: flowTypeName, + documentType: typeName, docText, - flowText, + typeText, hash: hash ? `@relayHash ${hash}` : null, concreteText: dedupeJSONStringify(generatedNode), devOnlyAssignments, diff --git a/packages/relay-compiler/core/RelayFindGraphQLTags.js b/packages/relay-compiler/core/RelayFindGraphQLTags.js new file mode 100644 index 0000000000000..681299c3aea13 --- /dev/null +++ b/packages/relay-compiler/core/RelayFindGraphQLTags.js @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const path = require('path'); +const util = require('util'); +const graphql = require('graphql'); + +const RelayCompilerCache = require('../util/RelayCompilerCache'); +const getModuleName = require('../util/getModuleName'); + +import type {File} from 'graphql-compiler'; +import type { + GraphQLTag, + GraphQLTagFinder, +} from '../language/RelayLanguagePluginInterface'; + +export type GraphQLTagFinderOptions = {| + validateNames: boolean, +|}; + +const cache = new RelayCompilerCache('RelayFindGraphQLTags', 'v1'); + +function memoizedFind( + tagFinder: GraphQLTagFinder, + text: string, + baseDir: string, + file: File, + options: GraphQLTagFinderOptions, +): Array { + invariant( + file.exists, + 'RelayFindGraphQLTags: Called with non-existent file `%s`', + file.relPath, + ); + return cache.getOrCompute( + file.hash + (options.validateNames ? '1' : '0'), + find.bind(null, tagFinder, text, path.join(baseDir, file.relPath), options), + ); +} + +function find( + tagFinder: GraphQLTagFinder, + text: string, + absPath: string, + {validateNames}: GraphQLTagFinderOptions, +): Array { + const tags = tagFinder(text, absPath); + if (validateNames) { + const moduleName = getModuleName(absPath); + tags.forEach(tag => validateTemplate(tag, moduleName, absPath)); + } + return tags.map(tag => tag.template); +} + +function validateTemplate( + {template, keyName, sourceLocationOffset}: GraphQLTag, + moduleName: string, + filePath: string, +) { + const ast = graphql.parse( + new graphql.Source(template, filePath, sourceLocationOffset), + ); + ast.definitions.forEach((def: any) => { + invariant( + def.name, + 'RelayFindGraphQLTags: In module `%s`, a definition of kind `%s` requires a name.', + moduleName, + def.kind, + ); + const definitionName = def.name.value; + if (def.kind === 'OperationDefinition') { + const operationNameParts = definitionName.match( + /^(.*)(Mutation|Query|Subscription)$/, + ); + invariant( + operationNameParts && definitionName.startsWith(moduleName), + 'RelayFindGraphQLTags: Operation names in graphql tags must be prefixed ' + + 'with the module name and end in "Mutation", "Query", or ' + + '"Subscription". Got `%s` in module `%s`.', + definitionName, + moduleName, + ); + } else if (def.kind === 'FragmentDefinition') { + if (keyName) { + invariant( + definitionName === moduleName + '_' + keyName, + 'RelayFindGraphQLTags: Container fragment names must be ' + + '`_`. Got `%s`, expected `%s`.', + definitionName, + moduleName + '_' + keyName, + ); + } else { + invariant( + definitionName.startsWith(moduleName), + 'RelayFindGraphQLTags: Fragment names in graphql tags must be prefixed ' + + 'with the module name. Got `%s` in module `%s`.', + definitionName, + moduleName, + ); + } + } + }); +} + +// TODO: Not sure why this is defined here rather than imported, is it so that it doesn’t get stripped in prod? +function invariant(condition, msg, ...args) { + if (!condition) { + throw new Error(util.format(msg, ...args)); + } +} + +module.exports = { + find, // Exported for testing only. + memoizedFind, +}; diff --git a/packages/relay-compiler/core/RelayJSModuleParser.js b/packages/relay-compiler/core/RelayJSModuleParser.js deleted file mode 100644 index 59c1acd3bfba6..0000000000000 --- a/packages/relay-compiler/core/RelayJSModuleParser.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright (c) 2013-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow strict-local - * @format - */ - -'use strict'; - -const FindGraphQLTags = require('../codegen/FindGraphQLTags'); -const GraphQL = require('graphql'); - -const fs = require('fs'); -const invariant = require('invariant'); -const path = require('path'); - -const {ASTCache, Profiler} = require('graphql-compiler'); - -import type {File, FileFilter} from 'graphql-compiler'; -import type {DocumentNode} from 'graphql'; - -const parseGraphQL = Profiler.instrument(GraphQL.parse, 'GraphQL.parse'); - -const FIND_OPTIONS = { - validateNames: true, -}; - -// Throws an error if parsing the file fails -function parseFile(baseDir: string, file: File): ?DocumentNode { - const text = fs.readFileSync(path.join(baseDir, file.relPath), 'utf8'); - - invariant( - text.indexOf('graphql') >= 0, - 'RelayJSModuleParser: Files should be filtered before passed to the ' + - 'parser, got unfiltered file `%s`.', - file, - ); - - const astDefinitions = []; - FindGraphQLTags.memoizedFind(text, baseDir, file, FIND_OPTIONS).forEach( - template => { - const ast = parseGraphQL(new GraphQL.Source(template, file.relPath)); - invariant( - ast.definitions.length, - 'RelayJSModuleParser: Expected GraphQL text to contain at least one ' + - 'definition (fragment, mutation, query, subscription), got `%s`.', - template, - ); - astDefinitions.push(...ast.definitions); - }, - ); - - return { - kind: 'Document', - definitions: astDefinitions, - }; -} - -function getParser(baseDir: string): ASTCache { - return new ASTCache({ - baseDir, - parse: parseFile, - }); -} - -function getFileFilter(baseDir: string): FileFilter { - return (file: File) => { - const text = fs.readFileSync(path.join(baseDir, file.relPath), 'utf8'); - return text.indexOf('graphql') >= 0; - }; -} - -module.exports = { - getParser, - getFileFilter, -}; diff --git a/packages/relay-compiler/core/RelaySourceModuleParser.js b/packages/relay-compiler/core/RelaySourceModuleParser.js new file mode 100644 index 0000000000000..f81194d1e4618 --- /dev/null +++ b/packages/relay-compiler/core/RelaySourceModuleParser.js @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +const GraphQL = require('graphql'); + +const fs = require('fs'); +const invariant = require('invariant'); +const path = require('path'); + +const {ASTCache, Profiler} = require('graphql-compiler'); + +const {memoizedFind} = require('./RelayFindGraphQLTags'); + +import type {GraphQLTagFinder} from '../language/RelayLanguagePluginInterface'; +import type {File, FileFilter} from 'graphql-compiler'; +import type {DocumentNode} from 'graphql'; + +const parseGraphQL = Profiler.instrument(GraphQL.parse, 'GraphQL.parse'); + +const FIND_OPTIONS = { + validateNames: true, +}; + +module.exports = (tagFinder: GraphQLTagFinder) => { + const memoizedTagFinder = memoizedFind.bind(null, tagFinder); + + // Throws an error if parsing the file fails + function parseFile(baseDir: string, file: File): ?DocumentNode { + const text = fs.readFileSync(path.join(baseDir, file.relPath), 'utf8'); + + invariant( + text.indexOf('graphql') >= 0, + 'RelaySourceModuleParser: Files should be filtered before passed to the ' + + 'parser, got unfiltered file `%s`.', + file, + ); + + const astDefinitions = []; + memoizedTagFinder(text, baseDir, file, FIND_OPTIONS).forEach(template => { + const ast = parseGraphQL(new GraphQL.Source(template, file.relPath)); + invariant( + ast.definitions.length, + 'RelaySourceModuleParser: Expected GraphQL text to contain at least one ' + + 'definition (fragment, mutation, query, subscription), got `%s`.', + template, + ); + astDefinitions.push(...ast.definitions); + }); + + return { + kind: 'Document', + definitions: astDefinitions, + }; + } + + function getParser(baseDir: string): ASTCache { + return new ASTCache({ + baseDir, + parse: parseFile, + }); + } + + function getFileFilter(baseDir: string): FileFilter { + return (file: File) => { + const text = fs.readFileSync(path.join(baseDir, file.relPath), 'utf8'); + return text.indexOf('graphql') >= 0; + }; + } + + return { + getParser, + getFileFilter, + }; +}; diff --git a/packages/relay-compiler/codegen/__tests__/FindGraphQLTags-test.js b/packages/relay-compiler/core/__tests__/RelayFindGraphQLTags-test.js similarity index 83% rename from packages/relay-compiler/codegen/__tests__/FindGraphQLTags-test.js rename to packages/relay-compiler/core/__tests__/RelayFindGraphQLTags-test.js index 38428f48d3ac3..ba36a826d0b0e 100644 --- a/packages/relay-compiler/codegen/__tests__/FindGraphQLTags-test.js +++ b/packages/relay-compiler/core/__tests__/RelayFindGraphQLTags-test.js @@ -10,23 +10,34 @@ 'use strict'; +const RelayFindGraphQLTags = require('RelayFindGraphQLTags'); const FindGraphQLTags = require('FindGraphQLTags'); -describe('FindGraphQLTags', () => { - function find(text) { - return FindGraphQLTags.find(text, '/path/to/FindGraphQLTags.js', { - validateNames: true, - }); +import type {GraphQLTagFinderOptions} from 'RelayFindGraphQLTags'; + +describe('RelayFindGraphQLTags', () => { + function find( + text, + options: GraphQLTagFinderOptions, + absPath: string = '/path/to/FindGraphQLTags.js', + ) { + return RelayFindGraphQLTags.find( + FindGraphQLTags.find, + text, + absPath, + options, + ); } describe('query parsing', () => { it('parses a simple file', () => { - expect(find('const foo = 1;')).toEqual([]); + expect(find('const foo = 1;', {validateNames: false})).toEqual([]); }); it('parses graphql templates', () => { expect( - find(` + find( + ` const foo = 1; foo(graphql\`fragment FindGraphQLTags on User { id }\`); graphql\`fragment FindGraphQLTags on User { name }\`; @@ -62,7 +73,9 @@ describe('FindGraphQLTags', () => { {}, graphql\`query FindGraphQLTagsRefetchQuery { me { name } }\` ); - `), + `, + {validateNames: false}, + ), ).toEqual([ 'fragment FindGraphQLTags on User { id }', 'fragment FindGraphQLTags on User { name }', @@ -77,7 +90,8 @@ describe('FindGraphQLTags', () => { it('parses modern JS syntax with Flow annotations', () => { expect( - find(` + find( + ` class RelayContainer extends React.Component { // graphql\`this in a comment\`; _loadMore = ( @@ -91,16 +105,21 @@ describe('FindGraphQLTags', () => { return <>A Fragment!; } } - `), + `, + {validateNames: false}, + ), ).toEqual(['fragment FindGraphQLTags on User { id }']); }); it('parses JS with functions sharing names with object prototype methods', () => { expect( - find(` + find( + ` toString(); foo(graphql\`fragment FindGraphQLTags on User { id }\`); - `), + `, + {validateNames: false}, + ), ).toEqual(['fragment FindGraphQLTags on User { id }']); }); }); @@ -116,6 +135,7 @@ describe('FindGraphQLTags', () => { ' id\n' + ' }\n' + '`);\n', + {validateNames: true}, ); }).toThrow('Syntax Error: Cannot parse the unexpected character "?".'); }); @@ -124,11 +144,9 @@ describe('FindGraphQLTags', () => { describe('query name validation', () => { it('throws for invalid query names', () => { expect(() => - FindGraphQLTags.find( - 'graphql`query NotModuleName { me { id } }`;', - '/path/to/FindGraphQLTags.js', - {validateNames: true}, - ), + find('graphql`query NotModuleName { me { id } }`;', { + validateNames: true, + }), ).toThrow( 'FindGraphQLTags: Operation names in graphql tags must be prefixed with ' + 'the module name and end in "Mutation", "Query", or "Subscription". ' + @@ -137,53 +155,55 @@ describe('FindGraphQLTags', () => { }); it('does not validate names when options is not set', () => { - FindGraphQLTags.find( - 'graphql`query NotModuleName { me { id } }`;', - '/path/to/FindGraphQLTags.js', - {validateNames: false}, - ); + find('graphql`query NotModuleName { me { id } }`;', { + validateNames: false, + }); }); it('parses queries with valid names', () => { expect( - find('graphql`query FindGraphQLTagsQuery { me { id } }`;'), + find('graphql`query FindGraphQLTagsQuery { me { id } }`;', { + validateNames: true, + }), ).toEqual(['query FindGraphQLTagsQuery { me { id } }']); }); it('parses queries with valid names from filepath', () => { expect( - FindGraphQLTags.find( + find( 'graphql`query TestComponentQuery { me { id } }`;', - './PathTo/SuperDuper/TestComponent.js', {validateNames: true}, + './PathTo/SuperDuper/TestComponent.js', ), ).toEqual(['query TestComponentQuery { me { id } }']); expect( - FindGraphQLTags.find( + find( 'graphql`query TestComponentQuery { me { id } }`;', - './PathTo/SuperDuper/TestComponent.react.js', {validateNames: true}, + './PathTo/SuperDuper/TestComponent.react.js', ), ).toEqual(['query TestComponentQuery { me { id } }']); expect( - FindGraphQLTags.find( + find( 'graphql`query TestComponentQuery { me { id } }`;', - './PathTo/SuperDuper/TestComponent.react.jsx', {validateNames: true}, + './PathTo/SuperDuper/TestComponent.react.jsx', ), ).toEqual(['query TestComponentQuery { me { id } }']); expect( - FindGraphQLTags.find( + find( 'graphql`query TestComponentQuery { me { id } }`;', - './PathTo/SuperDuper/TestComponent/index.js', {validateNames: true}, + './PathTo/SuperDuper/TestComponent/index.js', ), ).toEqual(['query TestComponentQuery { me { id } }']); }); it('throws for invalid top-level fragment names', () => { expect(() => - find('graphql`fragment NotModuleName on User { name }`;'), + find('graphql`fragment NotModuleName on User { name }`;', { + validateNames: true, + }), ).toThrow( 'FindGraphQLTags: Fragment names in graphql tags ' + 'must be prefixed with the module name. Got ' + @@ -193,17 +213,22 @@ describe('FindGraphQLTags', () => { it('parses top-level fragments with valid names', () => { expect( - find('graphql`fragment FindGraphQLTags on User { name }`;'), + find('graphql`fragment FindGraphQLTags on User { name }`;', { + validateNames: true, + }), ).toEqual(['fragment FindGraphQLTags on User { name }']); }); it('throws for invalid container fragment names', () => { expect(() => - find(` + find( + ` createFragmentContainer(Foo, { foo: graphql\`fragment FindGraphQLTags_notFoo on User { name }\`, }); - `), + `, + {validateNames: true}, + ), ).toThrow( 'FindGraphQLTags: Container fragment names must be ' + '`_`. Got `FindGraphQLTags_notFoo`, expected ' + @@ -213,11 +238,14 @@ describe('FindGraphQLTags', () => { it('parses container fragments with valid names', () => { expect( - find(` + find( + ` createFragmentContainer(Foo, { foo: graphql\`fragment FindGraphQLTags_foo on User { name }\`, }); - `), + `, + {validateNames: true}, + ), ).toEqual(['fragment FindGraphQLTags_foo on User { name }']); }); }); diff --git a/packages/relay-compiler/language/RelayLanguagePluginInterface.js b/packages/relay-compiler/language/RelayLanguagePluginInterface.js new file mode 100644 index 0000000000000..453010e28e472 --- /dev/null +++ b/packages/relay-compiler/language/RelayLanguagePluginInterface.js @@ -0,0 +1,262 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const RelayConcreteNode = require('../../relay-runtime/util/RelayConcreteNode'); + +import type {IRTransform, Root, Fragment} from 'graphql-compiler'; +import type {ScalarTypeMapping} from './javascript/RelayFlowTypeTransformers'; + +/** + * A language plugin allows relay-compiler to both read and write files for any + * language. + * + * When reading, the plugin is expected to parse and return GraphQL tags; and + * when writing the plugin is responsible for generating type information about + * the GraphQL selections made as well as generating the contents of the + * artifact file. + * + * This interface describes the details relay-compiler requires to be able to + * use the plugin and is expected to be returned by a {PluginInitializer}. + */ +export type PluginInterface = { + inputExtensions: string[], + outputExtension: string, + findGraphQLTags: GraphQLTagFinder, + formatModule: FormatModule, + typeGenerator: TypeGenerator, +}; + +/** + * The plugin is expected to have as its main default export a function that + * returns an object conforming to the plugin interface. + * + * For now a plugin doesn’t take any arguments, but may do so in the future. + */ +export type PluginInitializer = () => PluginInterface; + +export type GraphQLTag = { + /** + * Should hold the string content of the `graphql` tagged template literal, + * which is either an operation or fragment. + * + * @example + * + * grapqhl`query MyQuery { … }` + * grapqhl`fragment MyFragment on MyType { … }` + */ + template: string, + + /** + * In the case this tag was part of a fragment container and it used a node + * map as fragment spec, rather than a single tagged node, this should hold + * the prop key to which the node is assigned. + * + * @example + * + * createFragmentContainer( + * MyComponent, + * { + * keyName: graphql`fragment MyComponent_keyName { … }` + * } + * ) + * + */ + keyName: ?string, + + /** + * The location in the source file that the tag is placed at. + */ + sourceLocationOffset: { + /** + * The line in the source file that the tag is placed on. + * + * Lines use 1-based indexing. + */ + line: number, + + /** + * The column in the source file that the tag starts on. + * + * Columns use 1-based indexing. + */ + column: number, + }, +}; + +/** + * This function is responsible for extracting `GraphQLTag` objects from source + * files. + * + * @param {string} text The source file contents. + * @param {string} filePath The path to the source file on disk. + * @return {Array} All extracted `GraphQLTag` objects. + * + * @see {@link javascript/FindGraphQLTags.js} + */ +export type GraphQLTagFinder = ( + text: string, + filePath: string, +) => Array; + +/** + * The function that is responsible for generating the contents of the artifact + * file. + * + * @see {@link javascript/formatGeneratedModule.js} + */ +export type FormatModule = ({| + /** + * The filename of the module. + */ + moduleName: string, + + /** + * The type of artifact that this module represents. + * + * @todo Document when this can be `empty`. + */ + documentType: + | typeof RelayConcreteNode.FRAGMENT + | typeof RelayConcreteNode.REQUEST + | typeof RelayConcreteNode.BATCH_REQUEST + | null, + + /** + * The actual document that this module represents. + */ + docText: ?string, + + /** + * The IR for the document that this module represents. + */ + concreteText: string, + + /** + * The type information generated for the GraphQL selections made. + */ + typeText: string, + + /** + * The name of the relay-runtime module being used. + */ + relayRuntimeModule: string, + + /** + * A hash of the concrete node including the query text. + * + * @todo Document how this is different from `sourceHash`. + */ + hash: ?string, + + /** + * A hash of the document, which is used by relay-compiler to know if it needs + * to write a new version of the artifact. + * + * @todo Is this correct? And document how this is different from `hash`. + */ + sourceHash: string, + + /** + * @todo Document this. + */ + devOnlyAssignments: ?string, +|}) => string; + +/** + * The options that will be passed to the `generate` function of your plugin’s + * type generator. + */ +export type TypeGeneratorOptions = {| + /** + * A map of custom scalars to scalars that the plugin knows about and emits + * type information for. + * + * @example + * + * // The URL custom scalar is essentially a string and should be treated as + * // such by the language’s type system. + * { URL: 'String' } + */ + +customScalars: ScalarTypeMapping, + + /** + * Lists all other fragments relay-compiler knows about. Use this to know when + * to import/reference other artifacts. + */ + +existingFragmentNames: Set, + + /** + * The name of the relay-runtime module. + * + * This defaults to `relay-runtime`. + */ + +relayRuntimeModule: string, + + /** + * Whether or not relay-compiler will store artifacts next to the module that + * they originate from or all together in a single directory. + * + * Storing all artifacts in a single directory makes it easy to import and + * reference fragments defined in other artifacts without needing to use the + * Haste module system. + * + * This defaults to `false`. + */ + +useSingleArtifactDirectory: boolean, + + /** + * This option controls whether or not a catch-all entry is added to enum type + * definitions for values that may be added in the future. Enabling this means + * you will have to update your application whenever the GraphQL server schema + * adds new enum values to prevent it from breaking. + * + * This defaults to `false`. + */ + +noFutureProofEnums: boolean, + + /** + * @todo Document this. + */ + +inputFieldWhiteList: $ReadOnlyArray, + + /** + * Whether or not the Haste module system is being used. This will currently + * always be `false` for OSS users. + */ + +useHaste: boolean, + + /** + * @todo Document this. + */ + +enumsHasteModule: ?string, +|}; + +/** + * This object should hold the implementation required to generate types for the + * GraphQL selections made. + * + * @see {@link javascript/RelayFlowGenerator.js} + */ +export type TypeGenerator = { + /** + * Transforms that should be applied to the intermediate representation of the + * GraphQL document before passing to the `generate` function. + */ + transforms: Array, + + /** + * Given GraphQL document IR, this function should generate type information + * for e.g. the selections made. It can, however, also generate any other + * content such as importing other files, including other artifacts. + */ + generate: (node: Root | Fragment, options: TypeGeneratorOptions) => string, +}; diff --git a/packages/relay-compiler/codegen/FindGraphQLTags.js b/packages/relay-compiler/language/javascript/FindGraphQLTags.js similarity index 59% rename from packages/relay-compiler/codegen/FindGraphQLTags.js rename to packages/relay-compiler/language/javascript/FindGraphQLTags.js index 17d678586bd46..cb525ca25feb3 100644 --- a/packages/relay-compiler/codegen/FindGraphQLTags.js +++ b/packages/relay-compiler/language/javascript/FindGraphQLTags.js @@ -10,17 +10,12 @@ 'use strict'; -const RelayCompilerCache = require('../util/RelayCompilerCache'); - const babylon = require('@babel/parser'); -const getModuleName = require('../util/getModuleName'); -const graphql = require('graphql'); -const path = require('path'); const util = require('util'); const {Profiler} = require('graphql-compiler'); -import type {File} from 'graphql-compiler'; +import type {GraphQLTag} from '../RelayLanguagePluginInterface'; // Attempt to be as inclusive as possible of source text. const BABYLON_OPTIONS = { @@ -48,18 +43,9 @@ const BABYLON_OPTIONS = { strictMode: false, }; -type Options = {| - validateNames: boolean, -|}; - -function find( - text: string, - filePath: string, - {validateNames}: Options, -): Array { - const result = []; +function find(text: string): Array { + const result: Array = []; const ast = babylon.parse(text, BABYLON_OPTIONS); - const moduleName = getModuleName(filePath); const visitors = { CallExpression: node => { @@ -89,7 +75,6 @@ function find( '`key: graphql`.', node.callee.name, ); - const keyName = property.key.name; invariant( isGraphQLTag(property.value.tag), 'FindGraphQLTags: `%s` expects fragment definitions to be tagged ' + @@ -97,17 +82,11 @@ function find( node.callee.name, getSourceTextForLocation(text, property.value.tag.loc), ); - const template = getGraphQLText(property.value.quasi); - if (validateNames) { - validateTemplate( - template, - moduleName, - keyName, - filePath, - getSourceLocationOffset(property.value.quasi), - ); - } - result.push(template); + result.push({ + keyName: property.key.name, + template: getGraphQLText(property.value.quasi), + sourceLocationOffset: getSourceLocationOffset(property.value.quasi), + }); }); } else { invariant( @@ -123,17 +102,11 @@ function find( node.callee.name, getSourceTextForLocation(text, fragments.tag.loc), ); - const template = getGraphQLText(fragments.quasi); - if (validateNames) { - validateTemplate( - template, - moduleName, - null, - filePath, - getSourceLocationOffset(fragments.quasi), - ); - } - result.push(template); + result.push({ + keyName: null, + template: getGraphQLText(fragments.quasi), + sourceLocationOffset: getSourceLocationOffset(fragments.quasi), + }); } // Visit remaining arguments @@ -143,17 +116,11 @@ function find( }, TaggedTemplateExpression: node => { if (isGraphQLTag(node.tag)) { - const template = getGraphQLText(node.quasi); - if (validateNames) { - validateTemplate( - template, - moduleName, - null, - filePath, - getSourceLocationOffset(node.quasi), - ); - } - result.push(node.quasi.quasis[0].value.raw); + result.push({ + keyName: null, + template: node.quasi.quasis[0].value.raw, + sourceLocationOffset: getSourceLocationOffset(node.quasi), + }); } }, }; @@ -161,28 +128,6 @@ function find( return result; } -const cache = new RelayCompilerCache('FindGraphQLTags', 'v1'); - -function memoizedFind( - text: string, - baseDir: string, - file: File, - options: Options, -): Array { - invariant( - file.exists, - 'FindGraphQLTags: Called with non-existent file `%s`', - file.relPath, - ); - return cache.getOrCompute( - file.hash + (options.validateNames ? '1' : '0'), - () => { - const absPath = path.join(baseDir, file.relPath); - return find(text, absPath, options); - }, - ); -} - const CREATE_CONTAINER_FUNCTIONS = Object.create(null, { createFragmentContainer: {value: true}, createPaginationContainer: {value: true}, @@ -213,7 +158,7 @@ function getTemplateNode(quasi) { return quasis[0]; } -function getGraphQLText(quasi) { +function getGraphQLText(quasi): string { return getTemplateNode(quasi).value.raw; } @@ -235,50 +180,6 @@ function getSourceTextForLocation(text, loc) { return lines.join('\n'); } -function validateTemplate(template, moduleName, keyName, filePath, loc) { - const ast = graphql.parse(new graphql.Source(template, filePath, loc)); - ast.definitions.forEach((def: any) => { - invariant( - def.name, - 'FindGraphQLTags: In module `%s`, a definition of kind `%s` requires a name.', - moduleName, - def.kind, - ); - const definitionName = def.name.value; - if (def.kind === 'OperationDefinition') { - const operationNameParts = definitionName.match( - /^(.*)(Mutation|Query|Subscription)$/, - ); - invariant( - operationNameParts && definitionName.startsWith(moduleName), - 'FindGraphQLTags: Operation names in graphql tags must be prefixed ' + - 'with the module name and end in "Mutation", "Query", or ' + - '"Subscription". Got `%s` in module `%s`.', - definitionName, - moduleName, - ); - } else if (def.kind === 'FragmentDefinition') { - if (keyName) { - invariant( - definitionName === moduleName + '_' + keyName, - 'FindGraphQLTags: Container fragment names must be ' + - '`_`. Got `%s`, expected `%s`.', - definitionName, - moduleName + '_' + keyName, - ); - } else { - invariant( - definitionName.startsWith(moduleName), - 'FindGraphQLTags: Fragment names in graphql tags must be prefixed ' + - 'with the module name. Got `%s` in module `%s`.', - definitionName, - moduleName, - ); - } - } - }); -} - function invariant(condition, msg, ...args) { if (!condition) { throw new Error(util.format(msg, ...args)); @@ -314,5 +215,4 @@ function traverse(node, visitors) { module.exports = { find: Profiler.instrument(find, 'FindGraphQLTags.find'), - memoizedFind, }; diff --git a/packages/relay-compiler/core/RelayFlowBabelFactories.js b/packages/relay-compiler/language/javascript/RelayFlowBabelFactories.js similarity index 100% rename from packages/relay-compiler/core/RelayFlowBabelFactories.js rename to packages/relay-compiler/language/javascript/RelayFlowBabelFactories.js diff --git a/packages/relay-compiler/core/RelayFlowGenerator.js b/packages/relay-compiler/language/javascript/RelayFlowGenerator.js similarity index 94% rename from packages/relay-compiler/core/RelayFlowGenerator.js rename to packages/relay-compiler/language/javascript/RelayFlowGenerator.js index 99aab19584dab..09465d538fd4f 100644 --- a/packages/relay-compiler/core/RelayFlowGenerator.js +++ b/packages/relay-compiler/language/javascript/RelayFlowGenerator.js @@ -11,8 +11,8 @@ 'use strict'; const babelGenerator = require('@babel/generator').default; -const RelayMaskTransform = require('../transforms/RelayMaskTransform'); -const RelayRelayDirectiveTransform = require('../transforms/RelayRelayDirectiveTransform'); +const RelayMaskTransform = require('../../transforms/RelayMaskTransform'); +const RelayRelayDirectiveTransform = require('../../transforms/RelayRelayDirectiveTransform'); const invariant = require('invariant'); const nullthrows = require('nullthrows'); @@ -41,24 +41,14 @@ const { SchemaUtils, } = require('graphql-compiler'); -import type {ScalarTypeMapping} from './RelayFlowTypeTransformers'; +import type {TypeGeneratorOptions} from '../RelayLanguagePluginInterface'; import type {IRTransform, Fragment, Root} from 'graphql-compiler'; import type {GraphQLEnumType} from 'graphql'; const {isAbstractType} = SchemaUtils; -type Options = {| - +customScalars: ScalarTypeMapping, - +useHaste: boolean, - +enumsHasteModule: ?string, - +existingFragmentNames: Set, - +inputFieldWhiteList: $ReadOnlyArray, - +relayRuntimeModule: string, - +noFutureProofEnums: boolean, -|}; - export type State = {| - ...Options, + ...TypeGeneratorOptions, +generatedFragments: Set, +generatedInputObjectTypes: { [name: string]: GraphQLInputObjectType | 'pending', @@ -67,7 +57,10 @@ export type State = {| +usedFragments: Set, |}; -function generate(node: Root | Fragment, options: Options): string { +function generate( + node: Root | Fragment, + options: TypeGeneratorOptions, +): string { const ast = IRVisitor.visit(node, createVisitor(options)); return babelGenerator(ast).code; } @@ -254,7 +247,7 @@ function isPlural(node: Fragment): boolean { return Boolean(node.metadata && node.metadata.plural); } -function createVisitor(options: Options) { +function createVisitor(options: TypeGeneratorOptions) { const state = { customScalars: options.customScalars, enumsHasteModule: options.enumsHasteModule, @@ -266,6 +259,7 @@ function createVisitor(options: Options) { usedEnums: {}, usedFragments: new Set(), useHaste: options.useHaste, + useSingleArtifactDirectory: options.useSingleArtifactDirectory, noFutureProofEnums: options.noFutureProofEnums, }; @@ -484,6 +478,13 @@ function getFragmentImports(state: State) { // TODO(T22653277) support non-haste environments when importing // fragments imports.push(importTypes([refTypeName], usedFragment + '.graphql')); + } else if ( + state.useSingleArtifactDirectory && + state.existingFragmentNames.has(usedFragment) + ) { + imports.push( + importTypes([refTypeName], './' + usedFragment + '.graphql'), + ); } else { imports.push(anyTypeAlias(refTypeName)); } @@ -532,5 +533,5 @@ const FLOW_TRANSFORMS: Array = [ module.exports = { generate: Profiler.instrument(generate, 'RelayFlowGenerator.generate'), - flowTransforms: FLOW_TRANSFORMS, + transforms: FLOW_TRANSFORMS, }; diff --git a/packages/relay-compiler/core/RelayFlowTypeTransformers.js b/packages/relay-compiler/language/javascript/RelayFlowTypeTransformers.js similarity index 100% rename from packages/relay-compiler/core/RelayFlowTypeTransformers.js rename to packages/relay-compiler/language/javascript/RelayFlowTypeTransformers.js diff --git a/packages/relay-compiler/language/javascript/RelayLanguagePluginJavaScript.js b/packages/relay-compiler/language/javascript/RelayLanguagePluginJavaScript.js new file mode 100644 index 0000000000000..ed7faef591421 --- /dev/null +++ b/packages/relay-compiler/language/javascript/RelayLanguagePluginJavaScript.js @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +const RelayFlowGenerator = require('./RelayFlowGenerator'); +const formatGeneratedModule = require('./formatGeneratedModule'); +const {find} = require('./FindGraphQLTags'); + +import type {PluginInterface} from '../RelayLanguagePluginInterface'; + +module.exports = (): PluginInterface => ({ + inputExtensions: ['js', 'jsx'], + outputExtension: 'js', + typeGenerator: RelayFlowGenerator, + formatModule: formatGeneratedModule, + findGraphQLTags: find, +}); diff --git a/packages/relay-compiler/language/javascript/__tests__/FindGraphQLTags-test.js b/packages/relay-compiler/language/javascript/__tests__/FindGraphQLTags-test.js new file mode 100644 index 0000000000000..16ae47447df3a --- /dev/null +++ b/packages/relay-compiler/language/javascript/__tests__/FindGraphQLTags-test.js @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * 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 FindGraphQLTags = require('FindGraphQLTags'); + +describe('FindGraphQLTags', () => { + function find(text) { + return FindGraphQLTags.find(text).map(tag => tag.template); + } + + describe('query parsing', () => { + it('parses a simple file', () => { + expect(find('const foo = 1;')).toEqual([]); + }); + + it('parses graphql templates', () => { + expect( + find(` + const foo = 1; + foo(graphql\`fragment FindGraphQLTags on User { id }\`); + graphql\`fragment FindGraphQLTags on User { name }\`; + + createFragmentContainer(Component, { + foo: graphql\`fragment FindGraphQLTags_foo on Page { id }\`, + }); + createPaginationContainer( + Component, + {}, + { + query: graphql\`query FindGraphQLTagsPaginationQuery { me { id } }\`, + } + ); + createRefetchContainer( + Component, + {}, + graphql\`query FindGraphQLTagsRefetchQuery { me { id } }\` + ); + + Relay.createFragmentContainer(Component, { + foo: graphql\`fragment FindGraphQLTags_foo on Page { name }\`, + }); + Relay.createPaginationContainer( + Component, + {}, + { + query: graphql\`query FindGraphQLTagsPaginationQuery { me { name } }\`, + } + ); + Relay.createRefetchContainer( + Component, + {}, + graphql\`query FindGraphQLTagsRefetchQuery { me { name } }\` + ); + `), + ).toEqual([ + 'fragment FindGraphQLTags on User { id }', + 'fragment FindGraphQLTags on User { name }', + 'fragment FindGraphQLTags_foo on Page { id }', + 'query FindGraphQLTagsPaginationQuery { me { id } }', + 'query FindGraphQLTagsRefetchQuery { me { id } }', + 'fragment FindGraphQLTags_foo on Page { name }', + 'query FindGraphQLTagsPaginationQuery { me { name } }', + 'query FindGraphQLTagsRefetchQuery { me { name } }', + ]); + }); + + it('parses modern JS syntax with Flow annotations', () => { + expect( + find(` + class RelayContainer extends React.Component { + // graphql\`this in a comment\`; + _loadMore = ( + pageSize: number, + options?: ?RefetchOptions + ): ?Disposable => { + graphql\`fragment FindGraphQLTags on User { id }\`; + } + render() { + return <>A Fragment!; + } + } + `), + ).toEqual(['fragment FindGraphQLTags on User { id }']); + }); + + it('parses JS with functions sharing names with object prototype methods', () => { + expect( + find(` + toString(); + foo(graphql\`fragment FindGraphQLTags on User { id }\`); + `), + ).toEqual(['fragment FindGraphQLTags on User { id }']); + }); + }); +}); diff --git a/packages/relay-compiler/core/__tests__/RelayFlowGenerator-test.js b/packages/relay-compiler/language/javascript/__tests__/RelayFlowGenerator-test.js similarity index 78% rename from packages/relay-compiler/core/__tests__/RelayFlowGenerator-test.js rename to packages/relay-compiler/language/javascript/__tests__/RelayFlowGenerator-test.js index 5833d93aa7506..d340be5b9d0d8 100644 --- a/packages/relay-compiler/core/__tests__/RelayFlowGenerator-test.js +++ b/packages/relay-compiler/language/javascript/__tests__/RelayFlowGenerator-test.js @@ -20,7 +20,9 @@ const parseGraphQLText = require('parseGraphQLText'); const {transformASTSchema} = require('ASTConvert'); const {generateTestsFromFixtures} = require('RelayModernTestUtils'); -function generate(text, options) { +import type {TypeGeneratorOptions} from '../../RelayLanguagePluginInterface'; + +function generate(text, options: TypeGeneratorOptions) { const schema = transformASTSchema(RelayTestSchema, [ RelayRelayDirectiveTransform.SCHEMA_EXTENSION, ` @@ -33,7 +35,7 @@ function generate(text, options) { const {definitions} = parseGraphQLText(schema, text); return new GraphQLCompilerContext(RelayTestSchema, schema) .addAll(definitions) - .applyTransforms(RelayFlowGenerator.flowTransforms) + .applyTransforms(RelayFlowGenerator.transforms) .documents() .map(doc => RelayFlowGenerator.generate(doc, options)) .join('\n\n'); @@ -48,6 +50,7 @@ describe('RelayFlowGenerator', () => { inputFieldWhiteList: [], relayRuntimeModule: 'relay-runtime', useHaste: true, + useSingleArtifactDirectory: false, }), ); @@ -64,6 +67,7 @@ describe('RelayFlowGenerator', () => { inputFieldWhiteList: [], relayRuntimeModule: 'relay-runtime', useHaste: true, + useSingleArtifactDirectory: false, // This is what's different from the tests above. noFutureProofEnums: true, }); @@ -112,4 +116,25 @@ describe('RelayFlowGenerator', () => { expect(types).toContain('+name: ?LocalizedString,'); }); }); + + it('imports fragment refs from siblings in a single artifact dir', () => { + const text = ` + fragment Picture on Image { + ...PhotoFragment + } + `; + const types = generate(text, { + customScalars: {}, + enumsHasteModule: null, + existingFragmentNames: new Set(['PhotoFragment']), + inputFieldWhiteList: [], + relayRuntimeModule: 'relay-runtime', + // This is what's different from the tests above. + useHaste: false, + useSingleArtifactDirectory: true, + }); + expect(types).toContain( + 'import type { PhotoFragment$ref } from "./PhotoFragment.graphql";', + ); + }); }); diff --git a/packages/relay-compiler/core/__tests__/__snapshots__/RelayFlowGenerator-test.js.snap b/packages/relay-compiler/language/javascript/__tests__/__snapshots__/RelayFlowGenerator-test.js.snap similarity index 100% rename from packages/relay-compiler/core/__tests__/__snapshots__/RelayFlowGenerator-test.js.snap rename to packages/relay-compiler/language/javascript/__tests__/__snapshots__/RelayFlowGenerator-test.js.snap diff --git a/packages/relay-compiler/core/__tests__/fixtures/flow-generator/conditional.graphql b/packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/conditional.graphql similarity index 100% rename from packages/relay-compiler/core/__tests__/fixtures/flow-generator/conditional.graphql rename to packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/conditional.graphql diff --git a/packages/relay-compiler/core/__tests__/fixtures/flow-generator/fragment-spread.graphql b/packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/fragment-spread.graphql similarity index 100% rename from packages/relay-compiler/core/__tests__/fixtures/flow-generator/fragment-spread.graphql rename to packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/fragment-spread.graphql diff --git a/packages/relay-compiler/core/__tests__/fixtures/flow-generator/inline-fragment.graphql b/packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/inline-fragment.graphql similarity index 100% rename from packages/relay-compiler/core/__tests__/fixtures/flow-generator/inline-fragment.graphql rename to packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/inline-fragment.graphql diff --git a/packages/relay-compiler/core/__tests__/fixtures/flow-generator/linked-field.graphql b/packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/linked-field.graphql similarity index 100% rename from packages/relay-compiler/core/__tests__/fixtures/flow-generator/linked-field.graphql rename to packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/linked-field.graphql diff --git a/packages/relay-compiler/core/__tests__/fixtures/flow-generator/mutation-input-has-array.graphql b/packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/mutation-input-has-array.graphql similarity index 100% rename from packages/relay-compiler/core/__tests__/fixtures/flow-generator/mutation-input-has-array.graphql rename to packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/mutation-input-has-array.graphql diff --git a/packages/relay-compiler/core/__tests__/fixtures/flow-generator/mutation.graphql b/packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/mutation.graphql similarity index 100% rename from packages/relay-compiler/core/__tests__/fixtures/flow-generator/mutation.graphql rename to packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/mutation.graphql diff --git a/packages/relay-compiler/core/__tests__/fixtures/flow-generator/plural-fragment.graphql b/packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/plural-fragment.graphql similarity index 100% rename from packages/relay-compiler/core/__tests__/fixtures/flow-generator/plural-fragment.graphql rename to packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/plural-fragment.graphql diff --git a/packages/relay-compiler/core/__tests__/fixtures/flow-generator/recursive-fragments.graphql b/packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/recursive-fragments.graphql similarity index 100% rename from packages/relay-compiler/core/__tests__/fixtures/flow-generator/recursive-fragments.graphql rename to packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/recursive-fragments.graphql diff --git a/packages/relay-compiler/core/__tests__/fixtures/flow-generator/roots.graphql b/packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/roots.graphql similarity index 100% rename from packages/relay-compiler/core/__tests__/fixtures/flow-generator/roots.graphql rename to packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/roots.graphql diff --git a/packages/relay-compiler/core/__tests__/fixtures/flow-generator/scalar-field.graphql b/packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/scalar-field.graphql similarity index 100% rename from packages/relay-compiler/core/__tests__/fixtures/flow-generator/scalar-field.graphql rename to packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/scalar-field.graphql diff --git a/packages/relay-compiler/core/__tests__/fixtures/flow-generator/typename-on-union.graphql b/packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/typename-on-union.graphql similarity index 100% rename from packages/relay-compiler/core/__tests__/fixtures/flow-generator/typename-on-union.graphql rename to packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/typename-on-union.graphql diff --git a/packages/relay-compiler/core/__tests__/fixtures/flow-generator/unmasked-fragment-spreads.graphql b/packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/unmasked-fragment-spreads.graphql similarity index 100% rename from packages/relay-compiler/core/__tests__/fixtures/flow-generator/unmasked-fragment-spreads.graphql rename to packages/relay-compiler/language/javascript/__tests__/fixtures/flow-generator/unmasked-fragment-spreads.graphql diff --git a/packages/relay-compiler/codegen/formatGeneratedModule.js b/packages/relay-compiler/language/javascript/formatGeneratedModule.js similarity index 70% rename from packages/relay-compiler/codegen/formatGeneratedModule.js rename to packages/relay-compiler/language/javascript/formatGeneratedModule.js index 9522c9f37469c..ef0b26175111b 100644 --- a/packages/relay-compiler/codegen/formatGeneratedModule.js +++ b/packages/relay-compiler/language/javascript/formatGeneratedModule.js @@ -10,18 +10,21 @@ 'use strict'; -import type {FormatModule} from './writeRelayGeneratedFile'; +import type {FormatModule} from '../RelayLanguagePluginInterface'; const formatGeneratedModule: FormatModule = ({ moduleName, documentType, docText, concreteText, - flowText, + typeText, hash, relayRuntimeModule, sourceHash, }) => { + const documentTypeImport = documentType + ? `import type { ${documentType} } from '${relayRuntimeModule}';` + : ''; const docTextComment = docText ? '\n/*\n' + docText.trim() + '\n*/\n' : ''; const hashText = hash ? `\n * ${hash}` : ''; return `/** @@ -33,12 +36,12 @@ const formatGeneratedModule: FormatModule = ({ 'use strict'; /*:: -import type { ${documentType} } from '${relayRuntimeModule}'; -${flowText || ''} +${documentTypeImport} +${typeText || ''} */ ${docTextComment} -const node/*: ${documentType}*/ = ${concreteText}; +const node/*: ${documentType || 'empty'}*/ = ${concreteText}; // prettier-ignore (node/*: any*/).hash = '${sourceHash}'; module.exports = node; diff --git a/packages/relay-runtime/query/RelayModernGraphQLTag.js b/packages/relay-runtime/query/RelayModernGraphQLTag.js index f139f7d6de0a0..35ea410bd8cab 100644 --- a/packages/relay-runtime/query/RelayModernGraphQLTag.js +++ b/packages/relay-runtime/query/RelayModernGraphQLTag.js @@ -50,7 +50,9 @@ function getNode(taggedNode) { if (typeof fn !== 'function') { return (taggedNode: any); } - return fn(); + const data: any = fn(); + // Support for languages that work (best) with ES6 modules, such as TypeScript. + return data.default ? data.default : data; } function getFragment(taggedNode: GraphQLTaggedNode): ConcreteFragment {