diff --git a/README.md b/README.md index f4954a1ee2a..2bf31ae410f 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,7 @@ The [User Guide](https://github.com/facebookincubator/create-react-app/blob/mast - [Adding `` and `` Tags](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#adding-link-and-meta-tags) - [Running Tests](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#running-tests) - [Deployment](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#deployment) +- [Relay and GraphQl Support](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#relay-support) A copy of the user guide will be created as `README.md` in your project folder. diff --git a/packages/react-scripts/config/babel.dev.js b/packages/react-scripts/config/babel.dev.js index 0113b847824..236bf524d12 100644 --- a/packages/react-scripts/config/babel.dev.js +++ b/packages/react-scripts/config/babel.dev.js @@ -11,6 +11,7 @@ var path = require('path'); var findCacheDir = require('find-cache-dir'); +var relayPlugin = require('../plugins/relay'); module.exports = { // Don't try to find .babelrc because we want to force this configuration. @@ -49,3 +50,9 @@ module.exports = { }] ] }; + +// optional relay support https://facebook.github.io/relay +if (relayPlugin.isEnabled()) { + // relay QL babel transform needs to run before react https://facebook.github.io/relay/docs/guides-babel-plugin.html#react-native-configuration + module.exports.plugins.unshift(require.resolve('../plugins/relay/babelRelayPlugin')); +} diff --git a/packages/react-scripts/config/babel.prod.js b/packages/react-scripts/config/babel.prod.js index f53094ababd..6cf95ca7c3d 100644 --- a/packages/react-scripts/config/babel.prod.js +++ b/packages/react-scripts/config/babel.prod.js @@ -10,6 +10,7 @@ // @remove-on-eject-end var path = require('path'); +var relayPlugin = require('../plugins/relay'); module.exports = { // Don't try to find .babelrc because we want to force this configuration. @@ -47,3 +48,9 @@ module.exports = { // require.resolve('babel-plugin-transform-react-constant-elements') ] }; + +// optional relay support https://facebook.github.io/relay +if (relayPlugin.isEnabled()) { + // relay QL babel transform needs to run before react https://facebook.github.io/relay/docs/guides-babel-plugin.html#react-native-configuration + module.exports.plugins.unshift(require.resolve('../plugins/relay/babelRelayPlugin')); +} diff --git a/packages/react-scripts/config/paths.js b/packages/react-scripts/config/paths.js index 314bcc2051f..03a02ba57c7 100644 --- a/packages/react-scripts/config/paths.js +++ b/packages/react-scripts/config/paths.js @@ -43,8 +43,10 @@ module.exports = { appPackageJson: resolveApp('package.json'), appSrc: resolveApp('src'), testsSetup: resolveApp('src/setupTests.js'), + relaySchema: resolveApp('schema.json'), appNodeModules: resolveApp('node_modules'), ownNodeModules: resolveApp('node_modules'), + plugins: resolveApp('plugins'), nodePaths: nodePaths }; @@ -61,9 +63,11 @@ module.exports = { appPackageJson: resolveApp('package.json'), appSrc: resolveApp('src'), testsSetup: resolveApp('src/setupTests.js'), + relaySchema: resolveApp('schema.json'), appNodeModules: resolveApp('node_modules'), // this is empty with npm3 but node resolution searches higher anyway: ownNodeModules: resolveOwn('../node_modules'), + plugins: resolveApp('plugins'), nodePaths: nodePaths }; // @remove-on-eject-end @@ -76,8 +80,10 @@ module.exports = { appPackageJson: resolveOwn('../package.json'), appSrc: resolveOwn('../template/src'), testsSetup: resolveOwn('../template/src/setupTests.js'), + relaySchema: resolveOwn('../schema.json'), appNodeModules: resolveOwn('../node_modules'), ownNodeModules: resolveOwn('../node_modules'), + plugins: resolveOwn('../plugins'), nodePaths: nodePaths }; // @remove-on-publish-end diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index ced35306854..a80b288c6e1 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -13,6 +13,7 @@ "files": [ "bin", "config", + "plugins", "scripts", "template" ], @@ -32,6 +33,7 @@ "babel-plugin-transform-runtime": "6.15.0", "babel-preset-latest": "6.14.0", "babel-preset-react": "6.11.1", + "babel-relay-plugin": "0.9.3", "babel-runtime": "6.11.6", "case-sensitive-paths-webpack-plugin": "1.1.4", "chalk": "1.1.3", @@ -50,12 +52,14 @@ "filesize": "3.3.0", "find-cache-dir": "0.1.1", "fs-extra": "0.30.0", + "graphql": "0.7.0", "gzip-size": "3.0.0", "html-loader": "0.4.3", "html-webpack-plugin": "2.22.0", "http-proxy-middleware": "0.17.1", "jest": "15.1.1", "json-loader": "0.5.4", + "node-fetch": "1.6.1", "object-assign": "4.1.0", "opn": "4.0.2", "path-exists": "2.1.0", diff --git a/packages/react-scripts/plugins/relay/babelRelayPlugin.js b/packages/react-scripts/plugins/relay/babelRelayPlugin.js new file mode 100644 index 00000000000..491e858bde0 --- /dev/null +++ b/packages/react-scripts/plugins/relay/babelRelayPlugin.js @@ -0,0 +1,18 @@ +// @remove-on-eject-begin +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @relay + */ +// @remove-on-eject-end + +var paths = require('../../config/paths'); +var getbabelRelayPlugin = require('babel-relay-plugin'); + +var schema = require(paths.relaySchema); + +module.exports = getbabelRelayPlugin(schema.data); diff --git a/packages/react-scripts/plugins/relay/index.js b/packages/react-scripts/plugins/relay/index.js new file mode 100644 index 00000000000..10182920e0a --- /dev/null +++ b/packages/react-scripts/plugins/relay/index.js @@ -0,0 +1,153 @@ +// @remove-on-eject-begin +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @relay + */ +// @remove-on-eject-end + +var paths = require('../../config/paths'); +var chalk = require('chalk'); +var fetch = require('node-fetch'); +var graphQlutilities = require('graphql/utilities'); +var fs = require('fs'); + +/** + * relay requires 'react-relay' install and in package.json, GRAPHQL_URL env variable set to retrieve local copy of schema.json resulting from `npm run fetchRelaySchema` + */ +var isEnabled = function() { + var appPackageJson = require(paths.appPackageJson); + if (appPackageJson && appPackageJson.dependencies && appPackageJson.dependencies['react-relay']) { + + return true; + } + + return false +} + +var requireGraphQlConfig = function() { + return new Promise((resolve, reject) => { + //TODO we could use graphql-config package here instead + + // check that required env var REACT_APP_GRAPHQL_URL is setup + if (!process.env.REACT_APP_GRAPHQL_URL) { + // console.log(chalk.red('Relay requires a url to your graphql server')); + // console.log('Specifiy this in a ' + chalk.cyan('REACT_APP_GRAPHQL_URL') + ' environment variable.'); + // console.log(); + // process.exit(1); + + var errorMessage = chalk.red('Relay requires a url to your graphql server\n') + + 'Specifiy this in a ' + chalk.cyan('REACT_APP_GRAPHQL_URL') + ' environment variable.'; + reject(new Error(errorMessage)); + } + + console.log("Relay support - graphql configured successfully"); + resolve(); + }) +} + +var validateSchemaJson = function() { + return new Promise((resolve, reject) => { + // check that schema.json is there and is readable + try { + var schemaFileContents = fs.readFileSync(paths.relaySchema); + } catch (err) { + // console.log(chalk.red('babel-relay-plugin requires a local copy of your graphql schema')); + // console.log('Run ' + chalk.cyan('npm run fetchRelaySchema') + ' to fetch it from the ' + chalk.cyan('REACT_APP_GRAPHQL_URL') + ' environment variable.'); + // console.log(); + // console.error(err); + // console.log(); + // process.exit(1) + var errorMessage = chalk.red('babel-relay-plugin requires a local copy of your graphql schema\n') + + 'Run ' + chalk.cyan('npm run fetchRelaySchema') + ' to fetch it from the ' + chalk.cyan('REACT_APP_GRAPHQL_URL') + ' environment variable.'; + reject(new Error(errorMessage)); + } + + // check that schema.json is valid json + try { + var schemaJSON = JSON.parse(schemaFileContents); + } catch (err) { + // console.log(chalk.red('JSON parsing of the contents of ' + chalk.cyan(paths.relaySchema) + ' failed.')); + // console.log('Check the contents of ' + chalk.cyan(paths.relaySchema) + '. It does not appear to be valid json'); + // console.log('Also try running ' + chalk.cyan('npm run fetchRelaySchema') + ' to re-fetch the schema.json from the ' + chalk.cyan('REACT_APP_GRAPHQL_URL') + ' environment variable.'); + // console.log(); + // console.error(err); + // console.log(); + // process.exit(1) + var errorMessage = chalk.red('JSON parsing of the contents of ' + chalk.cyan(paths.relaySchema) + ' failed.\n') + + 'Check the contents of ' + chalk.cyan(paths.relaySchema) + '. It does not appear to be valid json\n' + + 'Also try running ' + chalk.cyan('npm run fetchRelaySchema') + ' to re-fetch the schema.json from the ' + chalk.cyan('REACT_APP_GRAPHQL_URL') + ' environment variable.'; + reject(new Error(errorMessage)); + } + + // check contents of schema.json has valid graphql schema + try { + var graphQLSchema = graphQlutilities.buildClientSchema(schemaJSON.data); + } catch (err) { + // console.log(chalk.red('Could not parse the contents of schema.json into a valid graphql schema that is compatiable with this version of Relay and babel-relay-plugin')); + // console.log('Upgrading graphql library on your server may be a solution.'); + // console.log(); + // console.error(err); + // console.log(); + // process.exit(1) + var errorMessage = chalk.red('Could not parse the contents of schema.json into a valid graphql schema that is compatiable with this version of Relay and babel-relay-plugin\n') + + 'Upgrading graphql library on your server may be a solution.'; + reject(new Error(errorMessage)); + } + + console.log('Relay support - schema.json is found and valid'); + resolve(); + }); +} + +// retreive JSON of graaphql schema via introspection for Babel Relay Plugin to use +var fetchRelaySchema = function() { + return fetch(process.env.REACT_APP_GRAPHQL_URL, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({'query': graphQlutilities.introspectionQuery}), + }) + .then(res => res.json()).then(schemaJSON => { + // verify that the schemaJSON is valid a graphQL Schema + var graphQLSchema = graphQlutilities.buildClientSchema(schemaJSON.data); + // Save relay compatible schema.json + fs.writeFileSync(paths.relaySchema, JSON.stringify(schemaJSON, null, 2)); + + // Save user readable schema.graphql + fs.writeFileSync(paths.relaySchema.replace('.json','.graphql'), graphQlutilities.printSchema(graphQLSchema)); + + console.log('Relay support - fetch schema.json from ' + chalk.cyan(process.env.REACT_APP_GRAPHQL_URL)); + }); +} + +// build does not fetch the relay schema, only uses local copy of shema.js. +// this helps, but does not guaruntee that builds, dev, and tests use the same version of the schema. +var build = function() { + return requireGraphQlConfig() + .then(validateSchemaJson) + .then(() => { + console.log(chalk.green('Relay support built successfully!')); + }) +} + +var start = function() { + return requireGraphQlConfig() + .then(fetchRelaySchema) + .then(validateSchemaJson) + .then(() => { + console.log(chalk.green('Relay support enabled successfully!')); + }) +} + +module.exports = { + isEnabled: isEnabled, + build: build, + start: start +}; diff --git a/packages/react-scripts/scripts/build.js b/packages/react-scripts/scripts/build.js index 71dcc798bba..12c12b2f72e 100644 --- a/packages/react-scripts/scripts/build.js +++ b/packages/react-scripts/scripts/build.js @@ -24,6 +24,7 @@ var paths = require('../config/paths'); var checkRequiredFiles = require('./utils/checkRequiredFiles'); var recursive = require('recursive-readdir'); var stripAnsi = require('strip-ansi'); +var plugins = require('./utils/plugins'); checkRequiredFiles(); @@ -54,23 +55,31 @@ function getDifferenceLabel(currentSize, previousSize) { // First, read the current file sizes in build directory. // This lets us display how much they changed later. -recursive(paths.appBuild, (err, fileNames) => { - var previousSizeMap = (fileNames || []) - .filter(fileName => /\.(js|css)$/.test(fileName)) - .reduce((memo, fileName) => { - var contents = fs.readFileSync(fileName); - var key = removeFileNameHash(fileName); - memo[key] = gzipSize(contents); - return memo; - }, {}); +plugins.build() + .then(() => { + recursive(paths.appBuild, (err, fileNames) => { + var previousSizeMap = (fileNames || []) + .filter(fileName => /\.(js|css)$/.test(fileName)) + .reduce((memo, fileName) => { + var contents = fs.readFileSync(fileName); + var key = removeFileNameHash(fileName); + memo[key] = gzipSize(contents); + return memo; + }, {}); - // Remove all content but keep the directory so that - // if you're in it, you don't end up in Trash - rimrafSync(paths.appBuild + '/*'); + // Remove all content but keep the directory so that + // if you're in it, you don't end up in Trash + rimrafSync(paths.appBuild + '/*'); - // Start the webpack build - build(previousSizeMap); -}); + // Start the webpack build + build(previousSizeMap); + }); + }) + .catch((err) => { + console.error(err); + console.log(); + process.exit(1) + }); // Print a detailed summary of build files. function printFileSizes(stats, previousSizeMap) { diff --git a/packages/react-scripts/scripts/eject.js b/packages/react-scripts/scripts/eject.js index 74c5c9ef99b..b0d04cd888e 100644 --- a/packages/react-scripts/scripts/eject.js +++ b/packages/react-scripts/scripts/eject.js @@ -47,7 +47,10 @@ prompt( path.join('scripts', 'utils', 'checkRequiredFiles.js'), path.join('scripts', 'utils', 'chrome.applescript'), path.join('scripts', 'utils', 'prompt.js'), - path.join('scripts', 'utils', 'WatchMissingNodeModulesPlugin.js') + path.join('scripts', 'utils', 'plugins.js'), + path.join('scripts', 'utils', 'WatchMissingNodeModulesPlugin.js'), + path.join('plugins', 'relay', 'index.js'), + path.join('plugins', 'relay', 'babelRelayPlugin.js') ]; // Ensure that the app folder is clean and we won't override any files @@ -69,6 +72,8 @@ prompt( fs.mkdirSync(path.join(appPath, 'config', 'jest')); fs.mkdirSync(path.join(appPath, 'scripts')); fs.mkdirSync(path.join(appPath, 'scripts', 'utils')); + fs.mkdirSync(path.join(appPath, 'plugins')); + fs.mkdirSync(path.join(appPath, 'plugins', 'relay')); files.forEach(function(file) { console.log('Copying ' + file + ' to ' + appPath); diff --git a/packages/react-scripts/scripts/start.js b/packages/react-scripts/scripts/start.js index b0290bfc492..0762e653922 100644 --- a/packages/react-scripts/scripts/start.js +++ b/packages/react-scripts/scripts/start.js @@ -24,6 +24,7 @@ var checkRequiredFiles = require('./utils/checkRequiredFiles'); var prompt = require('./utils/prompt'); var config = require('../config/webpack.config.dev'); var paths = require('../config/paths'); +var plugins = require('./utils/plugins'); // Tools like Cloud9 rely on this. var DEFAULT_PORT = process.env.PORT || 3000; @@ -311,9 +312,17 @@ function runDevServer(port, protocol) { function run(port) { var protocol = process.env.HTTPS === 'true' ? "https" : "http"; - checkRequiredFiles(); - setupCompiler(port, protocol); - runDevServer(port, protocol); + plugins.start() + .then(() => { + checkRequiredFiles(); + setupCompiler(port, protocol); + runDevServer(port, protocol); + }) + .catch((err) => { + console.error(err); + console.log(); + process.exit(1) + }); } // We attempt to use the default port but if it is busy, we offer the user to diff --git a/packages/react-scripts/scripts/utils/plugins.js b/packages/react-scripts/scripts/utils/plugins.js new file mode 100644 index 00000000000..62ac66b3494 --- /dev/null +++ b/packages/react-scripts/scripts/utils/plugins.js @@ -0,0 +1,29 @@ +// @remove-on-eject-begin +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +// @remove-on-eject-end + +var relayPlugin = require('../../plugins/relay'); + +var start = function() { + return Promise.all([ + (relayPlugin.isEnabled()) ? relayPlugin.start() : false, + ]) +} + +var build = function() { + return Promise.all([ + (relayPlugin.isEnabled()) ? relayPlugin.build() : false, + ]) +} + +module.exports = { + start: start, + build: build, +} diff --git a/packages/react-scripts/template/README.md b/packages/react-scripts/template/README.md index 085406077b5..e2588781553 100644 --- a/packages/react-scripts/template/README.md +++ b/packages/react-scripts/template/README.md @@ -29,6 +29,7 @@ You can find the most recent version of this guide [here](https://github.com/fac - [Adding `` and `` Tags](#adding-link-and-meta-tags) - [Referring to Static Assets from ``](#referring-to-static-assets-from-link-href) - [Generating Dynamic `` Tags on the Server](#generating-dynamic-meta-tags-on-the-server) +- [Relay & GraphQL Support](#relay-support) - [Running Tests](#running-tests) - [Filename Conventions](#filename-conventions) - [Command Line Interface](#command-line-interface) @@ -603,6 +604,101 @@ Then, on the server, regardless of the backend you use, you can read `index.html If you use a Node server, you can even share the route matching logic between the client and the server. However duplicating it also works fine in simple cases. +## Relay Support + +You can create a [Relay](https://facebook.github.io/relay/) application with create-react-app. You will need a standalone GraphQL server ([CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) enabled, most likely) for your create-react-app to connect to. The steps for setting up the standalone GraphQL server are outside the scope of these docs. + +To enable Relay in your create-react-app, install `react-relay` + +```cmd +npm install react-relay --save +``` + +Then define an environment variable `REACT_APP_GRAPHQL_URL` with a value that is the URL to your graphql server (for example `REACT_APP_GRAPHQL_URL=http://localhost:3001/graphql`). See [environment variables](#adding-custom-environment-variables) for more information. + +With your REACT_APP_GRAPHQL_URL environment variable defined, start your app. This could be as simple as: + +```cmd +REACT_APP_GRAPHQL_URL=http://localhost:3001/graphql npm start +``` + +This will configure the `babel-relay-plugin` for you and fetch your graphql schema. Woo! + +Next, add Relay to `my-app/src/index.js`. Setting the `DefaultNetworkLayer` to point to REACT_APP_GRAPHQL_URL is important. Here is an example. Note for this example you also need to `npm install react-router react-router-relay`: + +```js +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; +import './index.css'; +import Relay from 'react-relay'; +import { applyRouterMiddleware, Router, Route, /*Link,*/ browserHistory } from 'react-router'; +import useRelay from 'react-router-relay'; + + +const ViewerQueries = { + viewer: () => Relay.QL`query { viewer }` +}; + +Relay.injectNetworkLayer( + new Relay.DefaultNetworkLayer(process.env.REACT_APP_GRAPHQL_URL) +); + +ReactDOM.render( + + + + , + document.getElementById('root') +); +``` + +Next, add Components and Relay containers to your app. Here is an example of `my-app/src/App.js`. + +```js +import React, { Component } from 'react'; +import logo from './logo.svg'; +import './App.css'; +import Relay from 'react-relay'; + +class App extends Component { + render() { + return ( +
+
+ logo +

Welcome to React

+
+

+ To get started, edit src/App.js and save to reload. +

+

This came from Relay {this.props.viewer.id}

+
+ ); + } +} + +export default Relay.createContainer(App, { + fragments: { + viewer: () => Relay.QL` + fragment on User { + id + } + `, + }, +}); +``` + +>Note: Each time your the schema on your remote graphql server changes, you need to restart your local development server with `npm start`. This will fetch the latest version of your graphql schema so `babel-relay-plugin` can transform Relay.QL queries in your components. + ## Running Tests >Note: this feature is available with `react-scripts@0.3.0` and higher.