diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a24ee3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,108 @@ +### OSX ### +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### Windows ### +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + + +### Node ### +# Logs +logs +*.log +npm-debug.log* +phantomjsdriver.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Build +dist/ + +# Deployment +Procfile + + +### IDEs ### +.project diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9c2190a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,49 @@ +language: node_js + +node_js: + - "0.10" + - "0.12" + - "4" + +# Use container-based Travis infrastructure. +sudo: false + +branches: + only: + - master + +before_install: + # GUI for real browsers. + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start + +before_script: + # Install dev. stuff (e.g., selenium drivers). + - builder run install-dev + +script: + # Check archetype. + - npm --version + - npm run builder:check + + # Create global link for the archetypes for our + - npm link + - cd dev && npm link && cd .. + + # Initialize a fresh project from templates + - npm install -g builder-init + - mkdir .builder-init-tmp + - cd .builder-init-tmp + - >- + builder-init $PWD/.. --prompts='{ + "packageName":"whiz-bang", + "packageGitHubOrg":"Acme", + "packageDescription":"Whiz Bang", + "destination":"whiz-bang"}' + # Run initialized project's own CI with npm link'ed archetypes. + - cd whiz-bang + - npm link builder-react-app builder-react-app-dev + - npm install + - node_modules/.bin/builder run check-ci + - npm prune --production + - node_modules/.bin/builder run build diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..67f80e1 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,231 @@ +Development +=========== + +We use [builder][] and `npm` to control all aspects of development and +publishing. + +As a preliminary matter so you can type `builder` instead of +`./node_modules/.bin/builder` for all commands, please update your shell to include +`./node_modules/.bin` in `PATH` like: + +```sh +export PATH="${PATH}:./node_modules/.bin" +``` + +## Development + +All development tasks consist of watching the app bundle and the test bundle. + +Run the application with watched rebuilds: + +```sh +$ builder run dev # dev test/app server (OR) +$ builder run hot # hot reload test/app server (OR) +$ builder run prod # run the "REAL THING" with watchers +``` + +From there you can see: + +* App: [127.0.0.1:3000](http://127.0.0.1:3000/) +* Client tests: [127.0.0.1:3001/test/client/test.html](http://127.0.0.1:3001/test/client/test.html) + +### Webpack Config + +### moment + +If you are including [moment][] in your project, it will by default include more +locales than just the `en` locale. All non-en locales will be filtered out by +default in the archetype's webpack config. + + +## General Checks + +### In Development + +During development, you are expected to be running either: + +```sh +$ builder run dev +``` + +to build the lib and test files. With these running, you can run the faster + +```sh +$ builder run check-dev +``` + +Command. It is comprised of: + +```sh +$ builder run lint +$ builder run test-dev +``` + +Note that the tests here are not instrumented for code coverage and are thus +more development / debugging friendly. + +### Continuous Integration + +CI doesn't have source / test file watchers, so has to _build_ the test files +via the commands: + +```sh +$ builder run check # PhantomJS only +$ builder run check-cov # (OR) PhantomJS w/ coverage +$ builder run check-ci # (OR) PhantomJS,Firefox + coverage - available on Travis. +``` + +Which is currently comprised of: + +```sh +$ builder run lint # AND ... + +$ builder run test-base # PhantomJS only +$ builder run test-cov # (OR) PhantomJS w/ coverage +$ builder run test-ci # (OR) PhantomJS,Firefox + coverage +``` + +Note that `(test|check)-(cov|ci)` run code coverage and thus the +test code may be harder to debug because it is instrumented. + +### Client Tests + +The client tests rely on webpack dev server to create and serve the bundle +of the app/test code at: http://127.0.0.1:3001/assets/main.js which is done +with the task `builder run server-test` (part of `npm dev`). + +#### Code Coverage + +Code coverage reports are outputted to: + +``` +coverage/ + client/ + BROWSER_STRING/ + lcov-report/index.html # Viewable web report + func/ + lcov-report/index.html # Viewable web report + server/ + rest/ + lcov-report/index.html # Viewable web report + unit/ + lcov-report/index.html # Viewable web report + +``` + + +## Tests + +The test suites in this project can be found in the following locations: + +``` +test/server +test/client +test/func +``` + +### Backend Tests + +`test/server` + +Server-side (aka "backend") tests have two real flavors -- *unit* and *REST* +tests. To run all the server-side tests, run: + +```sh +$ builder run test-server +``` + +#### Server-side Unit Tests + +`test/server/spec` + +Pure JavaScript tests that import the server code and test it in isolation. + +* Extremely fast to execute. +* Typically test pure code logic in isolation. + +Run the tests with: + +```sh +$ builder run test-server-unit +``` + +#### Server-side REST Tests + +`test/server/rest` + +REST tests rely on spinning up the backend web application and using an HTTP +client to make real network requests to the server and validate responses. + +* Must set up / tear down the application web server. +* Issue real REST requests against server and verify responses. +* Fairly fast to execute (localhost network requests). +* Cover more of an "end-to-end" perspective on validation. + +Your project will need to create a `base.spec.js` file to manage server setup/teardown. +See the archetype templates for an example: +[`base.spec.js`](./init/test/server/rest/base.spec.js) + +Run the tests with: + +```sh +$ builder run test-server-rest +``` + +### Frontend Tests + +`test/client/spec` + +Client-side (aka "frontend") unit tests focus on one or more client application +files in isolation. Some aspects of these tests: + +* Extremely fast to execute. +* Execute via a test HTML driver page, not the web application HTML. +* Must create mock DOM and data fixtures. +* Mock out real browser network requests / time. +* Typically test some aspect of the UI from the user perspective. +* Run tests in the browser or from command line. +* May need to be bundled like your application code. + +Before functional tests can be run, you will need to need to set up selenium +by running: + +```sh +$ builder run install-dev +``` + +Build, then run the tests from the command line with: + +```sh +$ builder run test-client +$ builder run test-client-cov # With coverage +$ builder run test-client-dev # (Faster) Use existing `builder run dev` watchers. +``` + +### Functional Tests + +`test/func` + +Functional (aka "integration", "end-to-end") tests rely on a full, working +instance of the entire web application. These tests typically: + +* Are slower than the other test types. +* Take a "black box" approach to the application and interact only via the + actual web UI. +* Test user behaviors in an end-to-end manner. + +Your project will need to create a `base.spec.js` file to manage server setup/teardown. +See the archetype templates for an example: +[`base.spec.js`](./init/test/func/spec/base.spec.js) + +Run the tests with: + +```sh +$ builder run test-func +$ builder run test-func-cov # With coverage +$ builder run test-func-dev # (Faster) Use existing `builder run dev` watchers. +``` + +[builder]: https://github.com/FormidableLabs/builder +[builder usage]: https://github.com/FormidableLabs/builder#usage +[moment]: http://momentjs.com/ diff --git a/README.md b/README.md index 51d9422..a1ef1a6 100644 --- a/README.md +++ b/README.md @@ -1 +1,269 @@ -# builder-react-app +Builder Archetype: React App +================================== + +A React app archetype for [builder][]. + +This archetype provides both a robust set of scripts and default configs for a +standard React app project as well as a `builder-init` compatible app generator +for bootstrapping a new react app using this archetype. + +Features provided by this archetype's scripts: + +* spawning a node server with options for dev and hot-reload modes +* spawning a webpack server with options for dev and hot-reload modes +* running eslint using sensible default rules for client and server scripts and + their associated tests +* building production assets +* running tests including client unit tests (karma), server REST and unit tests + (mocha), and functional tests (mocha) +* generating coverage reports for client, server and functional tests using + istanbul +* helper scripts that group common scripts together + +Features provided by the builder-init compatible app generator + +* uses the builder-react-app archetype for ease of script and config management +* redux for state management +* react-router for routing +* express for API and app routes +* server-side react rendering w/ bootstrapped data + +## Installation + +To use the production and development workflows, install both this package +and the development module: + +```sh +$ npm install --save builder-react-app +$ npm install --save-dev builder-react-app-dev +``` + +Before functional tests can be run, you will need to also run: + +```sh +$ builder run install-dev +``` + +See the [development][] guide for information about using the `builder` command. + + +## Generator + +To bootstrap a new project from scratch with template files from this +archetype, you can use [builder-init][]: + +```sh +$ npm install -g builder-init +$ builder-init builder-react-app +``` + +This will download this archetype, prompt you for several template data values +and inflate the [archetype templates](./init) to real files at a chosen +directory. + + +## Project Structure + +See the [development][] guide for workflows associated with this archetype. + +The archetype assumes a file structure like the following: + +``` +server + index.js +test + client/ + spec/ + *.jsx? + main.js + test.html + func/ + spec/ + *.spec.js + setup.dev.js + setup.js + server/ + rest/ + *.spec.js + spec/ + *.spec.js + setup.js +.builderrc +package.json +``` + +This matches the [`builder-init` templates](init) found in the source of this +archetype. + + +## Usage Notes + +### Babel + +This archetype does not currently specify its own `.babelrc`. Your project +should specify its own in the root directory if you want non-default Babel +settings (like using stage 2, for instance), use a `.babelrc` like so: + +```json +{ + "stage": 2, + "nonStandard": true +} +``` + +## Tasks + +``` +$ builder help + +Usage: + + builder + +Actions: + + run, concurrent, envs, help + +Flags: General + + --builderrc: Path to builder config file (default: `.builderrc`) + + --help: Display help and exit + + --version: Display version and exit + +Tasks: + + npm:start + [builder-react-app] node server/index.js + + build + [builder-react-app] webpack --config node_modules/builder-react-app/config/webpack/webpack.config.js + + check + [builder-react-app] builder concurrent lint test + + check-ci + [builder-react-app] builder concurrent lint test-ci + + check-ci-win + [builder-react-app] builder concurrent lint test-ci-win + + check-cov + [builder-react-app] builder concurrent lint test-cov + + check-dev + [builder-react-app] builder concurrent lint test-dev + + dev + [builder-react-app] builder concurrent server-wds-test server-wds-dev server-dev + + hot + [builder-react-app] builder concurrent server-wds-test server-wds-hot server-hot + + install-dev + [builder-react-app] selenium-standalone install + + lint + [builder-react-app] builder run lint-client && builder run lint-client-test && builder run lint-server && builder run lint-server-test + + lint-client + [builder-react-app] eslint --ext .js,.jsx -c node_modules/builder-react-app/config/eslint/.eslintrc-client client templates + + lint-client-test + [builder-react-app] eslint --ext .js,.jsx -c node_modules/builder-react-app/config/eslint/.eslintrc-client-test test/client + + lint-server + [builder-react-app] eslint -c node_modules/builder-react-app/config/eslint/.eslintrc-server server shared + + lint-server-test + [builder-react-app] eslint -c node_modules/builder-react-app/config/eslint/.eslintrc-server-test test/server test/func + + prod + [builder-react-app] builder concurrent watch server sources + + server + [builder-react-app] nodemon --watch client --watch server --watch templates --ext js,jsx server/index.js + + server-dev + [builder-react-app] builder envs server '[{"WEBPACK_DEV":true}]' + + server-hot + [builder-react-app] builder envs server '[{"WEBPACK_HOT":true}]' + + server-wds-dev + [builder-react-app] webpack-dev-server --config node_modules/builder-react-app/config/webpack/webpack.config.dev.js --progress --colors --port 2992 + + server-wds-hot + [builder-react-app] webpack-dev-server --config node_modules/builder-react-app/config/webpack/webpack.config.hot.js --hot --progress --colors --port 2992 --inline + + server-wds-test + [builder-react-app] webpack-dev-server --port 3001 --config node_modules/builder-react-app/config/webpack/webpack.config.test.js --colors + + sources + [builder-react-app] http-server -p 3001 . + + test-base + [builder-react-app] builder run test-client && builder run test-server && builder run test-func + + test-ci + [builder-react-app] builder run test-client-ci && builder run test-server-cov && builder run test-func-cov + + test-ci-win + [builder-react-app] builder run test-client-ci-win && builder run test-server && echo 'TODO(36) fix Appveyor test-func' + + test-client + [builder-react-app] karma start node_modules/builder-react-app/config/karma/karma.conf.js + + test-client-ci + [builder-react-app] karma start --browsers PhantomJS,Firefox node_modules/builder-react-app/config/karma/karma.conf.coverage.js + + test-client-ci-win + [builder-react-app] karma start --browsers PhantomJS,IE node_modules/builder-react-app/config/karma/karma.conf.js + + test-client-cov + [builder-react-app] karma start node_modules/builder-react-app/config/karma/karma.conf.coverage.js + + test-client-dev + [builder-react-app] karma start node_modules/builder-react-app/config/karma/karma.conf.dev.js + + test-cov + [builder-react-app] builder run test-client-cov && builder run test-server-cov && builder run test-func-cov + + test-dev + [builder-react-app] builder run test-client-dev && builder run test-server && builder run test-func-dev + + test-func + [builder-react-app] mocha --opts node_modules/builder-react-app/config/mocha/func/mocha.opts test/func/spec + + test-func-cov + [builder-react-app] istanbul cover --config node_modules/builder-react-app/config/istanbul/.istanbul.func.yml _mocha -- --opts node_modules/builder-react-app/config/mocha/func/mocha.opts test/func/spec + + test-func-dev + [builder-react-app] mocha --opts node_modules/builder-react-app/config/mocha/func/mocha.dev.opts test/func/spec + + test-server + [builder-react-app] builder concurrent test-server-unit test-server-rest + + test-server-cov + [builder-react-app] builder concurrent test-server-unit-cov test-server-rest-cov + + test-server-rest + [builder-react-app] mocha --opts node_modules/builder-react-app/config/mocha/server/mocha.opts test/server/rest + + test-server-rest-cov + [builder-react-app] istanbul cover --config node_modules/builder-react-app/config/istanbul/.istanbul.server-rest.yml _mocha -- --opts node_modules/builder-react-app/config/mocha/server/mocha.opts test/server/rest + + test-server-unit + [builder-react-app] mocha --opts node_modules/builder-react-app/config/mocha/server/mocha.opts test/server/spec + + test-server-unit-cov + [builder-react-app] istanbul cover --config node_modules/builder-react-app/config/istanbul/.istanbul.server-unit.yml _mocha -- --opts node_modules/builder-react-app/config/mocha/server/mocha.opts test/server/spec + + watch + [builder-react-app] webpack --config node_modules/builder-react-app/config/webpack/webpack.config.js --watch --colors +``` + +[builder]: https://github.com/FormidableLabs/builder +[builder-init]: https://github.com/FormidableLabs/builder-init +[development]: ./DEVELOPMENT.md diff --git a/config/eslint/.eslintrc-client b/config/eslint/.eslintrc-client new file mode 100644 index 0000000..4b029bf --- /dev/null +++ b/config/eslint/.eslintrc-client @@ -0,0 +1,3 @@ +--- +extends: + - "defaults/configurations/walmart/es6-react" diff --git a/config/eslint/.eslintrc-client-test b/config/eslint/.eslintrc-client-test new file mode 100644 index 0000000..52d3c6b --- /dev/null +++ b/config/eslint/.eslintrc-client-test @@ -0,0 +1,10 @@ +--- +extends: + - "defaults/configurations/walmart/es6-react-test" + +globals: + expect: false + sandbox: false + +rules: + no-unused-expressions: 0 # Disable for Chai expression assertions. \ No newline at end of file diff --git a/config/eslint/.eslintrc-server b/config/eslint/.eslintrc-server new file mode 100644 index 0000000..0881893 --- /dev/null +++ b/config/eslint/.eslintrc-server @@ -0,0 +1,6 @@ +--- +extends: + - "defaults/configurations/walmart/es5-node" + +globals: + fetch: false diff --git a/config/eslint/.eslintrc-server-test b/config/eslint/.eslintrc-server-test new file mode 100644 index 0000000..723e34a --- /dev/null +++ b/config/eslint/.eslintrc-server-test @@ -0,0 +1,15 @@ +--- +extends: + - "defaults/configurations/walmart/es5-node" + +env: + mocha: true + +globals: + fetch: false + expect: false + sandbox: false + +rules: + no-unused-expressions: 0 # Disable for Chai expression assertions. + max-nested-callbacks: 0 # Disable for nested describes. diff --git a/config/istanbul/.istanbul.func.yml b/config/istanbul/.istanbul.func.yml new file mode 100644 index 0000000..ceaef9e --- /dev/null +++ b/config/istanbul/.istanbul.func.yml @@ -0,0 +1,6 @@ +reporting: + dir: coverage/func + reports: + - lcov + - json + - text-summary diff --git a/config/istanbul/.istanbul.server-rest.yml b/config/istanbul/.istanbul.server-rest.yml new file mode 100644 index 0000000..3b32a73 --- /dev/null +++ b/config/istanbul/.istanbul.server-rest.yml @@ -0,0 +1,6 @@ +reporting: + dir: coverage/server/rest + reports: + - lcov + - json + - text-summary diff --git a/config/istanbul/.istanbul.server-unit.yml b/config/istanbul/.istanbul.server-unit.yml new file mode 100644 index 0000000..8be76d1 --- /dev/null +++ b/config/istanbul/.istanbul.server-unit.yml @@ -0,0 +1,6 @@ +reporting: + dir: coverage/server/unit + reports: + - lcov + - json + - text-summary diff --git a/config/karma/karma.conf.coverage.js b/config/karma/karma.conf.coverage.js new file mode 100644 index 0000000..9606faa --- /dev/null +++ b/config/karma/karma.conf.coverage.js @@ -0,0 +1,27 @@ +"use strict"; +/* + * Karma Configuration: "coverage" version. + * + * This configuration is the same as basic one-shot version, just with coverage. + */ +var webpackCovCfg = require("../webpack/webpack.config.coverage"); +var path = require("path"); + +var ROOT = process.cwd(); + +module.exports = function (config) { + /* eslint-disable global-require */ + require("./karma.conf")(config); + config.set({ + reporters: ["spec", "coverage"], + webpack: webpackCovCfg, + coverageReporter: { + reporters: [ + { type: "json", file: "coverage.json" }, + { type: "lcov" }, + { type: "text-summary" } + ], + dir: path.join(ROOT, "coverage/client") + } + }); +}; diff --git a/config/karma/karma.conf.dev.js b/config/karma/karma.conf.dev.js new file mode 100644 index 0000000..10a8e60 --- /dev/null +++ b/config/karma/karma.conf.dev.js @@ -0,0 +1,31 @@ +"use strict"; +/* + * Karma Configuration: "dev" version. + * + * This configuration relies on a `webpack-dev-server` already running and + * bundling `webpack.config.test.js` on port 3001. If this is not running, + * then the alternate `karma.conf.js` file will _also_ run the webpack dev + * server during the test run. + */ +module.exports = function (config) { + config.set({ + frameworks: ["mocha", "phantomjs-shim"], + reporters: ["spec"], + browsers: ["PhantomJS"], + basePath: ".", // repository root. + files: [ + // Sinon has issues with webpack. Do global include. + require.resolve("sinon/pkg/sinon"), + + // Test bundle (must be created via `npm run dev|hot|server-test`) + "http://127.0.0.1:3001/assets/main.js" + ], + port: 9999, + singleRun: true, + client: { + mocha: { + ui: "bdd" + } + } + }); +}; diff --git a/config/karma/karma.conf.js b/config/karma/karma.conf.js new file mode 100644 index 0000000..5efee39 --- /dev/null +++ b/config/karma/karma.conf.js @@ -0,0 +1,49 @@ +"use strict"; +/* + * Karma Configuration: "full" version. + * + * This configuration runs a temporary `webpack-dev-server` and builds + * the test files one-off for just a single run. This is appropriate for a + * CI environment or if you're not otherwise running `npm run dev|hot`. + */ +var path = require("path"); +var webpackCfg = require("../webpack/webpack.config.test"); + +var ROOT = process.cwd(); +var MAIN_PATH = path.join(ROOT, "test/client/main.js"); +var PREPROCESSORS = {}; +PREPROCESSORS[MAIN_PATH] = ["webpack"]; + +module.exports = function (config) { + // Start with the "dev" (webpack-dev-server is already running) config + // and add in the webpack stuff. + /* eslint-disable global-require */ + require("./karma.conf.dev")(config); + + // Overrides. + config.set({ + preprocessors: PREPROCESSORS, + files: [ + // Sinon has issues with webpack. Do global include. + require.resolve("sinon/pkg/sinon"), + + // Test bundle (created via local webpack-dev-server in this config). + MAIN_PATH + ], + webpack: webpackCfg, + webpackServer: { + port: 3010, // Choose a non-conflicting port. + quiet: false, + noInfo: true, + stats: { + assets: false, + colors: true, + version: false, + hash: false, + timings: false, + chunks: false, + chunkModules: false + } + } + }); +}; diff --git a/config/mocha/func/mocha.dev.opts b/config/mocha/func/mocha.dev.opts new file mode 100644 index 0000000..02a60c6 --- /dev/null +++ b/config/mocha/func/mocha.dev.opts @@ -0,0 +1,3 @@ +--require test/func/setup.dev.js +--recursive +--timeout 5000 diff --git a/config/mocha/func/mocha.opts b/config/mocha/func/mocha.opts new file mode 100644 index 0000000..995f397 --- /dev/null +++ b/config/mocha/func/mocha.opts @@ -0,0 +1,3 @@ +--require test/func/setup.js +--recursive +--timeout 20000 diff --git a/config/mocha/server/mocha.opts b/config/mocha/server/mocha.opts new file mode 100644 index 0000000..b5fdf1c --- /dev/null +++ b/config/mocha/server/mocha.opts @@ -0,0 +1,2 @@ +--require test/server/setup.js +--recursive diff --git a/config/webpack/partials/app-entry.js b/config/webpack/partials/app-entry.js new file mode 100644 index 0000000..181cda2 --- /dev/null +++ b/config/webpack/partials/app-entry.js @@ -0,0 +1,13 @@ +"use strict"; + +var partial = require("webpack-partial").default; +var path = require("path"); + +var ROOT = process.cwd(); + +module.exports = function (config) { + return partial(config, { + context: path.join(ROOT, "client"), + entry: "./app.jsx" + }); +}; diff --git a/config/webpack/partials/babel.js b/config/webpack/partials/babel.js new file mode 100644 index 0000000..9f9a0ec --- /dev/null +++ b/config/webpack/partials/babel.js @@ -0,0 +1,18 @@ +"use strict"; + +var partial = require("webpack-partial").default; + +module.exports = function (config) { + return partial(config, { + module: { + loaders: [ + { + name: "babel", + test: /\.jsx?$/, + exclude: [/node_modules/], + loaders: [require.resolve("babel-loader") + "?optional=runtime"] + } + ] + } + }); +}; diff --git a/config/webpack/partials/binaries.js b/config/webpack/partials/binaries.js new file mode 100644 index 0000000..25b3c69 --- /dev/null +++ b/config/webpack/partials/binaries.js @@ -0,0 +1,17 @@ +"use strict"; + +var partial = require("webpack-partial").default; + +module.exports = function (config) { + return partial(config, { + module: { + loaders: [ + { + name: "binary", + test: /\.(png|svg|woff|woff2|ttf|eot|jpg|jpeg)$/i, + loader: require.resolve("url-loader") + "?limit=10000" + } + ] + } + }); +}; diff --git a/config/webpack/partials/css.js b/config/webpack/partials/css.js new file mode 100644 index 0000000..9d1e4dd --- /dev/null +++ b/config/webpack/partials/css.js @@ -0,0 +1,19 @@ +"use strict"; + +var partial = require("webpack-partial").default; +var ExtractTextPlugin = require("extract-text-webpack-plugin"); + +module.exports = function (config) { + return partial(config, { + module: { + loaders: [{ + name: "style", + test: /\.css$/, + loader: ExtractTextPlugin.extract( + require.resolve("style-loader"), + require.resolve("css-loader") + ) + }] + } + }); +}; diff --git a/config/webpack/partials/dev-output.js b/config/webpack/partials/dev-output.js new file mode 100644 index 0000000..a7b6bb5 --- /dev/null +++ b/config/webpack/partials/dev-output.js @@ -0,0 +1,23 @@ +"use strict"; + +var partial = require("webpack-partial").default; +var webpack = require("webpack"); +var ExtractTextPlugin = require("extract-text-webpack-plugin"); + +var ROOT = process.cwd(); + +module.exports = function (config) { + return partial(config, { + output: { + path: ROOT, + filename: "bundle.js", + publicPath: "/js/" + }, + plugins: [ + new webpack.NoErrorsPlugin(), + new ExtractTextPlugin("style.css", { + allChunks: true + }) + ] + }); +}; diff --git a/config/webpack/partials/moment-locale.js b/config/webpack/partials/moment-locale.js new file mode 100644 index 0000000..cdf92cd --- /dev/null +++ b/config/webpack/partials/moment-locale.js @@ -0,0 +1,15 @@ +"use strict"; + +var partial = require("webpack-partial").default; +var webpack = require("webpack"); + +module.exports = function (config) { + return partial(config, { + module: { + plugins: [ + // Moment by default includes all locales - this ensures that only english is loaded. + new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/) + ] + } + }); +}; diff --git a/config/webpack/partials/source-map.js b/config/webpack/partials/source-map.js new file mode 100644 index 0000000..e50ec56 --- /dev/null +++ b/config/webpack/partials/source-map.js @@ -0,0 +1,18 @@ +"use strict"; + +var partial = require("webpack-partial").default; +var SourceMapDevToolPlugin = require("webpack").SourceMapDevToolPlugin; + +module.exports = function (config) { + return partial(config, { + plugins: [ + new SourceMapDevToolPlugin({ + test: /\.(css|js)($|\?)/, + filename: "[file].map", + append: "\n//# sourceMappingURL=[url]", + module: true, + columns: true + }) + ] + }); +}; \ No newline at end of file diff --git a/config/webpack/partials/test-entry.js b/config/webpack/partials/test-entry.js new file mode 100644 index 0000000..45848da --- /dev/null +++ b/config/webpack/partials/test-entry.js @@ -0,0 +1,19 @@ +"use strict"; + +var partial = require("webpack-partial").default; +var path = require("path"); + +var ROOT = process.cwd(); + +module.exports = function (config) { + return partial(config, { + context: path.join(ROOT, "test/client"), + entry: "./main", + resolve: { + alias: { + // Allow root import of `client/FOO` from ROOT/client. + client: path.join(ROOT, "client") + } + } + }); +}; diff --git a/config/webpack/partials/test-output.js b/config/webpack/partials/test-output.js new file mode 100644 index 0000000..989e0fe --- /dev/null +++ b/config/webpack/partials/test-output.js @@ -0,0 +1,21 @@ +"use strict"; + +var partial = require("webpack-partial").default; +var ExtractTextPlugin = require("extract-text-webpack-plugin"); + +var ROOT = process.cwd(); + +module.exports = function (config) { + return partial(config, { + output: { + path: ROOT, + filename: "main.js", + publicPath: "/assets/" + }, + plugins: [ + new ExtractTextPlugin("style.css", { + allChunks: true + }) + ] + }); +}; diff --git a/config/webpack/webpack.config.coverage.js b/config/webpack/webpack.config.coverage.js new file mode 100644 index 0000000..5200271 --- /dev/null +++ b/config/webpack/webpack.config.coverage.js @@ -0,0 +1,39 @@ +"use strict"; + +/** + * Webpack frontend test (w/ coverage) configuration. + */ + +var compose = require("lodash/fp/flow"); +var testEntry = require("./partials/test-entry"); +var testOutput = require("./partials/test-output"); +var sourceMap = require("./partials/source-map"); + +var path = require("path"); +var ROOT = process.cwd(); + +module.exports = compose( + testEntry, + testOutput, + sourceMap +)({ + cache: true, + resolve: { + extensions: ["", ".js", ".jsx"] + }, + module: { + preLoaders: [ + // Manually instrument client code for code coverage. + // https://github.com/deepsweet/isparta-loader handles ES6 + normal JS. + { + name: "isparta", + test: /client\/.*\.jsx?$/, + exclude: [ + path.join(ROOT, "test/"), + path.join(ROOT, "node_modules/") + ], + loader: require.resolve("isparta-loader") + } + ] + } +}); diff --git a/config/webpack/webpack.config.dev.js b/config/webpack/webpack.config.dev.js new file mode 100644 index 0000000..a2c0233 --- /dev/null +++ b/config/webpack/webpack.config.dev.js @@ -0,0 +1,27 @@ +"use strict"; + +/** + * Webpack development configuration + */ + +var compose = require("lodash/fp/flow"); +var css = require("./partials/css"); +var babel = require("./partials/babel"); +var binaries = require("./partials/binaries"); +var appEntry = require("./partials/app-entry"); +var devOutput = require("./partials/dev-output"); +var sourceMap = require("./partials/source-map"); + +module.exports = compose( + css, + babel, + binaries, + appEntry, + devOutput, + sourceMap +)({ + cache: true, + resolve: { + extensions: ["", ".js", ".jsx"] + } +}); diff --git a/config/webpack/webpack.config.hot.js b/config/webpack/webpack.config.hot.js new file mode 100644 index 0000000..dd7391d --- /dev/null +++ b/config/webpack/webpack.config.hot.js @@ -0,0 +1,43 @@ +"use strict"; + +/** + * Webpack hot configuration + */ + +var compose = require("lodash/fp/flow"); +var babel = require("./partials/babel"); +var binaries = require("./partials/binaries"); +var appEntry = require("./partials/app-entry"); +var devOutput = require("./partials/dev-output"); +var sourceMap = require("./partials/source-map"); + +module.exports = compose( + appEntry, + babel, + binaries, + devOutput, + sourceMap +)({ + cache: true, + resolve: { + extensions: ["", ".js", ".jsx"] + }, + entry: [ + "webpack/hot/only-dev-server" + ], + module: { + loaders: [ + { + name: "babel", + loaders: [ + require.resolve("react-hot-loader") + ] + }, + { + name: "style", + test: /\.css$/, + loader: require.resolve("style-loader") + "!" + require.resolve("css-loader") + } + ] + } +}); diff --git a/config/webpack/webpack.config.js b/config/webpack/webpack.config.js new file mode 100644 index 0000000..106e286 --- /dev/null +++ b/config/webpack/webpack.config.js @@ -0,0 +1,63 @@ +"use strict"; + +/** + * Webpack production configuration + */ + +var compose = require("lodash/fp/flow"); +var css = require("./partials/css"); +var babel = require("./partials/babel"); +var binaries = require("./partials/binaries"); +var appEntry = require("./partials/app-entry"); +var momentLocale = require("./partials/moment-locale"); + +var webpack = require("webpack"); +var ExtractTextPlugin = require("extract-text-webpack-plugin"); +var StatsWriterPlugin = require("webpack-stats-plugin").StatsWriterPlugin; +var CleanPlugin = require("clean-webpack-plugin"); +var path = require("path"); + +var ROOT = process.cwd(); + +module.exports = compose( + css, + babel, + binaries, + appEntry, + momentLocale +)({ + cache: true, + output: { + path: path.join(ROOT, "dist/js"), + filename: "bundle.[hash].js" + }, + resolve: { + extensions: ["", ".js", ".jsx"] + }, + plugins: [ + // Clean + new CleanPlugin(["dist"], { + root: path.join(ROOT) + }), + + // Optimize + new webpack.optimize.UglifyJsPlugin(), + + // Extract CSS + new ExtractTextPlugin("style.[hash].css"), + + // Meta, debug info. + new webpack.DefinePlugin({ + // Signal production mode for React JS libs. + "process.env.NODE_ENV": JSON.stringify("production") + }), + new StatsWriterPlugin({ + // Context is relative to `output.path` / `dist/js` + filename: "../server/stats.json" + }), + new webpack.SourceMapDevToolPlugin( + "../map/[file].map", + "\n//# sourceMappingURL=http://127.0.0.1:3001/dist/map/[url]" + ) + ] +}); diff --git a/config/webpack/webpack.config.test.js b/config/webpack/webpack.config.test.js new file mode 100644 index 0000000..ed7a568 --- /dev/null +++ b/config/webpack/webpack.config.test.js @@ -0,0 +1,25 @@ +"use strict"; + +/** + * Webpack frontend test configuration. + */ + +var compose = require("lodash/fp/flow"); +var css = require("./partials/css"); +var babel = require("./partials/babel"); +var binaries = require("./partials/binaries"); +var testEntry = require("./partials/test-entry"); +var testOutput = require("./partials/test-output"); + +module.exports = compose( + css, + babel, + binaries, + testEntry, + testOutput +)({ + cache: true, + resolve: { + extensions: ["", ".js", ".jsx"] + } +}); diff --git a/dev/.gitignore b/dev/.gitignore new file mode 100644 index 0000000..a24ee3c --- /dev/null +++ b/dev/.gitignore @@ -0,0 +1,108 @@ +### OSX ### +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### Windows ### +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + + +### Node ### +# Logs +logs +*.log +npm-debug.log* +phantomjsdriver.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Build +dist/ + +# Deployment +Procfile + + +### IDEs ### +.project diff --git a/dev/README.md b/dev/README.md new file mode 100644 index 0000000..a1ef1a6 --- /dev/null +++ b/dev/README.md @@ -0,0 +1,269 @@ +Builder Archetype: React App +================================== + +A React app archetype for [builder][]. + +This archetype provides both a robust set of scripts and default configs for a +standard React app project as well as a `builder-init` compatible app generator +for bootstrapping a new react app using this archetype. + +Features provided by this archetype's scripts: + +* spawning a node server with options for dev and hot-reload modes +* spawning a webpack server with options for dev and hot-reload modes +* running eslint using sensible default rules for client and server scripts and + their associated tests +* building production assets +* running tests including client unit tests (karma), server REST and unit tests + (mocha), and functional tests (mocha) +* generating coverage reports for client, server and functional tests using + istanbul +* helper scripts that group common scripts together + +Features provided by the builder-init compatible app generator + +* uses the builder-react-app archetype for ease of script and config management +* redux for state management +* react-router for routing +* express for API and app routes +* server-side react rendering w/ bootstrapped data + +## Installation + +To use the production and development workflows, install both this package +and the development module: + +```sh +$ npm install --save builder-react-app +$ npm install --save-dev builder-react-app-dev +``` + +Before functional tests can be run, you will need to also run: + +```sh +$ builder run install-dev +``` + +See the [development][] guide for information about using the `builder` command. + + +## Generator + +To bootstrap a new project from scratch with template files from this +archetype, you can use [builder-init][]: + +```sh +$ npm install -g builder-init +$ builder-init builder-react-app +``` + +This will download this archetype, prompt you for several template data values +and inflate the [archetype templates](./init) to real files at a chosen +directory. + + +## Project Structure + +See the [development][] guide for workflows associated with this archetype. + +The archetype assumes a file structure like the following: + +``` +server + index.js +test + client/ + spec/ + *.jsx? + main.js + test.html + func/ + spec/ + *.spec.js + setup.dev.js + setup.js + server/ + rest/ + *.spec.js + spec/ + *.spec.js + setup.js +.builderrc +package.json +``` + +This matches the [`builder-init` templates](init) found in the source of this +archetype. + + +## Usage Notes + +### Babel + +This archetype does not currently specify its own `.babelrc`. Your project +should specify its own in the root directory if you want non-default Babel +settings (like using stage 2, for instance), use a `.babelrc` like so: + +```json +{ + "stage": 2, + "nonStandard": true +} +``` + +## Tasks + +``` +$ builder help + +Usage: + + builder + +Actions: + + run, concurrent, envs, help + +Flags: General + + --builderrc: Path to builder config file (default: `.builderrc`) + + --help: Display help and exit + + --version: Display version and exit + +Tasks: + + npm:start + [builder-react-app] node server/index.js + + build + [builder-react-app] webpack --config node_modules/builder-react-app/config/webpack/webpack.config.js + + check + [builder-react-app] builder concurrent lint test + + check-ci + [builder-react-app] builder concurrent lint test-ci + + check-ci-win + [builder-react-app] builder concurrent lint test-ci-win + + check-cov + [builder-react-app] builder concurrent lint test-cov + + check-dev + [builder-react-app] builder concurrent lint test-dev + + dev + [builder-react-app] builder concurrent server-wds-test server-wds-dev server-dev + + hot + [builder-react-app] builder concurrent server-wds-test server-wds-hot server-hot + + install-dev + [builder-react-app] selenium-standalone install + + lint + [builder-react-app] builder run lint-client && builder run lint-client-test && builder run lint-server && builder run lint-server-test + + lint-client + [builder-react-app] eslint --ext .js,.jsx -c node_modules/builder-react-app/config/eslint/.eslintrc-client client templates + + lint-client-test + [builder-react-app] eslint --ext .js,.jsx -c node_modules/builder-react-app/config/eslint/.eslintrc-client-test test/client + + lint-server + [builder-react-app] eslint -c node_modules/builder-react-app/config/eslint/.eslintrc-server server shared + + lint-server-test + [builder-react-app] eslint -c node_modules/builder-react-app/config/eslint/.eslintrc-server-test test/server test/func + + prod + [builder-react-app] builder concurrent watch server sources + + server + [builder-react-app] nodemon --watch client --watch server --watch templates --ext js,jsx server/index.js + + server-dev + [builder-react-app] builder envs server '[{"WEBPACK_DEV":true}]' + + server-hot + [builder-react-app] builder envs server '[{"WEBPACK_HOT":true}]' + + server-wds-dev + [builder-react-app] webpack-dev-server --config node_modules/builder-react-app/config/webpack/webpack.config.dev.js --progress --colors --port 2992 + + server-wds-hot + [builder-react-app] webpack-dev-server --config node_modules/builder-react-app/config/webpack/webpack.config.hot.js --hot --progress --colors --port 2992 --inline + + server-wds-test + [builder-react-app] webpack-dev-server --port 3001 --config node_modules/builder-react-app/config/webpack/webpack.config.test.js --colors + + sources + [builder-react-app] http-server -p 3001 . + + test-base + [builder-react-app] builder run test-client && builder run test-server && builder run test-func + + test-ci + [builder-react-app] builder run test-client-ci && builder run test-server-cov && builder run test-func-cov + + test-ci-win + [builder-react-app] builder run test-client-ci-win && builder run test-server && echo 'TODO(36) fix Appveyor test-func' + + test-client + [builder-react-app] karma start node_modules/builder-react-app/config/karma/karma.conf.js + + test-client-ci + [builder-react-app] karma start --browsers PhantomJS,Firefox node_modules/builder-react-app/config/karma/karma.conf.coverage.js + + test-client-ci-win + [builder-react-app] karma start --browsers PhantomJS,IE node_modules/builder-react-app/config/karma/karma.conf.js + + test-client-cov + [builder-react-app] karma start node_modules/builder-react-app/config/karma/karma.conf.coverage.js + + test-client-dev + [builder-react-app] karma start node_modules/builder-react-app/config/karma/karma.conf.dev.js + + test-cov + [builder-react-app] builder run test-client-cov && builder run test-server-cov && builder run test-func-cov + + test-dev + [builder-react-app] builder run test-client-dev && builder run test-server && builder run test-func-dev + + test-func + [builder-react-app] mocha --opts node_modules/builder-react-app/config/mocha/func/mocha.opts test/func/spec + + test-func-cov + [builder-react-app] istanbul cover --config node_modules/builder-react-app/config/istanbul/.istanbul.func.yml _mocha -- --opts node_modules/builder-react-app/config/mocha/func/mocha.opts test/func/spec + + test-func-dev + [builder-react-app] mocha --opts node_modules/builder-react-app/config/mocha/func/mocha.dev.opts test/func/spec + + test-server + [builder-react-app] builder concurrent test-server-unit test-server-rest + + test-server-cov + [builder-react-app] builder concurrent test-server-unit-cov test-server-rest-cov + + test-server-rest + [builder-react-app] mocha --opts node_modules/builder-react-app/config/mocha/server/mocha.opts test/server/rest + + test-server-rest-cov + [builder-react-app] istanbul cover --config node_modules/builder-react-app/config/istanbul/.istanbul.server-rest.yml _mocha -- --opts node_modules/builder-react-app/config/mocha/server/mocha.opts test/server/rest + + test-server-unit + [builder-react-app] mocha --opts node_modules/builder-react-app/config/mocha/server/mocha.opts test/server/spec + + test-server-unit-cov + [builder-react-app] istanbul cover --config node_modules/builder-react-app/config/istanbul/.istanbul.server-unit.yml _mocha -- --opts node_modules/builder-react-app/config/mocha/server/mocha.opts test/server/spec + + watch + [builder-react-app] webpack --config node_modules/builder-react-app/config/webpack/webpack.config.js --watch --colors +``` + +[builder]: https://github.com/FormidableLabs/builder +[builder-init]: https://github.com/FormidableLabs/builder-init +[development]: ./DEVELOPMENT.md diff --git a/dev/package.json b/dev/package.json new file mode 100644 index 0000000..220516b --- /dev/null +++ b/dev/package.json @@ -0,0 +1,48 @@ +{ + "name": "builder-react-app-dev", + "version": "0.1.0", + "description": "Builder Archetype - React App (Development)", + "main": "index.js", + "scripts": {}, + "repository": { + "type": "git", + "url": "git+https://github.com/FormidableLabs/builder-react-app.git" + }, + "author": "Zach Hale ", + "license": "MIT", + "bugs": { + "url": "https://github.com/FormidableLabs/builder-react-app/issues" + }, + "homepage": "https://github.com/FormidableLabs/builder-react-app#readme", + "dependencies": { + "babel-eslint": "^4.1.8", + "eslint": "^1.10.1", + "eslint-config-defaults": "^9.0.0", + "eslint-plugin-filenames": "^0.2.0", + "eslint-plugin-react": "^3.12.0", + "isparta-loader": "^0.2.0", + "istanbul": "^0.3.18", + "karma": "^0.13.9", + "karma-chrome-launcher": "^0.2.0", + "karma-coverage": "^0.5.0", + "karma-firefox-launcher": "^0.1.6", + "karma-ie-launcher": "^0.2.0", + "karma-mocha": "^0.2.0", + "karma-phantomjs-launcher": "^0.2.1", + "karma-phantomjs-shim": "^1.1.1", + "karma-safari-launcher": "^0.1.1", + "karma-sauce-launcher": "^0.2.14", + "karma-spec-reporter": "0.0.24", + "karma-webpack": "^1.7.0", + "lodash": "^4.5.1", + "phantomjs": "^1.9.18", + "react-hot-loader": "^1.2.8", + "selenium-standalone": "^4.5.3", + "webpack-dev-server": "^1.10.1" + }, + "peerDependencies": { + "mocha": "^2.2.5", + "sinon": "^1.16.1" + }, + "devDependencies": {} +} \ No newline at end of file diff --git a/init.js b/init.js new file mode 100644 index 0000000..051cdf9 --- /dev/null +++ b/init.js @@ -0,0 +1,65 @@ +"use strict"; + +// PascalCase (with first character capitalized). +var pascalCase = function (input) { + return input + .replace(/^\s+|\s+$/g, "") + .replace(/(^|[-_ ])+(.)/g, function (match, first, second) { + return second.toUpperCase(); + }); +}; + +/** + * Archetype `init` configuration. + * + * See: https://github.com/FormidableLabs/builder-init/blob/master/README.md#archetype-data + * for structuring this configuration file. + */ +module.exports = { + // Destination directory to write files to. + // + // This field is deep merged and added _last_ to the prompts so that archetype + // authors can add `default` values or override the default message. You + // could further override the `validate` function, but we suggest using the + // existing default as it checks the directory does not already exist (which + // is enforced later in code). + destination: { + default: function (data) { + return data.packageName; + } + }, + + // Prompts are Inquirer question objects. + // https://github.com/SBoudrias/Inquirer.js#question + // + // `builder-init` accepts: + // - an array of question objects (with a `name` property) + // - an object of question objects (keyed by `name`) + // + prompts: { + // See: https://github.com/npm/validate-npm-package-name + packageName: { + message: "Package / GitHub project name (e.g., 'whiz-bang-app|server')", + validate: function (val) { + return /^([a-z0-9]+\-?)+$/.test(val.trim()) || "Must be lower + dash-cased string"; + } + }, + packageGitHubOrg: { + message: "GitHub organization name (e.g., 'AcmeCorp')", + validate: function (val) { + return /^([^\s])*$/.test(val) || "Must be GitHub-valid organization username"; + } + }, + packageDescription: { + message: "Package description" + } + }, + + // Derived fields are asynchronous functions that are given the previous user + // input data of the form: `function (data, cb)`. They callback with: + // `(err, value)`. + derived: { + componentPath: function (data, cb) { cb(null, data.packageName); }, + componentName: function (data, cb) { cb(null, pascalCase(data.packageName)); } + } +}; diff --git a/init/.babelrc b/init/.babelrc new file mode 100644 index 0000000..83b26ea --- /dev/null +++ b/init/.babelrc @@ -0,0 +1,4 @@ +{ + "stage": 2, + "nonStandard": true +} diff --git a/init/.builderrc b/init/.builderrc new file mode 100644 index 0000000..61428e8 --- /dev/null +++ b/init/.builderrc @@ -0,0 +1,3 @@ +--- +archetypes: + - builder-react-app diff --git a/init/.eslintignore b/init/.eslintignore new file mode 100644 index 0000000..a261f29 --- /dev/null +++ b/init/.eslintignore @@ -0,0 +1 @@ +dist/* diff --git a/init/.travis.yml b/init/.travis.yml new file mode 100644 index 0000000..1a7b487 --- /dev/null +++ b/init/.travis.yml @@ -0,0 +1,45 @@ +language: node_js + +node_js: + - "0.10" + - "0.12" + - "4" + +# Use container-based Travis infrastructure. +sudo: false + +branches: + only: + - master + +before_install: + # GUI for real browsers. + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start + +before_script: + # Install dev. stuff (e.g., selenium drivers). + - builder run install-dev + +env: + # NOTE: **Cannot** have a space after `:` character in JSON string or else + # YAML parser will fail to parse correctly. + global: + # PhantomJS fails currently. (ROWDY_SETTINGS="local.phantomjs") + # https://github.com/FormidableLabs/converter-react/issues/34 + - ROWDY_SETTINGS="local.firefox" + +script: + # Run all base checks (with FF browser for functional tests). + - builder run check-ci + + # Manually send coverage reports to coveralls. + # - Aggregate client results + # - Single server and func test results + - ls coverage/client/*/lcov.info coverage/server/{rest,unit}/lcov.info coverage/func/lcov.info | cat + - cat coverage/client/*/lcov.info coverage/server/{rest,unit}/lcov.info coverage/func/lcov.info | ./node_modules/.bin/coveralls || echo "Coveralls upload failed" + + # Prune deps to just production and ensure we can still build + - npm prune --production + - npm install --production + - builder run build diff --git a/init/CONTRIBUTING.md b/init/CONTRIBUTING.md new file mode 100644 index 0000000..e6b804b --- /dev/null +++ b/init/CONTRIBUTING.md @@ -0,0 +1,12 @@ +Contributing +============ + +Thanks for helping out! + +## Development + +Run `builder run dev` to run the dev. application. + +## Checks, Tests + +Run `builder run check` before committing. diff --git a/init/DEVELOPMENT.md b/init/DEVELOPMENT.md new file mode 100644 index 0000000..ee2c40d --- /dev/null +++ b/init/DEVELOPMENT.md @@ -0,0 +1,61 @@ +Development +=========== + +All development tasks and common workflows can be found in the +[builder-react-app][] archetype [development guide][arch-dev]. + +## Development + +Notes on the development workflow in addition to the functionality +provided by the archetype are outlined below. + +### Server-side Unit Tests + +`test/server/spec` + +Programming notes: + +* Contains a Sinon [sandbox][] **with** fake timers and servers. + +#### Server-side REST Tests + +`test/server/rest` + +Programming notes: + +* Test against a remote server with environment variables: + * `TEST_REST_IS_REMOTE=true` (tests should only stub/spy if not remote) + * `TEST_REST_BASE_URL=http://example.com/` + +### Functional Tests + +`test/func` + +Programming notes: + +* Use the [webdriverio][] Selenium client libraries. +* Use the [rowdy][] configuration wrapper for webdriverio / Selenium +* Test against a remote server with environment variables: + * `TEST_FUNC_IS_REMOTE=true` (tests should only stub/spy if not remote) + * `TEST_FUNC_BASE_URL=http://example.com/` + +You can override settings and browser selections from the environment per +the [rowdy][] documentation. E.g., + +```sh +# Client and server logging. +$ ROWDY_OPTIONS='{ "client":{ "logger":true }, "server":{ "logger":true } }' \ + builder run test-func + +# Switch to Chrome +$ ROWDY_SETTINGS="local.chrome" \ + builder run test-func +``` + +And you've published! + +[builder-react-app]: https://github.com/FormidableLabs/builder-react-app +[arch-dev]: https://github.com/FormidableLabs/builder-react-app/blob/master/DEVELOPMENT.md +[sandbox]: http://sinonjs.org/docs/#sinon-sandbox +[webdriverio]: http://webdriver.io/ +[rowdy]: https://github.com/FormidableLabs/rowdy \ No newline at end of file diff --git a/init/README.md b/init/README.md new file mode 100644 index 0000000..3345784 --- /dev/null +++ b/init/README.md @@ -0,0 +1,20 @@ +# <%= packageName %> + +<%= packageDescription %> + +## Development + +All development tasks and common workflows can be found in the +[builder-react-app][] archetype [development guide][arch-dev]. + +This component was originally generated with [builder-init][], and uses +[builder][] to support the entire development / release lifecycle. + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) + +[builder]: https://github.com/FormidableLabs/builder +[builder-init]: https://github.com/FormidableLabs/builder-init +[builder-react-app]: https://github.com/FormidableLabs/builder-react-app +[arch-dev]: https://github.com/FormidableLabs/builder-react-app/blob/master/DEVELOPMENT.md diff --git a/init/appveyor.yml b/init/appveyor.yml new file mode 100644 index 0000000..68e9c53 --- /dev/null +++ b/init/appveyor.yml @@ -0,0 +1,40 @@ +# Good template: https://github.com/gruntjs/grunt/blob/master/appveyor.yml +environment: + global: + # PhantomJS fails currently. (ROWDY_SETTINGS="local.phantomjs") + # https://github.com/FormidableLabs/converter-react/issues/34 + ROWDY_SETTINGS: "local.firefox" + matrix: + - nodejs_version: "0.10" + - nodejs_version: "0.12" + - nodejs_version: "4" + +# Get the latest stable version of Node 0.STABLE.latest +install: + - ps: Install-Product node $env:nodejs_version + # Install and use local, modern NPM + - npm install npm@next + - node_modules\.bin\npm install + - node_modules\.bin\builder run install-dev + +build: off + +branches: + only: + - master + +test_script: + # Build environment. + - node --version + - node_modules\.bin\npm --version + - echo %ROWDY_SETTINGS% + + # Build and test. + - node_modules\.bin\builder run build + - node_modules\.bin\builder run check-ci-win + +matrix: + fast_finish: true + +cache: + - node_modules -> package.json # local npm modules diff --git a/init/client/actions/base.js b/init/client/actions/base.js new file mode 100644 index 0000000..1dc0bf5 --- /dev/null +++ b/init/client/actions/base.js @@ -0,0 +1,38 @@ +/** + * Actions: Base + * + * Uses base api to fetch data for the test page. + */ +import { fetchBase as fetchBaseApi } from "../utils/api"; + +export const FETCH_BASE = "FETCH_BASE"; +export const UPDATE_BASE = "UPDATE_BASE"; +export const BASE_ERROR = "BASE_ERROR"; + +export const updateBase = (data) => { + return { + type: UPDATE_BASE, + data + }; +}; + +export const baseError = (err) => { + return { + type: BASE_ERROR, + err + }; +}; + +export const fetchBase = () => { + return (dispatch) => { + dispatch(() => ({type: FETCH_BASE})); + + return fetchBaseApi() + .then((data) => { + dispatch(updateBase(data)); + }) + .catch((err) => { + dispatch(baseError(err)); + }); + }; +}; diff --git a/init/client/app.jsx b/init/client/app.jsx new file mode 100644 index 0000000..181a595 --- /dev/null +++ b/init/client/app.jsx @@ -0,0 +1,43 @@ +/** + * Client entry point. + */ +/*globals document:false */ +import React from "react"; +import ReactDOM from "react-dom"; +import { Provider } from "react-redux"; +import { Router, useRouterHistory } from "react-router"; +import createBrowserHistory from "history/lib/createBrowserHistory"; + +import routes from "./routes"; +import createStore from "./store/create-store"; + +import "./styles/app.css"; + +const history = useRouterHistory(createBrowserHistory)(); +const rootEl = document.getElementById("js-content"); + +// Although our Flux store is not a singleton, from the point of view of the +// client-side application, we instantiate a single instance here which the +// entire app will share. (So the client app _has_ an effective singleton). +let store = createStore(); + +const render = () => { + ReactDOM.render( + + + , rootEl + ); +}; + +// Try server bootstrap _first_ because doesn't need a fetch. +const serverBootstrapEl = document.querySelector(".js-bootstrap"); +if (serverBootstrapEl) { + try { + const serverBootstrap = JSON.parse(serverBootstrapEl.innerHTML); + store = createStore(serverBootstrap); + /*eslint-disable no-empty*/ + } catch (err) { /* Ignore error. */ } + /*eslint-enable no-empty*/ +} + +render(); diff --git a/init/client/components/home.jsx b/init/client/components/home.jsx new file mode 100644 index 0000000..cbde158 --- /dev/null +++ b/init/client/components/home.jsx @@ -0,0 +1,34 @@ +import React from "react"; +import { connect } from "react-redux"; +import { fetchBase } from "../actions/base"; + +export class Home extends React.Component { + componentDidMount() { + if (!this.props.base) { + this.props.dispatch(fetchBase()); + } + } + + render() { + const content = this.props.baseError ? + `Error: ${this.props.baseError}` : + this.props.base; + return ( +
+

Home

+

{content}

+
+ ); + } +} + +Home.propTypes = { + dispatch: React.PropTypes.func, + base: React.PropTypes.string, + baseError: React.PropTypes.string +}; + +export default connect((state) => ({ + base: state.base.base, + baseError: state.base.baseError +}))(Home); diff --git a/init/client/components/navigation.jsx b/init/client/components/navigation.jsx new file mode 100644 index 0000000..60d5520 --- /dev/null +++ b/init/client/components/navigation.jsx @@ -0,0 +1,33 @@ +import React from "react"; +import { Link } from "react-router"; + +const listStyle = { + listStyleType: "none", + margin: 0, + padding: 0 +}; + +const listItemStyle = { + marginRight: "1em", + display: "inline-block" +}; + +class Navigation extends React.Component { + render() { + return ( + + ); + } +} + +export default Navigation; diff --git a/init/client/components/{{componentPath}}.jsx b/init/client/components/{{componentPath}}.jsx new file mode 100644 index 0000000..492e010 --- /dev/null +++ b/init/client/components/{{componentPath}}.jsx @@ -0,0 +1,14 @@ +/** + * Convert input. + */ +import React from "react"; + +class ReactPage extends React.Component { + render() { + return ( +

React Page

+ ); + } +} + +export default ReactPage; diff --git a/init/client/containers/page.jsx b/init/client/containers/page.jsx new file mode 100644 index 0000000..7050945 --- /dev/null +++ b/init/client/containers/page.jsx @@ -0,0 +1,26 @@ +/** + * Container page. + */ +import React from "react"; +import Navigation from "../components/navigation"; + +class Page extends React.Component { + render() { + return ( +
+ +

<%= packageName %>

+ {this.props.children} +
+ ); + } +} + +Page.propTypes = { + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.node), + React.PropTypes.node + ]) +}; + +export default Page; diff --git a/init/client/reducers/base.js b/init/client/reducers/base.js new file mode 100644 index 0000000..48f8f15 --- /dev/null +++ b/init/client/reducers/base.js @@ -0,0 +1,28 @@ +import { + FETCH_BASE, + UPDATE_BASE, + BASE_ERROR +} from "../actions/base"; + +export default (state = { + base: null +}, action) => { + switch (action.type) { + case FETCH_BASE: + return Object.assign({}, state, { + base: null, + baseError: null + }); + case UPDATE_BASE: + return Object.assign({}, state, { + base: action.data.base, + baseError: null + }); + case BASE_ERROR: + return Object.assign({}, state, { + baseError: action.err.message || action.err.toString() + }); + default: + return state; + } +}; diff --git a/init/client/reducers/index.js b/init/client/reducers/index.js new file mode 100644 index 0000000..ee3ddde --- /dev/null +++ b/init/client/reducers/index.js @@ -0,0 +1,10 @@ +import { combineReducers } from "redux"; +import { routeReducer } from "react-router-redux"; +import baseReducer from "./base"; + +const rootReducer = combineReducers({ + routing: routeReducer, + base: baseReducer +}); + +export default rootReducer; diff --git a/init/client/routes.jsx b/init/client/routes.jsx new file mode 100644 index 0000000..a64fe6b --- /dev/null +++ b/init/client/routes.jsx @@ -0,0 +1,14 @@ +import React from "react"; +import { Route, IndexRoute } from "react-router"; +import Page from "./containers/page"; +import Home from "./components/home"; +import ReactPage from "./components/<%= componentPath %>"; + +const routes = ( + + + + +); + +export default routes; diff --git a/init/client/store/create-store.js b/init/client/store/create-store.js new file mode 100644 index 0000000..12c7a8f --- /dev/null +++ b/init/client/store/create-store.js @@ -0,0 +1,17 @@ +import { createStore as reduxCreateStore, applyMiddleware } from "redux"; +import thunkMiddleware from "redux-thunk"; +import createLogger from "redux-logger"; +import rootReducer from "../reducers"; + +const loggerMiddleware = createLogger(); + +const createStoreWithMiddleware = applyMiddleware( + thunkMiddleware, + loggerMiddleware +)(reduxCreateStore); + +const createStore = (initialState) => { + return createStoreWithMiddleware(rootReducer, initialState); +}; + +export default createStore; diff --git a/init/client/styles/app.css b/init/client/styles/app.css new file mode 100644 index 0000000..c7dfbed --- /dev/null +++ b/init/client/styles/app.css @@ -0,0 +1,3 @@ +body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} diff --git a/init/client/utils/api.js b/init/client/utils/api.js new file mode 100644 index 0000000..ba6e1a0 --- /dev/null +++ b/init/client/utils/api.js @@ -0,0 +1,30 @@ +/** + * Fetch data from rest API. + */ +import "isomorphic-fetch"; +import { http as httpConstants } from "../../shared/constants"; + +const api = { + BASE_URL: "", + + // Statefully set the base port and host (for server-side). + setBaseUrl: (host, port) => { + if (host) { + api.BASE_URL = `http://${host}`; + if (port) { + api.BASE_URL = `${api.BASE_URL}:${port}`; + } + } + }, + + fetchBase: () => + fetch(`${api.BASE_URL}/api/base`) + .then((res) => { + if (res.status >= httpConstants.BAD_REQUEST) { + throw new Error("Bad server response"); + } + return res.json(); + }) +}; + +export default api; diff --git a/init/package.json b/init/package.json new file mode 100644 index 0000000..0727355 --- /dev/null +++ b/init/package.json @@ -0,0 +1,56 @@ +{ + "name": "<%= packageName %>", + "version": "1.0.0", + "description": "<%= packageDescription || packageName %>", + "main": "lib/index.js",<% if (packageGitHubOrg) { %> + "repository": { + "type": "git", + "url": "https://github.com/<%= packageGitHubOrg %>/<%= packageName %>.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/<%= packageGitHubOrg %>/<%= packageName %>/issues" + }, + "homepage": "https://github.com/<%= packageGitHubOrg %>/<%= packageName %>",<% } %> + "scripts": {}, + "dependencies": { + "babel-runtime": "^5.8.19", + "builder": "^2.4.0", + "builder-react-app": "<%= archetype.package.version ? '^' + archetype.package.version : '*' %>", + "compression": "^1.5.2", + "es6-promise": "^3.0.2", + "express": "^4.13.1", + "express-bunyan-logger": "^1.2.0", + "history": "^2.0.0", + "isomorphic-fetch": "^2.1.1", + "react": "^0.14.7", + "react-dom": "^0.14.7", + "react-redux": "^4.4.0", + "react-router": "^2.0.0", + "react-router-redux": "^4.0.0", + "redux": "^3.0.2", + "redux-logger": "^2.0.1", + "redux-thunk": "^1.0.0", + "serve-favicon": "^2.3.0", + "webpack": "^1.12.14" + }, + "devDependencies": { + "builder-react-app-dev": "<%= archetype.devPackage.version ? '^' + archetype.devPackage.version : '*' %>", + "chai": "^3.2.0", + "coveralls": "^2.11.4", + "guacamole": "^1.1.2", + "http-server": "^0.9.0", + "jade": "^1.11.0", + "marked": "^0.3.4", + "mocha": "^2.2.5", + "nodemon": "^1.4.0", + "react-addons-test-utils": "^0.14.7", + "recluster": "^0.4.0", + "rowdy": "^0.4.0", + "saucelabs": "^0.1.1", + "sinon": "^1.16.1", + "sinon-chai": "^2.8.0", + "supertest": "^1.0.1", + "webdriverio": "^3.1.0" + } +} diff --git a/init/server/index.js b/init/server/index.js new file mode 100644 index 0000000..16f9443 --- /dev/null +++ b/init/server/index.js @@ -0,0 +1,185 @@ +"use strict"; + +/** + * Express web server. + */ +// Globals +var HOST = process.env.HOST || "127.0.0.1"; +var defaultPort = 3000; +var PORT = process.env.PORT || defaultPort; +var RENDER_JS = true; +var RENDER_SS = true; + +// Hooks / polyfills +require("babel/register"); +// Prevent node from attempting to require .css files on the server +require.extensions[".css"] = function () { return null; }; + +var path = require("path"); +var express = require("express"); +var compress = require("compression"); + +var app = module.exports = express(); +var util = require("./util"); + +// ---------------------------------------------------------------------------- +// Setup, Static Routes +// ---------------------------------------------------------------------------- +app.use(compress()); + +// Logger +var logger = require("express-bunyan-logger"); +app.use(logger()); + +// Smart favicon handling +var favicon = require("serve-favicon"); +app.use(favicon(path.join(__dirname, "../static/favicon.ico"))); + +// Static libraries and application HTML page. +app.use("/js", express.static(path.join(__dirname, "../dist/js"))); + +// ---------------------------------------------------------------------------- +// REST API +// ---------------------------------------------------------------------------- +app.get("/api/base", function (req, res) { + res.json({ base: util.getBase() }); +}); + +// ---------------------------------------------------------------------------- +// Application. +// ---------------------------------------------------------------------------- +// Client-side imports +var React = require("react"); +var ReactDOMServer = require("react-dom/server"); +var Provider = require("react-redux").Provider; +var ReactRouter = require("react-router"); +var match = ReactRouter.match; +var RouterContext = ReactRouter.RouterContext; + +var createStore = require("../client/store/create-store"); +var routes = require("../client/routes"); +var clientApi = require("../client/utils/api"); +var httpConstants = require("../shared/constants").http; + +// Server-side React +var Index = React.createFactory(require("../templates/index")); +// Have to manually hack in the doctype because not contained with single +// element for full page. +var renderPage = function (component) { + return "" + ReactDOMServer.renderToStaticMarkup(component); +}; + +// JS Bundle sources. +var WEBPACK_TEST_BUNDLE = process.env.WEBPACK_TEST_BUNDLE; // Switch to test webpack-dev-server +var WEBPACK_DEV = process.env.WEBPACK_DEV === "true"; // Switch to dev webpack-dev-server +var WEBPACK_HOT = process.env.WEBPACK_HOT === "true"; + +// Dev bundle URLs +var devBundleJsUrl = "http://127.0.0.1:2992/js/bundle.js"; +var devBundleCssUrl = "http://127.0.0.1:2992/js/style.css"; + +var bundles = function (renderJs) { + // JS/CSS bundle rendering. + var bundleJs; + var bundleCss; + + if (WEBPACK_TEST_BUNDLE) { + bundleJs = renderJs ? WEBPACK_TEST_BUNDLE : null; + bundleCss = devBundleCssUrl; + } else if (WEBPACK_HOT) { + // In hot mode, there is no CSS file because styles are inlined in a