From 1b17b95f3a064a48d0ce4e79d9f5d1a0290eff80 Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Fri, 4 Sep 2015 20:16:36 -0700 Subject: [PATCH 01/10] A set of prototyping tools for Relay --- .gitignore | 1 + website-tutorial/HelloApp.js | 40 +++ website-tutorial/HelloSchema.js | 28 ++ website-tutorial/RelayPlayground.css | 186 +++++++++++ website-tutorial/RelayPlayground.js | 305 ++++++++++++++++++ .../babelRelayPlaygroundPlugin.js | 17 + website-tutorial/evalSchema.js | 18 ++ website-tutorial/graphiql.js | 40 +++ website-tutorial/logo.svg | 1 + website-tutorial/package.json | 39 +++ website-tutorial/playground.html | 10 + website-tutorial/playground.js | 28 ++ website-tutorial/webpack.config.js | 76 +++++ website/server/generate.js | 3 + website/server/server.js | 7 + 15 files changed, 799 insertions(+) create mode 100644 website-tutorial/HelloApp.js create mode 100644 website-tutorial/HelloSchema.js create mode 100644 website-tutorial/RelayPlayground.css create mode 100644 website-tutorial/RelayPlayground.js create mode 100644 website-tutorial/babelRelayPlaygroundPlugin.js create mode 100644 website-tutorial/evalSchema.js create mode 100644 website-tutorial/graphiql.js create mode 100644 website-tutorial/logo.svg create mode 100644 website-tutorial/package.json create mode 100644 website-tutorial/playground.html create mode 100644 website-tutorial/playground.js create mode 100644 website-tutorial/webpack.config.js diff --git a/.gitignore b/.gitignore index e48c9b4da5480..8e96906310be5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ npm-debug.log website/build website/src/relay/docs website/src/relay/graphql +website/src/relay/tutorial .nvmrc diff --git a/website-tutorial/HelloApp.js b/website-tutorial/HelloApp.js new file mode 100644 index 0000000000000..5a852601062b1 --- /dev/null +++ b/website-tutorial/HelloApp.js @@ -0,0 +1,40 @@ +class HelloApp extends React.Component { + render() { + return ( +

+ {this.props.greetings.hello} +

+ ); + } +} + +HelloApp = Relay.createContainer(HelloApp, { + fragments: { + greetings: () => Relay.QL` + fragment on Greetings { + hello, + } + `, + } +}); + +class HelloRoute extends Relay.Route { + static routeName = 'Hello'; + static queries = { + greetings: (Component) => Relay.QL` + query GreetingsQuery { + greetings { + ${Component.getFragment('greetings')}, + }, + } + `, + }; +} + +ReactDOM.render( + , + mountNode +); diff --git a/website-tutorial/HelloSchema.js b/website-tutorial/HelloSchema.js new file mode 100644 index 0000000000000..5d381e9227e69 --- /dev/null +++ b/website-tutorial/HelloSchema.js @@ -0,0 +1,28 @@ +import { + GraphQLObjectType, + GraphQLSchema, + GraphQLString, +} from 'graphql'; + +var GREETINGS = { + hello: 'Hello world', +}; + +var GreetingsType = new GraphQLObjectType({ + name: 'Greetings', + fields: () => ({ + hello: {type: GraphQLString}, + }), +}); + +export default new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: () => ({ + greetings: { + type: GreetingsType, + resolve: () => GREETINGS, + }, + }), + }), +}); diff --git a/website-tutorial/RelayPlayground.css b/website-tutorial/RelayPlayground.css new file mode 100644 index 0000000000000..bded585371ec1 --- /dev/null +++ b/website-tutorial/RelayPlayground.css @@ -0,0 +1,186 @@ +@import '~normalize.css'; +@import '~codemirror/lib/codemirror.css'; +@import '~codemirror/theme/solarized.css'; + +body { + font-family: proxima-nova, 'Helvetica Neue', Helvetica, Arial, sans-serif; +} +.rpShell { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; +} +.rpCodeEditor { + bottom: 0; + border-right: 1px solid #eee8d5; + left: 0; + position: absolute; + right: 50%; + right: calc(50% + 1px); + top: 0; +} +.rpCodeEditorNav, .rpResultHeader { + background-color: hsl(345, 7%, 23%); + height: 30px; + line-height: 30px; +} +.rpCodeEditorNav button { + background-color: hsl(345, 7%, 63%); + border-bottom: none; + border-radius: 4px 4px 0 0; + border: 1px; + border-width: 1px 1px 0; + border-style: solid; + border-color: hsl(345, 7%, 18%); + box-sizing: border-box; + color: hsl(345, 7%, 23%); + display: inline-block; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + line-height: 23px; + margin-left: 3px; + outline: none; + padding: 0 10px; + vertical-align: bottom; +} +.rpCodeEditorNav .rpButtonActive { + background-color: #fdf6e3; + border-color: transparent; +} +.rpResult { + bottom: 0; + left: 50%; + position: absolute; + right: 0; + top: 0; +} +.rpResultHeader { + color: white; + font-size: 16px; + font-weight: 400; + margin: 0; + padding: 0 10px 0 44px; +} +.rpActivity { + left: 10px; + position: absolute; + top: 0; +} +.rpActivity::after { + background-image: url(./logo.svg); + background-size: 100%; + content: ''; + display: block; + height: 30px; + position: relative; + width: 30px; +} +@keyframes lookBusy { + from { + top: 5px; + left: 0; + opacity: 1; + animation-timing-function: ease-in; + } + 23% { + top: 5px; + left: 13px; + opacity: 0.5; + } + 30% { + top: 7px; + left: 15px; + } + 37% { + top: 10px; + left: 13px; + } + 50% { + opacity: 0.2; + transform: scale(0.2); + } + 53% { + top: 10px; + left: 7px; + } + 60% { + top: 12px; + left: 5px; + } + 67% { + top: 15px; + left: 7px; + animation-timing-function: ease-out; + } + to { + top: 15px; + left: 20px; + opacity: 1; + } +} +.rpActivityBusy::before { + animation: lookBusy 600ms linear infinite; + animation-direction: alternate; + background: white; + border-radius: 5px; + content: ''; + display: block; + height: 10px; + position: absolute; + top: 5px; + transition: 3s linear; + width: 10px; +} +.rpResultOutput { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 30px; + padding: 10px; +} +.rpError { + background-color: rgb(204, 0, 0); + bottom: 0; + color: white; + left: 0; + padding: 16px; + position: absolute; + right: 0; + top: 0; +} +.rpError h1 { + margin: 0; + line-height: 36px; + font-size: 24px; +} +.rpErrorStack { + bottom: 0; + font-family: monospace; + font-size: 12px; + left: 0; + margin: 0; + padding: 16px; + position: absolute; + right: 0; + top: 52px; +} +.ReactCodeMirror { + border-top: 1px solid #eee8d5; + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 30px; +} +.CodeMirror { + bottom: 0; + height: auto; + left: 0; + position: absolute; + right: 0; + top: 0; +} diff --git a/website-tutorial/RelayPlayground.js b/website-tutorial/RelayPlayground.js new file mode 100644 index 0000000000000..afe2fe90a95be --- /dev/null +++ b/website-tutorial/RelayPlayground.js @@ -0,0 +1,305 @@ +import './RelayPlayground.css'; +import 'codemirror/mode/javascript/javascript'; + +import Codemirror from 'react-codemirror'; +import React from 'react'; +import ReactDOM from 'react/lib/ReactDOM'; +import Relay from 'react-relay'; window.Relay = Relay; + +import babel from 'babel-core/browser'; +import babelRelayPlaygroundPlugin from './babelRelayPlaygroundPlugin'; +import debounce from 'lodash.debounce'; +import defer from 'lodash.defer'; +import delay from 'lodash.delay'; +import errorCatcher from 'babel-plugin-react-error-catcher/error-catcher'; +import errorCatcherPlugin from 'babel-plugin-react-error-catcher'; +import evalSchema from './evalSchema'; +import getBabelRelayPlugin from 'babel-relay-plugin'; +import {introspectionQuery} from 'graphql/utilities'; +import {graphql} from 'graphql'; + +var {PropTypes} = React; + +const CODE_EDITOR_OPTIONS = { + extraKeys: { + Tab(cm) { + // Insert spaces when the tab key is pressed + var spaces = Array(cm.getOption('indentUnit') + 1).join(' '); + cm.replaceSelection(spaces); + }, + }, + indentWithTabs: false, + lineNumbers: true, + mode: 'javascript', + tabSize: 2, + theme: 'solarized light', +}; +const ERROR_TYPES = { + graphql: 'GraphQL Validation', + runtime: 'Runtime', + schema: 'Schema', + syntax: 'Syntax', +}; +const RENDER_STEP_EXAMPLE_CODE = +`ReactDOM.render( + , + mountNode +);`; + +class PlaygroundRenderer extends React.Component { + componentDidMount() { + this._container = document.createElement('div'); + this.refs.mountPoint.appendChild(this._container); + this._updateTimeoutId = defer(this._update); + } + componentDidUpdate(prevProps) { + if (this._updateTimeoutId != null) { + clearTimeout(this._updateTimeoutId); + } + this._updateTimeoutId = defer(this._update); + } + componentWillUnmount() { + if (this._updateTimeoutId != null) { + clearTimeout(this._updateTimeoutId); + } + try { + ReactDOM.unmountComponentAtNode(this._container); + } catch(e) {} + } + _update = () => { + ReactDOM.render(React.Children.only(this.props.children), this._container); + } + render() { + return
; + } +} + +export default class RelayPlayground extends React.Component { + static propTypes = { + initialAppSource: PropTypes.string, + initialSchemaSource: PropTypes.string, + }; + state = { + appElement: null, + appSource: this.props.initialAppSource, + busy: false, + editTarget: 'app', + error: null, + schemaSource: this.props.initialSchemaSource, + }; + componentDidMount() { + // Hijack console.warn to collect GraphQL validation warnings (we hope) + this._originalConsoleWarn = console.warn; + var collectedWarnings = []; + console.warn = (...args) => { + collectedWarnings.push([Date.now(), args]); + this._originalConsoleWarn.apply(console, args); + } + // Hijack window.onerror to catch any stray fatals + this._originalWindowOnerror = window.onerror; + window.onerror = (message, url, lineNumber, something, error) => { + // GraphQL validation warnings are followed closely by a thrown exception. + // Console warnings that appear too far before this exception are probably + // not related to GraphQL. Throw those out. + if (/GraphQL validation error/.test(message)) { + var recentWarnings = collectedWarnings + .filter(([createdAt, args]) => Date.now() - createdAt <= 500) + .reduce((memo, [createdAt, args]) => memo.concat(args), []); + this.setState({ + error: {stack: recentWarnings.join('\n')}, + errorType: ERROR_TYPES.graphql, + }); + } else { + this.setState({error, errorType: ERROR_TYPES.runtime}); + } + collectedWarnings = []; + return false; + }; + this._updateSchema(this.state.schemaSource, this.state.appSource); + } + componentDidUpdate(prevProps, prevState) { + var appChanged = this.state.appSource !== prevState.appSource; + var schemaChanged = this.state.schemaSource !== prevState.schemaSource; + if (appChanged || schemaChanged) { + this.setState({busy: true}); + this._handleSourceCodeChange( + this.state.appSource, + schemaChanged ? this.state.schemaSource : null, + ); + } + } + componentWillUnmount() { + clearTimeout(this._errorReporterTimeout); + clearTimeout(this._warningScrubberTimeout); + this._handleSourceCodeChange.cancel(); + console.warn = this._originalConsoleWarn; + window.onerror = this._originalWindowOnerror; + } + _handleSourceCodeChange = debounce((appSource, schemaSource) => { + if (schemaSource != null) { + this._updateSchema(schemaSource, appSource); + } else { + this._updateApp(appSource); + } + }, 300, {trailing: true}) + _updateApp = (appSource) => { + clearTimeout(this._errorReporterTimeout); + // We're running in a browser. Create a require() shim to catch any imports. + var require = (path) => { + switch (path) { + // The errorCatcherPlugin injects a series of import statements into the + // program body. Return locally bound variables in these three cases: + case '//error-catcher.js': + return (React, filename, displayName, reporter) => { + // When it fatals, render an empty in place of the app. + return errorCatcher(React, filename, , reporter); + }; + case 'react': + return React; + case 'reporterProxy': + return (error, instance, filename, displayName) => { + this._errorReporterTimeout = defer( + this.setState.bind(this), + {error, errorType: ERROR_TYPES.runtime} + ); + }; + + default: throw new Error(`Cannot find module "${path}"`); + } + }; + try { + var {code} = babel.transform(appSource, { + filename: 'RelayPlayground', + plugins : [ + babelRelayPlaygroundPlugin, + this._babelRelayPlugin, + errorCatcherPlugin('reporterProxy'), + ], + retainLines: true, + sourceMaps: 'inline', + stage: 0, + }); + var result = eval(code); + if ( + React.isValidElement(result) && + result.type.name === 'RelayRootContainer' + ) { + this.setState({ + appElement: React.cloneElement(result, {forceFetch: true}), + }); + } else { + this.setState({ + appElement: ( +
+

+ Render a Relay.RootContainer into mountNode to get + started. +

+

+ Example: +

+
{RENDER_STEP_EXAMPLE_CODE}
+
+ ), + }); + } + this.setState({error: null}); + } catch(error) { + this.setState({error, errorType: ERROR_TYPES.syntax}); + } + this.setState({busy: false}); + } + _updateCode = (newSource) => { + var sourceStorageKey = `${this.state.editTarget}Source`; + this.setState({[sourceStorageKey]: newSource}); + } + _updateEditTarget = (editTarget) => { + this.setState({editTarget}); + } + _updateSchema = (schemaSource, appSource) => { + try { + var Schema = evalSchema(schemaSource); + } catch(error) { + this.setState({error, errorType: ERROR_TYPES.schema}); + return; + } + graphql(Schema, introspectionQuery).then((result) => { + if ( + this.state.schemaSource !== schemaSource || + this.state.appSource !== appSource + ) { + // This version of the code is stale. Bail out. + return; + } + this._babelRelayPlugin = getBabelRelayPlugin(result.data); + Relay.injectNetworkLayer({ + sendMutation: (mutationRequest) => { + // TODO + }, + sendQueries: (queryRequests) => { + return Promise.all(queryRequests.map(queryRequest => { + var graphQLQuery = queryRequest.getQueryString(); + console.log(graphQLQuery); + graphql(Schema, graphQLQuery).then(result => { + if (result.errors) { + queryRequest.reject(new Error(result.errors)); + } else { + queryRequest.resolve({response: result.data}); + } + }); + })); + }, + supports: () => false, + }); + this._updateApp(appSource); + }); + } + render() { + var sourceCode = this.state.editTarget === 'schema' + ? this.state.schemaSource + : this.state.appSource; + return ( +
+
+ + +
+
+

+ Relay Playground + +

+
+ {this.state.error + ?
+

{this.state.errorType} Error

+
{this.state.error.stack}
+
+ : {this.state.appElement} + } +
+
+
+ ); + } +} diff --git a/website-tutorial/babelRelayPlaygroundPlugin.js b/website-tutorial/babelRelayPlaygroundPlugin.js new file mode 100644 index 0000000000000..bfe2ffdba8059 --- /dev/null +++ b/website-tutorial/babelRelayPlaygroundPlugin.js @@ -0,0 +1,17 @@ +export default function ({Plugin, types: t}) { + return new Plugin('babel-relay-playground', { + visitor: { + CallExpression(node) { + var callee = this.get('callee'); + if ( + callee.matchesPattern('React.render') || + callee.matchesPattern('ReactDOM.render') + ) { + // We found a ReactDOM.render(...) type call. + // Pluck the ReactElement from the call, and export it instead. + return t.exportDefaultDeclaration(node.arguments[0]); + } + }, + }, + }); +} diff --git a/website-tutorial/evalSchema.js b/website-tutorial/evalSchema.js new file mode 100644 index 0000000000000..1dc906077e5a0 --- /dev/null +++ b/website-tutorial/evalSchema.js @@ -0,0 +1,18 @@ +import babel from 'babel-core/browser'; + +var GraphQL = require('graphql'); +var GraphQLRelay = require('graphql-relay'); + +export default function(source) { + // Make these modules available to the schema author through a require shim. + function require(path) { + switch(path) { + case 'graphql': return GraphQL; + case 'graphql-relay': return GraphQLRelay; + + default: throw new Error(`Cannot find module "${path}"`); + } + } + var {code} = babel.transform(source, {code: true, ast: false}); + return eval(code); +} diff --git a/website-tutorial/graphiql.js b/website-tutorial/graphiql.js new file mode 100644 index 0000000000000..6f9583c467227 --- /dev/null +++ b/website-tutorial/graphiql.js @@ -0,0 +1,40 @@ +import 'babel/polyfill'; +import 'graphiql/graphiql.css'; + +import GraphiQL from 'graphiql'; +import React from 'react'; window.React = React; +import ReactDOM from 'react/lib/ReactDOM'; + +import evalSchema from './evalSchema'; +import queryString from 'query-string'; +import {graphql} from 'graphql'; + +if ( + /^https?:\/\/facebook.github.io\//.test(document.referrer) || + /^localhost/.test(document.location.host) +) { + var { + query, + schema: schemaSource, + } = queryString.parse(location.search); +} + +var Schema; +if (schemaSource) { + Schema = evalSchema(schemaSource); +} else { + Schema = require('./HelloSchema'); +} + +function graphQLFetcher(graphQLParams) { + return graphql(Schema, graphQLParams.query); +} + +var mountPoint = document.createElement('div'); +mountPoint.style.height = '100%'; +document.body.appendChild(mountPoint); + +ReactDOM.render( + , + mountPoint +); diff --git a/website-tutorial/logo.svg b/website-tutorial/logo.svg new file mode 100644 index 0000000000000..83b535f02381c --- /dev/null +++ b/website-tutorial/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website-tutorial/package.json b/website-tutorial/package.json new file mode 100644 index 0000000000000..1dc8c6d2f537a --- /dev/null +++ b/website-tutorial/package.json @@ -0,0 +1,39 @@ +{ + "private": true, + "name": "relay-tutorial", + "description": "Code to support the tutorial on the Relay website", + "version": "0.2.0", + "repository": "facebook/relay", + "license": "BSD-3-Clause", + "dependencies": { + "autoprefixer-loader": "2.1.0", + "babel": "5.8.23", + "babel-core": "5.8.23", + "babel-loader": "5.3.2", + "babel-plugin-react-error-catcher": "1.1.9", + "babel-relay-plugin": "../scripts/babel-relay-plugin", + "codemirror": "5.6.0", + "css-loader": "0.16.0", + "file-loader": "0.8.4", + "graphiql": "0.1.3", + "graphql": "0.4.2", + "graphql-relay": "0.3.2", + "html-webpack-plugin": "1.6.1", + "lodash.debounce": "3.1.1", + "lodash.defer": "3.1.0", + "lodash.delay": "3.1.0", + "minimist": "1.2.0", + "normalize.css": "3.0.3", + "query-string": "2.4.0", + "raw-loader": "0.5.1", + "react": "^0.14.0-beta3", + "react-codemirror": "steveluscher/react-codemirror#13-14-react", + "react-relay": "../", + "style-loader": "0.12.3", + "webpack": "1.12.0" + }, + "scripts": { + "start": "webpack --watch --progress --colors", + "build": "NODE_ENV=production webpack -p --progress --colors --target-dir=build" + } +} diff --git a/website-tutorial/playground.html b/website-tutorial/playground.html new file mode 100644 index 0000000000000..a6e62f9577250 --- /dev/null +++ b/website-tutorial/playground.html @@ -0,0 +1,10 @@ + + + + + {%= o.htmlWebpackPlugin.options.title %} + + + + + diff --git a/website-tutorial/playground.js b/website-tutorial/playground.js new file mode 100644 index 0000000000000..ca0b3ca92de0f --- /dev/null +++ b/website-tutorial/playground.js @@ -0,0 +1,28 @@ +import 'babel/polyfill'; + +import React from 'react'; window.React = React; +import ReactDOM from 'react/lib/ReactDOM'; +import RelayPlayground from './RelayPlayground'; + +import queryString from 'query-string'; + +if ( + /^https?:\/\/facebook.github.io\//.test(document.referrer) || + /^localhost/.test(document.location.host) +) { + var { + schema: schemaSource, + source: appSource, + } = queryString.parse(location.search); +} + +var mountPoint = document.createElement('div'); +document.body.appendChild(mountPoint); + +ReactDOM.render( + , + mountPoint +); diff --git a/website-tutorial/webpack.config.js b/website-tutorial/webpack.config.js new file mode 100644 index 0000000000000..b620065dbb8b9 --- /dev/null +++ b/website-tutorial/webpack.config.js @@ -0,0 +1,76 @@ +var DefinePlugin = require('webpack/lib/DefinePlugin'); +var HTMLWebpackPlugin = require('html-webpack-plugin'); +var UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin'); + +var argv = require('minimist')(process.argv.slice(2)); +var path = require('path'); + +var BUILD_DIR = argv['target-dir'] || 'src'; + +module.exports = { + entry: { + graphiql: './graphiql', + playground: './playground', + }, + module: { + loaders: [ + { + test: /\.css$/, + loader: 'style!css!autoprefixer-loader?browsers=last 2 versions', + }, + { + test: /\.js$/, + exclude: /(node_modules|bower_components)/, + loader: 'babel?stage=0', + }, + { + test: /\.svg$/, + loader: 'file-loader', + }, + ], + }, + output: { + path: path.resolve(__dirname, '../website', BUILD_DIR, 'relay/tutorial'), + filename: '[name].js' + }, + plugins: [ + new DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify( + process.env.NODE_ENV === 'production' ? 'production' : 'development' + ), + }), + new HTMLWebpackPlugin({ + chunks: ['graphiql'], + filename: 'graphiql.html', + title: 'GraphiQL' + }), + new HTMLWebpackPlugin({ + chunks: ['playground'], + filename: 'playground.html', + inject: true, + template: 'playground.html', + title: 'Relay Playground' + }), + ].concat(process.env.NODE_ENV === 'production' + ? [ + new UglifyJsPlugin({ + compress: { + screw_ie8: true, + warnings: false, + }, + mangle: { + except: [ + // Babel does a constructor.name check for 'Plugin'. + 'Plugin', + // We do a constructor.name check to make sure that the developer is + // trying to ReactDOM.render() a Relay.RootContainer into the + // playground + 'RelayRootContainer', + ], + }, + sourceMap: false, + }), + ] + : [] + ), +} diff --git a/website/server/generate.js b/website/server/generate.js index 8c7ba3ce17cf3..36bedcddc0ac5 100644 --- a/website/server/generate.js +++ b/website/server/generate.js @@ -4,6 +4,7 @@ var glob = require('glob'); var fs = require('fs-extra'); var mkdirp = require('mkdirp'); var server = require('./server.js'); +var exec = require('child_process').execSync; // Sadly, our setup fatals when doing multiple concurrent requests // I don't have the time to dig into why, it's easier to just serialize @@ -32,6 +33,8 @@ var queue = (function() { return {push: push}; })(); +exec('npm run build', {cwd: path.resolve(__dirname, '../../website-tutorial')}); + buildGraphQLSpec('build'); glob('src/**/*.*', function(er, files) { diff --git a/website/server/server.js b/website/server/server.js index f1eeb499705f1..1cdfa2f178026 100644 --- a/website/server/server.js +++ b/website/server/server.js @@ -10,6 +10,7 @@ var optimist = require('optimist'); var path = require('path'); var reactMiddleware = require('react-page-middleware'); var serveStatic = require('serve-static'); +var spawn = require('child_process').spawn; var argv = optimist.argv; @@ -21,6 +22,12 @@ if (argv.$0.indexOf('./server/generate.js') !== -1) { // Using a different port so that you can publish the website // and keeping the server up at the same time. port = 8079; +} else { + // Build (and watch) the tutorial support material + spawn('npm', ['start'], { + cwd: path.resolve(__dirname, '../../website-tutorial'), + stdio: 'inherit' + }); } var buildOptions = { From 36f9d474a1352551c3d1bdb1a7125c8ba670b5f5 Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Mon, 7 Sep 2015 17:59:51 -0700 Subject: [PATCH 02/10] Nicer borders between the playground and the code editor --- website-tutorial/RelayPlayground.css | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/website-tutorial/RelayPlayground.css b/website-tutorial/RelayPlayground.css index bded585371ec1..9d830d72f3243 100644 --- a/website-tutorial/RelayPlayground.css +++ b/website-tutorial/RelayPlayground.css @@ -14,11 +14,9 @@ body { } .rpCodeEditor { bottom: 0; - border-right: 1px solid #eee8d5; left: 0; position: absolute; right: 50%; - right: calc(50% + 1px); top: 0; } .rpCodeEditorNav, .rpResultHeader { @@ -26,6 +24,12 @@ body { height: 30px; line-height: 30px; } +.rpCodeEditorNav { + border-right: 1px solid hsl(345, 7%, 13%); +} +.rpResultHeader { + border-left: 1px solid hsl(345, 7%, 33%); +} .rpCodeEditorNav button { background-color: hsl(345, 7%, 63%); border-bottom: none; @@ -169,6 +173,7 @@ body { top: 52px; } .ReactCodeMirror { + border-right: 1px solid #eee8d5; border-top: 1px solid #eee8d5; bottom: 0; left: 0; From 894795332841ac786beed30401a9842d25fe0f48 Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Mon, 7 Sep 2015 18:14:14 -0700 Subject: [PATCH 03/10] Get rid of a stray console.log() --- website-tutorial/RelayPlayground.js | 1 - 1 file changed, 1 deletion(-) diff --git a/website-tutorial/RelayPlayground.js b/website-tutorial/RelayPlayground.js index afe2fe90a95be..b8bfb638e5fc5 100644 --- a/website-tutorial/RelayPlayground.js +++ b/website-tutorial/RelayPlayground.js @@ -242,7 +242,6 @@ export default class RelayPlayground extends React.Component { sendQueries: (queryRequests) => { return Promise.all(queryRequests.map(queryRequest => { var graphQLQuery = queryRequest.getQueryString(); - console.log(graphQLQuery); graphql(Schema, graphQLQuery).then(result => { if (result.errors) { queryRequest.reject(new Error(result.errors)); From d9d4f8bcbee680dc5a092ec68e659893bb7f6001 Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Mon, 7 Sep 2015 18:15:34 -0700 Subject: [PATCH 04/10] Added mutations to the prototyper --- website-tutorial/RelayPlayground.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/website-tutorial/RelayPlayground.js b/website-tutorial/RelayPlayground.js index b8bfb638e5fc5..9ca985cc60a69 100644 --- a/website-tutorial/RelayPlayground.js +++ b/website-tutorial/RelayPlayground.js @@ -237,7 +237,15 @@ export default class RelayPlayground extends React.Component { this._babelRelayPlugin = getBabelRelayPlugin(result.data); Relay.injectNetworkLayer({ sendMutation: (mutationRequest) => { - // TODO + var graphQLQuery = mutationRequest.getQueryString(); + var variables = mutationRequest.getVariables(); + graphql(Schema, graphQLQuery, null, variables).then(result => { + if (result.errors) { + mutationRequest.reject(new Error(result.errors)); + } else { + mutationRequest.resolve({response: result.data}); + } + }); }, sendQueries: (queryRequests) => { return Promise.all(queryRequests.map(queryRequest => { From 9000a1939ec6730ddd0eb7212a1075b7515eb666 Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Mon, 7 Sep 2015 18:22:43 -0700 Subject: [PATCH 05/10] Bubble query errors up to the error reporter --- website-tutorial/RelayPlayground.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/website-tutorial/RelayPlayground.js b/website-tutorial/RelayPlayground.js index 9ca985cc60a69..74089f1be563a 100644 --- a/website-tutorial/RelayPlayground.js +++ b/website-tutorial/RelayPlayground.js @@ -36,6 +36,7 @@ const CODE_EDITOR_OPTIONS = { }; const ERROR_TYPES = { graphql: 'GraphQL Validation', + query: 'Query', runtime: 'Runtime', schema: 'Schema', syntax: 'Syntax', @@ -242,6 +243,10 @@ export default class RelayPlayground extends React.Component { graphql(Schema, graphQLQuery, null, variables).then(result => { if (result.errors) { mutationRequest.reject(new Error(result.errors)); + this.setState({ + error: {stack: result.errors.map(e => e.message).join('\n')}, + errorType: ERROR_TYPES.query, + }); } else { mutationRequest.resolve({response: result.data}); } @@ -253,6 +258,10 @@ export default class RelayPlayground extends React.Component { graphql(Schema, graphQLQuery).then(result => { if (result.errors) { queryRequest.reject(new Error(result.errors)); + this.setState({ + error: {stack: result.errors.map(e => e.message).join('\n')}, + errorType: ERROR_TYPES.query, + }); } else { queryRequest.resolve({response: result.data}); } From 171b113f5e5ae213fe7fcffa9b6c47aa5524198c Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Mon, 7 Sep 2015 18:35:25 -0700 Subject: [PATCH 06/10] Rename website-tutorial to website-prototyping-tools --- .gitignore | 2 +- {website-tutorial => website-prototyping-tools}/HelloApp.js | 0 .../HelloSchema.js | 0 .../RelayPlayground.css | 0 .../RelayPlayground.js | 0 .../babelRelayPlaygroundPlugin.js | 0 {website-tutorial => website-prototyping-tools}/evalSchema.js | 0 {website-tutorial => website-prototyping-tools}/graphiql.js | 0 {website-tutorial => website-prototyping-tools}/logo.svg | 0 {website-tutorial => website-prototyping-tools}/package.json | 4 ++-- .../playground.html | 0 {website-tutorial => website-prototyping-tools}/playground.js | 0 .../webpack.config.js | 2 +- website/server/generate.js | 4 +++- website/server/server.js | 4 ++-- 15 files changed, 9 insertions(+), 7 deletions(-) rename {website-tutorial => website-prototyping-tools}/HelloApp.js (100%) rename {website-tutorial => website-prototyping-tools}/HelloSchema.js (100%) rename {website-tutorial => website-prototyping-tools}/RelayPlayground.css (100%) rename {website-tutorial => website-prototyping-tools}/RelayPlayground.js (100%) rename {website-tutorial => website-prototyping-tools}/babelRelayPlaygroundPlugin.js (100%) rename {website-tutorial => website-prototyping-tools}/evalSchema.js (100%) rename {website-tutorial => website-prototyping-tools}/graphiql.js (100%) rename {website-tutorial => website-prototyping-tools}/logo.svg (100%) rename {website-tutorial => website-prototyping-tools}/package.json (90%) rename {website-tutorial => website-prototyping-tools}/playground.html (100%) rename {website-tutorial => website-prototyping-tools}/playground.js (100%) rename {website-tutorial => website-prototyping-tools}/webpack.config.js (99%) diff --git a/.gitignore b/.gitignore index 8e96906310be5..4503dcdb8b243 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,5 @@ npm-debug.log website/build website/src/relay/docs website/src/relay/graphql -website/src/relay/tutorial +website/src/relay/prototyping .nvmrc diff --git a/website-tutorial/HelloApp.js b/website-prototyping-tools/HelloApp.js similarity index 100% rename from website-tutorial/HelloApp.js rename to website-prototyping-tools/HelloApp.js diff --git a/website-tutorial/HelloSchema.js b/website-prototyping-tools/HelloSchema.js similarity index 100% rename from website-tutorial/HelloSchema.js rename to website-prototyping-tools/HelloSchema.js diff --git a/website-tutorial/RelayPlayground.css b/website-prototyping-tools/RelayPlayground.css similarity index 100% rename from website-tutorial/RelayPlayground.css rename to website-prototyping-tools/RelayPlayground.css diff --git a/website-tutorial/RelayPlayground.js b/website-prototyping-tools/RelayPlayground.js similarity index 100% rename from website-tutorial/RelayPlayground.js rename to website-prototyping-tools/RelayPlayground.js diff --git a/website-tutorial/babelRelayPlaygroundPlugin.js b/website-prototyping-tools/babelRelayPlaygroundPlugin.js similarity index 100% rename from website-tutorial/babelRelayPlaygroundPlugin.js rename to website-prototyping-tools/babelRelayPlaygroundPlugin.js diff --git a/website-tutorial/evalSchema.js b/website-prototyping-tools/evalSchema.js similarity index 100% rename from website-tutorial/evalSchema.js rename to website-prototyping-tools/evalSchema.js diff --git a/website-tutorial/graphiql.js b/website-prototyping-tools/graphiql.js similarity index 100% rename from website-tutorial/graphiql.js rename to website-prototyping-tools/graphiql.js diff --git a/website-tutorial/logo.svg b/website-prototyping-tools/logo.svg similarity index 100% rename from website-tutorial/logo.svg rename to website-prototyping-tools/logo.svg diff --git a/website-tutorial/package.json b/website-prototyping-tools/package.json similarity index 90% rename from website-tutorial/package.json rename to website-prototyping-tools/package.json index 1dc8c6d2f537a..e7cb4d065bf68 100644 --- a/website-tutorial/package.json +++ b/website-prototyping-tools/package.json @@ -1,7 +1,7 @@ { "private": true, - "name": "relay-tutorial", - "description": "Code to support the tutorial on the Relay website", + "name": "relay-prototyping-tools", + "description": "In-browser prototyping tools for Relay applications", "version": "0.2.0", "repository": "facebook/relay", "license": "BSD-3-Clause", diff --git a/website-tutorial/playground.html b/website-prototyping-tools/playground.html similarity index 100% rename from website-tutorial/playground.html rename to website-prototyping-tools/playground.html diff --git a/website-tutorial/playground.js b/website-prototyping-tools/playground.js similarity index 100% rename from website-tutorial/playground.js rename to website-prototyping-tools/playground.js diff --git a/website-tutorial/webpack.config.js b/website-prototyping-tools/webpack.config.js similarity index 99% rename from website-tutorial/webpack.config.js rename to website-prototyping-tools/webpack.config.js index b620065dbb8b9..0754b52f70b4f 100644 --- a/website-tutorial/webpack.config.js +++ b/website-prototyping-tools/webpack.config.js @@ -30,7 +30,7 @@ module.exports = { ], }, output: { - path: path.resolve(__dirname, '../website', BUILD_DIR, 'relay/tutorial'), + path: path.resolve(__dirname, '../website', BUILD_DIR, 'relay/prototyping'), filename: '[name].js' }, plugins: [ diff --git a/website/server/generate.js b/website/server/generate.js index 36bedcddc0ac5..1e9e61d5874bf 100644 --- a/website/server/generate.js +++ b/website/server/generate.js @@ -33,7 +33,9 @@ var queue = (function() { return {push: push}; })(); -exec('npm run build', {cwd: path.resolve(__dirname, '../../website-tutorial')}); +exec('npm run build', { + cwd: path.resolve(__dirname, '../../website-prototyping-tools') +}); buildGraphQLSpec('build'); diff --git a/website/server/server.js b/website/server/server.js index 1cdfa2f178026..41d27b394f1be 100644 --- a/website/server/server.js +++ b/website/server/server.js @@ -23,9 +23,9 @@ if (argv.$0.indexOf('./server/generate.js') !== -1) { // and keeping the server up at the same time. port = 8079; } else { - // Build (and watch) the tutorial support material + // Build (and watch) the prototyping tools spawn('npm', ['start'], { - cwd: path.resolve(__dirname, '../../website-tutorial'), + cwd: path.resolve(__dirname, '../../website-prototyping-tools'), stdio: 'inherit' }); } From 861002897809d6444ade8143248903237c185895 Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Mon, 7 Sep 2015 19:29:43 -0700 Subject: [PATCH 07/10] Upgrade react-codemirror to 0.1.5 --- website-prototyping-tools/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website-prototyping-tools/package.json b/website-prototyping-tools/package.json index e7cb4d065bf68..2c6f21fe41492 100644 --- a/website-prototyping-tools/package.json +++ b/website-prototyping-tools/package.json @@ -27,7 +27,7 @@ "query-string": "2.4.0", "raw-loader": "0.5.1", "react": "^0.14.0-beta3", - "react-codemirror": "steveluscher/react-codemirror#13-14-react", + "react-codemirror": "0.1.5", "react-relay": "../", "style-loader": "0.12.3", "webpack": "1.12.0" From 057f52e270f1ae0a0b2f7d06e7180127e5d171ac Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Mon, 7 Sep 2015 19:30:20 -0700 Subject: [PATCH 08/10] The prototyping tool now remembers your work in localStorage if given a cacheKey --- website-prototyping-tools/RelayPlayground.js | 8 +++++ website-prototyping-tools/playground.js | 32 ++++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/website-prototyping-tools/RelayPlayground.js b/website-prototyping-tools/RelayPlayground.js index 74089f1be563a..7b5b252c5f992 100644 --- a/website-prototyping-tools/RelayPlayground.js +++ b/website-prototyping-tools/RelayPlayground.js @@ -82,6 +82,8 @@ export default class RelayPlayground extends React.Component { static propTypes = { initialAppSource: PropTypes.string, initialSchemaSource: PropTypes.string, + onAppSourceChange: PropTypes.func, + onSchemaSourceChange: PropTypes.func, }; state = { appElement: null, @@ -216,6 +218,12 @@ export default class RelayPlayground extends React.Component { _updateCode = (newSource) => { var sourceStorageKey = `${this.state.editTarget}Source`; this.setState({[sourceStorageKey]: newSource}); + if (this.state.editTarget === 'app' && this.props.onAppSourceChange) { + this.props.onAppSourceChange(newSource); + } + if (this.state.editTarget === 'schema' && this.props.onSchemaSourceChange) { + this.props.onSchemaSourceChange(newSource); + } } _updateEditTarget = (editTarget) => { this.setState({editTarget}); diff --git a/website-prototyping-tools/playground.js b/website-prototyping-tools/playground.js index ca0b3ca92de0f..1784ba274475f 100644 --- a/website-prototyping-tools/playground.js +++ b/website-prototyping-tools/playground.js @@ -6,6 +6,8 @@ import RelayPlayground from './RelayPlayground'; import queryString from 'query-string'; +var queryParams = queryString.parse(location.search); + if ( /^https?:\/\/facebook.github.io\//.test(document.referrer) || /^localhost/.test(document.location.host) @@ -13,7 +15,21 @@ if ( var { schema: schemaSource, source: appSource, - } = queryString.parse(location.search); + } = queryParams; +} + +var {cacheKey} = queryParams; +var appSourceCacheKey; +var schemaSourceCacheKey; +if (cacheKey) { + appSourceCacheKey = `rp-${cacheKey}-source`; + if (localStorage.getItem(appSourceCacheKey) != null) { + appSource = localStorage.getItem(appSourceCacheKey); + } + schemaSourceCacheKey = `rp-${cacheKey}-schema`; + if (localStorage.getItem(schemaSourceCacheKey) != null) { + schemaSource = localStorage.getItem(schemaSourceCacheKey); + } } var mountPoint = document.createElement('div'); @@ -21,8 +37,18 @@ document.body.appendChild(mountPoint); ReactDOM.render( , mountPoint ); From 1a565f27d66889d10d76cae092964d18d7e89310 Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Tue, 8 Sep 2015 15:28:00 -0700 Subject: [PATCH 09/10] Removed renegade CSS --- website-prototyping-tools/RelayPlayground.css | 1 - 1 file changed, 1 deletion(-) diff --git a/website-prototyping-tools/RelayPlayground.css b/website-prototyping-tools/RelayPlayground.css index 9d830d72f3243..c8dd9e58e189c 100644 --- a/website-prototyping-tools/RelayPlayground.css +++ b/website-prototyping-tools/RelayPlayground.css @@ -34,7 +34,6 @@ body { background-color: hsl(345, 7%, 63%); border-bottom: none; border-radius: 4px 4px 0 0; - border: 1px; border-width: 1px 1px 0; border-style: solid; border-color: hsl(345, 7%, 18%); From 1a6d5c665943c655f55f2ecb745833bd83414f8b Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Tue, 8 Sep 2015 15:28:20 -0700 Subject: [PATCH 10/10] The output area now scrolls independently --- website-prototyping-tools/RelayPlayground.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website-prototyping-tools/RelayPlayground.css b/website-prototyping-tools/RelayPlayground.css index c8dd9e58e189c..948de0494a600 100644 --- a/website-prototyping-tools/RelayPlayground.css +++ b/website-prototyping-tools/RelayPlayground.css @@ -140,10 +140,11 @@ body { .rpResultOutput { bottom: 0; left: 0; + overflow: auto; + padding: 10px; position: absolute; right: 0; top: 30px; - padding: 10px; } .rpError { background-color: rgb(204, 0, 0);