diff --git a/README.md b/README.md index 9e39d2d83..a24393585 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ yo react-server # compile and run the new app npm run compile npm run start -# go to http://localhost:3010 +# go to http://localhost:3000 ``` That hooks you up with [`react-server-cli`](packages/react-server-cli), which diff --git a/docs/guides/react-server-cli.md b/docs/guides/react-server-cli.md index 12020cf08..f7ab0a818 100644 --- a/docs/guides/react-server-cli.md +++ b/docs/guides/react-server-cli.md @@ -1,71 +1,76 @@ -A simple command line app that will compile a routes file for the client and start up express. To use: +## A simple command line tool to build and run React Server sites -1. `npm install --save react-server-cli react-server` -2. Add `./node_modules/react-server-cli/bin/react-server-cli` as the value for `scripts.start` in package.json. -3. `npm start` from the directory that has your routes.js file. +To get started: -## Routes Format - -Note that the routes file needs to be in a bit different format than what we have used in the past in `react-server`. Rather than `routes.route.page` being a function, it needs to be a path to a file that exports a page class. Middleware also needs to be an array of file paths. For example: - -```javascript -module.exports = { - // these will be applied to every page in the site. - middleware: ["./FooMiddleware", "./BarMiddleware"], - - // this maps URLs to modules that export a Page class. - routes: { - BazRoute: { - path: ["/"], - method: "get", // optional - page: "./BazPage" - }, - BakRoute: { - path: ["/bak"], - page: "./BakPage" - } - } -}; +```bash +$ npm install -g react-server-cli +$ react-server init +$ react-server add-page '/' Homepage +$ react-server start ``` ## What It Does -The CLI builds and runs a `react-server` project, using Express. It compiles JS(X) and CSS into efficiently loadable bundles with code splitting using webpack, and it supports hot reloading of React components on the client-side during development. +The CLI builds and runs a React Server project, using Express. It compiles +JS(X) and CSS into efficiently loadable bundles with code splitting using +webpack, and it supports hot reloading of React components on the client-side +during development. ## Built-in Features ### Babel Compilation -It's rare to see a project these days in the JavaScript world that isn't at least experimenting with ES2015 and ES7. To make this easier, all code in your project will be run through Babel, and source maps will be generated back to the original file. +It's rare to see a project these days in the JavaScript world that isn't at +least experimenting with ES2015 and ES7. To make this easier, all code in your +project will be run through Babel, and source maps will be generated back to +the original file. -To take advantage of the Babel compilation, you need to install the Babel plugins and presets you want and reference them in a `.babelrc` file in your code directory. For more on the `.babelrc` format, see [its documentation here](https://babeljs.io/docs/usage/babelrc/). +To take advantage of the Babel compilation, you need to install the Babel +plugins and presets you want and reference them in a `.babelrc` file in your +code directory. For more on the `.babelrc` format, see [its documentation +here](https://babeljs.io/docs/usage/babelrc/). -## Options -Smart defaults are the goal, and `react-server-cli` has two base modes: **development** and **production**. `react-server-cli` will determine which base mode it's in by looking at the NODE_ENV environment variable. If it's not "production", then `react-server-cli` will assume we are in development mode. +## Configuration -### Ways to add options +### Routes + +This is where URLs are mapped to pages. It's also where global middleware +(applied to all pages in the site) is defined. + +By default this is created at the site's root directory as `routes.json`. -There are three ways to pass options to the CLI, through the command line, `.reactserverrc` JSON files, or as a `reactServer` entry in `package.json` files. It searches for options this way: +```json +{ + "middleware": [ + "middleware/FooMiddleware", + "middleware/BarMiddleware" + ], + "routes": { + "BazPage": { + "path": ["/"], + "page": "pages/BazPage" + }, + "BakPage": { + "path": ["/bak"], + "page": "pages/BakPage" + } + } +} +``` -1. If there are any options arguments on the command line, they are used. For the options which aren't specified: -1. `react-server-cli` looks at the current directory. - 1. If there is a JSON file named `.reactserverrc` in the directory, its settings are used and we skip to step #4. Otherwise: - 1. If there is a `package.json` file in the current directory with an entry named `reactServer`, its settings are used and we skip to step #4. Otherwise: -1. Go back to step 2 in the parent of the current directory. Repeat until you either find a config or hit the root directory. -1. If there are any options that still aren't specified, the defaults are used. +## Server config -Note that the programmatic API also searches for config files, although options sent in explicitly to the API function override the config file. +You can define JSON options either in a `.reactserverrc` or in a +`reactServer` object in your `package.json`. -### JSON options can be set per environment +You can provide environment-specific values in a sub-object at the key `env`. -If you are using either `.reactserverrc` or `package.json` to set your react-server options, you can provide environment-specific values in a sub-object at the key `env`. It looks like this: +It looks like this: ```json { + "routes": "routes.json", "port": "5000", "env": { - "staging": { - "port": "4000" - }, "production": { "port": "80" } @@ -73,33 +78,111 @@ If you are using either `.reactserverrc` or `package.json` to set your react-ser } ``` -The values in a particular environment override the main settings. In this example configuration `port` will be set to 80 if `process.env.NODE_ENV` is `production`, 4000 if `process.env.NODE_ENV` is `staging`, and 5000 for any other situation. +The values in a particular environment override the main settings. In this +example configuration `port` will be set to 80 if `process.env.NODE_ENV` is +`production`, and 5000 otherwise. + + +## Options +Smart defaults are the goal, and `react-server-cli` has two base modes: +**development** and **production**. `react-server-cli` will determine which +base mode it's in by looking at the NODE_ENV environment variable. If it's not +"production", then `react-server-cli` will assume we are in development mode. + +### Ways to add options + +There are three ways to pass options to the CLI, through the command line, +`.reactserverrc` JSON files, or as a `reactServer` entry in `package.json` +files. If there's no config file (or package.json config) in the current +working directory, then parent directories are searched up to the root of the +filesystem. Options passed to the CLI take final precedence. ### Development mode: making a great DX -Development mode is the default, and its goals are rapid startup and code-test loops. Hot mode is enabled for all code, although at this time, editing the routes file or modules that export a Page class still requires a browser reload to see changes. Modules that export a React component should reload without a browser refresh. +Development mode is the default, and its goals are rapid startup and code-test +loops. Hot mode is enabled for all code, although at this time, editing the +routes file or modules that export a Page class still requires a browser +reload to see changes. Modules that export a React component should reload +without a browser refresh. -In development mode, code is not minified in order to speed up startup time, so please do not think that the sizes of bundles in development mode is indicative of how big they will be in production. In fact, it's really best not to do any kind of perf testing in development mode; it will just lead you astray. +In development mode, code is not minified in order to speed up startup time, +so please do not think that the sizes of bundles in development mode is +indicative of how big they will be in production. In fact, it's really best +not to do any kind of perf testing in development mode; it will just lead you +astray. -We are also considering completely getting rid of server-side rendering in development mode by default to speed startup. +We are also considering completely getting rid of server-side rendering in +development mode by default to speed startup. ### Production mode: optimizing delivery -Production mode's priority is optimization at the expense of startup time. A separate code bundle is generated for every entry point into your app so that there is at most just one JS and one CSS file loaded by the framework. All code is minified, and hot reloading is turned off. +Production mode's priority is optimization at the expense of startup time. A +separate code bundle is generated for every entry point into your app so that +there is at most just one JS and one CSS file loaded by the framework. All +code is minified, and hot reloading is turned off. #### Building static files for production use -In many production configurations, you may not want `react-server-cli` to serve up your static JavaScript and CSS files. Typically, this is because you have a more performant static file server already set up or because you upload all your static files to a CDN server. +In many production configurations, you may not want `react-server-cli` to +serve up your static JavaScript and CSS files. Typically, this is because you +have a more performant static file server already set up or because you upload +all your static files to a CDN server. + +To use `react-server-cli` in this sort of production setup, use `react-server +compile` to generate static assets. + +```bash +$ NODE_ENV=production react-server compile +$ # Upload `__clientTemp/build` to static file server +$ NODE_ENV=production react-server start +``` + +In this case you'll want to specify a `jsUrl` key in your production config: + +```json +{ + ... + "env": { + "production": { + ... + "jsUrl": "http://mystaticfileserver.com/somedirectory/" + } + } +} +``` + +### Commands + +#### `init` -To use `react-server-cli` in this sort of production setup, follow these steps: +Generate: +- `routes.json` +- `.reactserverrc` +- `.babelrc` -1. `react-server-cli --production --compile-only` compiles the JavaScript and CSS files into the directory `__clientTemp/build`. -1. Upload the contents of `__clientTemp/build` to your static file server. -1. `react-server-cli --production --js-url="http://mystaticfileserver.com/somedirectory/"` to start your HTML server depending on JavaScript and CSS files from your static file server. +Install: +- `react-server` -### Setting Options Manually +#### `add-page ` -While development and production mode are good starting points, you can of course choose to override any of the setup by setting the following options: +Add a stub of a new page class. + +#### `start` + +Start the server. If running with local client assets, build those. + +#### `compile` + +Compile the client JavaScript only, and don't start any servers. This is what +you want to do if you are building the client JavaScript to be hosted on a CDN +or separate server. Unless you have a very specific reason, it's almost always +a good idea to only do this in production mode. + +### Options + +The following options are available. Note that options on the command-line +are dash-separated (e.g. `--js-port`), but options in config files are +camel-cased (e.g. `jsPort`). (TODO: Support dash-separated options in config) #### --routes The routes file to load. @@ -107,7 +190,8 @@ The routes file to load. Defaults to **"./routes.js"**. #### --host -The hostname to use when starting up the server. If `jsUrl` is set, then this argument is ignored, and any host name can be used. +The hostname to use when starting up the server. If `jsUrl` is set, then this +argument is ignored, and any host name can be used. Defaults to **localhost**. @@ -122,7 +206,9 @@ The port to use when `react-server-cli` is serving up the client JavaScript. Defaults to **3001**. #### --hot, -h -Use hot reloading of client JavaScript. Modules that export React components will reload without refreshing the browser. This option is incompatible with --long-term-caching. +Use hot reloading of client JavaScript. Modules that export React components +will reload without refreshing the browser. This option is incompatible with +--long-term-caching. Defaults to **true** in development mode and **false** in production mode. @@ -132,58 +218,72 @@ Minify client JavaScript and CSS. Defaults to **false** in development mode and **true** in production. #### --long-term-caching -Adds hashes to all JavaScript and CSS file names output by the build, allowing for the static files to be served with far-future expires headers. This option is incompatible with --hot. +Adds hashes to all JavaScript and CSS file names output by the build, allowing +for the static files to be served with far-future expires headers. This option +is incompatible with --hot. Defaults to **false** in development mode and **true** in production. -#### --compile-only -Compile the client JavaScript only, and don't start any servers. This is what you want to do if you are building the client JavaScript to be hosted on a CDN or separate server. Unless you have a very specific reason, it's almost always a good idea to only do this in production mode. - -Defaults to **false**. - #### --js-url -A URL base for the pre-compiled client JavaScript; usually this is a base URL on a CDN or separate server. Setting a value for js-url means that react-server-cli will not compile the client JavaScript at all, and it will not serve up any of the client JavaScript. Obviously, this means that --js-url overrides and ignores all of the options related to JavaScript compilation and serving: --hot, --js-port, and --minify. +A URL base for the pre-compiled client JavaScript; usually this is a base URL +on a CDN or separate server. Setting a value for js-url means that +react-server-cli will not compile the client JavaScript at all, and it will +not serve up any of the client JavaScript. Obviously, this means that --js-url +overrides and ignores all of the options related to JavaScript compilation and +serving: --hot, --js-port, and --minify. Defaults to **null**. #### --https -If true, the server will start up using https with a self-signed certificate. Note that browsers do not trust self-signed certificates by default, so you will have to click through some warning screens. This is a quick and dirty way to test HTTPS, but it has some limitations and should never be used in production. Requires OpenSSL to be installed. +If true, the server will start up using https with a self-signed certificate. +Note that browsers do not trust self-signed certificates by default, so you +will have to click through some warning screens. This is a quick and dirty way +to test HTTPS, but it has some limitations and should never be used in +production. Requires OpenSSL to be installed. Defaults to **false**. #### --https-key -Start the server using HTTPS with this private key file in PEM format. Requires `https-cert` to be set as well. +Start the server using HTTPS with this private key file in PEM format. +Requires `https-cert` to be set as well. Default is **none**. #### --https-cert -Start the server using HTTPS with this cert file in PEM format. Requires `https-key` to be set as well. +Start the server using HTTPS with this cert file in PEM format. Requires +`https-key` to be set as well. Default is **none**. #### --https-ca -Start the server using HTTPS with this certificate authority file in PEM format. Also requires `https-key` and `https-cert` to start the server. +Start the server using HTTPS with this certificate authority file in PEM +format. Also requires `https-key` and `https-cert` to start the server. Default is **none**. #### --https-pfx -Start the server using HTTPS with this file containing the private key, certificate and CA certs of the server in PFX or PKCS12 format. Mutually exclusive with `https-key`, `https-cert`, and `https-ca`. +Start the server using HTTPS with this file containing the private key, +certificate and CA certs of the server in PFX or PKCS12 format. Mutually +exclusive with `https-key`, `https-cert`, and `https-ca`. Default is **none**. #### --https-passphrase -A passphrase for the private key or pfx file. Requires `https-key` or `https-pfx` to be set. +A passphrase for the private key or pfx file. Requires `https-key` or +`https-pfx` to be set. Default is **none**. #### --log-level -Sets the severity level for the logs being reported. Values are, in ascending order of severity: 'debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency'. +Sets the severity level for the logs being reported. Values are, in ascending +order of severity: 'debug', 'info', 'notice', 'warning', 'error', 'critical', +'alert', 'emergency'. Default is **'debug'** in development mode and **'notice'** in production. @@ -192,20 +292,30 @@ Shows command line options. ## API -You can also call `react-server-cli` programmatically. The module has a single named export, `start`, which takes has the following signature: +You can also call `react-server-cli` programmatically. The module has a +named export, `run`, which takes has the following signature: ```javascript -import {start} from `react-server-cli` - -start(routesRelativePath, { - port: 3000, - jsPort: 3001, - hot: true, - minify: false, - compileOnly: false, - jsUrl: null, - longTermCaching: true, -}) +import {run} from "react-server-cli" + +run({ + command : "start", + routes : "./routes.json", + port : 3000, + jsPort : 3001, + hot : true, + minify : false, + compileOnly : false, + jsUrl : null, + longTermCaching : true, +}); ``` -All of the values in the second argument are optional, and they have the same defaults as the corresponding CLI arguments explained above. (Also note that if an option isn't present, the programmatic API will search for a config file in the same way as the CLI.) +The module also exports a `parseCliArgs` function that will let you implement +your own CLI: + +```javascript +import {run, parseCliArgs} from "react-server-cli" + +run(parseCliArgs()); +``` diff --git a/docs/testing-ports.md b/docs/testing-ports.md index b36a3c1ec..47e7be041 100644 --- a/docs/testing-ports.md +++ b/docs/testing-ports.md @@ -4,7 +4,7 @@ simultaneously, so port conflicts will fail the tests. Make sure to update this manifest when adding a test that starts a server. ## generator-react-server -3010, 3011 +3000, 3001 ## react-server-integration-test -8771, 3001 +8771, 8772 diff --git a/packages/generator-react-server/generators/app/templates/package.json b/packages/generator-react-server/generators/app/templates/package.json index e97cf1ccf..0226dcfa3 100644 --- a/packages/generator-react-server/generators/app/templates/package.json +++ b/packages/generator-react-server/generators/app/templates/package.json @@ -4,7 +4,7 @@ "description": "A react-server instance", "main": "HelloWorld.js", "scripts": { - "start": "react-server --port 3010 --js-port 3011", + "start": "react-server start", "test": "xo && nsp check && ava test.js" }, "license": "Apache-2.0", diff --git a/packages/generator-react-server/generators/app/templates/test.js b/packages/generator-react-server/generators/app/templates/test.js index 2c4210057..5d408d455 100644 --- a/packages/generator-react-server/generators/app/templates/test.js +++ b/packages/generator-react-server/generators/app/templates/test.js @@ -23,7 +23,7 @@ function getResponseCode(url) { return new Promise((resolve, reject) => { const req = http.get({ hostname: 'localhost', - port: 3010, + port: 3000, path: url }, res => { resolve(res.statusCode); diff --git a/packages/react-server-cli/package.json b/packages/react-server-cli/package.json index 5b88e05b1..c42cf9346 100644 --- a/packages/react-server-cli/package.json +++ b/packages/react-server-cli/package.json @@ -26,6 +26,7 @@ "json-loader": "^0.5.4", "less": "^2.7.1", "less-loader": "^2.2.3", + "lodash": "^4.14.1", "mkdirp": "~0.5.1", "node-libs-browser": "~0.5.2", "null-loader": "~0.1.1", diff --git a/packages/react-server-cli/src/ConfigurationError.js b/packages/react-server-cli/src/ConfigurationError.js new file mode 100644 index 000000000..5702e4a46 --- /dev/null +++ b/packages/react-server-cli/src/ConfigurationError.js @@ -0,0 +1,13 @@ +// Can't use `instanceof` with babel-ified subclasses of builtins. +// +// https://phabricator.babeljs.io/T3083 +// +// Gotta do this the old-fashioned way. :p +// +export default function ConfigurationError(message) { + this.name = 'ConfigurationError'; + this.message = message; + this.stack = (new Error()).stack; +} +ConfigurationError.prototype = Object.create(Error.prototype); +ConfigurationError.prototype.constructor = ConfigurationError; diff --git a/packages/react-server-cli/src/cli.js b/packages/react-server-cli/src/cli.js index bc25aac0d..4f59b8352 100755 --- a/packages/react-server-cli/src/cli.js +++ b/packages/react-server-cli/src/cli.js @@ -1,5 +1,5 @@ require("babel-core/register"); -const {start, parseCliArgs} = require("."); +const {run, parseCliArgs} = require("."); -start(parseCliArgs()); +run(parseCliArgs()); diff --git a/packages/react-server-cli/src/commands/add-page.js b/packages/react-server-cli/src/commands/add-page.js new file mode 100644 index 000000000..e7b73e081 --- /dev/null +++ b/packages/react-server-cli/src/commands/add-page.js @@ -0,0 +1,53 @@ +import _ from "lodash"; +import fs from "fs"; +import {join} from "path"; +import chalk from "chalk"; +import mkdirp from "mkdirp"; +import fileExists from "../fileExists"; +import ConfigurationError from "../ConfigurationError"; + +const PAGE_SOURCE = _.template(` +import React from "react"; + +export default class <%= className %> { + handleRoute(next) { + // Kick off data requests here. + return next(); + } + + getElements() { + return
This is <%= className %>.
+ } +} +`); + +export default function addPage(options){ + const {routesFile, routesPath, routes} = options; + + const path = options._[3]; + const className = options._[4]; + + if (!path || !className) { + throw new ConfigurationError("Usage: react-server add-page "); + } + + const page = join("pages", className + ".js"); + + if (fileExists(page)) { + throw new ConfigurationError(`Found a pre-existing ${page}. Aborting.`); + } + + mkdirp("pages"); + + console.log(chalk.yellow("Generating " + page)); + + fs.writeFileSync(page, PAGE_SOURCE({className})); + + routes.routes[className] = { path, page }; + + console.log(chalk.yellow("Updating " + routesFile)); + + fs.writeFileSync(routesPath, JSON.stringify(routes, null, " ") + "\n"); + + console.log(chalk.green("All set!")); +} diff --git a/packages/react-server-cli/src/commands/compile.js b/packages/react-server-cli/src/commands/compile.js new file mode 100644 index 000000000..b9009e055 --- /dev/null +++ b/packages/react-server-cli/src/commands/compile.js @@ -0,0 +1,24 @@ +import compileClient from "../compileClient" +import handleCompilationErrors from "../handleCompilationErrors"; +import setupLogging from "../setupLogging"; +import logProductionWarnings from "../logProductionWarnings"; +import {logging} from "../react-server"; + +const logger = logging.getLogger(__LOGGER__); + +export default function compile(options){ + setupLogging(options); + logProductionWarnings(options); + + const {compiler} = compileClient(options); + + logger.notice("Starting compilation of client JavaScript..."); + compiler.run((err, stats) => { + const error = handleCompilationErrors(err, stats); + if (!error) { + logger.notice("Successfully compiled client JavaScript."); + } else { + logger.error(error); + } + }); +} diff --git a/packages/react-server-cli/src/commands/init.js b/packages/react-server-cli/src/commands/init.js new file mode 100644 index 000000000..9bbd18009 --- /dev/null +++ b/packages/react-server-cli/src/commands/init.js @@ -0,0 +1,72 @@ +import _ from "lodash"; +import fs from "fs"; +import {spawnSync} from "child_process"; +import chalk from "chalk"; +import fileExists from "../fileExists"; +import ConfigurationError from "../ConfigurationError"; + +const DEPENDENCIES = [ + "react-server", + "babel-preset-react-server", + + // TODO: Modernize our peer deps and remove versions here. + "superagent@1.2.0", + "react@~0.14.2", + "react-dom@~0.14.2", +] + +const DEV_DEPENDENCIES = [ + + // TODO: These, too. + "webpack-dev-server@~1.13.0", + "webpack@^1.13.1", +] + +const CONFIG = { + "routes.json": { + middleware: [], + routes: {}, + }, + ".reactserverrc": { + routesFile: "routes.json", + port: 3000, + env: { + production: { + port: 80, + }, + }, + }, + ".babelrc": { + presets: ["react-server"], + }, +} + +export default function init(){ + + if (!fileExists("package.json")) { + console.log(chalk.yellow("No package.json found. Running `npm init --yes`")); + spawnSync("npm", ["init", "--yes"], {stdio: "inherit"}); + } + + Object.keys(CONFIG).forEach(fn => { + if (fileExists(fn)) { + throw new ConfigurationError(`Found a pre-existing ${fn}. Aborting.`); + } + }); + + console.log(chalk.yellow("Installing dependencies")); + + spawnSync("npm", ["install", "--save", ...DEPENDENCIES], {stdio: "inherit"}); + + console.log(chalk.yellow("Installing devDependencies")); + + spawnSync("npm", ["install", "--save-dev", ...DEV_DEPENDENCIES], {stdio: "inherit"}); + + _.forEach(CONFIG, (config, fn) => { + console.log(chalk.yellow("Generating " + fn)); + + fs.writeFileSync(fn, JSON.stringify(config, null, " ") + "\n"); + }); + + console.log(chalk.green("All set!")); +} diff --git a/packages/react-server-cli/src/startServer.js b/packages/react-server-cli/src/commands/start.js similarity index 51% rename from packages/react-server-cli/src/startServer.js rename to packages/react-server-cli/src/commands/start.js index cc28c408d..f0b6d5865 100644 --- a/packages/react-server-cli/src/startServer.js +++ b/packages/react-server-cli/src/commands/start.js @@ -1,17 +1,14 @@ import http from "http" import https from "https" import express from "express" -import path from "path" import pem from "pem" import compression from "compression" -import defaultOptions from "./defaultOptions" import WebpackDevServer from "webpack-dev-server" -import compileClient from "./compileClient" -import mergeOptions from "./mergeOptions" -import findOptionsInFiles from "./findOptionsInFiles" -import callerDependency from "./callerDependency"; - -const reactServer = require(callerDependency("react-server")); +import compileClient from "../compileClient" +import handleCompilationErrors from "../handleCompilationErrors"; +import reactServer from "../react-server"; +import setupLogging from "../setupLogging"; +import logProductionWarnings from "../logProductionWarnings"; const logger = reactServer.logging.getLogger(__LOGGER__); @@ -19,103 +16,59 @@ const logger = reactServer.logging.getLogger(__LOGGER__); // stop. started is a promise that resolves when all necessary servers have been // started. stop is a method to stop all servers. It takes no arguments and // returns a promise that resolves when the server has stopped. -export default (options = {}) => { - // for the option properties that weren't sent in, look for a config file - // (either .reactserverrc or a reactServer section in a package.json). for - // options neither passed in nor in a config file, use the defaults. - options = mergeOptions(defaultOptions, findOptionsInFiles() || {}, options); - - setupLogging(options.logLevel, options.timingLogLevel, options.gaugeLogLevel); +export default function start(options){ + setupLogging(options); logProductionWarnings(options); - return startImpl(options); -} - -const startImpl = ({ - routesFile, - host, + const { port, jsPort, hot, - minify, - compileOnly, jsUrl, - stats, - https: httpsOptions, + httpsOptions, longTermCaching, -}) => { - if ((httpsOptions.key || httpsOptions.cert || httpsOptions.ca) && httpsOptions.pfx) { - throw new Error("If you set https.pfx, you can't set https.key, https.cert, or https.ca."); - } + } = options; - const routesPath = path.resolve(process.cwd(), routesFile); - const routes = require(routesPath); - - const outputUrl = jsUrl || `${httpsOptions ? "https" : "http"}://${host}:${jsPort}/`; - - const {serverRoutes, compiler} = compileClient(routes, { - routesDir: path.dirname(routesPath), - hot, - minify, - outputUrl, - longTermCaching, - stats, - }); + const {serverRoutes, compiler} = compileClient(options); - if (compileOnly) { - logger.notice("Starting compilation of client JavaScript..."); - compiler.run((err, stats) => { - const error = handleCompilationErrors(err, stats); - if (!error) { - logger.notice("Successfully compiled client JavaScript."); - } else { - logger.error(error); - } - }); - // TODO: it's odd that this returns something different than the other branch; - // should probably separate compile and start into two different exported functions. - return null; - } else { - const startServers = (keys) => { - // if jsUrl is set, we need to run the compiler, but we don't want to start a JS - // server. - let startJsServer = startDummyJsServer; + const startServers = (keys) => { + // if jsUrl is set, we need to run the compiler, but we don't want to start a JS + // server. + let startJsServer = startDummyJsServer; - if (!jsUrl) { - // if jsUrl is not set, we need to start up a JS server, either hot load - // or static. - startJsServer = hot ? startHotLoadJsServer : startStaticJsServer; - } + if (!jsUrl) { + // if jsUrl is not set, we need to start up a JS server, either hot load + // or static. + startJsServer = hot ? startHotLoadJsServer : startStaticJsServer; + } - logger.notice("Starting servers...") + logger.notice("Starting servers...") - const jsServer = startJsServer(compiler, jsPort, longTermCaching, keys); - const htmlServerPromise = serverRoutes.then(serverRoutesFile => startHtmlServer(serverRoutesFile, port, keys)); + const jsServer = startJsServer(compiler, jsPort, longTermCaching, keys); + const htmlServerPromise = serverRoutes.then(serverRoutesFile => startHtmlServer(serverRoutesFile, port, keys)); - return { - stop: () => Promise.all([jsServer.stop(), htmlServerPromise.then(server => server.stop())]), - started: Promise.all([jsServer.started, htmlServerPromise.then(server => server.started)]) - .catch(e => {logger.error(e); throw e}) - .then(() => logger.notice(`Ready for requests on port ${port}.`)), - }; - } + return { + stop: () => Promise.all([jsServer.stop(), htmlServerPromise.then(server => server.stop())]), + started: Promise.all([jsServer.started, htmlServerPromise.then(server => server.started)]) + .catch(e => {logger.error(e); throw e}) + .then(() => logger.notice(`Ready for requests on port ${port}.`)), + }; + } - if (httpsOptions === true) { - // if httpsOptions was true (and so didn't send in keys), generate keys. - pem.createCertificate({days:1, selfSigned:true}, (err, keys) => { - if (err) throw err; - return startServers({key: keys.serviceKey, cert:keys.certificate}); - }); - } else if (httpsOptions) { - // in this case, we assume that httpOptions is an object that can be passed - // in to https.createServer as options. - return startServers(httpsOptions); - } else { - // use http. - return startServers(); - } + if (httpsOptions === true) { + // if httpsOptions was true (and so didn't send in keys), generate keys. + pem.createCertificate({days:1, selfSigned:true}, (err, keys) => { + if (err) throw err; + return startServers({key: keys.serviceKey, cert:keys.certificate}); + }); + } else if (httpsOptions) { + // in this case, we assume that httpOptions is an object that can be passed + // in to https.createServer as options. + return startServers(httpsOptions); } - return null; // For eslint. :p + + // Default. Use http. + return startServers(); } // given the server routes file and a port, start a react-server HTML server at @@ -246,29 +199,6 @@ const startDummyJsServer = (compiler /*, port, longTermCaching, httpsOptions*/) }; }; -// takes in the err and stats object returned by a webpack compilation and returns -// an error object if something serious happened, or null if things are ok. -const handleCompilationErrors = (err, stats) => { - if (err) { - logger.error("Error during webpack build."); - logger.error(err); - return new Error(err); - // TODO: inspect stats to see if there are errors -sra. - } else if (stats.hasErrors()) { - console.error("There were errors in the JavaScript compilation."); - stats.toJson().errors.forEach((error) => { - console.error(error); - }); - return new Error("There were errors in the JavaScript compilation."); - } else if (stats.hasWarnings()) { - logger.warning("There were warnings in the JavaScript compilation. Note that this is normal if you are minifying your code."); - // for now, don't enumerate warnings; they are absolutely useless in minification mode. - // TODO: handle this more intelligently, perhaps with a --reportwarnings flag or with different - // behavior based on whether or not --minify is set. - } - return null; -} - // returns a method that can be used to stop the server. the returned method // returns a promise to indicate when the server is actually stopped. const serverToStopPromise = (server) => { @@ -305,31 +235,3 @@ const serverToStopPromise = (server) => { }); }; } - -const setupLogging = (logLevel, timingLogLevel, gaugeLogLevel) => { - reactServer.logging.setLevel('main', logLevel); - reactServer.logging.setLevel('time', timingLogLevel); - reactServer.logging.setLevel('gauge', gaugeLogLevel); -} - -const logProductionWarnings = ({hot, minify, jsUrl, longTermCaching}) => { - // if the server is being launched with some bad practices for production mode, then we - // should output a warning. if arg.jsurl is set, then hot and minify are moot, since - // we aren't serving JavaScript & CSS at all. - if ((!jsUrl && (hot || !minify)) || process.env.NODE_ENV !== "production" || !longTermCaching) { //eslint-disable-line no-process-env - logger.warning("PRODUCTION WARNING: the following current settings are discouraged in production environments. (If you are developing, carry on!):"); - if (hot) { - logger.warning("-- Hot reload is enabled. To disable, set hot to false (--hot=false at the command-line) or set NODE_ENV=production."); - } - - if (!minify) { - logger.warning("-- Minification is disabled. To enable, set minify to true (--minify at the command-line) or set NODE_ENV=production."); - } - - if (!longTermCaching) { - logger.warning("-- Long-term caching is disabled. To enable, set longTermCaching to true (--long-term-caching at the command-line) or set NODE_ENV=production to turn on."); - } - logger.info(`NODE_ENV is set to ${process.env.NODE_ENV}`); //eslint-disable-line no-process-env - } - -} diff --git a/packages/react-server-cli/src/compileClient.js b/packages/react-server-cli/src/compileClient.js index 37f9ac7ce..28140c8ce 100644 --- a/packages/react-server-cli/src/compileClient.js +++ b/packages/react-server-cli/src/compileClient.js @@ -20,8 +20,9 @@ import callerDependency from "./callerDependency" // once the compiler has been run. The file path returned from the promise // can be required and passed in to reactServer.middleware(). // TODO: add options for sourcemaps. -export default (routes, opts = {}) => { +export default (opts = {}) => { const { + routes, workingDir = "./__clientTemp", routesDir = ".", outputDir = workingDir + "/build", @@ -343,9 +344,9 @@ const writeClientBootstrapFile = (outputDir, opts) => { } var reactServer = require("react-server"); window.rfBootstrap = function() { - reactServer.logging.setLevel('main', ${opts.logLevel}); - reactServer.logging.setLevel('time', ${opts.timingLogLevel}); - reactServer.logging.setLevel('gauge', ${opts.gaugeLogLevel}); + reactServer.logging.setLevel('main', ${JSON.stringify(opts.logLevel)}); + reactServer.logging.setLevel('time', ${JSON.stringify(opts.timingLogLevel)}); + reactServer.logging.setLevel('gauge', ${JSON.stringify(opts.gaugeLogLevel)}); new reactServer.ClientController({routes: require("./routes_client")}).init(); }` ); diff --git a/packages/react-server-cli/src/fileExists.js b/packages/react-server-cli/src/fileExists.js new file mode 100644 index 000000000..627b5e89b --- /dev/null +++ b/packages/react-server-cli/src/fileExists.js @@ -0,0 +1,10 @@ +import fs from "fs"; + +export default function fileExists(fn) { + try { + fs.statSync(fn); + return true; + } catch (e) { + return false; + } +} diff --git a/packages/react-server-cli/src/handleCompilationErrors.js b/packages/react-server-cli/src/handleCompilationErrors.js new file mode 100644 index 000000000..929b14fe7 --- /dev/null +++ b/packages/react-server-cli/src/handleCompilationErrors.js @@ -0,0 +1,28 @@ +import {logging} from "./react-server"; + +const logger = logging.getLogger(__LOGGER__); + +// takes in the err and stats object returned by a webpack compilation and returns +// an error object if something serious happened, or null if things are ok. +export default function handleCompilationErrors (err, stats){ + if (err) { + logger.error("Error during webpack build."); + logger.error(err); + return new Error(err); + // TODO: inspect stats to see if there are errors -sra. + } else if (stats.hasErrors()) { + console.error("There were errors in the JavaScript compilation."); + stats.toJson().errors.forEach((error) => { + console.error(error); + }); + return new Error("There were errors in the JavaScript compilation."); + } else if (stats.hasWarnings()) { + logger.warning("There were warnings in the JavaScript compilation. Note that this is normal if you are minifying your code."); + // for now, don't enumerate warnings; they are absolutely useless in minification mode. + // TODO: handle this more intelligently, perhaps with a --reportwarnings flag or with different + // behavior based on whether or not --minify is set. + } + return null; +} + + diff --git a/packages/react-server-cli/src/index.js b/packages/react-server-cli/src/index.js index 049d4411c..01565a58b 100644 --- a/packages/react-server-cli/src/index.js +++ b/packages/react-server-cli/src/index.js @@ -1,4 +1,6 @@ const fs = require('fs'); +const run = require("./run").default; +const parseCliArgs = require("./parseCliArgs").default; require.extensions['.css'] = require.extensions['.less'] = @@ -14,6 +16,6 @@ require.extensions['.md'] = }; module.exports = { - start: require("./startServer").default, - parseCliArgs: require("./parseCliArgs").default, + parseCliArgs, + run, }; diff --git a/packages/react-server-cli/src/logProductionWarnings.js b/packages/react-server-cli/src/logProductionWarnings.js new file mode 100644 index 000000000..36c507b34 --- /dev/null +++ b/packages/react-server-cli/src/logProductionWarnings.js @@ -0,0 +1,25 @@ +import reactServer from "./react-server"; + +const logger = reactServer.logging.getLogger(__LOGGER__); + +export default function logProductionWarnings({hot, minify, jsUrl, longTermCaching}){ + // if the server is being launched with some bad practices for production mode, then we + // should output a warning. if arg.jsurl is set, then hot and minify are moot, since + // we aren't serving JavaScript & CSS at all. + if ((!jsUrl && (hot || !minify)) || process.env.NODE_ENV !== "production" || !longTermCaching) { //eslint-disable-line no-process-env + logger.warning("PRODUCTION WARNING: the following current settings are discouraged in production environments. (If you are developing, carry on!):"); + if (hot) { + logger.warning("-- Hot reload is enabled. To disable, set hot to false (--hot=false at the command-line) or set NODE_ENV=production."); + } + + if (!minify) { + logger.warning("-- Minification is disabled. To enable, set minify to true (--minify at the command-line) or set NODE_ENV=production."); + } + + if (!longTermCaching) { + logger.warning("-- Long-term caching is disabled. To enable, set longTermCaching to true (--long-term-caching at the command-line) or set NODE_ENV=production to turn on."); + } + logger.info(`NODE_ENV is set to ${process.env.NODE_ENV}`); //eslint-disable-line no-process-env + } + +} diff --git a/packages/react-server-cli/src/parseCliArgs.js b/packages/react-server-cli/src/parseCliArgs.js index bb6c28e8c..5f3627196 100644 --- a/packages/react-server-cli/src/parseCliArgs.js +++ b/packages/react-server-cli/src/parseCliArgs.js @@ -2,8 +2,8 @@ import yargs from "yargs" import fs from "fs" export default (args = process.argv) => { - var parsedArgs = yargs(args) - .usage('Usage: $0 [options]') + var argsDefinition = yargs(args) + .usage('Usage: $0 [options]') .option("routes-file", { describe: "The routes file to load. Default is 'routes.js'.", }) @@ -94,10 +94,27 @@ export default (args = process.argv) => { .help('?') .alias('?', 'help') .demand(0) - .argv; - if (parsedArgs.https && (parsedArgs.httpsKey || parsedArgs.httpsCert || parsedArgs.httpsCa || parsedArgs.httpsPfx || parsedArgs.httpsPassphrase)) { - throw new Error("If you set https to true, you must not set https-key, https-cert, https-ca, https-pfx, or https-passphrase."); + const commands = { + "start" : "Start the server", + "compile" : "Compile static assets", + "init" : "Initialize a React Server site", + "add-page" : "Add a page to an existing site", + } + + Object.keys(commands) + .forEach(k => argsDefinition = argsDefinition.command(k, commands[k])); + + var parsedArgs = argsDefinition.argv; + + parsedArgs.command = parsedArgs._[2]; + + if (!commands[parsedArgs.command]) { + argsDefinition.showHelp(); + if (parsedArgs.command) { + console.log("Invalid command: " + parsedArgs.command); + } + process.exit(1); // eslint-disable-line no-process-exit } // we remove all the options that have undefined as their value; those are the @@ -110,7 +127,7 @@ export default (args = process.argv) => { const sslize = argv => { if (argv.httpsKey || argv.httpsCert || argv.httpsCa || argv.httpsPfx || argv.httpsPassphrase) { - argv.https = { + argv.httpsOptions = { key: argv.httpsKey ? fs.readFileSync(argv.httpsKey) : undefined, cert: argv.httpsCert ? fs.readFileSync(argv.httpsCert) : undefined, ca: argv.httpsCa ? fs.readFileSync(argv.httpsCa) : undefined, @@ -119,6 +136,14 @@ const sslize = argv => { } } + if (argv.https && (argv.httpsKey || argv.httpsCert || argv.httpsCa || argv.httpsPfx || argv.httpsPassphrase)) { + throw new Error("If you set https to true, you must not set https-key, https-cert, https-ca, https-pfx, or https-passphrase."); + } + + if ((argv.key || argv.cert || argv.ca) && argv.pfx) { + throw new Error("If you set https.pfx, you can't set https.key, https.cert, or https.ca."); + } + return argv; } diff --git a/packages/react-server-cli/src/react-server.js b/packages/react-server-cli/src/react-server.js new file mode 100644 index 000000000..c17e9147d --- /dev/null +++ b/packages/react-server-cli/src/react-server.js @@ -0,0 +1,4 @@ +import callerDependency from "./callerDependency"; + +// We need react-server to be a singleton, and we want our caller's copy. +module.exports = require(callerDependency("react-server")); diff --git a/packages/react-server-cli/src/run.js b/packages/react-server-cli/src/run.js new file mode 100644 index 000000000..820f008d0 --- /dev/null +++ b/packages/react-server-cli/src/run.js @@ -0,0 +1,44 @@ +import path from "path"; +import chalk from "chalk"; +import mergeOptions from "./mergeOptions" +import findOptionsInFiles from "./findOptionsInFiles" +import defaultOptions from "./defaultOptions" +import ConfigurationError from "./ConfigurationError" + +/* eslint-disable consistent-return */ +export default function run(options = {}) { + + // for the option properties that weren't sent in, look for a config file + // (either .reactserverrc or a reactServer section in a package.json). for + // options neither passed in nor in a config file, use the defaults. + options = mergeOptions(defaultOptions, findOptionsInFiles() || {}, options); + + const { + routesFile, + jsUrl, + jsPort, + host, + httpsOptions, + } = options; + + options.routesPath = path.resolve(process.cwd(), routesFile); + options.routesDir = path.dirname(options.routesPath); + + try { + options.routes = require(options.routesPath); + } catch (e) { + // Pass. Commands need to check for routes themselves. + } + + options.outputUrl = jsUrl || `${httpsOptions ? "https" : "http"}://${host}:${jsPort}/`; + + try { + return require("./" + path.join("commands", options.command)).default(options); + } catch (e) { + if (e instanceof ConfigurationError) { + console.error(chalk.red(e.message)); + } else { + throw e; + } + } +} diff --git a/packages/react-server-cli/src/setupLogging.js b/packages/react-server-cli/src/setupLogging.js new file mode 100644 index 000000000..1ca8f5eab --- /dev/null +++ b/packages/react-server-cli/src/setupLogging.js @@ -0,0 +1,7 @@ +import reactServer from "./react-server"; + +export default function setupLogging({logLevel, timingLogLevel, gaugeLogLevel}){ + reactServer.logging.setLevel("main", logLevel); + reactServer.logging.setLevel("time", timingLogLevel); + reactServer.logging.setLevel("gauge", gaugeLogLevel); +} diff --git a/packages/react-server-integration-tests/src/specRuntime/testHelper.js b/packages/react-server-integration-tests/src/specRuntime/testHelper.js index 04161c1e8..3c6d7f001 100644 --- a/packages/react-server-integration-tests/src/specRuntime/testHelper.js +++ b/packages/react-server-integration-tests/src/specRuntime/testHelper.js @@ -3,7 +3,7 @@ var fs = require("fs"), mkdirp = require("mkdirp"), path = require("path"), Browser = require('zombie'), - start = require('react-server-cli').start, + CLI = require('react-server-cli'), crypto = require('crypto'); // This needs to be different from the port used by the tests that still live @@ -101,10 +101,12 @@ var startServer = function (specFile, routes) { var routesFile = writeRoutesFile(specFile, routes, testTempDir); - return start({ + return CLI.run({ + command: "start", routesFile: routesFile, hot: false, port: PORT, + jsPort: +PORT+1, logLevel: "emergency", timingLogLevel: "none", gaugeLogLevel: "no", diff --git a/packages/react-server-test-pages/cli.js b/packages/react-server-test-pages/cli.js index 2724ef461..d146afff3 100644 --- a/packages/react-server-test-pages/cli.js +++ b/packages/react-server-test-pages/cli.js @@ -1,5 +1,5 @@ require("babel-core/register"); -const {start, parseCliArgs} = require("react-server-cli"); +const {run, parseCliArgs} = require("react-server-cli"); -start(parseCliArgs()); +run(parseCliArgs()); diff --git a/packages/react-server-test-pages/package.json b/packages/react-server-test-pages/package.json index d5ac493e1..dfbceb0de 100644 --- a/packages/react-server-test-pages/package.json +++ b/packages/react-server-test-pages/package.json @@ -6,9 +6,9 @@ "main": "index.js", "scripts": { "prepublish": "gulp build", - "start": "node ./cli.js", + "start": "node ./cli.js start", "test": "gulp test", - "debug": "node-debug --debug-brk=0 -p 9000 react-server-cli", + "debug": "node-debug --debug-brk=0 -p 9000 ./cli.js start", "clean": "rimraf npm-debug.log* __clientTemp target" }, "author": "Bo Borgerson", diff --git a/packages/react-server-website/.reactserverrc b/packages/react-server-website/.reactserverrc index 68e145741..c6c754f11 100644 --- a/packages/react-server-website/.reactserverrc +++ b/packages/react-server-website/.reactserverrc @@ -1,19 +1,18 @@ { "routes": "./routes.js", "host": "localhost", - "port": 3000, - "js-port": 3001, + "port": 3010, + "jsPort": 3011, "hot": false, "minify": false, - "long-term-caching": false, - "compile-only": false, + "longTermCaching": false, "https": false, - "log-level": "debug", + "logLevel": "debug", "env": { - "staging": { - "port": "4000" - }, "production": { + "jsUrl": "/assets/", + "logLevel": "error", + "minify": true, "port": "80" } } diff --git a/packages/react-server-website/cli.js b/packages/react-server-website/cli.js index 2724ef461..d146afff3 100755 --- a/packages/react-server-website/cli.js +++ b/packages/react-server-website/cli.js @@ -1,5 +1,5 @@ require("babel-core/register"); -const {start, parseCliArgs} = require("react-server-cli"); +const {run, parseCliArgs} = require("react-server-cli"); -start(parseCliArgs()); +run(parseCliArgs()); diff --git a/packages/react-server-website/components/content/HomeGetStartedSection.md b/packages/react-server-website/components/content/HomeGetStartedSection.md index 9a037086d..205796c0e 100644 --- a/packages/react-server-website/components/content/HomeGetStartedSection.md +++ b/packages/react-server-website/components/content/HomeGetStartedSection.md @@ -16,7 +16,7 @@ yo react-server npm run compile npm run start -# go to http://localhost:3010 +# go to http://localhost:3000 ``` That hooks you up with [react-server-cli](/docs/guides/react-server-cli), which will get you up and running right away. diff --git a/packages/react-server-website/package.json b/packages/react-server-website/package.json index 09ba1ad84..01bcfc70e 100644 --- a/packages/react-server-website/package.json +++ b/packages/react-server-website/package.json @@ -5,9 +5,9 @@ "main": "HelloWorld.js", "private": true, "scripts": { - "build-assets": "npm run docs && node ./cli.js --compile-only --minify", - "start-prod": "node ./cli.js --port 3010 --minify --js-url /assets/", - "start": "node ./cli.js --port 3010 --js-port 3011", + "build-assets": "npm run docs && NODE_ENV=production node ./cli.js compile", + "start-prod": "NODE_ENV=production node ./cli.js start", + "start": "node ./cli.js start", "test": "eslint routes.js gulpfile.js pages/ components/ && nsp check", "docs": "docco -o docs ./components/*.js ./lib/*.js ./middleware/*.js ./pages/*.js *.js && node build-dir-contents.js" }, diff --git a/packages/react-server/README.md b/packages/react-server/README.md index 2db579f06..fbb0071b4 100644 --- a/packages/react-server/README.md +++ b/packages/react-server/README.md @@ -36,7 +36,7 @@ yo react-server # compile and run the new app npm run compile npm run start -# go to http://localhost:3010 +# go to http://localhost:3000 ``` That hooks you up with [`react-server-cli`](packages/react-server-cli), which