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 = {