` components and React Router to your template component as props (See #12)
383 |
384 | ## Development
385 |
386 | The source for this plugin is transpiled using Babel. Most importantly this allows us to use JSX, but it also provides access to all ES6 features. During development you probably want to watch the source files and compile them whenever they change. To do this:
387 |
388 | #### To `watch`
389 |
390 | ```
391 | npm run watch
392 | ```
393 |
394 | #### To `build`
395 |
396 | ```
397 | npm run build
398 | ```
399 |
400 | Make sure to run the project locally to be sure everything works as expected (we don't yet have a test suite). To do this link this repo locally using NPM. From the source directory:
401 |
402 | ```
403 | npm link .
404 | ```
405 |
406 | Then you can link it within any local NPM project:
407 |
408 | Now when you `require` or `import` it you will get the local version.
409 |
410 | ```
411 | npm link react-static-webpack-plugin
412 | ```
413 |
414 | #### To `test`
415 |
416 | First, make sure you've installed all the test dependencies. This means installing all `node_modules` within the `example/` directory. You can do this with the provided script.
417 |
418 | ```
419 | ./install_test_dependencies.sh
420 | ```
421 |
422 | Now you can run the tests:
423 |
424 | ```
425 | npm test
426 | ```
427 |
428 | Runs ESLint, Flow type checking and the suite of Wepback tests.
429 |
430 | #### Running individual tests
431 |
432 | If there is one specific test failing and you want to run it individually you can do so. Make sure you have `ava` installed globally:
433 |
434 | ```
435 | npm install -g ava
436 | ```
437 |
438 | Then you can run invidual tests by running a command similar to this. For example, to test only the Redux tests you can run:
439 |
440 | ```
441 | NODE_ENV=production DEBUG=react-static-webpack-plugin* ava --verbose ./example/redux/test.js
442 | ```
443 |
444 | The `DEBUG` env variable tells the plugin to be very verbose in its logging output.
445 |
446 | [boilerplate]: https://github.com/iansinnott/react-static-boilerplate
447 |
448 | ## License
449 |
450 | MIT © [Ian Sinnott](http://iansinnott.com)
451 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | ## Set up yarn and install test dependencies for all example dirs
2 | machine:
3 | environment:
4 | YARN_VERSION: 0.18.1
5 | PATH: "${PATH}:${HOME}/.yarn/bin:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin"
6 | node:
7 | version: 6.9.1
8 |
9 | dependencies:
10 | pre:
11 | - |
12 | if [[ ! -e ~/.yarn/bin/yarn || $(yarn --version) != "${YARN_VERSION}" ]]; then
13 | echo "Download and install Yarn."
14 | curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version $YARN_VERSION
15 | else
16 | echo "The correct version of Yarn is already installed."
17 | fi
18 | post:
19 | - ./install_test_dependencies.sh
20 | override:
21 | - yarn install
22 | cache_directories:
23 | - ~/.yarn
24 | - ~/.cache/yarn
25 |
26 | test:
27 | override:
28 | - yarn test
29 |
--------------------------------------------------------------------------------
/example/basic-with-router/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "build": "webpack",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "babel-core": "^6.8.0",
14 | "babel-loader": "^6.2.4",
15 | "react": "^15.4.1",
16 | "react-dom": "^15.4.1",
17 | "react-router": "^3.0.0",
18 | "webpack": "^1.13.0"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/example/basic-with-router/src/components.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { Link, IndexLink } from 'react-router';
3 |
4 | export const Home = React.createClass({
5 | render() {
6 | return (
7 |
8 |
9 |
React Static Boilerplate
10 |
11 |
Why React static?
12 |
13 | Dev friendly
14 | User friendly
15 | SEO friendly
16 |
17 |
18 | );
19 | },
20 | });
21 |
22 | export const About = React.createClass({
23 | render() {
24 | return (
25 |
26 |
27 |
About
28 |
29 |
Welcome, to about us.
30 |
31 | );
32 | },
33 | });
34 |
35 | export const NotFound = React.createClass({
36 | render() {
37 | return (
38 |
39 |
Not found
40 |
41 | );
42 | },
43 | });
44 |
45 | /**
46 | * NOTE: As of 2015-11-09 react-transform does not support a functional
47 | * component as the base compoenent that's passed to ReactDOM.render, so we
48 | * still use createClass here.
49 | */
50 | export const App = React.createClass({
51 | propTypes: {
52 | children: PropTypes.any,
53 | },
54 | render() {
55 | return (
56 |
57 |
58 | Home
59 | About
60 |
61 | {this.props.children}
62 |
63 | );
64 | },
65 | });
66 |
--------------------------------------------------------------------------------
/example/basic-with-router/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { Router, browserHistory } from 'react-router';
4 |
5 | // Import your routes so that you can pass them to the component
6 | import routes from './routes.js';
7 |
8 | render(
9 | ,
10 | document.getElementById('root')
11 | );
12 |
--------------------------------------------------------------------------------
/example/basic-with-router/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, IndexRoute } from 'react-router';
3 |
4 | import { App, Home, About, NotFound } from './components.js';
5 |
6 | export const routes = (
7 |
8 |
9 |
10 |
11 |
12 | );
13 |
14 | export default routes;
15 |
--------------------------------------------------------------------------------
/example/basic-with-router/template.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Html = (props) => (
4 |
5 |
6 | {props.title}
7 |
8 |
9 |
10 |
11 |
12 |
13 | );
14 |
15 | export default Html;
16 |
--------------------------------------------------------------------------------
/example/basic-with-router/test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import fs from 'fs';
3 | import path from 'path';
4 |
5 | import options from './webpack.config.js';
6 | import { compileWebpack } from '../../utils.js';
7 |
8 | let stats;
9 |
10 | test.before(async t => {
11 | stats = await compileWebpack(options);
12 | });
13 |
14 | test('Compiles routes nested at one level', t => {
15 | const files = stats.toJson().assets.map(x => x.name);
16 |
17 | t.true(files.includes('index.html'));
18 | t.true(files.includes('about.html'));
19 | t.true(files.includes('404.html'));
20 | });
21 |
22 | test('Output files contain titles specified in routes file', t => {
23 | const outputFilepath = path.join(options.output.path, 'index.html');
24 | const outputFileContents = fs.readFileSync(outputFilepath, { encoding: 'utf8' });
25 |
26 | t.true(outputFileContents.includes('App '));
27 | });
28 |
--------------------------------------------------------------------------------
/example/basic-with-router/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | var path = require('path');
3 |
4 | var ReactStaticPlugin = require('../../dist');
5 |
6 | module.exports = {
7 | devtool: 'source-map',
8 |
9 | context: __dirname,
10 |
11 | entry: {
12 | app: './src/index.js',
13 | },
14 |
15 | output: {
16 | path: path.join(__dirname, 'public'),
17 | filename: '[name].js',
18 | libraryTarget: 'umd',
19 | publicPath: '/',
20 | },
21 |
22 | plugins: [
23 | new ReactStaticPlugin({
24 | routes: './src/routes.js',
25 | template: './template.js',
26 | }),
27 | ],
28 |
29 | module: {
30 | loaders: [
31 | {
32 | test: /\.js$/,
33 | loaders: ['babel'],
34 | exclude: path.join(__dirname, 'node_modules'),
35 | },
36 | ],
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/example/css-modules/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react", "stage-1"],
3 | "env": {
4 | "development": {
5 | "presets": ["react-hmre"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/example/css-modules/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/example/css-modules/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *.DS_Store
3 | public/
4 | .tmp/
5 | nginx.conf
6 | npm-debug.log
7 | .idea
8 |
--------------------------------------------------------------------------------
/example/css-modules/docker-compose.yml:
--------------------------------------------------------------------------------
1 | # This Docker Compose config will get a full static site up and running on nginx
2 | # quickly so you can test out how the site will run on a real server.
3 | static:
4 | image: nginx:1.9
5 | ports:
6 | - "8080:80"
7 | - "8443:443"
8 | volumes:
9 | - "./nginx.conf:/etc/nginx/nginx.conf"
10 | - "./public:/var/www/html"
11 |
--------------------------------------------------------------------------------
/example/css-modules/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-static-boilerplate",
3 | "version": "0.3.0",
4 | "description": "A boilerplate for building static sites with React and React Router",
5 | "engines": {
6 | "node": "^6.1"
7 | },
8 | "scripts": {
9 | "clean": "rimraf public",
10 | "lint": "eslint src",
11 | "conf": "babel-node ./scripts/generate-nginx-conf.js",
12 | "test": "echo 'No tests specified.'",
13 | "start:dev": "babel-node ./server.js",
14 | "start": "npm run start:dev",
15 | "build:static": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js",
16 | "build": "npm run clean && npm run build:static",
17 | "preversion": "npm test",
18 | "postversion": "git push && git push --tags",
19 | "bump:patch": "npm version patch -m \"v%s\"",
20 | "bump:minor": "npm version minor -m \"v%s\"",
21 | "bump": "npm run bump:patch"
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "https://github.com/iansinnott/react-static-boilerplate"
26 | },
27 | "bugs": {
28 | "url": "https://github.com/iansinnott/react-static-boilerplate/issues"
29 | },
30 | "author": "Ian Sinnott (http://iansinnott.com)",
31 | "license": "MIT",
32 | "homepage": "",
33 | "dependencies": {
34 | "autoprefixer-loader": "^3.2.0",
35 | "axis": "^0.6.1",
36 | "babel-cli": "^6.9.0",
37 | "babel-core": "^6.9.0",
38 | "babel-eslint": "^6.0.4",
39 | "babel-loader": "^6.2.4",
40 | "babel-plugin-react-transform": "^2.0.2",
41 | "babel-preset-es2015": "^6.9.0",
42 | "babel-preset-react": "^6.5.0",
43 | "babel-preset-react-hmre": "^1.1.1",
44 | "babel-preset-stage-1": "^6.5.0",
45 | "classnames": "^2.2.5",
46 | "cross-env": "^1.0.7",
47 | "css-loader": "^0.23.1",
48 | "eslint": "^2.10.2",
49 | "eslint-config-rackt": "^1.1.1",
50 | "eslint-plugin-react": "^5.1.1",
51 | "express": "^4.13.4",
52 | "extract-text-webpack-plugin": "^1.0.1",
53 | "file-loader": "^0.8.5",
54 | "history": "^2.1.1",
55 | "normalize.css": "^4.1.1",
56 | "react": "^15.4.1",
57 | "react-dom": "^15.4.1",
58 | "react-router": "^3.0.0",
59 | "react-transform-catch-errors": "^1.0.2",
60 | "react-transform-hmr": "^1.0.4",
61 | "redbox-react": "^1.2.5",
62 | "rimraf": "^2.5.2",
63 | "rupture": "^0.6.1",
64 | "style-loader": "^0.13.1",
65 | "stylus": "^0.54.5",
66 | "stylus-loader": "^2.1.0",
67 | "url-loader": "^0.5.7",
68 | "webpack": "^1.13.1",
69 | "webpack-dev-middleware": "^1.6.1",
70 | "webpack-hot-middleware": "^2.10.0"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/example/css-modules/scripts/generate-nginx-conf.js:
--------------------------------------------------------------------------------
1 | console.log(
2 | `# NOTE: This file uses the nginx.conf from the official nginx docker image as a
3 | # starting point. Feel free to customize as you see fit, but remember things may
4 | # break if you change configurations you aren't familiar with.
5 | #
6 | # NOTE: Although we don't define it here, this container will be run
7 | # non-daemonized so that docker can manage the process. The official nginx
8 | # container specifies the non daemon option on its own so adding it here will
9 | # cause a duplication error when you try to run nginx.
10 |
11 | user nginx;
12 | worker_processes 1;
13 |
14 | error_log /var/log/nginx/error.log warn;
15 | pid /var/run/nginx.pid;
16 |
17 |
18 | events {
19 | worker_connections 1024;
20 | }
21 |
22 |
23 | http {
24 | include /etc/nginx/mime.types;
25 | default_type application/octet-stream;
26 |
27 | log_format main '$remote_addr - $remote_user [$time_local] "$request" '
28 | '$status $body_bytes_sent "$http_referer" '
29 | '"$http_user_agent" "$http_x_forwarded_for"';
30 |
31 | access_log /var/log/nginx/access.log main;
32 |
33 | sendfile on;
34 | #tcp_nopush on;
35 |
36 | keepalive_timeout 65;
37 |
38 | # CUSTOM CONFIG
39 | # This is how we host our static React site.
40 | server {
41 |
42 | # Listen on this port. This would be 80 or 443 on a prod server. Adjust this
43 | # to suit your own needs.
44 | listen 80;
45 |
46 | # Server base URL goes here if applicable
47 | #server_name trustar.co;
48 |
49 | location / {
50 |
51 | # Enable gzip. NOTE: text/html files are always gzipped when enabled
52 | gzip on;
53 | gzip_min_length 1000;
54 | gzip_types text/plain text/css application/javascript application/json image/x-icon;
55 |
56 | # The location of the static files to server
57 | root /var/www/html;
58 |
59 | # Remove trailing slashes. /about/ -> /about
60 | # This is important because of how static files are generated.
61 | rewrite ^/(.*)/$ /$1 permanent;
62 |
63 | # If migrating from a dynamic site you may want to redirect requests to a
64 | # certain path to a different server. This example redirects all traffic
65 | # to /blog to blog.example.com
66 | #rewrite ^/blog(.*)$ $scheme://blog.example.com$1 redirect;
67 |
68 | # Use 404.html as the error page
69 | error_page 404 /404.html;
70 |
71 | # If a matching file can't be found, handle this request as a 404, which
72 | # will return the 404 page because of the above directive
73 | try_files $uri $uri.html $uri/index.html =404;
74 |
75 | }
76 |
77 | }
78 | }`
79 | );
80 |
--------------------------------------------------------------------------------
/example/css-modules/server.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NOTE: This file must be run with babel-node as Node is not yet compatible
3 | * with all of ES6 and we also use JSX.
4 | */
5 | import url from 'url';
6 | import React, { PropTypes } from 'react';
7 | import { renderToStaticMarkup } from 'react-dom/server';
8 | import express from 'express';
9 | import webpack from 'webpack';
10 |
11 | import config from './webpack.config.dev.js';
12 |
13 | const Html = ({
14 | title = 'Rainbow Unicorns',
15 | bundle = '/app.js',
16 | body = '',
17 | favicon = '',
18 | stylesheet = '',
19 | }) => (
20 |
21 |
22 |
23 |
24 |
25 | {title}
26 | {favicon ? : null}
27 | {stylesheet ? : null}
28 |
29 |
30 |
31 |
32 |
33 |
34 | );
35 | Html.propTypes = {
36 | title: PropTypes.string,
37 | bundle: PropTypes.string,
38 | body: PropTypes.string,
39 | favicon: PropTypes.string,
40 | stylesheet: PropTypes.string,
41 | };
42 |
43 | /**
44 | * Render the entire web page to a string. We use render to static markup here
45 | * to avoid react hooking on to the document HTML that will not be managed by
46 | * React. The body prop is a string that contains the actual document body,
47 | * which react will hook on to.
48 | *
49 | * We also take this opportunity to prepend the doctype string onto the
50 | * document.
51 | *
52 | * @param {object} props
53 | * @return {string}
54 | */
55 | const renderDocumentToString = props =>
56 | '' + renderToStaticMarkup( );
57 |
58 | const app = express();
59 | const compiler = webpack(config);
60 |
61 | app.use(require('webpack-dev-middleware')(compiler, {
62 | noInfo: true,
63 | publicPath: config.output.publicPath,
64 | }));
65 |
66 | app.use(require('webpack-hot-middleware')(compiler));
67 |
68 | // Send the boilerplate HTML payload down for all get requests. Routing will be
69 | // handled entirely client side and we don't make an effort to pre-render pages
70 | // before they are served when in dev mode.
71 | app.get('*', (req, res) => {
72 | const html = renderDocumentToString({
73 | bundle: config.output.publicPath + 'app.js',
74 | });
75 | res.send(html);
76 | });
77 |
78 | // NOTE: url.parse can't handle URLs without a protocol explicitly defined. So
79 | // if we parse '//localhost:8888' it doesn't work. We manually add a protocol even
80 | // though we are only interested in the port.
81 | const { port } = url.parse('http:' + config.output.publicPath);
82 |
83 | app.listen(port, 'localhost', err => {
84 | if (err) {
85 | console.error(err);
86 | return;
87 | }
88 | console.log(`Dev server listening at http://localhost:${port}`);
89 | });
90 |
--------------------------------------------------------------------------------
/example/css-modules/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { Link, IndexLink } from 'react-router';
3 | import classnames from 'classnames/bind';
4 | import 'normalize.css';
5 |
6 | // Using CSS Modules so we assign the styles to a variable
7 | import s from './App.styl';
8 | const cx = classnames.bind(s);
9 | import logo from './react-logo.png';
10 |
11 | // Favicon link is in the template, this just makes webpack package it up for us
12 | import './favicon.ico';
13 |
14 | export class Home extends React.Component {
15 | render() {
16 | return (
17 |
18 |
19 |
20 |
React Static Boilerplate
21 |
22 |
Why React static?
23 |
24 | Dev friendly
25 | User friendly
26 | SEO friendly
27 |
28 |
29 | );
30 | }
31 | }
32 |
33 | export class About extends React.Component {
34 | render() {
35 | return (
36 |
37 |
38 |
About Page
39 |
40 |
Welcome to the about page...
41 |
42 | );
43 | }
44 | }
45 |
46 | export class NotFound extends React.Component {
47 | render() {
48 | return (
49 |
50 |
Not found
51 |
52 | );
53 | }
54 | }
55 |
56 | /**
57 | * NOTE: As of 2015-11-09 react-transform does not support a functional
58 | * component as the base compoenent that's passed to ReactDOM.render, so we
59 | * still use createClass here.
60 | */
61 | export class App extends React.Component {
62 | static propTypes = {
63 | children: PropTypes.any,
64 | }
65 | render() {
66 | return (
67 |
68 |
69 | Home
70 | About
71 |
72 | {this.props.children}
73 |
74 | );
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/example/css-modules/src/components/App.styl:
--------------------------------------------------------------------------------
1 | @require '../lib/vars.styl'
2 |
3 | *
4 | box-sizing border-box
5 |
6 | body
7 | background react-blue
8 | font-family helvetica
9 | color darken(white, 10%)
10 | font-size 18px
11 |
12 | h1
13 | margin 20px 0
14 |
15 | pre
16 | font-family mono
17 | line-height 1.5
18 | border-radius 3px
19 | border 1px solid border-gray
20 |
21 | .page
22 | padding 20px
23 |
24 | .siteTitle
25 | display flex
26 | align-items center
27 |
28 | +below(420px)
29 | display block
30 |
31 | img
32 | display inline-block
33 | width 80px
34 | height 80px
35 | margin-right 20px
36 |
37 | +below(420px)
38 | display block
39 | margin 0 auto
40 |
41 | .hl
42 | font-weight bold
43 | color react-blue
44 | .App
45 | background black
46 | min-width 320px
47 |
48 | h1
49 | color react-blue
50 | font-weight 300
51 | font-size 52px
52 |
53 | p
54 | color white
55 |
56 | .nav
57 | background darken(black, 10%)
58 | padding-left 20px
59 |
60 | a
61 | display inline-block
62 | padding 20px
63 | text-transform uppercase
64 | text-decoration none
65 | color darken(white, 20%)
66 | &:hover
67 | color white
68 |
69 | .active
70 | background black
71 |
72 |
73 | .testableModuleClassName
74 | display block
75 |
--------------------------------------------------------------------------------
/example/css-modules/src/components/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iansinnott/react-static-webpack-plugin/f4abf1f0719fbe8ddabf7dbeb702ac181d485889/example/css-modules/src/components/favicon.ico
--------------------------------------------------------------------------------
/example/css-modules/src/components/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iansinnott/react-static-webpack-plugin/f4abf1f0719fbe8ddabf7dbeb702ac181d485889/example/css-modules/src/components/react-logo.png
--------------------------------------------------------------------------------
/example/css-modules/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { Router, browserHistory } from 'react-router';
4 |
5 | // Import your routes so that you can pass them to the component
6 | import routes from './routes.js';
7 |
8 | render(
9 | ,
10 | document.getElementById('root')
11 | );
12 |
--------------------------------------------------------------------------------
/example/css-modules/src/lib/vars.styl:
--------------------------------------------------------------------------------
1 | mono = Consolas, 'Liberation Mono', Menlo, Courier, monospace
2 | helvetica = 'Helvetica Neue', Helvetica, Arial, sans-serif
3 | js-yellow = #f5da55
4 | react-blue = #00d8ff
5 | black = #333
6 |
7 | // Use with inputs / buttons you don't want users to interact with
8 | uninteractive()
9 | user-select none
10 | pointer-events none
11 | cursor not-allowed
12 |
--------------------------------------------------------------------------------
/example/css-modules/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, IndexRoute } from 'react-router';
3 |
4 | import { App, About, Home, NotFound } from './components/App.js';
5 |
6 | export const routes = (
7 |
8 |
9 |
10 |
11 |
12 | );
13 |
14 | export default routes;
15 |
--------------------------------------------------------------------------------
/example/css-modules/template.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Html = (props) => (
4 |
5 |
6 | {props.title}
7 |
8 |
9 |
10 |
11 |
12 |
13 | );
14 |
15 | export default Html;
16 |
--------------------------------------------------------------------------------
/example/css-modules/test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import webpack from 'webpack';
3 | import fs from 'fs';
4 | import path from 'path';
5 |
6 | import options from './webpack.config.prod.js';
7 |
8 | let stats;
9 |
10 | test.cb.before(t => {
11 | webpack(options, (err, _stats) => {
12 | if (err) {
13 | return t.end(err);
14 | } else if (_stats.hasErrors()) {
15 | return t.end(_stats.toString());
16 | }
17 |
18 | stats = _stats;
19 |
20 | t.end();
21 | });
22 | });
23 |
24 | test('Outputs the desired files', t => {
25 | const { assets } = stats.toJson();
26 | const files = assets.map(x => x.name);
27 |
28 | t.true(files.includes('index.html'));
29 | t.true(files.includes('about.html'));
30 | t.true(files.includes('404.html'));
31 | t.true(files.includes('app.css'));
32 | t.true(files.includes('app.js'));
33 | });
34 |
35 | test('Compiles local CSS classes (CSS Modules)', t => {
36 | const { assets } = stats.toJson();
37 | const files = assets.map(x => x.name);
38 | const outputFilepath = path.join(options.output.path, 'index.html');
39 | const outputFileContents = fs.readFileSync(outputFilepath, { encoding: 'utf8' });
40 |
41 | // Simply make sure this classname isn't found
42 | t.false(outputFileContents.includes('testableModuleClassName'));
43 | });
44 |
45 | test('Supports minification', t => {
46 | const { assets } = stats.toJson();
47 | const files = assets.map(x => x.name);
48 | const bundle = assets[files.indexOf('app.js')];
49 |
50 | // Test size in MB. We want to make sure this bundle was minified since we
51 | // are using the minify JS plugin
52 | t.true((bundle.size / 1000) < 300);
53 | });
54 |
--------------------------------------------------------------------------------
/example/css-modules/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | var path = require('path');
3 | var webpack = require('webpack');
4 | var axis = require('axis');
5 | var rupture = require('rupture');
6 |
7 | // Set up dev host host and HMR host. For the dev host this is pretty self
8 | // explanatory: We use a different live-reload server to server our static JS
9 | // files in dev, so we need to be able to actually point a script tag to that
10 | // host so it can load the right files. The HRM host is a bit stranger. For more
11 | // details on why we need this URL see the readme and:
12 | // https://github.com/glenjamin/webpack-hot-middleware/issues/37
13 | var DEV_PORT = process.env.DEV_PORT || 3000;
14 | var DEV_HOST = '//localhost:' + DEV_PORT + '/';
15 | var HMR_HOST = DEV_HOST + '__webpack_hmr';
16 |
17 | module.exports = {
18 | devtool: 'inline-source-map',
19 |
20 | context: __dirname,
21 |
22 | entry: {
23 | app: [
24 | 'webpack-hot-middleware/client?path=' + HMR_HOST,
25 | './src/index.js',
26 | ],
27 | },
28 |
29 | output: {
30 | path: path.join(__dirname, 'public'),
31 | filename: '[name].js',
32 | publicPath: DEV_HOST,
33 | },
34 |
35 | plugins: [
36 | new webpack.HotModuleReplacementPlugin(),
37 | new webpack.NoErrorsPlugin(),
38 | ],
39 |
40 | module: {
41 | loaders: [
42 | {
43 | test: /\.js$/,
44 | loaders: ['babel'],
45 | include: path.join(__dirname, 'src'),
46 | },
47 | {
48 | test: /\.css$/,
49 | loaders: ['style', 'css'],
50 | },
51 | {
52 | test: /\.styl/,
53 | loaders: [
54 | 'style',
55 | 'css?modules&importLoaders=2&localIdentName=[name]__[local]__[hash:base64:6]',
56 | 'autoprefixer',
57 | 'stylus',
58 | ],
59 | },
60 | {
61 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
62 | loaders: ['url?limit=10000&mimetype=application/font-woff'],
63 | },
64 | {
65 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
66 | loaders: ['file'],
67 | },
68 | {
69 | test: /\.(png|jpg|gif|ico)$/,
70 | loaders: ['file?name=[name].[ext]'],
71 | },
72 | ],
73 | },
74 |
75 | stylus: {
76 | use: [axis(), rupture()],
77 | },
78 | };
79 |
--------------------------------------------------------------------------------
/example/css-modules/webpack.config.prod.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | var path = require('path');
3 | var webpack = require('webpack');
4 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
5 | var axis = require('axis');
6 | var rupture = require('rupture');
7 | var ReactStaticPlugin = require('../../dist');
8 |
9 | module.exports = {
10 |
11 | context: __dirname,
12 |
13 | entry: {
14 | app: ['./src/index.js'],
15 | },
16 |
17 | output: {
18 | path: path.join(__dirname, 'public'),
19 | filename: '[name].js',
20 | libraryTarget: 'umd',
21 | publicPath: '/',
22 | },
23 |
24 | plugins: [
25 | new ReactStaticPlugin({
26 | routes: './src/routes.js',
27 | stylesheet: '/app.css',
28 | template: './template.js'
29 | }),
30 | new ExtractTextPlugin('[name].css', { allChunks: true }),
31 | new webpack.optimize.OccurenceOrderPlugin(),
32 | new webpack.DefinePlugin({
33 | 'process.env': {
34 | 'NODE_ENV': JSON.stringify('production'),
35 | },
36 | }),
37 | new webpack.optimize.UglifyJsPlugin({
38 | screw_ie8: true,
39 | compressor: { warnings: false },
40 | }),
41 | ],
42 |
43 | module: {
44 | loaders: [
45 | {
46 | test: /\.js$/,
47 | loaders: ['babel'],
48 | exclude: path.join(__dirname, 'node_modules'),
49 | },
50 | {
51 | test: /\.css$/,
52 | loader: ExtractTextPlugin.extract('style', 'css'),
53 | },
54 | {
55 | test: /\.styl/,
56 | loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=2!autoprefixer!stylus'),
57 | },
58 | {
59 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
60 | loaders: ['url?limit=10000&mimetype=application/font-woff'],
61 | },
62 | {
63 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
64 | loaders: ['file'],
65 | },
66 | {
67 | test: /\.(png|jpg|gif|ico)$/,
68 | loaders: ['file?name=[name].[ext]'],
69 | },
70 | ],
71 | },
72 |
73 | stylus: {
74 | use: [axis(), rupture()],
75 | },
76 | };
77 |
--------------------------------------------------------------------------------
/example/deep-route-nesting/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "build": "webpack",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "babel-core": "^6.8.0",
14 | "babel-loader": "^6.2.4",
15 | "react": "^15.4.1",
16 | "react-dom": "^15.4.1",
17 | "react-router": "^3.0.0",
18 | "webpack": "^1.13.0"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/example/deep-route-nesting/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { Link, IndexLink } from 'react-router';
3 |
4 | export const Home = React.createClass({
5 | render() {
6 | return (
7 |
8 |
9 |
React Static Boilerplate
10 |
11 |
Why React static?
12 |
13 | Dev friendly
14 | User friendly
15 | SEO friendly
16 |
17 |
18 | );
19 | },
20 | });
21 |
22 | export const About = React.createClass({
23 | render() {
24 | return (
25 |
26 |
27 |
About
28 |
29 |
Welcome, to about us.
30 |
31 | );
32 | },
33 | });
34 |
35 | export const NotFound = React.createClass({
36 | render() {
37 | return (
38 |
39 |
Not found
40 |
41 | );
42 | },
43 | });
44 |
45 | /**
46 | * NOTE: As of 2015-11-09 react-transform does not support a functional
47 | * component as the base compoenent that's passed to ReactDOM.render, so we
48 | * still use createClass here.
49 | */
50 | export const App = React.createClass({
51 | propTypes: {
52 | children: PropTypes.any,
53 | },
54 | render() {
55 | return (
56 |
57 |
58 | Home
59 | About
60 |
61 | {this.props.children}
62 |
63 | );
64 | },
65 | });
66 |
--------------------------------------------------------------------------------
/example/deep-route-nesting/src/Products.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React from 'react';
3 |
4 | export class Products extends React.Component {
5 | render() {
6 | return (
7 |
8 |
A list of all products
9 |
10 | {this.props.children}
11 |
12 |
13 | );
14 | }
15 | }
16 |
17 | export class Product extends React.Component {
18 | render() {
19 | return (
20 |
21 |
This is a specific product
22 |
23 | {this.props.children}
24 |
25 |
26 | );
27 | }
28 | }
29 |
30 | export class ProductColors extends React.Component {
31 | render() {
32 | return (
33 |
34 |
A list of product colors
35 |
36 | {this.props.children}
37 |
38 |
39 | );
40 | }
41 | }
42 |
43 | export class ProductColor extends React.Component {
44 | render() {
45 | return (
46 |
47 |
A single product color
48 |
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/example/deep-route-nesting/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { Router, browserHistory } from 'react-router';
4 |
5 | // Import your routes so that you can pass them to the component
6 | import routes from './routes.js';
7 |
8 | render(
9 | ,
10 | document.getElementById('root')
11 | );
12 |
--------------------------------------------------------------------------------
/example/deep-route-nesting/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, IndexRoute } from 'react-router';
3 |
4 | import { App, Home, About, NotFound } from './App.js';
5 | import {
6 | Products,
7 | Product,
8 | ProductColors,
9 | ProductColor,
10 | } from './Products.js';
11 |
12 | export const routes = (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 |
30 | export default routes;
31 |
--------------------------------------------------------------------------------
/example/deep-route-nesting/template.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Html = (props) => (
4 |
5 |
6 | {props.title}
7 |
8 |
9 |
10 |
11 |
12 |
13 | );
14 |
15 | export default Html;
16 |
--------------------------------------------------------------------------------
/example/deep-route-nesting/test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 |
3 | import options from './webpack.config.js';
4 | import { compileWebpack } from '../../utils.js';
5 |
6 | test('Compiles deeply nested routes', async t => {
7 | const stats = await compileWebpack(options);
8 | const files = stats.toJson().assets.map(x => x.name);
9 |
10 | t.true(files.includes('products/third.html'));
11 | t.true(files.includes('products.html'));
12 | t.true(files.includes('products/first/index.html'));
13 | t.true(files.includes('products/second/index.html'));
14 | t.true(files.includes('products/third/colors.html'));
15 | t.true(files.includes('products/third/colors/green/index.html'));
16 | t.true(files.includes('products/third/colors/blue/index.html'));
17 | t.true(files.includes('index.html'));
18 | t.true(files.includes('about/index.html'));
19 | t.true(files.includes('404.html'));
20 | });
21 |
--------------------------------------------------------------------------------
/example/deep-route-nesting/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | var path = require('path');
3 |
4 | var ReactStaticPlugin = require('../../dist');
5 |
6 | module.exports = {
7 | devtool: 'source-map',
8 |
9 | context: __dirname,
10 |
11 | entry: {
12 | app: './src/index.js',
13 | },
14 |
15 | output: {
16 | path: path.join(__dirname, 'public'),
17 | filename: '[name].js',
18 | libraryTarget: 'umd',
19 | publicPath: '/',
20 | },
21 |
22 | plugins: [
23 | new ReactStaticPlugin({
24 | routes: './src/routes.js',
25 | template: './template.js',
26 | }),
27 | ],
28 |
29 | module: {
30 | loaders: [
31 | {
32 | test: /\.js$/,
33 | loaders: ['babel'],
34 | exclude: path.join(__dirname, 'node_modules'),
35 | },
36 | ],
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/example/extract-css/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react", "es2015"],
3 | "env": {
4 | "development": {
5 | "presets": ["react-hmre"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/example/extract-css/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *.DS_Store
3 | public/
4 | .tmp/
5 | nginx.conf
6 | npm-debug.log
7 |
--------------------------------------------------------------------------------
/example/extract-css/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-static-boilerplate",
3 | "version": "0.2.0",
4 | "description": "A barebones boilerplate for building apps with React",
5 | "engines": {
6 | "node": "^4.2"
7 | },
8 | "scripts": {
9 | "clean": "rimraf public",
10 | "eslint": "eslint src",
11 | "lint": "npm run -s eslint",
12 | "conf": "babel-node ./scripts/generate-nginx-conf.js",
13 | "test": "echo 'No tests specified.'",
14 | "start:dev": "babel-node ./server.js",
15 | "start": "npm run start:dev",
16 | "build:static": "NODE_ENV=production webpack --config webpack.config.prod.js",
17 | "build": "npm run clean && npm run build:static",
18 | "preversion": "npm test",
19 | "postversion": "git push && git push --tags",
20 | "bump:patch": "npm version patch -m \"v%s\"",
21 | "bump:minor": "npm version minor -m \"v%s\"",
22 | "bump": "npm run bump:patch"
23 | },
24 | "repository": {
25 | "type": "git",
26 | "url": "https://github.com/iansinnott/react-static-boilerplate"
27 | },
28 | "bugs": {
29 | "url": "https://github.com/iansinnott/react-static-boilerplate/issues"
30 | },
31 | "author": "Ian Sinnott (http://iansinnott.com)",
32 | "license": "MIT",
33 | "homepage": "",
34 | "devDependencies": {
35 | "autoprefixer-loader": "^3.1.0",
36 | "axis": "^0.5.2",
37 | "babel-cli": "^6.5.1",
38 | "babel-core": "^6.5.1",
39 | "babel-loader": "^6.2.2",
40 | "babel-plugin-react-transform": "^2.0.0",
41 | "babel-preset-es2015": "^6.5.0",
42 | "babel-preset-react": "^6.5.0",
43 | "babel-preset-react-hmre": "^1.1.0",
44 | "css-loader": "^0.23.0",
45 | "eslint": "^1.10.3",
46 | "eslint-config-airbnb": "^5.0.0",
47 | "eslint-plugin-react": "^3.12.0",
48 | "express": "^4.13.3",
49 | "extract-text-webpack-plugin": "^1.0.1",
50 | "file-loader": "^0.8.5",
51 | "normalize.css": "^3.0.3",
52 | "react": "^15.4.1",
53 | "react-dom": "^15.4.1",
54 | "react-transform-catch-errors": "^1.0.0",
55 | "react-transform-hmr": "^1.0.1",
56 | "redbox-react": "^1.2.0",
57 | "rimraf": "^2.4.4",
58 | "rupture": "^0.6.1",
59 | "style-loader": "^0.13.0",
60 | "stylus": "~0.54.5",
61 | "stylus-loader": "~2.1.0",
62 | "url-loader": "^0.5.7",
63 | "webpack": "^1.12.9",
64 | "webpack-dev-middleware": "^1.4.0",
65 | "webpack-hot-middleware": "^2.6.0"
66 | },
67 | "dependencies": {}
68 | }
69 |
--------------------------------------------------------------------------------
/example/extract-css/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable prefer-template, no-console */
2 | /**
3 | * NOTE: This file must be run with babel-node as Node is not yet compatible
4 | * with all of ES6 and we also use JSX.
5 | */
6 | import url from 'url';
7 | import React from 'react';
8 | import { renderToStaticMarkup } from 'react-dom/server';
9 | import express from 'express';
10 | import webpack from 'webpack';
11 |
12 | import config from './webpack.config.dev.js';
13 |
14 | const Html = ({
15 | title = 'React Starter',
16 | bundle = '/app.js',
17 | body = '',
18 | favicon = '',
19 | stylesheet = '',
20 | }) => (
21 |
22 |
23 |
24 |
25 |
26 | {title}
27 | {favicon && }
28 | {stylesheet && }
29 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 |
37 | /**
38 | * Render the entire web page to a string. We use render to static markup here
39 | * to avoid react hooking on to the document HTML that will not be managed by
40 | * React. The body prop is a string that contains the actual document body,
41 | * which react will hook on to.
42 | *
43 | * We also take this opportunity to prepend the doctype string onto the
44 | * document.
45 | *
46 | * @param {object} props
47 | * @return {string}
48 | */
49 | const renderToDocumentString = props =>
50 | '' + renderToStaticMarkup( );
51 |
52 | const app = express();
53 | const compiler = webpack(config);
54 |
55 | app.use(require('webpack-dev-middleware')(compiler, {
56 | noInfo: true,
57 | publicPath: config.output.publicPath,
58 | }));
59 |
60 | app.use(require('webpack-hot-middleware')(compiler));
61 |
62 | // Send the boilerplate HTML payload down for all get requests. Routing will be
63 | // handled entirely client side and we don't make an effort to pre-render pages
64 | // before they are served when in dev mode.
65 | app.get('*', (req, res) => {
66 | const html = renderToDocumentString({
67 | bundle: config.output.publicPath + 'app.js',
68 | });
69 | res.send(html);
70 | });
71 |
72 | // NOTE: url.parse can't handle URLs without a protocol explicitly defined. So
73 | // if we parse '//localhost:8888' it doesn't work. We manually add a protocol even
74 | // though we are only interested in the port.
75 | const { port } = url.parse(`http:${config.output.publicPath}`);
76 |
77 | app.listen(port, 'localhost', err => {
78 | if (err) throw err;
79 | console.log(`Dev server listening at http://localhost:${port}`);
80 | });
81 |
--------------------------------------------------------------------------------
/example/extract-css/src/components/App.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | body {
6 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
7 | color: #fafafa;
8 | font-size: 18px;
9 | }
10 |
11 | .App {
12 | background: #222;
13 | min-width: 320px;
14 | padding: 20px;
15 | text-align: center;
16 | }
17 |
18 | .App img {
19 | display: inline-block;
20 | width: 80px;
21 | height: 80px;
22 | }
23 |
--------------------------------------------------------------------------------
/example/extract-css/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // import 'normalize.css';
3 |
4 | // Using CSS Modules so we assign the styles to a variable
5 | import './App.css';
6 |
7 | // Favicon link is in the template, this just makes webpack package it up for us
8 | // import './favicon.ico';
9 |
10 | /**
11 | * NOTE: As of 2015-11-09 react-transform does not support a functional
12 | * component as the base compoenent that's passed to ReactDOM.render, so we
13 | * still use createClass here.
14 | */
15 | class App extends React.Component {
16 | render() {
17 | return (
18 |
19 |
20 |
Big wins
21 |
22 | );
23 | }
24 | }
25 |
26 | export default App;
27 |
--------------------------------------------------------------------------------
/example/extract-css/src/components/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iansinnott/react-static-webpack-plugin/f4abf1f0719fbe8ddabf7dbeb702ac181d485889/example/extract-css/src/components/favicon.ico
--------------------------------------------------------------------------------
/example/extract-css/src/components/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iansinnott/react-static-webpack-plugin/f4abf1f0719fbe8ddabf7dbeb702ac181d485889/example/extract-css/src/components/react-logo.png
--------------------------------------------------------------------------------
/example/extract-css/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 |
4 | import App from './components/App.js';
5 |
6 | render( , document.getElementById('root'));
7 |
--------------------------------------------------------------------------------
/example/extract-css/template.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Html = (props) => (
4 |
5 |
6 | {props.title}
7 |
8 |
9 |
10 |
11 |
12 |
13 | );
14 |
15 | export default Html;
16 |
--------------------------------------------------------------------------------
/example/extract-css/test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import webpack from 'webpack';
3 |
4 | import options from './webpack.config.prod.js';
5 |
6 | test.cb('Compiles files that import CSS', t => {
7 | webpack(options, (err, stats) => {
8 | if (err) {
9 | return t.end(err);
10 | } else if (stats.hasErrors()) {
11 | return t.end(stats.toString());
12 | }
13 |
14 | const files = stats.toJson().assets.map(x => x.name);
15 |
16 | t.true(files.includes('index.html'));
17 | t.true(files.includes('app.css'));
18 |
19 | t.end();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/example/extract-css/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | var path = require('path');
3 | var webpack = require('webpack');
4 | var rupture = require('rupture');
5 |
6 | // Set up dev host host and HMR host. For the dev host this is pretty self
7 | // explanatory: We use a different live-reload server to server our static JS
8 | // files in dev, so we need to be able to actually point a script tag to that
9 | // host so it can load the right files. The HRM host is a bit stranger. For more
10 | // details on why we need this URL see the readme and:
11 | // https://github.com/glenjamin/webpack-hot-middleware/issues/37
12 | var DEV_PORT = process.env.DEV_PORT || 3000;
13 | var DEV_HOST = '//localhost:' + DEV_PORT + '/';
14 | var HMR_HOST = DEV_HOST + '__webpack_hmr';
15 |
16 | module.exports = {
17 | devtool: 'inline-source-map',
18 |
19 | context: __dirname,
20 |
21 | entry: {
22 | app: [
23 | 'webpack-hot-middleware/client?path=' + HMR_HOST,
24 | './src/index.js',
25 | ],
26 | },
27 |
28 | output: {
29 | path: path.join(__dirname, 'public'),
30 | filename: '[name].js',
31 | publicPath: DEV_HOST,
32 | },
33 |
34 | plugins: [
35 | new webpack.HotModuleReplacementPlugin(),
36 | new webpack.NoErrorsPlugin(),
37 | ],
38 |
39 | module: {
40 | loaders: [
41 | {
42 | test: /\.js$/,
43 | loaders: ['babel'],
44 | include: path.join(__dirname, 'src'),
45 | },
46 | {
47 | test: /\.css$/,
48 | loaders: ['style', 'css'],
49 | },
50 | {
51 | test: /\.styl/,
52 | loaders: [
53 | 'style',
54 | 'css?modules&importLoaders=2&localIdentName=[name]__[local]__[hash:base64:6]',
55 | 'autoprefixer',
56 | 'stylus',
57 | ],
58 | },
59 | {
60 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
61 | loaders: ['url?limit=10000&mimetype=application/font-woff'],
62 | },
63 | {
64 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
65 | loaders: ['file'],
66 | },
67 | {
68 | test: /\.(png|jpg|gif|ico)$/,
69 | loaders: ['file?name=[name].[ext]'],
70 | },
71 | ],
72 | },
73 |
74 | stylus: {
75 | use: [rupture()],
76 | },
77 | };
78 |
--------------------------------------------------------------------------------
/example/extract-css/webpack.config.prod.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | var path = require('path');
3 | var webpack = require('webpack');
4 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
5 | var ReactStaticPlugin = require('../../dist');
6 |
7 | module.exports = {
8 | devtool: 'source-map',
9 |
10 | context: __dirname,
11 |
12 | entry: {
13 | app: ['./src/index.js'],
14 | },
15 |
16 | output: {
17 | path: path.join(__dirname, 'public'),
18 | filename: '[name].js',
19 | publicPath: '/',
20 | },
21 |
22 | plugins: [
23 | new webpack.optimize.OccurenceOrderPlugin(),
24 | new webpack.DefinePlugin({
25 | 'process.env': {
26 | 'NODE_ENV': JSON.stringify('production'),
27 | },
28 | }),
29 | new webpack.optimize.UglifyJsPlugin({
30 | screw_ie8: true,
31 | compressor: { warnings: false },
32 | }),
33 | new ReactStaticPlugin({
34 | component: './src/components/App.js',
35 | template: './template.js',
36 | stylesheet: '/app.css',
37 | }),
38 | new ExtractTextPlugin('[name].css', { allChunks: true }),
39 | ],
40 |
41 | module: {
42 | loaders: [
43 | {
44 | test: /\.js$/,
45 | loaders: ['babel'],
46 | exclude: path.join(__dirname, 'node_modules'),
47 | },
48 | {
49 | test: /\.css$/,
50 | loader: ExtractTextPlugin.extract('style', 'css'),
51 | },
52 | ],
53 | },
54 |
55 | };
56 |
--------------------------------------------------------------------------------
/example/redux/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react", "stage-1"],
3 | "env": {
4 | "development": {
5 | "presets": ["react-hmre"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/example/redux/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *.DS_Store
3 | public/
4 | .tmp/
5 | npm-debug.log
6 |
--------------------------------------------------------------------------------
/example/redux/.nvmrc:
--------------------------------------------------------------------------------
1 | 6
2 |
--------------------------------------------------------------------------------
/example/redux/README.md:
--------------------------------------------------------------------------------
1 | # Redux Static Site Example
2 |
3 | This is just an example. See the top-level readme for instructions.
4 |
--------------------------------------------------------------------------------
/example/redux/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-example",
3 | "version": "0.2.0",
4 | "description": "Redux Example",
5 | "engines": {
6 | "node": "^5.11"
7 | },
8 | "scripts": {
9 | "clean": "rimraf public",
10 | "lint": "eslint src",
11 | "test": "echo 'No tests specified.'",
12 | "start:dev": "babel-node ./server.js",
13 | "start": "npm run start:dev",
14 | "build:static": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js",
15 | "build": "npm run clean && npm run build:static",
16 | "preversion": "npm test",
17 | "postversion": "git push && git push --tags",
18 | "bump:patch": "npm version patch -m \"v%s\"",
19 | "bump:minor": "npm version minor -m \"v%s\"",
20 | "bump": "npm run bump:patch"
21 | },
22 | "author": "Ian Sinnott (http://iansinnott.com)",
23 | "license": "MIT",
24 | "homepage": "",
25 | "devDependencies": {
26 | "autoprefixer-loader": "^3.1.0",
27 | "babel-cli": "^6.5.1",
28 | "babel-core": "^6.5.1",
29 | "babel-eslint": "^6.0.2",
30 | "babel-loader": "^6.2.2",
31 | "babel-plugin-react-transform": "^2.0.0",
32 | "babel-preset-es2015": "^6.5.0",
33 | "babel-preset-modern-browsers": "^2.0.1",
34 | "babel-preset-react": "^6.5.0",
35 | "babel-preset-react-hmre": "^1.1.0",
36 | "babel-preset-stage-1": "~6.5.0",
37 | "cross-env": "^1.0.7",
38 | "css-loader": "^0.23.0",
39 | "eslint": "^2.7.0",
40 | "eslint-config-rackt": "^1.1.1",
41 | "eslint-plugin-react": "^5.1.1",
42 | "extract-text-webpack-plugin": "^1.0.1",
43 | "faker": "~3.1.0",
44 | "file-loader": "^0.8.5",
45 | "json-loader": "^0.5.4",
46 | "react-static-webpack-plugin": "^1.0.1",
47 | "react-transform-catch-errors": "^1.0.0",
48 | "react-transform-hmr": "^1.0.1",
49 | "redbox-react": "^1.2.0",
50 | "style-loader": "^0.13.0",
51 | "stylus-loader": "^2.0.0",
52 | "url-loader": "^0.5.7",
53 | "webpack": "^1.12.9",
54 | "webpack-dev-middleware": "^1.4.0",
55 | "webpack-hot-middleware": "^2.6.0"
56 | },
57 | "dependencies": {
58 | "axios": "^0.11.1",
59 | "axis": "^0.6.1",
60 | "babel-polyfill": "^6.9.0",
61 | "classnames": "^2.2.5",
62 | "express": "^4.13.3",
63 | "flag-icon-css": "~2.3.0",
64 | "history": "^2.0.0",
65 | "immutable": "~3.8.1",
66 | "lodash": "^4.13.1",
67 | "node-uuid": "~1.4.7",
68 | "normalize.css": "^4.0.0",
69 | "pleasejs": "^0.4.2",
70 | "react": "^15.4.1",
71 | "react-dom": "^15.4.1",
72 | "react-fa": "~4.1.1",
73 | "react-redux": "^4.4.5",
74 | "react-router": "^3.0.0",
75 | "react-spinkit": "^1.1.7",
76 | "redux": "~3.5.2",
77 | "redux-immutable": "^3.0.6",
78 | "rimraf": "^2.4.4",
79 | "rupture": "^0.6.1",
80 | "stylus": "^0.54.5"
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/example/redux/render-to-document-string.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { renderToStaticMarkup } from 'react-dom/server';
3 |
4 | const Html = ({
5 | title = 'Redux Example',
6 | bundle = '/app.js',
7 | body = '',
8 | favicon = '',
9 | stylesheet = '',
10 | }) => (
11 |
12 |
13 |
14 |
15 |
16 | {title}
17 |
18 | {favicon ? : null}
19 | {stylesheet ? : null}
20 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 | Html.propTypes = {
28 | title: PropTypes.string,
29 | bundle: PropTypes.string,
30 | body: PropTypes.string,
31 | favicon: PropTypes.string,
32 | stylesheet: PropTypes.string,
33 | };
34 |
35 | /**
36 | * Render the entire web page to a string. We use render to static markup here
37 | * to avoid react hooking on to the document HTML that will not be managed by
38 | * React. The body prop is a string that contains the actual document body,
39 | * which react will hook on to.
40 | *
41 | * We also take this opportunity to prepend the doctype string onto the
42 | * document.
43 | *
44 | * @param {object} props
45 | * @return {string}
46 | */
47 | export const renderDocumentToString = props => {
48 | return `${renderToStaticMarkup( )}`;
49 | };
50 |
51 |
--------------------------------------------------------------------------------
/example/redux/server.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NOTE: This file must be run with babel-node as Node is not yet compatible
3 | * with all of ES6 and we also use JSX.
4 | */
5 | import http from 'http';
6 | import url from 'url';
7 | import express from 'express';
8 | import webpack from 'webpack';
9 |
10 | import config from './webpack.config.dev.js';
11 | import { renderDocumentToString } from './render-to-document-string.js';
12 |
13 | const app = express();
14 | const compiler = webpack(config);
15 |
16 | app.use(require('webpack-dev-middleware')(compiler, {
17 | noInfo: true,
18 | publicPath: config.output.publicPath,
19 | }));
20 |
21 | app.use(require('webpack-hot-middleware')(compiler));
22 |
23 | // Send the boilerplate HTML payload down for all get requests. Routing will be
24 | // handled entirely client side and we don't make an effort to pre-render pages
25 | // before they are served when in dev mode.
26 | app.get('*', (req, res) => {
27 | const html = renderDocumentToString({
28 | bundle: config.output.publicPath + 'app.js',
29 | });
30 | res.send(html);
31 | });
32 |
33 | // NOTE: url.parse can't handle URLs without a protocol explicitly defined. So
34 | // if we parse '//localhost:8888' it doesn't work. We manually add a protocol even
35 | // though we are only interested in the port.
36 | const { port } = url.parse('http:' + config.output.publicPath);
37 |
38 | const server = http.createServer(app);
39 |
40 | server.listen(port, 'localhost', (err) => {
41 | if (err) {
42 | console.error(err);
43 | return;
44 | }
45 |
46 | console.log(`Dev server listening at http://localhost:${port}`);
47 | });
48 |
--------------------------------------------------------------------------------
/example/redux/src/components/App.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | import React, { PropTypes } from 'react';
3 | import { Link, IndexLink } from 'react-router';
4 | import 'normalize.css/normalize.css';
5 | import classnames from 'classnames/bind';
6 | import 'react-fa';
7 |
8 | // Using CSS Modules so we assign the styles to a variable
9 | import s from './App.styl';
10 | const cx = classnames.bind(s);
11 | import '../lib/expose-globals.js';
12 |
13 | // Favicon link is in the template, this just makes webpack package it up for us
14 | import './favicon.ico';
15 |
16 | export class NotFound extends React.Component {
17 | render() {
18 | return (
19 |
20 |
Not found
21 |
22 | );
23 | }
24 | }
25 |
26 | export class Home extends React.Component {
27 | render() {
28 | return (
29 |
30 | h
31 |
32 | );
33 | }
34 | }
35 |
36 | const NavLink = (props) => (
37 |
38 | );
39 |
40 | class Header extends React.Component {
41 | render() {
42 | return (
43 |
44 |
45 |
46 |
47 |
48 | Who Are We?
49 | How it Works
50 |
51 |
52 | Log In
53 | Sign Up
54 |
55 |
56 | );
57 | }
58 | }
59 |
60 | class Footer extends React.Component {
61 | handleSubmit = (e) => {
62 | e.preventDefault();
63 | const input = e.target.elements.email;
64 | const email = input.value.trim();
65 | console.log(`Enrolling ${email}...`);
66 | input.value = '';
67 | };
68 |
69 | render() {
70 | return (
71 |
72 |
73 |
74 |
75 |
Create & Discover inspired React Apps.
76 |
86 |
87 |
88 |
About
89 | About Us
90 | How it Works
91 | Investors
92 | Press
93 |
94 |
95 |
Customer Care
96 | Contact Us
97 | Privacy Policy
98 | Terms of Use
99 |
100 |
101 |
Community
102 | Our Blog
103 |
104 |
105 |
116 |
Language
117 |
120 | English
121 | 繁體中文
122 |
123 |
Currency
124 |
127 | US Dollar
128 | New Taiwan Dollar
129 |
130 |
131 |
132 |
133 | {process.env.__VERSION__}
134 | © {new Date().getFullYear()} React Example
135 |
136 |
137 | );
138 | }
139 | }
140 |
141 | export class App extends React.Component {
142 | static propTypes = {
143 | children: PropTypes.any,
144 | };
145 |
146 | render() {
147 | return (
148 |
149 |
150 | {this.props.children}
151 |
152 |
153 | );
154 | }
155 | }
156 |
157 | export default App;
158 |
--------------------------------------------------------------------------------
/example/redux/src/components/App.styl:
--------------------------------------------------------------------------------
1 | @import url(https://fonts.googleapis.com/css?family=Lato:400,100,300,700italic,400italic)
2 |
3 | @require '../lib/vars.styl'
4 |
5 | .page
6 | composes page from '../lib/shared.styl'
7 |
8 | .siteTitle
9 | composes siteTitle from '../lib/shared.styl'
10 |
11 | *
12 | box-sizing border-box
13 |
14 | body
15 | background react-blue
16 | font-family lato
17 | font-size 18px
18 | color black
19 |
20 | h1
21 | margin 20px 0
22 |
23 | pre
24 | font-family mono
25 | line-height 1.5
26 | border-radius 3px
27 | border 1px solid border-gray
28 |
29 | a
30 | input
31 | textarea
32 | button
33 | border none
34 | outline none
35 | +desktop()
36 | appearance none
37 |
38 | .App
39 | min-width 320px
40 |
41 | h1
42 | color pink
43 | font-weight 300
44 | font-size 52px
45 |
46 | .logo
47 | border-radius 50%
48 | display block
49 | margin-right 20px
50 | +mobile()
51 | margin-right 0
52 | img
53 | height 40px
54 | width auto
55 |
56 | /* **************************************************************************
57 | * Header
58 | * *************************************************************************/
59 | .Header
60 | background white
61 | padding 0 20px
62 | display flex
63 | align-items center
64 | justify-content space-between
65 | height 60px
66 | margin-bottom 60px
67 | +mobile()
68 | margin-bottom 0
69 | a
70 | display block
71 |
72 | .logo
73 | padding 0
74 | img
75 | border-radius 50%
76 | display block
77 | height 50px
78 |
79 | .active
80 | color black
81 |
82 | .middle
83 | .right
84 | a
85 | display inline-block
86 | text-decoration none
87 | font-size 1rem
88 | &:not(:first-child)
89 | margin-left 20px
90 |
91 |
92 | .middle
93 | display flex
94 | height 100%
95 | +mobile()
96 | display none
97 |
98 | a
99 | color pink
100 | display flex
101 | align-items center
102 | height 100%
103 |
104 | .right
105 | a
106 | padding 5px 8px
107 | border-radius 3px
108 |
109 | .logIn
110 | color pink
111 | &.active
112 | color black
113 |
114 | .signUp
115 | color white
116 | background pink
117 | &.active
118 | color white
119 | background lighten(black, 20%)
120 |
121 |
122 | /* **************************************************************************
123 | * Footer
124 | * *************************************************************************/
125 | .Footer
126 | composes page from '../lib/shared.styl'
127 |
128 | .logo
129 | display block
130 | width 150px
131 | height auto
132 |
133 | form
134 | +mobile()
135 | width 100%
136 | input
137 | width 100%
138 | border-bottom 1px solid pink
139 | padding .5rem 0
140 | margin-bottom .5rem
141 | button
142 | padding 10px
143 | p
144 | color pink
145 |
146 | .cols
147 | display flex
148 | flex-shrink 0
149 | width 100%
150 | justify-content space-between
151 | +below(700px)
152 | flex-direction column
153 |
154 | .col
155 | flex 1 20%
156 | +below(700px)
157 | margin-top 20px
158 | a
159 | display block
160 | font-size 14px
161 | text-decoration none
162 | color lighten(black, 20%)
163 | &:hover
164 | color black
165 | text-decoration underline
166 | &.active
167 | color black
168 | text-decoration underline
169 | +below(700px)
170 | line-height 2
171 | font-size 1rem
172 |
173 | h4
174 | margin-top 0
175 |
176 | .social
177 | +below(700px)
178 | select
179 | margin-top 20px
180 | width 100%
181 | option
182 | text-align center
183 | h4
184 | margin-top 20px
185 | +below(700px)
186 | display none
187 | .icons
188 | a
189 | display inline-block
190 | width 40px
191 | img
192 | opacity .8
193 | width 100%
194 | max-width auto
195 | &:hover
196 | opacity 1
197 |
198 | .copy
199 | display flex
200 | justify-content space-between
201 | width 100%
202 | color darken(white, 20%)
203 | +below(700px)
204 | margin-top 20px
205 | small
206 | display block
207 |
--------------------------------------------------------------------------------
/example/redux/src/components/Pages.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | import React from 'react';
3 | import classnames from 'classnames/bind';
4 | import { connect } from 'react-redux';
5 | import { Map } from 'immutable';
6 |
7 | import { getFormValues } from '../modules/things/utils.js';
8 | import { updateForm, initialize, teardown, addThing } from '../modules/things';
9 |
10 | import s from './Pages.styl';
11 | const cx = classnames.bind(s);
12 |
13 | export class Who extends React.Component {
14 | render() {
15 | return (
16 |
17 |
18 |
Who are we?
19 |
20 |
We're awesome.
21 |
22 | );
23 | }
24 | }
25 |
26 | export class How extends React.Component {
27 | render() {
28 | return (
29 |
30 |
31 |
How it works
32 |
33 |
It works because you log in and we agregate your data :D.
34 |
35 | );
36 | }
37 | }
38 |
39 | class Home extends React.Component {
40 | componentDidMount() {
41 | this.props.dispatch(initialize());
42 | }
43 |
44 | componentWillUnmount() {
45 | this.props.dispatch(teardown());
46 | }
47 |
48 | handleFormChange = (e) => {
49 | const { value } = e.target;
50 | this.props.dispatch(updateForm('thing', value));
51 | };
52 |
53 | handleSubmit = (e) => {
54 | e.preventDefault();
55 |
56 | const { formValues } = this.props;
57 | this.props.dispatch(addThing({ text: formValues.get('thing') }));
58 | this.props.dispatch(updateForm('thing', ''));
59 | };
60 |
61 | render() {
62 | const { formValues, things } = this.props;
63 | return (
64 |
65 |
66 |
This is the home page
67 |
68 |
It's a great page.
69 |
77 |
78 | {things.valueSeq().map(x => (
79 |
{x.text}
80 | ))}
81 |
82 |
83 | );
84 | }
85 | }
86 |
87 | const mapStateToProps = (state) => {
88 | return {
89 | things: state.getIn(['things', 'data'], Map()),
90 | formValues: getFormValues(state),
91 | };
92 | };
93 |
94 | export default connect(mapStateToProps)(Home);
95 |
--------------------------------------------------------------------------------
/example/redux/src/components/Pages.styl:
--------------------------------------------------------------------------------
1 | @require '../lib/vars.styl'
2 |
3 | .page
4 | composes page from '../lib/shared.styl'
5 |
6 | .siteTitle
7 | composes siteTitle from '../lib/shared.styl'
8 |
9 |
10 | .form
11 | width 100%
12 | input
13 | width 100%
14 | border solid 1px #999
15 | padding 10px
16 | border-radius 3px
17 | &:focus
18 | border-color blue
19 | box-shadow inset 0 0 2px blue
20 |
--------------------------------------------------------------------------------
/example/redux/src/components/SignUp.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | import React from 'react';
3 | import classnames from 'classnames/bind';
4 |
5 | import s from './SignUp.styl';
6 | const cx = classnames.bind(s);
7 |
8 | export class LogIn extends React.Component {
9 | render() {
10 | return (
11 |
12 |
13 |
Log In
14 |
15 |
Log in now. It's fast and free!
16 |
17 | );
18 | }
19 | }
20 |
21 | export class SignUp extends React.Component {
22 | render() {
23 | return (
24 |
25 |
26 |
Sign Up
27 |
28 |
Sign up right here.
29 |
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/example/redux/src/components/SignUp.styl:
--------------------------------------------------------------------------------
1 | @require '../lib/vars.styl'
2 |
3 | .page
4 | composes page from '../lib/shared.styl'
5 |
6 | .siteTitle
7 | composes siteTitle from '../lib/shared.styl'
8 |
9 |
10 |
--------------------------------------------------------------------------------
/example/redux/src/components/banner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iansinnott/react-static-webpack-plugin/f4abf1f0719fbe8ddabf7dbeb702ac181d485889/example/redux/src/components/banner.jpg
--------------------------------------------------------------------------------
/example/redux/src/components/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iansinnott/react-static-webpack-plugin/f4abf1f0719fbe8ddabf7dbeb702ac181d485889/example/redux/src/components/favicon.ico
--------------------------------------------------------------------------------
/example/redux/src/components/logo-full.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
11 |
16 |
21 |
22 |
--------------------------------------------------------------------------------
/example/redux/src/index.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | import React from 'react';
3 | import { render } from 'react-dom';
4 | import { Router, browserHistory } from 'react-router';
5 | import { Provider } from 'react-redux';
6 |
7 | import store from './redux/store.js';
8 | import routes from './routes.js';
9 |
10 | render((
11 |
12 |
13 |
14 | ), document.getElementById('root'));
15 |
--------------------------------------------------------------------------------
/example/redux/src/lib/components/Spinner.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactSpinner from 'react-spinkit';
3 | import classnames from 'classnames/bind';
4 |
5 | import s from './Spinner.styl';
6 | const cx = classnames.bind(s);
7 |
8 | export const Spinner = ({ className, ...props }) => (
9 |
10 |
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/example/redux/src/lib/components/Spinner.styl:
--------------------------------------------------------------------------------
1 | @require '../vars.styl'
2 |
3 | // The dots in the three bounce
4 | .Spinner
5 | text-align center
6 |
7 | :global .three-bounce > div
8 | background-color pink
9 | animation-duration 0.8s
10 | width 8px
11 | height 8px
12 |
13 | &:not(:last-child)
14 | margin-right 5px
15 |
--------------------------------------------------------------------------------
/example/redux/src/lib/components/index.js:
--------------------------------------------------------------------------------
1 | export * from './Spinner.js';
2 |
--------------------------------------------------------------------------------
/example/redux/src/lib/expose-globals.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'development') {
2 | window.React = require('react');
3 | window.Redux = require('redux');
4 | window.axios = require('axios');
5 | window.Immutable = require('immutable');
6 |
7 | window.store = require('../redux/store.js');
8 | window.Things = require('../modules/things');
9 | }
10 |
--------------------------------------------------------------------------------
/example/redux/src/lib/fb-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/example/redux/src/lib/gplus-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/example/redux/src/lib/shared.styl:
--------------------------------------------------------------------------------
1 | .page
2 | padding 20px
3 |
4 | .siteTitle
5 | display flex
6 | align-items center
7 |
8 | +below(420px)
9 | display block
10 |
11 |
--------------------------------------------------------------------------------
/example/redux/src/lib/twitter-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/example/redux/src/lib/vars.styl:
--------------------------------------------------------------------------------
1 | mono = Consolas, 'Liberation Mono', Menlo, Courier, monospace
2 | helvetica = 'Helvetica Neue', Helvetica, Arial, sans-serif
3 | lato = Lato, helvetica
4 | black = #333
5 |
6 | // Muahahaha, not really pink
7 | pink = #24bbd6
8 |
9 | light-blue = #e7edfa
10 |
11 | // Use with inputs / buttons you don't want users to interact with
12 | uninteractive()
13 | user-select none
14 | pointer-events none
15 | cursor not-allowed
16 |
--------------------------------------------------------------------------------
/example/redux/src/modules/forms/index.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | import { Map, fromJS } from 'immutable';
3 |
4 | const REGISTER_FORM = 'redux-example/forms/REGISTER_FORM';
5 | const UNREGISTER_FORM = 'redux-example/forms/UNREGISTER_FORM';
6 | const UPDATE_FORM = 'redux-example/forms/UPDATE_FORM';
7 |
8 | const initialState = Map({});
9 |
10 | /* **************************************************************************
11 | * Reducer
12 | * *************************************************************************/
13 |
14 | export default function reducer(state = initialState, action) {
15 | const { type, payload } = action;
16 |
17 | switch (type) {
18 | case REGISTER_FORM:
19 | const initialValues = payload.initialValues
20 | ? fromJS(payload.initialValues)
21 | : Map({});
22 | return state.set(payload.id, fromJS({
23 | initialValues,
24 | currentValues: initialValues,
25 | }));
26 | case UNREGISTER_FORM:
27 | return state.delete(payload);
28 | case UPDATE_FORM:
29 | return state.setIn(payload.keypath, payload.value);
30 | default:
31 | return state;
32 | }
33 | }
34 |
35 | /* **************************************************************************
36 | * Action Creators
37 | * *************************************************************************/
38 |
39 | /**
40 | * type initialForm = { id: string, initialValues: Object | Map }
41 | */
42 | export function registerForm(initialForm) {
43 | return {
44 | type: REGISTER_FORM,
45 | payload: initialForm,
46 | };
47 | }
48 |
49 | /**
50 | * type formId: string
51 | */
52 | export function unregisterForm(formId) {
53 | return {
54 | type: REGISTER_FORM,
55 | payload: formId,
56 | };
57 | }
58 |
59 | /**
60 | * type update = { keypath: string[], value: string | number | boolean }
61 | */
62 | export function updateForm(update) {
63 | return {
64 | type: UPDATE_FORM,
65 | payload: update,
66 | };
67 | }
68 |
--------------------------------------------------------------------------------
/example/redux/src/modules/things/index.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | import { fromJS, Record } from 'immutable';
3 |
4 | import * as Forms from '../forms';
5 | import UUID from 'node-uuid';
6 |
7 | // A totally fake uuid function
8 | const uuid = () => UUID.v4();
9 |
10 | const ADD_THING = 'redux-example/things/ADD_THING';
11 | export const FORM_ID = 'redux-example/things/_form';
12 |
13 | const Thing = Record({
14 | id: null,
15 | text: '',
16 | created: Date.now(),
17 | updated: Date.now(),
18 | });
19 |
20 | const fromJSON = (things) => {
21 | return fromJS(things)
22 | .map(Thing)
23 | .toOrderedMap()
24 | .mapKeys((k, v) => v.id);
25 | };
26 |
27 | /**
28 | * This is the initial state of our app, and also displayes the shape. The app
29 | * state shape should never be any more or less than this.
30 | */
31 | const initialState = fromJS({
32 | loading: false,
33 | error: null,
34 | data: fromJSON(
35 | [uuid(), uuid(), uuid()].map((id, i) => ({ id, text: `Testable Title for Thing ${i}` }))
36 | ),
37 | });
38 |
39 | /* **************************************************************************
40 | * Reducer
41 | * *************************************************************************/
42 |
43 | export default function reducer(state = initialState, action) {
44 | const { type, payload } = action;
45 | switch (type) {
46 | case ADD_THING:
47 | return state.setIn(['data', payload.id], payload);
48 | default:
49 | return state;
50 | }
51 | }
52 |
53 | /* **************************************************************************
54 | * Action Creators
55 | * *************************************************************************/
56 |
57 | export function addThing(thing) {
58 | if (!thing.id) {
59 | thing.id = uuid();
60 | }
61 |
62 | return {
63 | type: ADD_THING,
64 | payload: thing,
65 | };
66 | }
67 |
68 | export function initialize() {
69 | return Forms.registerForm({ id: FORM_ID, initialValues: {} });
70 | }
71 |
72 | export function teardown() {
73 | return Forms.unregisterForm(FORM_ID);
74 | }
75 |
76 | export function updateForm(key, value) {
77 | return Forms.updateForm({
78 | keypath: [FORM_ID, 'currentValues', key],
79 | value,
80 | });
81 | }
82 |
--------------------------------------------------------------------------------
/example/redux/src/modules/things/utils.js:
--------------------------------------------------------------------------------
1 | import { Map } from 'immutable';
2 |
3 | import { FORM_ID } from './index.js';
4 |
5 | export const getFormValues = (state) => {
6 | return state.getIn(['forms', FORM_ID, 'currentValues'], Map());
7 | };
8 |
--------------------------------------------------------------------------------
/example/redux/src/redux/loggerMiddleware.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | import type { Store, Action } from './types.js';
3 |
4 | export default function loggerMiddleware(store: Store) {
5 | return (next: Function) => (action: Action) => {
6 | console.group(action.type);
7 | console.info('dispatching', action);
8 | let result = next(action);
9 | console.log('next state', store.getState().toJS()); // Because we're using Immutable
10 | console.groupEnd(action.type);
11 | return result;
12 | };
13 | }
14 |
--------------------------------------------------------------------------------
/example/redux/src/redux/promiseMiddleware.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | import type { Store, Dispatch } from './types.js';
3 |
4 | function isPromise(x: any) {
5 | return x && typeof x.then === 'function';
6 | }
7 |
8 | function isAsyncAction(action) {
9 | return isPromise(action.promise) && Array.isArray(action.types);
10 | }
11 |
12 | export default function promiseMiddleware({ dispatch }: Store) {
13 | return (next: Dispatch) => (action: any) => {
14 | // If given a promise, use it and dispatch the result
15 | if (isPromise(action)) {
16 | return action.then(dispatch);
17 | }
18 |
19 | // If this is a three-action triplet then treat is at such. This is the real
20 | // meat of this middleware
21 | if (isAsyncAction(action)) {
22 | const [ REQUEST, SUCCESS, FAILURE ] = action.types;
23 | dispatch({ type: REQUEST });
24 | return action.promise.then(
25 | result => dispatch({ type: SUCCESS, payload: result }),
26 | error => {
27 | dispatch({ payload: error, error: true, type: FAILURE });
28 | return Promise.reject(error);
29 | }
30 | );
31 | }
32 |
33 | // Otherwise just pass the action on
34 | return next(action);
35 | };
36 | }
37 |
--------------------------------------------------------------------------------
/example/redux/src/redux/store.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | import { createStore, applyMiddleware } from 'redux';
3 | import { combineReducers } from 'redux-immutable';
4 | import { Map } from 'immutable';
5 |
6 | import loggerMiddleware from './loggerMiddleware.js';
7 | import promiseMiddleware from './promiseMiddleware.js';
8 | import things from '../modules/things';
9 | import forms from '../modules/forms';
10 |
11 | const reducer = combineReducers({
12 | things,
13 | forms,
14 | });
15 |
16 | const middlewares = [promiseMiddleware];
17 |
18 | if (process.env.NODE_ENV === 'development') {
19 | middlewares.push(loggerMiddleware);
20 | }
21 |
22 | // NOTE: This is where we could read initial state from the window object if we
23 | // decided to stringify it in our template. See template.js for details.
24 | const store = createStore(reducer, Map(), applyMiddleware(...middlewares));
25 |
26 | export default store;
27 |
--------------------------------------------------------------------------------
/example/redux/src/redux/types.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | import type { Map } from 'immutable';
3 |
4 | export type Action = {
5 | type: string,
6 | payload?: any,
7 | };
8 |
9 | export type Dispatch = (a: Action) => void;
10 |
11 | export type Store = {
12 | dispatch: Dispatch,
13 | getState: () => Map,
14 | };
15 |
--------------------------------------------------------------------------------
/example/redux/src/routes.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | import React from 'react';
3 | import { Route, IndexRoute } from 'react-router';
4 |
5 | import { App, NotFound } from './components/App.js';
6 | import Home, { Who, How } from './components/Pages.js';
7 | import { LogIn, SignUp } from './components/SignUp.js';
8 |
9 | /**
10 | * The route configuration for the whole app.
11 | */
12 |
13 | export const routes = (
14 |
15 |
16 |
17 | {/* Pages */}
18 |
19 |
20 |
21 | {/* Account */}
22 |
23 |
24 |
25 | {/* Not Found */}
26 |
27 |
28 | );
29 |
30 | export default routes;
31 |
--------------------------------------------------------------------------------
/example/redux/template.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Html = (props) => (
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {props.title}
13 |
14 | {/*
15 | Pass the initial state to our template so that our store can grab it
16 | from the window object on initialization.
17 |
18 | NOTE: In this example we use Immutable.js, so we need to call toJS on
19 | state. However, if you are not using Immutable this will not be necessary
20 | */}
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 |
30 | export default Html;
31 |
--------------------------------------------------------------------------------
/example/redux/test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import webpack from 'webpack';
3 | import fs from 'fs';
4 | import path from 'path';
5 |
6 | import options from './webpack.config.prod.js';
7 |
8 | let stats;
9 |
10 | test.cb.before(t => {
11 | webpack(options, (err, _stats) => {
12 | if (err) {
13 | return t.end(err);
14 | } else if (_stats.hasErrors()) {
15 | return t.end(_stats.toString());
16 | }
17 |
18 | stats = _stats;
19 |
20 | t.end();
21 | });
22 | });
23 |
24 | test('Outputs the desired files', t => {
25 | const { assets } = stats.toJson();
26 | const files = assets.map(x => x.name);
27 |
28 | t.true(files.includes('index.html'));
29 | t.true(files.includes('who.html'));
30 | t.true(files.includes('how.html'));
31 | t.true(files.includes('log-in.html'));
32 | t.true(files.includes('sign-up.html'));
33 | t.true(files.includes('404.html'));
34 | t.true(files.includes('app.css'));
35 | t.true(files.includes('app.js'));
36 | });
37 |
38 | test('Supports redux apps that must be wrapped in ', t => {
39 | const outputFilepath = path.join(options.output.path, 'index.html');
40 | const outputFileContents = fs.readFileSync(outputFilepath, { encoding: 'utf8' });
41 |
42 | t.true(outputFileContents.includes('window.__initial_state'));
43 | t.true(outputFileContents.includes('Testable Title for Thing 0'));
44 | t.true(outputFileContents.includes('Testable Title for Thing 1'));
45 | t.true(outputFileContents.includes('Testable Title for Thing 2'));
46 | });
47 |
48 | test('Bundle was minified', t => {
49 | const { assets } = stats.toJson();
50 | const files = assets.map(x => x.name);
51 | const bundle = assets[files.indexOf('app.js')];
52 |
53 | t.true((bundle.size / 1000) < 500);
54 | });
55 |
--------------------------------------------------------------------------------
/example/redux/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | var path = require('path');
3 | var webpack = require('webpack');
4 | var axis = require('axis');
5 | var rupture = require('rupture');
6 | var execSync = require('child_process').execSync;
7 |
8 | // Set up dev host host and HMR host. For the dev host this is pretty self
9 | // explanatory: We use a different live-reload server to server our static JS
10 | // files in dev, so we need to be able to actually point a script tag to that
11 | // host so it can load the right files. The HRM host is a bit stranger. For more
12 | // details on why we need this URL see the readme and:
13 | // https://github.com/glenjamin/webpack-hot-middleware/issues/37
14 | var DEV_PORT = process.env.DEV_PORT || 3000;
15 | var DEV_HOST = '//localhost:' + DEV_PORT + '/';
16 | var HMR_HOST = DEV_HOST + '__webpack_hmr';
17 |
18 | module.exports = {
19 | devtool: 'inline-source-map',
20 |
21 | context: __dirname,
22 |
23 | entry: {
24 | app: [
25 | 'webpack-hot-middleware/client?path=' + HMR_HOST,
26 | './src/index.js',
27 | ],
28 | },
29 |
30 | output: {
31 | path: path.join(__dirname, 'public'),
32 | filename: '[name].js',
33 | publicPath: DEV_HOST,
34 | },
35 |
36 | plugins: [
37 | new webpack.HotModuleReplacementPlugin(),
38 | new webpack.NoErrorsPlugin(),
39 | new webpack.DefinePlugin({
40 | 'process.env': {
41 | NODE_ENV: JSON.stringify('development'),
42 | __VERSION__: JSON.stringify(execSync('git describe', { encoding: 'utf-8' }).trim()),
43 | },
44 | }),
45 | ],
46 |
47 | module: {
48 | loaders: [
49 | {
50 | test: /\.js$/,
51 | loaders: ['babel'],
52 | include: path.join(__dirname, 'src'),
53 | },
54 | {
55 | test: /\.json$/,
56 | loaders: ['json'],
57 | },
58 | {
59 | test: /\.css$/,
60 | loaders: ['style', 'css'],
61 | },
62 | {
63 | test: /\.styl/,
64 | loaders: [
65 | 'style',
66 | 'css?modules&importLoaders=2&localIdentName=[name]__[local]__[hash:base64:6]',
67 | 'autoprefixer',
68 | 'stylus',
69 | ],
70 | },
71 | {
72 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
73 | loaders: ['url?limit=10000&mimetype=application/font-woff'],
74 | },
75 | {
76 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
77 | loaders: ['file'],
78 | },
79 | {
80 | test: /\.(png|jpg|gif|ico)$/,
81 | loaders: ['file?name=[name].[ext]'],
82 | },
83 | ],
84 | },
85 |
86 | stylus: {
87 | use: [axis(), rupture()],
88 | },
89 | };
90 |
--------------------------------------------------------------------------------
/example/redux/webpack.config.prod.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | var path = require('path');
3 | var webpack = require('webpack');
4 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
5 | var axis = require('axis');
6 | var rupture = require('rupture');
7 | var execSync = require('child_process').execSync;
8 | var ReactStaticPlugin = require('../../dist');
9 |
10 | module.exports = {
11 | devtool: 'source-map',
12 |
13 | context: __dirname,
14 |
15 | entry: {
16 | app: [
17 | 'babel-polyfill',
18 | './src/index.js',
19 | ],
20 | },
21 |
22 | output: {
23 | path: path.join(__dirname, 'public'),
24 | filename: '[name].js',
25 | publicPath: '/',
26 | },
27 |
28 | plugins: [
29 | new ExtractTextPlugin('[name].css', { allChunks: true }),
30 | // new webpack.optimize.OccurenceOrderPlugin(),
31 | new webpack.DefinePlugin({
32 | 'process.env': {
33 | NODE_ENV: JSON.stringify('production'),
34 | __VERSION__: JSON.stringify(execSync('git describe', { encoding: 'utf-8' }).trim()),
35 | },
36 | }),
37 | new webpack.optimize.UglifyJsPlugin({
38 | screw_ie8: true,
39 | compressor: { warnings: false },
40 | }),
41 |
42 | // Generate the static site
43 | new ReactStaticPlugin({
44 | routes: './src/routes.js',
45 | reduxStore: './src/redux/store.js',
46 | template: './template.js',
47 | }),
48 | ],
49 |
50 | module: {
51 | loaders: [
52 | {
53 | test: /\.js$/,
54 | loaders: ['babel'],
55 | exclude: path.join(__dirname, 'node_modules'),
56 | },
57 | {
58 | test: /\.json$/,
59 | loaders: ['json'],
60 | },
61 | {
62 | test: /\.css$/,
63 | loader: ExtractTextPlugin.extract('style', 'css'),
64 | },
65 | {
66 | test: /\.styl/,
67 | loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=2!autoprefixer!stylus'),
68 | },
69 | {
70 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
71 | loaders: ['url?limit=10000&mimetype=application/font-woff'],
72 | },
73 | {
74 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
75 | loaders: ['file'],
76 | },
77 | {
78 | test: /\.(png|jpg|gif|ico)$/,
79 | loaders: ['file?name=[name].[ext]'],
80 | },
81 | ],
82 | },
83 |
84 | stylus: {
85 | use: [axis(), rupture()],
86 | },
87 | };
88 |
--------------------------------------------------------------------------------
/example/webpack-2/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["es2015", {"modules": false}],
4 | "react",
5 | "stage-1"
6 | ],
7 | "env": {
8 | "development": {
9 | "presets": ["react-hmre"]
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/example/webpack-2/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/example/webpack-2/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // You can also add 'zen/flow' if you want Flow linting support
3 | extends: ['zen/base', 'zen/react'],
4 | }
5 |
--------------------------------------------------------------------------------
/example/webpack-2/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *.DS_Store
3 | build/
4 | .tmp/
5 | nginx.conf
6 | npm-debug.log
7 | .idea
8 | node_modules.bak
9 | my-site.conf
10 |
--------------------------------------------------------------------------------
/example/webpack-2/README.md:
--------------------------------------------------------------------------------
1 | # Webpack 2 Example
2 |
3 | [Webpack 2](https://github.com/webpack/webpack/releases) is now released and stable, so let's use it! This directory is a working examples of setting up Webpack 2 using the react-static-webpack-plugin.
4 |
--------------------------------------------------------------------------------
/example/webpack-2/client/components/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, IndexLink } from 'react-router';
3 | import classnames from 'classnames/bind';
4 |
5 | // Using CSS Modules so we assign the styles to a variable
6 | import s from './App.styl';
7 | const cx = classnames.bind(s);
8 | import logo from './react-logo.png';
9 |
10 | // Favicon link is in the template, this just makes webpack package it up for us
11 | import './favicon.ico';
12 |
13 | export class Home extends React.Component {
14 | render() {
15 | return (
16 |
17 |
18 |
19 |
React Static Boilerplate
20 |
21 |
Why React static?
22 |
23 | Dev friendly
24 | User friendly
25 | SEO friendly
26 |
27 |
28 | );
29 | }
30 | }
31 |
32 | export class About extends React.Component {
33 | render() {
34 | return (
35 |
36 |
37 |
About Page
38 |
39 |
Welcome to the about page...
40 |
41 | );
42 | }
43 | }
44 |
45 | export class NotFound extends React.Component {
46 | render() {
47 | return (
48 |
49 |
Not found
50 |
51 | );
52 | }
53 | }
54 |
55 | /**
56 | * NOTE: As of 2015-11-09 react-transform does not support a functional
57 | * component as the base compoenent that's passed to ReactDOM.render, so we
58 | * still use createClass here.
59 | */
60 | export class App extends React.Component {
61 | static propTypes = {
62 | children: React.PropTypes.node,
63 | }
64 | render() {
65 | return (
66 |
67 |
68 | Home
69 | About
70 |
71 | {this.props.children}
72 |
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/example/webpack-2/client/components/App.styl:
--------------------------------------------------------------------------------
1 | @require '../lib/vars.styl'
2 |
3 | *
4 | box-sizing border-box
5 |
6 | body
7 | background react-blue
8 | font-family helvetica
9 | color darken(white, 10%)
10 | font-size 18px
11 |
12 | h1
13 | margin 20px 0
14 |
15 | pre
16 | font-family mono
17 | line-height 1.5
18 | border-radius 3px
19 | border 1px solid border-gray
20 |
21 | .page
22 | padding 20px
23 |
24 | .siteTitle
25 | display flex
26 | align-items center
27 |
28 | +below(420px)
29 | display block
30 |
31 | img
32 | display inline-block
33 | width 80px
34 | height 80px
35 | margin-right 20px
36 |
37 | +below(420px)
38 | display block
39 | margin 0 auto
40 |
41 | .hl
42 | font-weight bold
43 | color react-blue
44 | .App
45 | background black
46 | min-width 320px
47 |
48 | h1
49 | color react-blue
50 | font-weight 300
51 | font-size 52px
52 |
53 | p
54 | color white
55 |
56 | .nav
57 | background darken(black, 10%)
58 | padding-left 20px
59 |
60 | a
61 | display inline-block
62 | padding 20px
63 | text-transform uppercase
64 | text-decoration none
65 | color darken(white, 20%)
66 | &:hover
67 | color white
68 |
69 | .active
70 | background black
71 |
72 |
73 | // This is simply included in order to allow css modules to pick it up and the
74 | // tests to pass
75 | .testableModuleClassName
76 | background transparent
77 |
--------------------------------------------------------------------------------
/example/webpack-2/client/components/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iansinnott/react-static-webpack-plugin/f4abf1f0719fbe8ddabf7dbeb702ac181d485889/example/webpack-2/client/components/favicon.ico
--------------------------------------------------------------------------------
/example/webpack-2/client/components/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iansinnott/react-static-webpack-plugin/f4abf1f0719fbe8ddabf7dbeb702ac181d485889/example/webpack-2/client/components/react-logo.png
--------------------------------------------------------------------------------
/example/webpack-2/client/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { Router, browserHistory } from 'react-router';
4 |
5 | // Import your routes so that you can pass them to the component
6 | import routes from './routes.js';
7 |
8 | render((
9 |
10 | ), document.getElementById('root'));
11 |
--------------------------------------------------------------------------------
/example/webpack-2/client/lib/vars.styl:
--------------------------------------------------------------------------------
1 | mono = Consolas, 'Liberation Mono', Menlo, Courier, monospace
2 | helvetica = 'Helvetica Neue', Helvetica, Arial, sans-serif
3 | js-yellow = #f5da55
4 | react-blue = #00d8ff
5 | black = #333
6 |
7 | // Use with inputs / buttons you don't want users to interact with
8 | uninteractive()
9 | user-select none
10 | pointer-events none
11 | cursor not-allowed
12 |
--------------------------------------------------------------------------------
/example/webpack-2/client/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, IndexRoute } from 'react-router';
3 |
4 | import { App, About, Home, NotFound } from './components/App.js';
5 |
6 | export const routes = (
7 |
8 |
9 |
10 |
11 |
12 | );
13 |
14 | export default routes;
15 |
--------------------------------------------------------------------------------
/example/webpack-2/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-static-boilerplate",
3 | "version": "2.0.0",
4 | "description": "A boilerplate for building static sites with React and React Router",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/iansinnott/react-static-boilerplate"
8 | },
9 | "bugs": {
10 | "url": "https://github.com/iansinnott/react-static-boilerplate/issues"
11 | },
12 | "author": "Ian Sinnott (http://iansinnott.com)",
13 | "license": "MIT",
14 | "homepage": "",
15 | "engines": {
16 | "node": "^6.9.1"
17 | },
18 | "dependencies": {
19 | "autoprefixer": "^6.5.3",
20 | "axis": "^0.6.1",
21 | "babel-cli": "^6.9.0",
22 | "babel-core": "^6.9.0",
23 | "babel-eslint": "^7.1.1",
24 | "babel-loader": "^6.2.4",
25 | "babel-plugin-react-transform": "^2.0.2",
26 | "babel-preset-es2015": "^6.9.0",
27 | "babel-preset-react": "^6.5.0",
28 | "babel-preset-react-hmre": "^1.1.1",
29 | "babel-preset-stage-1": "^6.5.0",
30 | "classnames": "^2.2.5",
31 | "cross-env": "^3.1.3",
32 | "css-loader": "^0.26.0",
33 | "eslint": "^3.11.1",
34 | "eslint-config-rackt": "^1.1.1",
35 | "eslint-config-zen": "^2.0.0",
36 | "eslint-plugin-flowtype": "^2.29.1",
37 | "eslint-plugin-react": "^6.8.0",
38 | "express": "^4.13.4",
39 | "extract-text-webpack-plugin": "^2.1.0",
40 | "file-loader": "^0.9.0",
41 | "history": "^4.5.0",
42 | "normalize.css": "^5.0.0",
43 | "postcss-loader": "^1.2.0",
44 | "react": "^15.1.0",
45 | "react-dom": "^15.1.0",
46 | "react-router": "^3.0.0",
47 | "react-transform-catch-errors": "^1.0.2",
48 | "react-transform-hmr": "^1.0.4",
49 | "redbox-react": "^1.2.5",
50 | "rimraf": "^2.5.2",
51 | "rupture": "^0.6.1",
52 | "style-loader": "^0.13.1",
53 | "stylus": "^0.54.5",
54 | "stylus-loader": "^2.1.0",
55 | "url-loader": "^0.5.7",
56 | "webpack": "^2.2.0",
57 | "webpack-dev-middleware": "^1.6.1",
58 | "webpack-hot-middleware": "^2.10.0"
59 | },
60 | "scripts": {
61 | "build-app-time": "app-time build",
62 | "clean": "rimraf build",
63 | "lint": "eslint client",
64 | "conf": "babel-node ./scripts/generate-nginx-conf.js",
65 | "test": "echo 'No tests specified.'",
66 | "start:dev": "babel-node ./server.js",
67 | "start": "npm run start:dev",
68 | "build:static": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js",
69 | "build": "npm run clean && npm run build:static",
70 | "preversion": "npm test",
71 | "postversion": "git push && git push --tags",
72 | "bump:patch": "npm version patch -m \"v%s\"",
73 | "bump:minor": "npm version minor -m \"v%s\"",
74 | "bump:major": "npm version major -m \"v%s\"",
75 | "bump": "npm run bump:patch"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/example/webpack-2/server.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NOTE: This file must be run with babel-node as Node is not yet compatible
3 | * with all of ES6 and we also use JSX.
4 | */
5 | const url = require('url');
6 | const React = require('react');
7 | const { renderToStaticMarkup } = require('react-dom/server');
8 | const express = require('express');
9 | const webpack = require('webpack');
10 |
11 | const config = require('./webpack.config.dev.js');
12 | const Html = require('./template.js');
13 |
14 | /**
15 | * Render the entire web page to a string. We use render to static markup here
16 | * to avoid react hooking on to the document HTML that will not be managed by
17 | * React. The body prop is a string that contains the actual document body,
18 | * which react will hook on to.
19 | *
20 | * We also take this opportunity to prepend the doctype string onto the
21 | * document.
22 | *
23 | * @param {object} props
24 | * @return {string}
25 | */
26 | const renderDocumentToString = props =>
27 | '' + renderToStaticMarkup( );
28 |
29 | const app = express();
30 | const compiler = webpack(config);
31 |
32 | app.use(require('webpack-dev-middleware')(compiler, {
33 | noInfo: true,
34 | publicPath: config.output.publicPath,
35 | }));
36 |
37 | app.use(require('webpack-hot-middleware')(compiler));
38 |
39 | // Send the boilerplate HTML payload down for all get requests. Routing will be
40 | // handled entirely client side and we don't make an effort to pre-render pages
41 | // before they are served when in dev mode.
42 | app.get('*', (req, res) => {
43 | const html = renderDocumentToString({
44 | bundle: config.output.publicPath + 'app.js',
45 | });
46 | res.send(html);
47 | });
48 |
49 | // NOTE: url.parse can't handle URLs without a protocol explicitly defined. So
50 | // if we parse '//localhost:8888' it doesn't work. We manually add a protocol even
51 | // though we are only interested in the port.
52 | const { port } = url.parse('http:' + config.output.publicPath);
53 |
54 | app.listen(port, 'localhost', err => {
55 | if (err) {
56 | console.error(err);
57 | return;
58 | }
59 | console.log(`Dev server listening at http://localhost:${port}`);
60 | });
61 |
--------------------------------------------------------------------------------
/example/webpack-2/template.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const T = React.PropTypes;
3 |
4 | const Html = ({
5 | title = 'Rainbow Unicorns',
6 | bundle = 'app.js',
7 | body = '',
8 | favicon = 'favicon.ico',
9 | stylesheet = 'app.css',
10 | }) => (
11 |
12 |
13 |
14 |
15 |
16 | {title}
17 | {favicon ? : null}
18 | {stylesheet ? : null}
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 |
27 | Html.propTypes = {
28 | title: T.string,
29 | bundle: T.string,
30 | body: T.string,
31 | favicon: T.string,
32 | stylesheet: T.string,
33 | };
34 |
35 | module.exports = Html;
36 |
--------------------------------------------------------------------------------
/example/webpack-2/test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * NOTE: Since this is webpack 2 and we don't transpile imports i needed to
3 | * remove any import syntax from even this test file.
4 | */
5 | const test = require('ava');
6 | const webpack = require('webpack');
7 | const fs = require('fs');
8 | const path = require('path');
9 |
10 | const config = require('./webpack.config.prod.js');
11 |
12 | let stats;
13 |
14 | test.cb.before(t => {
15 | webpack(config).run((err, _stats) => {
16 | if (err) {
17 | return t.end(err);
18 | } else if (_stats.hasErrors()) {
19 | return t.end(_stats.toString());
20 | }
21 |
22 | stats = _stats;
23 |
24 | t.end();
25 | });
26 | });
27 |
28 | test('Outputs the desired files', t => {
29 | const { assets } = stats.toJson();
30 | const files = assets.map(x => x.name);
31 |
32 | t.true(files.includes('index.html'));
33 | t.true(files.includes('about.html'));
34 | t.true(files.includes('404.html'));
35 | t.true(files.includes('app.css'));
36 | t.true(files.includes('app.js'));
37 | });
38 |
39 | test('Compiles local CSS classes (CSS Modules)', t => {
40 | const outputFilepath = path.join(config.output.path, 'index.html');
41 | const outputFileContents = fs.readFileSync(outputFilepath, { encoding: 'utf8' });
42 |
43 | // Simply make sure this classname isn't found
44 | t.false(outputFileContents.includes('testableModuleClassName'));
45 | });
46 |
47 | test('Supports minification', t => {
48 | const { assets } = stats.toJson();
49 | const files = assets.map(x => x.name);
50 | const bundle = assets[files.indexOf('app.js')];
51 |
52 | // Test size in MB. We want to make sure this bundle was minified since we
53 | // are using the minify JS plugin
54 | t.true((bundle.size / 1000) < 300);
55 | });
56 |
--------------------------------------------------------------------------------
/example/webpack-2/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const rupture = require('rupture');
4 | const autoprefixer = require('autoprefixer');
5 |
6 | // Set up dev host host and HMR host. For the dev host this is pretty self
7 | // explanatory: We use a different live-reload server to server our static JS
8 | // files in dev, so we need to be able to actually point a script tag to that
9 | // host so it can load the right files. The HRM host is a bit stranger. For more
10 | // details on why we need this URL see the readme and:
11 | // https://github.com/glenjamin/webpack-hot-middleware/issues/37
12 | const DEV_PORT = process.env.DEV_PORT || 3000;
13 | const DEV_HOST = '//localhost:' + DEV_PORT + '/';
14 | const HMR_HOST = DEV_HOST + '__webpack_hmr';
15 |
16 | module.exports = {
17 | devtool: 'inline-source-map',
18 |
19 | entry: {
20 | app: [
21 | 'normalize.css',
22 | 'webpack-hot-middleware/client?path=' + HMR_HOST,
23 | './client/index.js',
24 | ],
25 | },
26 |
27 | output: {
28 | path: path.join(__dirname, 'public'),
29 | filename: '[name].js',
30 | publicPath: DEV_HOST,
31 | },
32 |
33 | plugins: [
34 | new webpack.HotModuleReplacementPlugin(),
35 | new webpack.NoErrorsPlugin(),
36 | new webpack.LoaderOptionsPlugin({
37 | options: {
38 | postcss: [autoprefixer({ browsers: ['last 2 versions'] })],
39 | stylus: {
40 | use: [rupture()],
41 | },
42 | },
43 | }),
44 | ],
45 |
46 | module: {
47 | rules: [
48 | {
49 | test: /\.js$/,
50 | include: path.join(__dirname, 'client'),
51 | loader: 'babel-loader',
52 | },
53 | {
54 | test: /\.css$/,
55 | use: [
56 | { loader: 'style-loader' },
57 | { loader: 'css-loader' },
58 | ],
59 | },
60 | {
61 | test: /\.styl$/,
62 | use: [
63 | { loader: 'style-loader' },
64 | {
65 | loader: 'css-loader',
66 | options: {
67 | module: true,
68 | importLoaders: 2,
69 | localIdentName: '[name]__[local]__[hash:base64:6]',
70 | },
71 | },
72 | { loader: 'postcss-loader' },
73 | {
74 | loader: 'stylus-loader',
75 | },
76 | ],
77 | },
78 | {
79 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
80 | use: [
81 | {
82 | loader: 'url-loader',
83 | options: { limit: 10000, mimetype: 'mimetype=application/font-woff' },
84 | },
85 | ],
86 | },
87 | {
88 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
89 | loader: 'file-loader',
90 | },
91 | {
92 | test: /\.(png|jpg|gif|ico)$/,
93 | use: [
94 | { loader: 'file-loader', options: { name: '[name].[ext]' } },
95 | ],
96 | },
97 | ],
98 | },
99 | };
100 |
--------------------------------------------------------------------------------
/example/webpack-2/webpack.config.prod.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
4 | const rupture = require('rupture');
5 | const autoprefixer = require('autoprefixer');
6 | const ReactStaticPlugin = require('../../dist');
7 |
8 | module.exports = {
9 | devtool: 'source-map',
10 |
11 | context: __dirname,
12 |
13 | entry: {
14 | app: [
15 | 'normalize.css',
16 | './client/index.js',
17 | ],
18 | },
19 |
20 | output: {
21 | path: path.join(__dirname, 'build'),
22 | filename: '[name].js',
23 | publicPath: '/',
24 | },
25 |
26 | plugins: [
27 | new webpack.LoaderOptionsPlugin({
28 | minimize: true,
29 | options: {
30 | postcss: [autoprefixer({ browsers: ['last 2 versions'] })],
31 | stylus: {
32 | use: [rupture()],
33 | },
34 | },
35 | }),
36 | new ExtractTextPlugin({
37 | filename: '[name].css',
38 | allChunks: true,
39 | }),
40 | new webpack.DefinePlugin({
41 | 'process.env': {
42 | NODE_ENV: JSON.stringify('production'),
43 | },
44 | }),
45 | new webpack.optimize.UglifyJsPlugin({
46 | screw_ie8: true,
47 | sourceMap: true,
48 | compressor: { warnings: false },
49 | }),
50 | new ReactStaticPlugin({
51 | routes: './client/routes.js',
52 | template: './template.js',
53 | }),
54 | ],
55 |
56 | module: {
57 | rules: [
58 | {
59 | test: /\.js$/,
60 | exclude: path.join(__dirname, 'node_modules'),
61 | loader: 'babel-loader',
62 | },
63 | {
64 | test: /\.css$/,
65 | loader: ExtractTextPlugin.extract({
66 | fallbackLoader: 'style-loader',
67 | loader: 'css-loader',
68 | }),
69 | },
70 | {
71 | test: /\.styl$/,
72 | loader: ExtractTextPlugin.extract({
73 | fallbackLoader: 'style-loader',
74 | loader: [
75 | {
76 | loader: 'css-loader',
77 | query: {
78 | modules: true,
79 | importLoaders: 2,
80 | },
81 | },
82 | { loader: 'postcss-loader' },
83 | { loader: 'stylus-loader' },
84 | ],
85 | }),
86 | },
87 | {
88 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
89 | use: [
90 | {
91 | loader: 'url-loader',
92 | options: { limit: 10000, mimetype: 'mimetype=application/font-woff' },
93 | },
94 | ],
95 | },
96 | {
97 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
98 | loader: 'file-loader',
99 | },
100 | {
101 | test: /\.(png|jpg|gif|ico)$/,
102 | use: [
103 | { loader: 'file-loader', options: { name: '[name].[ext]' } },
104 | ],
105 | },
106 | ],
107 | },
108 | };
109 |
--------------------------------------------------------------------------------
/example/with-auth0/README.md:
--------------------------------------------------------------------------------
1 | # Example with Auth0
2 |
3 | The primary purpose of this example is to serve as a test case for a module that requires a browser environment to function. In this case the `auth0-js` module requires a browser environment. So this example and its tests serve to ensure that react-static-webpack-plugin will work even with modules that require a browser.
4 |
--------------------------------------------------------------------------------
/example/with-auth0/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "build": "webpack",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "auth0-js": "^7.6.0",
14 | "babel-core": "^6.8.0",
15 | "babel-loader": "^6.2.4",
16 | "jwt-decode": "^2.1.0",
17 | "react": "^15.2.1",
18 | "react-dom": "^15.2.1",
19 | "react-router": "^3.0.0",
20 | "webpack": "^1.13.0"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/example/with-auth0/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { Link, IndexLink } from 'react-router';
3 |
4 | export const Home = React.createClass({
5 | render() {
6 | return (
7 |
8 |
9 |
React Static Boilerplate
10 |
11 |
Why React static?
12 |
13 | Dev friendly
14 | User friendly
15 | SEO friendly
16 |
17 |
18 | );
19 | },
20 | });
21 |
22 | export const About = React.createClass({
23 | render() {
24 | return (
25 |
26 |
27 |
About
28 |
29 |
Welcome, to about us.
30 |
31 | );
32 | },
33 | });
34 |
35 | export const NotFound = React.createClass({
36 | render() {
37 | return (
38 |
39 |
Not found
40 |
41 | );
42 | },
43 | });
44 |
45 | /**
46 | * NOTE: As of 2015-11-09 react-transform does not support a functional
47 | * component as the base compoenent that's passed to ReactDOM.render, so we
48 | * still use createClass here.
49 | */
50 | export const App = React.createClass({
51 | propTypes: {
52 | children: PropTypes.any,
53 | },
54 | render() {
55 | return (
56 |
57 |
58 | Home
59 | About
60 |
61 | {this.props.children}
62 |
63 | );
64 | },
65 | });
66 |
--------------------------------------------------------------------------------
/example/with-auth0/src/AuthService.js:
--------------------------------------------------------------------------------
1 | import Auth0 from 'auth0-js';
2 | import decode from 'jwt-decode';
3 |
4 | /**
5 | * See: https://auth0.com/docs/quickstart/spa/react/03-session-handling
6 | */
7 |
8 | const getTokenExpirationDate = (token) => {
9 | const decoded = decode(token);
10 | if (!decoded.exp) return null;
11 | const date = new Date(0);
12 |
13 | date.setUTCSeconds(decoded.exp);
14 | return date;
15 | };
16 |
17 | const isTokenExpired = (token) => {
18 | const date = getTokenExpirationDate(token);
19 |
20 | if (date === null) return false;
21 |
22 | const offsetSeconds = 0;
23 |
24 | // $FlowIssue: date is clearly not null because of the above check
25 | return !(date.valueOf() > (new Date().valueOf() + (offsetSeconds * 1000)));
26 | };
27 |
28 | export default class AuthService {
29 | constructor(id, domain) {
30 | this.auth = new Auth0({
31 | clientID: id,
32 | domain,
33 | responseType: 'token',
34 | });
35 | }
36 |
37 | logIn(params, onError) {
38 | this.auth.login(params, onError);
39 | }
40 |
41 | signUp(params, onError) {
42 | this.auth.signup(params, onError);
43 | }
44 |
45 | parseHash(hash) {
46 | const parsed = this.auth.parseHash(hash);
47 |
48 | if (parsed && parsed.idToken) {
49 | this.setToken(parsed.idToken);
50 | }
51 | }
52 |
53 | // Checks if there is a saved token and it's still valid
54 | loggedIn() {
55 | const token = this.getToken();
56 | return !!token && !isTokenExpired(token);
57 | }
58 |
59 | // Saves user token to local storage
60 | setToken(idToken) {
61 | try {
62 | localStorage.setItem('id_token', idToken);
63 | } catch (err) {
64 | console.error('Could not set id_token');
65 | }
66 | }
67 |
68 | // Retrieves the user token from local storage
69 | getToken() {
70 | try {
71 | return localStorage.getItem('id_token');
72 | } catch (err) {
73 | console.error('Error accessing item id_token from localStorage');
74 | return undefined;
75 | }
76 | }
77 |
78 | // Clear user token and profile data from local storage
79 | logOut() {
80 | try {
81 | localStorage.removeItem('id_token');
82 | } catch (err) {
83 | console.log('Error removing item id_token from localStorage');
84 | }
85 | }
86 |
87 | getProfile(cb) {
88 | this.auth.getProfile(this.getToken(), cb);
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/example/with-auth0/src/Products.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React from 'react';
3 |
4 | export class Products extends React.Component {
5 | render() {
6 | return (
7 |
8 |
A list of all products
9 |
10 | {this.props.children}
11 |
12 |
13 | );
14 | }
15 | }
16 |
17 | export class Product extends React.Component {
18 | render() {
19 | return (
20 |
21 |
This is a specific product
22 |
23 | {this.props.children}
24 |
25 |
26 | );
27 | }
28 | }
29 |
30 | export class ProductColors extends React.Component {
31 | render() {
32 | return (
33 |
34 |
A list of product colors
35 |
36 | {this.props.children}
37 |
38 |
39 | );
40 | }
41 | }
42 |
43 | export class ProductColor extends React.Component {
44 | render() {
45 | return (
46 |
47 |
A single product color
48 |
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/example/with-auth0/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { Router, browserHistory } from 'react-router';
4 |
5 | // Import your routes so that you can pass them to the component
6 | import routes from './routes.js';
7 |
8 | render(
9 | ,
10 | document.getElementById('root')
11 | );
12 |
--------------------------------------------------------------------------------
/example/with-auth0/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, IndexRoute } from 'react-router';
3 |
4 | import { App, Home, About, NotFound } from './App.js';
5 | import {
6 | Products,
7 | Product,
8 | ProductColors,
9 | ProductColor,
10 | } from './Products.js';
11 | import AuthService from './AuthService.js';
12 |
13 | const auth = new AuthService('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'test.auth0.com');
14 |
15 | // onEnter callback to validate authentication in private routes
16 | const requireAuth = (nextState, replace) => {
17 | if (!auth.loggedIn()) {
18 | console.log('User not authorized. Redirecting to login...');
19 | replace({ pathname: '/login' });
20 | }
21 | };
22 |
23 | export const routes = (
24 |
25 |
26 |
27 |
28 |
29 |
30 | {/* All routes here require auth */}
31 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 |
48 | export default routes;
49 |
--------------------------------------------------------------------------------
/example/with-auth0/template.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const getRedirect = ({ reactStaticCompilation }) => {
4 | return reactStaticCompilation && reactStaticCompilation.redirectLocation;
5 | };
6 |
7 | const Html = (props) => (
8 |
9 |
10 |
11 |
12 |
13 | Super awesome package
14 |
15 |
16 |
17 | {getRedirect(props) && (
18 |
19 | {props.reactStaticCompilation.location} {' '}
20 | not found. Redirecting you to {getRedirect(props).pathname} ...
21 |
22 | )}
23 |
24 |
25 |
26 |
27 | );
28 |
29 | export default Html;
30 |
--------------------------------------------------------------------------------
/example/with-auth0/test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import path from 'path';
3 | import fs from 'fs';
4 |
5 | import options from './webpack.config.js';
6 | import { compileWebpack } from '../../utils.js';
7 |
8 | const readFile = (filepath) => fs.readFileSync(filepath, { encoding: 'utf8' });
9 |
10 | test('Supports custom JSX template files', async t => {
11 | await compileWebpack(options);
12 |
13 | const filepath = path.join(options.output.path, 'index.html');
14 | const contents = readFile(filepath);
15 |
16 | t.true(contents.includes('Super awesome package'));
17 | });
18 |
19 | test('Compiles all files as expected', async t => {
20 | const stats = await compileWebpack(options);
21 | const files = stats.toJson().assets.map(x => x.name);
22 |
23 | t.true(files.includes('products.html'));
24 | t.true(files.includes('products/first.html'));
25 | t.true(files.includes('products/second.html'));
26 | t.true(files.includes('products/third.html'));
27 | t.true(files.includes('products/third/colors.html'));
28 | t.true(files.includes('products/third/colors/green.html'));
29 | t.true(files.includes('products/third/colors/blue.html'));
30 | t.true(files.includes('index.html'));
31 | t.true(files.includes('about.html'));
32 | t.true(files.includes('404.html'));
33 | });
34 |
35 | test('Protected files were compiled with empty body', async t => {
36 | const stats = await compileWebpack(options);
37 |
38 | const publicContents = [
39 | '/products/first.html',
40 | '/products/second.html',
41 | ].map(x => readFile(options.output.path + x));
42 |
43 | publicContents.forEach(x => {
44 | t.false(x.includes('Redirecting you to /login ...'));
45 | t.true(x.includes('This is a specific product'));
46 | });
47 |
48 | const privateContents = [
49 | '/products/third.html',
50 | '/products/third/colors.html',
51 | '/products/third/colors/green.html',
52 | '/products/third/colors/blue.html',
53 | ].map(x => readFile(options.output.path + x));
54 |
55 | // Assert that these contain the specified redirect text and do NOT contain
56 | // text rendered within react. I.e. they did not have their body rendered.
57 | privateContents.forEach(x => {
58 | t.true(x.includes('Redirecting you to /login ...'));
59 | t.false(x.includes('A list of product colors'));
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/example/with-auth0/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | var path = require('path');
3 |
4 | var ReactStaticPlugin = require('../../dist');
5 |
6 | module.exports = {
7 | devtool: 'source-map',
8 |
9 | context: __dirname,
10 |
11 | entry: {
12 | app: './src/index.js',
13 | },
14 |
15 | output: {
16 | path: path.join(__dirname, 'public'),
17 | filename: '[name].js',
18 | libraryTarget: 'umd',
19 | publicPath: '/',
20 | },
21 |
22 | plugins: [
23 | new ReactStaticPlugin({
24 | routes: './src/routes.js',
25 | template: './template.js',
26 | }),
27 | ],
28 |
29 | module: {
30 | loaders: [
31 | {
32 | test: /\.js$/,
33 | loaders: ['babel'],
34 | exclude: path.join(__dirname, 'node_modules'),
35 | },
36 | ],
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/example/with-mock-auth/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "build": "webpack",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "babel-core": "^6.8.0",
14 | "babel-loader": "^6.2.4",
15 | "react": "^15.2.1",
16 | "react-dom": "^15.2.1",
17 | "react-router": "^3.0.0",
18 | "webpack": "^1.13.0"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/example/with-mock-auth/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { Link, IndexLink } from 'react-router';
3 |
4 | export const Home = React.createClass({
5 | render() {
6 | return (
7 |
8 |
9 |
React Static Boilerplate
10 |
11 |
Why React static?
12 |
13 | Dev friendly
14 | User friendly
15 | SEO friendly
16 |
17 |
18 | );
19 | },
20 | });
21 |
22 | export const About = React.createClass({
23 | render() {
24 | return (
25 |
26 |
27 |
About
28 |
29 |
Welcome, to about us.
30 |
31 | );
32 | },
33 | });
34 |
35 | export const NotFound = React.createClass({
36 | render() {
37 | return (
38 |
39 |
Not found
40 |
41 | );
42 | },
43 | });
44 |
45 | /**
46 | * NOTE: As of 2015-11-09 react-transform does not support a functional
47 | * component as the base compoenent that's passed to ReactDOM.render, so we
48 | * still use createClass here.
49 | */
50 | export const App = React.createClass({
51 | propTypes: {
52 | children: PropTypes.any,
53 | },
54 | render() {
55 | return (
56 |
57 |
58 | Home
59 | About
60 |
61 | {this.props.children}
62 |
63 | );
64 | },
65 | });
66 |
--------------------------------------------------------------------------------
/example/with-mock-auth/src/Products.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React from 'react';
3 |
4 | export class Products extends React.Component {
5 | render() {
6 | return (
7 |
8 |
A list of all products
9 |
10 | {this.props.children}
11 |
12 |
13 | );
14 | }
15 | }
16 |
17 | export class Product extends React.Component {
18 | render() {
19 | return (
20 |
21 |
This is a specific product
22 |
23 | {this.props.children}
24 |
25 |
26 | );
27 | }
28 | }
29 |
30 | export class ProductColors extends React.Component {
31 | render() {
32 | return (
33 |
34 |
A list of product colors
35 |
36 | {this.props.children}
37 |
38 |
39 | );
40 | }
41 | }
42 |
43 | export class ProductColor extends React.Component {
44 | render() {
45 | return (
46 |
47 |
A single product color
48 |
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/example/with-mock-auth/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { Router, browserHistory } from 'react-router';
4 |
5 | // Import your routes so that you can pass them to the component
6 | import routes from './routes.js';
7 |
8 | render(
9 | ,
10 | document.getElementById('root')
11 | );
12 |
--------------------------------------------------------------------------------
/example/with-mock-auth/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, IndexRoute } from 'react-router';
3 |
4 | import { App, Home, About, NotFound } from './App.js';
5 | import {
6 | Products,
7 | Product,
8 | ProductColors,
9 | ProductColor,
10 | } from './Products.js';
11 |
12 | const loggedIn = () => {
13 | /**
14 | * TODO: Implement your auth logic to determine if a user is logged in
15 | * already...
16 | *
17 | * You can also check out the Auth0 example for a more complete
18 | * implementation.
19 | */
20 | return false;
21 | };
22 |
23 | const requireAuth = (nextState, replace) => {
24 | if (!loggedIn()) {
25 | replace({ pathname: '/login' });
26 | }
27 | };
28 |
29 | export const routes = (
30 |
31 |
32 |
33 |
34 |
35 |
36 | {/* All routes here require auth */}
37 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | );
53 |
54 | export default routes;
55 |
--------------------------------------------------------------------------------
/example/with-mock-auth/template.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const getRedirect = ({ reactStaticCompilation }) => {
4 | return reactStaticCompilation && reactStaticCompilation.redirectLocation;
5 | };
6 |
7 | const Html = (props) => (
8 |
9 |
10 |
11 |
12 |
13 | Super awesome package
14 |
15 |
16 |
17 | {getRedirect(props) && (
18 |
19 | {props.reactStaticCompilation.location} {' '}
20 | not found. Redirecting you to {getRedirect(props).pathname} ...
21 |
22 | )}
23 |
24 |
25 |
26 |
27 | );
28 |
29 | export default Html;
30 |
--------------------------------------------------------------------------------
/example/with-mock-auth/test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import path from 'path';
3 | import fs from 'fs';
4 |
5 | import options from './webpack.config.js';
6 | import { compileWebpack } from '../../utils.js';
7 |
8 | const readFile = (filepath) => fs.readFileSync(filepath, { encoding: 'utf8' });
9 |
10 | test('Supports custom JSX template files', async t => {
11 | await compileWebpack(options);
12 |
13 | const filepath = path.join(options.output.path, 'index.html');
14 | const contents = readFile(filepath);
15 |
16 | t.true(contents.includes('Super awesome package'));
17 | });
18 |
19 | test('Compiles all files as expected', async t => {
20 | const stats = await compileWebpack(options);
21 | const files = stats.toJson().assets.map(x => x.name);
22 |
23 | t.true(files.includes('products.html'));
24 | t.true(files.includes('products/first.html'));
25 | t.true(files.includes('products/second.html'));
26 | t.true(files.includes('products/third.html'));
27 | t.true(files.includes('products/third/colors.html'));
28 | t.true(files.includes('products/third/colors/green.html'));
29 | t.true(files.includes('products/third/colors/blue.html'));
30 | t.true(files.includes('index.html'));
31 | t.true(files.includes('about.html'));
32 | t.true(files.includes('404.html'));
33 | });
34 |
35 | test('Protected files were compiled with empty body', async t => {
36 | const stats = await compileWebpack(options);
37 |
38 | const publicContents = [
39 | '/products/first.html',
40 | '/products/second.html',
41 | ].map(x => readFile(options.output.path + x));
42 |
43 | publicContents.forEach(x => {
44 | t.false(x.includes('Redirecting you to /login ...'));
45 | t.true(x.includes('This is a specific product'));
46 | });
47 |
48 | const privateContents = [
49 | '/products/third.html',
50 | '/products/third/colors.html',
51 | '/products/third/colors/green.html',
52 | '/products/third/colors/blue.html',
53 | ].map(x => readFile(options.output.path + x));
54 |
55 | // Assert that these contain the specified redirect text and do NOT contain
56 | // text rendered within react. I.e. they did not have their body rendered.
57 | privateContents.forEach(x => {
58 | t.true(x.includes('Redirecting you to /login ...'));
59 | t.false(x.includes('A list of product colors'));
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/example/with-mock-auth/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | var path = require('path');
3 |
4 | var ReactStaticPlugin = require('../../dist');
5 |
6 | module.exports = {
7 | devtool: 'source-map',
8 |
9 | context: __dirname,
10 |
11 | entry: {
12 | app: './src/index.js',
13 | },
14 |
15 | output: {
16 | path: path.join(__dirname, 'public'),
17 | filename: '[name].js',
18 | libraryTarget: 'umd',
19 | publicPath: '/',
20 | },
21 |
22 | plugins: [
23 | new ReactStaticPlugin({
24 | routes: './src/routes.js',
25 | template: './template.js',
26 | }),
27 | ],
28 |
29 | module: {
30 | loaders: [
31 | {
32 | test: /\.js$/,
33 | loaders: ['babel'],
34 | exclude: path.join(__dirname, 'node_modules'),
35 | },
36 | ],
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/example/with-static-markup/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "build": "webpack",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "babel-core": "^6.8.0",
14 | "babel-loader": "^6.2.4",
15 | "react": "^15.2.1",
16 | "react-dom": "^15.2.1",
17 | "react-router": "^3.0.0",
18 | "webpack": "^1.13.0"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/example/with-static-markup/src/components.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { Link, IndexLink } from 'react-router';
3 |
4 | export const Home = React.createClass({
5 | render() {
6 | return (
7 |
8 |
9 |
React Static Boilerplate
10 |
11 |
Why React static?
12 |
13 | Dev friendly
14 | User friendly
15 | SEO friendly
16 |
17 |
18 | );
19 | },
20 | });
21 |
22 | export const About = React.createClass({
23 | render() {
24 | return (
25 |
26 |
27 |
About
28 |
29 |
Welcome, to about us.
30 |
31 | );
32 | },
33 | });
34 |
35 | export const NotFound = React.createClass({
36 | render() {
37 | return (
38 |
39 |
Not found
40 |
41 | );
42 | },
43 | });
44 |
45 | /**
46 | * NOTE: As of 2015-11-09 react-transform does not support a functional
47 | * component as the base compoenent that's passed to ReactDOM.render, so we
48 | * still use createClass here.
49 | */
50 | export const App = React.createClass({
51 | propTypes: {
52 | children: PropTypes.any,
53 | },
54 | render() {
55 | return (
56 |
57 |
58 | Home
59 | About
60 |
61 | {this.props.children}
62 |
63 | );
64 | },
65 | });
66 |
--------------------------------------------------------------------------------
/example/with-static-markup/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { Router, browserHistory } from 'react-router';
4 |
5 | // Import your routes so that you can pass them to the component
6 | import routes from './routes.js';
7 |
8 | render(
9 | ,
10 | document.getElementById('root')
11 | );
12 |
--------------------------------------------------------------------------------
/example/with-static-markup/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, IndexRoute } from 'react-router';
3 |
4 | import { App, Home, About, NotFound } from './components.js';
5 |
6 | export const routes = (
7 |
8 |
9 |
10 |
11 |
12 | );
13 |
14 | export default routes;
15 |
--------------------------------------------------------------------------------
/example/with-static-markup/template.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Html = (props) => (
4 |
5 |
6 |
7 |
8 |
9 | Super awesome package
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 |
19 | export default Html;
20 |
--------------------------------------------------------------------------------
/example/with-static-markup/test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import path from 'path';
3 | import fs from 'fs';
4 |
5 | import options from './webpack.config.js';
6 | import { compileWebpack } from '../../utils.js';
7 |
8 | test('Supports rendering to static markup...', async t => {
9 | await compileWebpack(options);
10 |
11 | const outputFilepath = path.join(options.output.path, 'index.html');
12 | const outputFileContents = fs.readFileSync(outputFilepath, { encoding: 'utf8' });
13 |
14 | t.true(!outputFileContents.includes('data-reactid'));
15 | });
16 |
--------------------------------------------------------------------------------
/example/with-static-markup/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | var path = require('path');
3 |
4 | var ReactStaticPlugin = require('../../dist');
5 |
6 | module.exports = {
7 | devtool: 'source-map',
8 |
9 | context: __dirname,
10 |
11 | entry: {
12 | app: './src/index.js',
13 | },
14 |
15 | output: {
16 | path: path.join(__dirname, 'public'),
17 | filename: '[name].js',
18 | libraryTarget: 'umd',
19 | publicPath: '/',
20 | },
21 |
22 | plugins: [
23 | new ReactStaticPlugin({
24 | routes: './src/routes.js',
25 | template: './template.js',
26 | renderToStaticMarkup: true
27 | }),
28 | ],
29 |
30 | module: {
31 | loaders: [
32 | {
33 | test: /\.js$/,
34 | loaders: ['babel'],
35 | exclude: path.join(__dirname, 'node_modules'),
36 | },
37 | ],
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/example/with-template/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "build": "webpack",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "babel-core": "^6.8.0",
14 | "babel-loader": "^6.2.4",
15 | "react": "^15.2.1",
16 | "react-dom": "^15.2.1",
17 | "react-router": "^3.0.0",
18 | "webpack": "^1.13.0"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/example/with-template/src/components.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import { Link, IndexLink } from 'react-router';
3 |
4 | export const Home = React.createClass({
5 | render() {
6 | return (
7 |
8 |
9 |
React Static Boilerplate
10 |
11 |
Why React static?
12 |
13 | Dev friendly
14 | User friendly
15 | SEO friendly
16 |
17 |
18 | );
19 | },
20 | });
21 |
22 | export const About = React.createClass({
23 | render() {
24 | return (
25 |
26 |
27 |
About
28 |
29 |
Welcome, to about us.
30 |
31 | );
32 | },
33 | });
34 |
35 | export const NotFound = React.createClass({
36 | render() {
37 | return (
38 |
39 |
Not found
40 |
41 | );
42 | },
43 | });
44 |
45 | /**
46 | * NOTE: As of 2015-11-09 react-transform does not support a functional
47 | * component as the base compoenent that's passed to ReactDOM.render, so we
48 | * still use createClass here.
49 | */
50 | export const App = React.createClass({
51 | propTypes: {
52 | children: PropTypes.any,
53 | },
54 | render() {
55 | return (
56 |
57 |
58 | Home
59 | About
60 |
61 | {this.props.children}
62 |
63 | );
64 | },
65 | });
66 |
--------------------------------------------------------------------------------
/example/with-template/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { Router, browserHistory } from 'react-router';
4 |
5 | // Import your routes so that you can pass them to the component
6 | import routes from './routes.js';
7 |
8 | render(
9 | ,
10 | document.getElementById('root')
11 | );
12 |
--------------------------------------------------------------------------------
/example/with-template/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, IndexRoute } from 'react-router';
3 |
4 | import { App, Home, About, NotFound } from './components.js';
5 |
6 | export const routes = (
7 |
8 |
9 |
10 |
11 |
12 | );
13 |
14 | export default routes;
15 |
--------------------------------------------------------------------------------
/example/with-template/template.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Html = (props) => (
4 |
5 |
6 |
7 |
8 |
9 | Super awesome package
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 |
19 | export default Html;
20 |
--------------------------------------------------------------------------------
/example/with-template/test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import path from 'path';
3 | import fs from 'fs';
4 |
5 | import options from './webpack.config.js';
6 | import { compileWebpack } from '../../utils.js';
7 |
8 | test('Supports custom JSX template files', async t => {
9 | await compileWebpack(options);
10 |
11 | const outputFilepath = path.join(options.output.path, 'index.html');
12 | const outputFileContents = fs.readFileSync(outputFilepath, { encoding: 'utf8' });
13 |
14 | t.true(outputFileContents.includes('Super awesome package'));
15 | });
16 |
--------------------------------------------------------------------------------
/example/with-template/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | var path = require('path');
3 |
4 | var ReactStaticPlugin = require('../../dist');
5 |
6 | module.exports = {
7 | devtool: 'source-map',
8 |
9 | context: __dirname,
10 |
11 | entry: {
12 | app: './src/index.js',
13 | },
14 |
15 | output: {
16 | path: path.join(__dirname, 'public'),
17 | filename: '[name].js',
18 | libraryTarget: 'umd',
19 | publicPath: '/',
20 | },
21 |
22 | plugins: [
23 | new ReactStaticPlugin({
24 | routes: './src/routes.js',
25 | template: './template.js',
26 | }),
27 | ],
28 |
29 | module: {
30 | loaders: [
31 | {
32 | test: /\.js$/,
33 | loaders: ['babel'],
34 | exclude: path.join(__dirname, 'node_modules'),
35 | },
36 | ],
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/install_test_dependencies.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | echo "Installing test dependencies..."
3 | echo "0/10"
4 | (cd example/basic-with-router; yarn install &> /dev/null)
5 | echo "1/10"
6 | (cd example/css-modules; yarn install &> /dev/null)
7 | echo "2/10"
8 | (cd example/deep-route-nesting; yarn install &> /dev/null)
9 | echo "3/10"
10 | (cd example/extract-css; yarn install &> /dev/null)
11 | echo "4/10"
12 | (cd example/with-template; yarn install &> /dev/null)
13 | echo "5/10"
14 | (cd example/with-mock-auth; yarn install &> /dev/null)
15 | echo "6/10"
16 | (cd example/redux; yarn install &> /dev/null)
17 | echo "7/10"
18 | (cd example/with-static-markup; yarn install &> /dev/null)
19 | echo "8/10"
20 | (cd example/with-auth0; yarn install &> /dev/null)
21 | echo "9/10"
22 | (cd example/webpack-2; yarn install &> /dev/null)
23 | echo "10/10. Finished installing test all dependencies."
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-static-webpack-plugin",
3 | "version": "2.1.2",
4 | "description": "Build full static sites using React, React Router and Webpack",
5 | "license": "MIT",
6 | "repository": "iansinnott/react-static-webpack-plugin",
7 | "main": "dist/index.js",
8 | "author": {
9 | "name": "Ian Sinnott",
10 | "email": "ian@iansinnott.com",
11 | "url": "iansinnott.com"
12 | },
13 | "scripts": {
14 | "lint": "eslint src",
15 | "lint:example": "eslint example/**/src",
16 | "test": "npm run lint && npm run flowcheck && npm run build && npm run test:unit",
17 | "test:unit": "cross-env NODE_ENV=production ava --verbose test.js example/**/test.js",
18 | "flowcheck": "flow check",
19 | "watch": "babel -w -d dist src",
20 | "start": "npm run watch",
21 | "clean": "rimraf dist",
22 | "build": "mkdir -p dist && npm run clean && npm run build:compile",
23 | "build:compile": "babel -d dist src",
24 | "bump:patch": "npm version patch -m \"v%s\"",
25 | "bump:minor": "npm version minor -m \"v%s\"",
26 | "bump:major": "npm version major -m \"v%s\"",
27 | "bump": "npm run bump:patch",
28 | "preversion": "npm test",
29 | "postversion": "git push && git push --tags",
30 | "prepublish": "npm run build"
31 | },
32 | "ava": {
33 | "require": [
34 | "babel-register"
35 | ],
36 | "babel": "inherit"
37 | },
38 | "keywords": [
39 | "react",
40 | "react-router",
41 | "webpack",
42 | "static",
43 | "generator"
44 | ],
45 | "dependencies": {
46 | "bluebird": "^3.4.1",
47 | "debug": "^3.0.0",
48 | "jsdom": "^9.9.1",
49 | "lodash": "^4.11.1"
50 | },
51 | "devDependencies": {
52 | "ava": "^0.17.0",
53 | "babel": "^6.3.26",
54 | "babel-cli": "^6.3.17",
55 | "babel-core": "^6.18.2",
56 | "babel-eslint": "^8.0.0",
57 | "babel-preset-es2015": "^6.3.13",
58 | "babel-preset-react": "^6.3.13",
59 | "babel-preset-stage-1": "^6.5.0",
60 | "babel-register": "^6.8.0",
61 | "cross-env": "^4.0.0",
62 | "eslint": "^3.10.2",
63 | "eslint-config-zen": "^3.0.0",
64 | "eslint-plugin-flowtype": "^2.25.0",
65 | "eslint-plugin-react": "^7.0.0",
66 | "extract-text-webpack-plugin": "^1.0.1",
67 | "flow-bin": "^0.59.0",
68 | "history": "^4.4.0",
69 | "react": "^15.2.1",
70 | "react-dom": "^15.2.1",
71 | "react-redux": "^5.0.0",
72 | "react-router": "^3.0.0",
73 | "redux": "^3.5.2",
74 | "rimraf": "^2.5.0",
75 | "webpack": "^1.13.1"
76 | },
77 | "peerDependencies": {
78 | "extract-text-webpack-plugin": ">= 1.0.1 < 3",
79 | "history": ">= 4 < 5",
80 | "react": ">= 0.14.0",
81 | "react-dom": ">= 0.14.0",
82 | "react-router": ">= 2.4.0 < 4",
83 | "webpack": ">= 1.13.1 < 3"
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | export type OptionsShape = {
3 | routes: string,
4 | template?: string,
5 | reduxStore?: string,
6 | renderToStaticMarkup?: boolean,
7 | bundle?: string,
8 | stylesheet?: string,
9 | favicon?: string,
10 |
11 | src?: string, // Deprecated. Use routes instead
12 | };
13 |
14 | type RedirectLocation = {
15 | pathname: string,
16 | search: string,
17 | hash: string,
18 | query: Object,
19 | key: string,
20 | action: string, // Actually an emum, but i'm not sure what all the values are: 'REPLACE' | ...
21 | state: ?Object,
22 | };
23 |
24 | type RenderProps = {
25 | router: Object,
26 | location: Object,
27 | routes: Array,
28 | params: Object,
29 | components: Array,
30 | createElement: Function,
31 | };
32 |
33 | export type MatchShape = {
34 | error: Error,
35 | renderProps: ?RenderProps,
36 | redirectLocation: ?RedirectLocation,
37 | };
38 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @flow
3 | *
4 | * Webpack Plugin Resources:
5 | * - https://github.com/andreypopp/webpack-stylegen/blob/master/lib/webpack/index.js#L5
6 | * - https://github.com/webpack/extract-text-webpack-plugin/blob/v1.0.1/loader.js#L62
7 | * - https://github.com/kevlened/debug-webpack-plugin/blob/master/index.js
8 | * - https://github.com/ampedandwired/html-webpack-plugin/blob/v2.0.3/index.js
9 | */
10 | import React from 'react';
11 | import { renderToString, renderToStaticMarkup } from 'react-dom/server';
12 | import { match, RouterContext } from 'react-router';
13 | import Promise from 'bluebird';
14 | import isFunction from 'lodash/isFunction';
15 |
16 | import {
17 | getAllPaths,
18 | getExtraneousAssets,
19 | compileAsset,
20 | renderSingleComponent,
21 | getAssetKey,
22 | prefix,
23 | addHash,
24 | log,
25 | } from './utils.js';
26 | import type {
27 | OptionsShape,
28 | MatchShape,
29 | } from './constants.js';
30 |
31 | const debug = require('debug')('react-static-webpack-plugin:index');
32 |
33 | const renderToStaticDocument = (Component, props) => {
34 | return '' + renderToStaticMarkup( );
35 | };
36 |
37 | const validateOptions = (options) => {
38 | if (!options) {
39 | throw new Error('No options provided');
40 | }
41 | if (!options.routes && !options.component) {
42 | throw new Error('No component or routes param provided');
43 | }
44 | if (!options.template) {
45 | throw new Error('No template param provided');
46 | }
47 | if (options.renderToStaticMarkup && typeof(options.renderToStaticMarkup) !== 'boolean') {
48 | throw new Error('Optional param renderToStaticMarkup must have a value of either true or false');
49 | }
50 | };
51 |
52 | function StaticSitePlugin(options: OptionsShape) {
53 | validateOptions(options);
54 | this.options = options;
55 | }
56 |
57 | /**
58 | * The same as the RR match function, but promisified.
59 | */
60 | const promiseMatch = (args) => new Promise((resolve, reject) => {
61 | match(args, (error, redirectLocation, renderProps) => {
62 | resolve({ error, redirectLocation, renderProps });
63 | }, reject);
64 | });
65 |
66 | function handleEmitAssets(assets, compilation) {
67 | debug('HANDLE: First call to handleEmitAssets');
68 | if (assets instanceof Error) {
69 | debug('HANDLE: Oh no, error here');
70 | throw assets;
71 | }
72 |
73 | if (!assets) {
74 | debug('HANDLE: Assets failed!');
75 | throw new Error(
76 | 'Compilation completed with undefined assets. This likely means\n' +
77 | 'react-static-webpack-plugin had trouble compiling one of the entry\n' +
78 | 'points specified in options. To get more detail, try running again\n' +
79 | 'but prefix your build command with:\n\n' +
80 | ' DEBUG=react-static-webpack-plugin*\n\n' +
81 | 'That will enable debug logging and output more detailed information.'
82 | );
83 | }
84 |
85 | // Remove all the now extraneous compiled assets and any sourceamps that
86 | // may have been generated for them
87 | getExtraneousAssets().forEach(key => {
88 | debug(`HANDLE: Removing extraneous asset and associated sourcemap. Asset name: "${key}"`);
89 | delete compilation.assets[key];
90 | delete compilation.assets[key + '.map'];
91 | });
92 |
93 | let [routes, template, store] = assets;
94 |
95 | if (!routes) {
96 | debug('HANDLE: No routes found');
97 | throw new Error(`Entry file compiled with empty source: ${this.options.routes || this.options.component}`);
98 | }
99 |
100 | routes = routes.routes || routes.default || routes;
101 |
102 | if (template) {
103 | template = template.default || template;
104 | }
105 |
106 | if (store) {
107 | store = store.store || store.default || store;
108 | }
109 |
110 | if (this.options.template && !isFunction(template)) {
111 | debug('HANDLE: Template function is not a function');
112 | throw new Error(`Template file did not compile with renderable default export: ${this.options.template}`);
113 | }
114 |
115 | // Set up the render function that will be used later on
116 | this.render = (props) => renderToStaticDocument(template, props);
117 |
118 | let manifest = Object.keys(compilation.assets).reduce((agg, k) => {
119 | agg[k] = k;
120 | return agg;
121 | }, {});
122 |
123 | const manifestKey = this.options.manifest || 'manifest.json'; // TODO: Is it wise to default this? Maybe it should be explicit?
124 | try {
125 | const manifestAsset = compilation.assets[manifestKey];
126 | if (manifestAsset) {
127 | manifest = JSON.parse(manifestAsset.source());
128 | } else {
129 | debug('HANDLE: No manifest file found so default manifest will be provided');
130 | }
131 | } catch (err) {
132 | debug('HANDLE: Error parsing manifest file:', err);
133 | }
134 |
135 | debug('HANDLE: manifest', manifest);
136 |
137 | // Support rendering a single component without the need for react router.
138 | if (!this.options.routes && this.options.component) {
139 | debug('HANDLE: Entrypoint specified with `component` option. Rendering individual component.');
140 | const options = {
141 | ...addHash(this.options, compilation.hash),
142 | manifest,
143 | };
144 | compilation.assets['index.html'] = renderSingleComponent(routes, options, this.render, store);
145 | return Promise.resolve();
146 | }
147 |
148 | const paths = getAllPaths(routes);
149 | debug('HANDLE: Parsed routes:', paths);
150 |
151 | // Make sure the user has installed redux dependencies if they passed in a
152 | // store
153 | let Provider;
154 | if (store) {
155 | try {
156 | Provider = require('react-redux').Provider;
157 | } catch (err) {
158 | debug('HANDLE: Oh no, error here', err);
159 | err.message = `Looks like you provided the 'reduxStore' option but there was an error importing these dependencies. Did you forget to install 'redux' and 'react-redux'?\n${err.message}`;
160 | throw err;
161 | }
162 | }
163 |
164 | const promises = paths.map(location => {
165 | debug(`HANDLE: Mapping location "${location}"`);
166 | return promiseMatch({ routes, location })
167 | .then(({ error, redirectLocation, renderProps }: MatchShape): void => {
168 | let { options } = this;
169 | const logPrefix = 'react-static-webpack-plugin:';
170 | const emptyBodyWarning = 'Route will be rendered with an empty body.';
171 | let component;
172 |
173 | if (redirectLocation) {
174 | debug(`HANDLE: Redirect encountered. Ignoring route: "${location}"`, redirectLocation);
175 | log(`${logPrefix} Redirect encountered: ${location} -> ${redirectLocation.pathname}. ${emptyBodyWarning}`);
176 | } else if (error) {
177 | debug('HANDLE: Error encountered matching route', location, error, redirectLocation, renderProps);
178 | log(`${logPrefix} Error encountered rendering route "${location}". ${emptyBodyWarning}`);
179 | } else if (!renderProps) {
180 | debug('HANDLE: No renderProps found matching route', location, error, redirectLocation, renderProps);
181 | log(`${logPrefix} No renderProps found matching route "${location}". ${emptyBodyWarning}`);
182 | } else if (store) {
183 | debug(`HANDLE: Redux store provided. Rendering "${location}" within Provider.`);
184 | component = (
185 |
186 |
187 |
188 | );
189 |
190 | // Make sure initialState will be provided to the template
191 | options = { ...options, initialState: store.getState() };
192 | } else {
193 | debug('HANDLE: Creating routing context. Default case');
194 | component = ; // Successful render
195 | }
196 |
197 | let title = '';
198 |
199 | if (renderProps) {
200 | const route = renderProps.routes[renderProps.routes.length - 1]; // See NOTE
201 | title = route.title;
202 | }
203 |
204 | const reactStaticCompilation = {
205 | error,
206 | redirectLocation,
207 | renderProps,
208 | location,
209 | options, // NOTE: Options is duplciated as a root level prop below, but removing that would mean a major version bump
210 | };
211 |
212 | const renderMethod = this.options.renderToStaticMarkup === true
213 | ? renderToStaticMarkup
214 | : renderToString;
215 |
216 | // NOTE: Rendering a component can cause issues if thought isn't given
217 | // to server side rendering. We are doing SSR here, plain and simple.
218 | // Considering that, it's important to remember what lifecycle hooks
219 | // will and won't be called in the rendered component. Namely,
220 | // componentWillMount is the only one that will be called. And also the
221 | // constructor if you consider it a lifecylce hook. componentWillUnmount
222 | // is NEVER CALLED, so if you did anything that needs to be cleaned up
223 | // it never will be during this render call. For example, don't set up
224 | // an interval within constructor or componentWillMount, because it will
225 | // likely hold on to some memory and never let it go. This can cause
226 | // webpack to hang after emit. There's no error, it just never fully
227 | // completes.
228 | let body;
229 | try {
230 | if (component) {
231 | debug('HANDLE: Trying to render component', component, renderMethod.name);
232 | body = renderMethod(component); // See NOTE
233 | } else {
234 | debug('HANDLE: No coponent provided. Moving on');
235 | }
236 | } catch (err) {
237 | debug('HANDLE: Error rendering component', err);
238 | } finally {
239 | debug('HANDLE: Finally setting body to empty string if it wasn\'t already set');
240 | body = body || '';
241 | }
242 |
243 | debug(`HANDLE: Rendering "${location}" with body = ${body}`);
244 |
245 | const assetKey = getAssetKey(location);
246 | const doc = this.render({
247 | ...addHash(options, compilation.hash),
248 | title,
249 | body,
250 | reactStaticCompilation,
251 | manifest,
252 | });
253 |
254 | debug(`HANDLE: Finishing up, adding asset key to compilation.assets["${assetKey}"]`);
255 | compilation.assets[assetKey] = {
256 | source() { return doc; },
257 | size() { return doc.length; },
258 | };
259 | });
260 | });
261 |
262 | return Promise.all(promises);
263 | }
264 |
265 | /**
266 | * `compiler` is an instance of the Compiler
267 | * https://github.com/webpack/webpack/blob/master/lib/Compiler.js#L143
268 | *
269 | * NOTE: renderProps.routes is always passed as an array of route elements. For
270 | * deeply nested routes the last element in the array is the route we want to
271 | * get the props of, so we grab it out of the array and use it. This lets us do
272 | * things like:
273 | *
274 | *
275 | *
276 | * Then set the document title to the title defined on that route
277 | *
278 | * NOTE: Sometimes when matching routes we do not get an error but nore do we
279 | * get renderProps. In my experience this usually means we hit an IndexRedirect
280 | * or some form of Route that doesn't actually have a component to render. In
281 | * these cases we simply keep on moving and don't render anything.
282 | *
283 | * TODO:
284 | * - Allow passing a function for title?
285 | *
286 | */
287 | StaticSitePlugin.prototype.apply = function(compiler) {
288 | let compilationPromise;
289 |
290 | /**
291 | * Compile everything that needs to be compiled. This is what the 'make'
292 | * plugin is excellent for.
293 | */
294 | compiler.plugin('make', (compilation, cb) => {
295 | const { component, routes, template, reduxStore } = this.options;
296 |
297 | // Promise loggers. These are simply for debugging
298 | const promiseLog = (str) => (x) => {
299 | debug(`COMPILATION LOG: --${str}--`, x);
300 | return x;
301 | };
302 |
303 | const promiseErr = (str) => (x) => {
304 | debug(`COMPILATION ERR: --${str}--`, x);
305 | return Promise.reject(x);
306 | };
307 |
308 | // Compile routes and template
309 | const promises = [
310 | compileAsset({
311 | filepath: routes || component,
312 | outputFilename: prefix('routes.js'),
313 | compilation,
314 | context: compiler.context,
315 | }).then(promiseLog('routes')).catch(promiseErr('routes')),
316 | compileAsset({
317 | filepath: template,
318 | outputFilename: prefix('template.js'),
319 | compilation,
320 | context: compiler.context,
321 | }).then(promiseLog('template')).catch(promiseErr('template')),
322 | ];
323 |
324 | if (reduxStore) {
325 | promises.push(
326 | compileAsset({
327 | filepath: reduxStore,
328 | outputFilename: prefix('store.js'),
329 | compilation,
330 | context: compiler.context,
331 | }).then(promiseLog('reduxStore')).catch(promiseErr('reduxStore'))
332 | );
333 | }
334 |
335 | compilationPromise = Promise.all(promises)
336 | .catch(err => Promise.reject(new Error(err)))
337 | .finally(cb);
338 | });
339 |
340 | /**
341 | * NOTE: It turns out that vm.runInThisContext works fine while evaluate
342 | * failes. It seems evaluate the routes file in this example as empty, which
343 | * it should not be... Not sure if switching to vm from evaluate will cause
344 | * breakage so i'm leaving it in here with this note for now.
345 | *
346 | * compiler seems to be an instance of the Compiler
347 | * https://github.com/webpack/webpack/blob/master/lib/Compiler.js#L143
348 | *
349 | * NOTE: renderProps.routes is always passed as an array of route elements. For
350 | * deeply nested routes the last element in the array is the route we want to
351 | * get the props of, so we grab it out of the array and use it. This lets us do
352 | * things like:
353 | *
354 | *
355 | *
356 | * Then set the document title to the title defined on that route
357 | *
358 | * NOTE: Sometimes when matching routes we do not get an error but nore do we
359 | * get renderProps. In my experience this usually means we hit an IndexRedirect
360 | * or some form of Route that doesn't actually have a component to render. In
361 | * these cases we simply keep on moving and don't render anything.
362 | *
363 | * TODO:
364 | * - Allow passing a function for title
365 | */
366 | compiler.plugin('emit', (compilation, cb) => {
367 | compilationPromise
368 | .catch(err => {
369 | debug('EMIT: Oh no, error here', err);
370 | cb(err);
371 | }) // TODO: Eval failed, likely a syntax error in build
372 | .then(assets =>
373 | handleEmitAssets.call(this, assets, compilation))
374 | .then(() => {
375 | debug('EMIT: All green. Assets emit just fine');
376 | cb();
377 | })
378 | .catch(err => {
379 | debug('EMIT: Another error here', err);
380 | cb(err);
381 | });
382 | });
383 | };
384 |
385 | module.exports = StaticSitePlugin;
386 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | import path from 'path';
3 | import fs from 'fs';
4 | import vm from 'vm';
5 | import flattenDeep from 'lodash/flattenDeep';
6 | import isString from 'lodash/isString';
7 | import React from 'react';
8 | import { renderToString } from 'react-dom/server';
9 | import Promise from 'bluebird';
10 | import SingleEntryPlugin from 'webpack/lib/SingleEntryPlugin';
11 | import { jsdom, evalVMScript } from 'jsdom';
12 |
13 | import type { OptionsShape } from './constants.js';
14 |
15 | const debug = require('debug')('react-static-webpack-plugin:utils');
16 |
17 | let extraneousAssets: string[] = [];
18 |
19 | // Allow other files to get access to the extraneous assets
20 | export const getExtraneousAssets = () => extraneousAssets;
21 |
22 | /**
23 | * Adde a namespace/prefix to a filename so as to avoid naming conflicts with
24 | * things the user has created.
25 | */
26 | export const prefix = (name: string): string => {
27 | return `__react-static-webpack-plugin__${name}`;
28 | };
29 |
30 | type CompileAssetOptionsShape = {
31 | filepath: string,
32 | outputFilename: string,
33 | compilation: Object,
34 | context: string,
35 | };
36 |
37 | /**
38 | * Get all candidate absolute paths for the extract text plugin. If there are
39 | * none that's fine. We use these paths to patch extract-text-wepback-plugin
40 | * during compilation so that it doesn't throw a fit about loader-plugin
41 | * interop.
42 | */
43 | const getExtractTextPluginPaths = (compilation) => {
44 | const { options, context } = compilation.compiler;
45 | let paths = new Set(); // Ensure unique paths
46 |
47 | if (context) {
48 | paths.add(path.resolve(context, './node_modules/extract-text-webpack-plugin'));
49 | }
50 |
51 | if (options && options.context) {
52 | paths.add(path.resolve(options.context, './node_modules/extract-text-webpack-plugin'));
53 | }
54 |
55 | try {
56 | if (options.resolve.modules && options.resolve.modules.length) {
57 | options.resolve.modules.forEach(x => {
58 | paths.add(path.resolve(x, './extract-text-webpack-plugin'));
59 | });
60 | }
61 | } catch (err) { debug('Error resolving options.resolve.modules'); }
62 |
63 | try {
64 | if (options.resolveLoader.modules && options.resolveLoader.modules.length) {
65 | options.resolveLoader.modules.forEach(x => {
66 | paths.add(path.resolve(x, './extract-text-webpack-plugin'));
67 | });
68 | }
69 | } catch (err) { debug('Error resolving options.resolveLoader.modules'); }
70 |
71 | return Array.from(paths).filter(fs.existsSync);
72 | };
73 |
74 | /**
75 | * Given the filepath of an asset (say js file) compile it and return the source
76 | */
77 | type CompileAsset = (a: CompileAssetOptionsShape) => Promise;
78 | export const compileAsset: CompileAsset = (opts) => {
79 | const { filepath, outputFilename, compilation, context } = opts;
80 | const compilerName = `react-static-webpack compiling "${filepath}"`;
81 | const outputOptions = {
82 | filename: outputFilename,
83 | publicPath: compilation.outputOptions.publicPath,
84 | };
85 | let rawAssets = {};
86 |
87 | debug(`Compilation context "${context}"`);
88 | debug(`Compiling "${path.resolve(context, filepath)}"`);
89 |
90 | const childCompiler = compilation.createChildCompiler(compilerName, outputOptions);
91 | childCompiler.apply(new SingleEntryPlugin(context, filepath));
92 |
93 | // Patch extract text plugin
94 | childCompiler.plugin('this-compilation', (compilation) => {
95 | const extractTextPluginPaths = getExtractTextPluginPaths(compilation);
96 | debug('this-compilation patching extractTextPluginPaths %O', extractTextPluginPaths);
97 |
98 | // NOTE: This is taken directly from extract-text-webpack-plugin
99 | // https://github.com/webpack/extract-text-webpack-plugin/blob/v1.0.1/loader.js#L62
100 | //
101 | // It seems that returning true allows the use of css modules while
102 | // setting this equal to false makes the import of css modules fail, which
103 | // means rendered pages do not have the correct classnames.
104 | // loaderContext[x] = false;
105 | compilation.plugin('normal-module-loader', (loaderContext) => {
106 | extractTextPluginPaths.forEach(x => {
107 | loaderContext[x] = (content, opt) => { // See NOTE
108 | return true;
109 | };
110 | });
111 | });
112 |
113 | /**
114 | * In order to evaluate the raw compiled source files of assets instead of
115 | * the minified or otherwise optimized version we hook into here and hold on
116 | * to any chunk assets before they are compiled.
117 | *
118 | * NOTE: It is uncertain so far whether this source is actually the same as
119 | * the unoptimized source of the file in question. I.e. it may not be the
120 | * fullly compiled / bundled module code we want since this is not the emit
121 | * callback.
122 | */
123 | compilation.plugin('optimize-chunk-assets', (chunks, cb) => {
124 | const files: string[] = [];
125 |
126 | // Collect all asset names
127 | chunks.forEach((chunk) => {
128 | chunk.files.forEach((file) => files.push(file));
129 | });
130 | compilation.additionalChunkAssets.forEach((file) => files.push(file));
131 |
132 | rawAssets = files.reduce((agg, file) => {
133 | agg[file] = compilation.assets[file];
134 | return agg;
135 | }, {});
136 |
137 | // Update the extraneous assets to remove
138 | // TODO: This does not actually collect all the apropriate assets. What we
139 | // want is EVERY file that was compiled during this compilation, since we
140 | // don't want to output any of them. So far this only gets the associated
141 | // js files, like routes.js (with prefix)
142 | extraneousAssets = [ ...extraneousAssets, ...files ];
143 |
144 | cb();
145 | });
146 | });
147 |
148 | // Run the compilation async and return a promise
149 | // NOTE: For some reason, require simply doesn't work as expected in the
150 | // evaluated string src code. This was meant to be a temporary fix to fix the
151 | // issue of requiring node-uuid. It would be better to find a way to fully
152 | // support any module code. Specifically, code like this failse because the
153 | // require function simply does not return the built in crypto module:
154 | // https://github.com/crypto-browserify/crypto-browserify/blob/v3.2.6/rng.js
155 | return new Promise((resolve, reject) => {
156 | childCompiler.runAsChild(function(err, entries, childCompilation) {
157 | if (err) {
158 | debug('ERROR in compilation: ', err);
159 | reject(err);
160 | }
161 |
162 | // Resolve / reject the promise
163 | if (childCompilation.errors && childCompilation.errors.length) {
164 | const errorDetails = childCompilation.errors.map((err) => {
165 | return err.message + (err.error ? ':\n' + err.error : '');
166 | }).join('\n');
167 |
168 | reject(new Error('Child compilation failed:\n' + errorDetails));
169 | } else {
170 | let asset = compilation.assets[outputFilename];
171 |
172 | // See 'optimize-chunk-assets' above
173 | if (rawAssets[outputFilename]) {
174 | debug(`Using raw source for ${filepath}`);
175 | asset = rawAssets[outputFilename];
176 | }
177 |
178 | resolve(asset);
179 | }
180 | });
181 | })
182 | .then((asset) => {
183 | if (asset instanceof Error) {
184 | debug(`${filepath} failed to copmile. Rejecting...`);
185 | return Promise.reject(asset);
186 | }
187 |
188 | debug(`${filepath} compiled. Processing source...`);
189 |
190 | // Instantiate browser sandbox
191 | const doc = jsdom('');
192 | const win = doc.defaultView;
193 |
194 | // Pre-compile asset source
195 | const script = new vm.Script(asset.source(), {
196 | filename: filepath,
197 | displayErrors: true
198 | });
199 |
200 | // Run it in the JSDOM context
201 | return evalVMScript(win, script);
202 | })
203 | .catch((err) => {
204 | debug(`${filepath} failed to process. Rejecting...`);
205 | return Promise.reject(err);
206 | });
207 | };
208 |
209 | // can be an React Element or a POJO
210 | type RouteShape = {
211 | component?: any,
212 | props?: Object,
213 | childRoutes?: Object[],
214 | path?: string,
215 | };
216 |
217 | /**
218 | * NOTE: We could likely use createRoutes to our advantage here. It may simplify
219 | * the code we currently use to recurse over the virtual dom tree:
220 | *
221 | * import { createRoutes } from 'react-router';
222 | * console.log(createRoutes(routes)); =>
223 | * [ { path: '/',
224 | * component: [Function: Layout],
225 | * childRoutes: [ [Object], [Object] ] } ]
226 | *
227 | * Ex:
228 | * const routes = (
229 | *
230 | *
231 | *
232 | * );
233 | *
234 | * getAllPaths(routes); => ['/', '/about]
235 | */
236 | type GetNestedPaths = (a?: RouteShape | RouteShape[], b?: string) => any[];
237 | export const getNestedPaths: GetNestedPaths = (route, prefix = '') => {
238 | if (!route) return [];
239 |
240 | if (Array.isArray(route)) return route.map(x => getNestedPaths(x, prefix));
241 |
242 | let path = route.props && route.props.path || route.path;
243 | // Some routes such as redirects or index routes do not have a path. Skip
244 | // them.
245 | if (!path) return [];
246 |
247 | path = prefix + path;
248 | const nextPrefix = path === '/' ? path : path + '/';
249 | const childRoutes = route.props && route.props.children || route.childRoutes;
250 | return [path, ...getNestedPaths(childRoutes, nextPrefix)];
251 | };
252 |
253 | export const getAllPaths = (routes: RouteShape | RouteShape[]): string[] => {
254 | return flattenDeep(getNestedPaths(routes));
255 | };
256 |
257 | /**
258 | * Given a string location (i.e. path) return a relevant HTML filename.
259 | * Ex: '/' -> 'index.html'
260 | * Ex: '/about' -> 'about.html'
261 | * Ex: '/about/' -> 'about/index.html'
262 | * Ex: '/about/team' -> 'about/team.html'
263 | *
264 | * NOTE: Don't want leading slash
265 | * i.e. 'path/to/file.html' instead of '/path/to/file.html'
266 | *
267 | * NOTE: There is a lone case where the users specifices a not found route that
268 | * results in a '/*' location. In this case we output 404.html, since it's
269 | * assumed that this is a 404 route. See the RR changelong for details:
270 | * https://github.com/rackt/react-router/blob/1.0.x/CHANGES.md#notfound-route
271 | */
272 | export const getAssetKey = (location: string): string => {
273 | const basename = path.basename(location);
274 | const dirname = path.dirname(location).slice(1); // See NOTE above
275 | let filename;
276 |
277 | if (location.slice(-1) === '/') {
278 | filename = !basename ? 'index.html' : basename + '/index.html';
279 | } else if (basename === '*') {
280 | filename = '404.html';
281 | } else {
282 | filename = basename + '.html';
283 | }
284 |
285 | const result = dirname ? (dirname + path.sep + filename) : filename;
286 |
287 | debug(`Getting asset key: "${location}" -> "${result}"`);
288 |
289 | return result;
290 | };
291 |
292 | /**
293 | * Test if a React Element is a React Router Route or not. Note that this tests
294 | * the actual object (i.e. React Element), not a constructor. As such we
295 | * immediately deconstruct out the type property as that is what we want to
296 | * test.
297 | *
298 | * NOTE: Testing whether Component.type was an instanceof Route did not work.
299 | *
300 | * NOTE: This is a fragile test. The React Router API is notorious for
301 | * introducing breaking changes, so of the RR team changed the manditory path
302 | * and component props this would fail.
303 | */
304 | type IsRoute = (a: { type: Object }) => boolean;
305 | export const isRoute: IsRoute = ({ type: component }) => {
306 | return component && component.propTypes.path && component.propTypes.component;
307 | };
308 |
309 | type Asset = {
310 | source(): string,
311 | size(): number,
312 | };
313 |
314 | type Renderer = (a: OptionsShape) => string;
315 |
316 | /**
317 | * If not provided with any React Router Routes we try to render whatever asset
318 | * was passed to the plugin as a React component. The use case would be anyone
319 | * who doesn't need/want to add RR as a dependency to their app and instead only
320 | * wants a single HTML file output.
321 | *
322 | * NOTE: In the case of a single component we use the static prop 'title' to get
323 | * the page title. If this is not provided then the title will default to
324 | * whatever is provided in the template.
325 | */
326 | type RenderSingleComponent = (a: Object, b: OptionsShape, c: Renderer, d: ?Object) => Asset;
327 | export const renderSingleComponent: RenderSingleComponent = (imported, options, render, store) => {
328 | const Component = imported.default || imported;
329 | let body;
330 |
331 | let component = ;
332 |
333 | // Wrap the component in a Provider if the user passed us a redux store
334 | if (store) {
335 | debug('Store provider. Rendering single component within Provider.');
336 | try {
337 | const { Provider } = require('react-redux');
338 | component = (
339 |
340 |
341 |
342 | );
343 |
344 | // Make sure initialState will be provided to the template. Don't mutate
345 | // options directly
346 | options = { ...options, initialState: store.getState() }; // eslint-disable-line no-param-reassign
347 | } catch (err) {
348 | err.message = `Could not require react-redux. Did you forget to install it?\n${err.message}`;
349 | throw err;
350 | }
351 | }
352 |
353 | try {
354 | debug('Rendering single component.');
355 | body = renderToString(component);
356 | } catch (err) {
357 | throw new Error(`Invalid single component. Make sure you added your component as the default export from ${options.routes}`);
358 | }
359 |
360 | const doc = render({
361 | ...options,
362 | title: Component.title, // See NOTE
363 | body,
364 | });
365 |
366 | return {
367 | source() { return doc; },
368 | size() { return doc.length; },
369 | };
370 | };
371 |
372 | /**
373 | * Only log in prod and dev. I.e. do not log on CI. The reason for logging
374 | * during prod is that we usually only build our bundles with
375 | * NODE_ENV=production prefixing the build command.
376 | */
377 | export const log = (...args: Array): void => {
378 | if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'production') {
379 | console.log(...args);
380 | }
381 | };
382 |
383 | /**
384 | * Add hash to all options that includes '[hash]' ex: bundle.[hash].js
385 | * NOTE: Only one hash for all files. So even if the css did not change it will get a new hash if the js changed.
386 | *
387 | * @param {Options} options
388 | * @param {string} hash
389 | */
390 |
391 | type AddHash = (a: Object, b: string) => Object;
392 | export const addHash: AddHash = (options, hash) => {
393 | return Object.keys(options).reduce((previous, current) => {
394 | if (!isString(options[current])) {
395 | previous[current] = options[current];
396 | return previous;
397 | }
398 |
399 | previous[current] = (options[current] && hash ? options[current].replace('[hash]', hash) : options[current]);
400 | return previous;
401 | }, {});
402 | };
403 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import test from 'ava';
3 | import React from 'react';
4 | import { Route, Link, IndexRoute, Redirect, IndexRedirect } from 'react-router';
5 |
6 | /**
7 | * NOTE: Ava does not pass imported source through babel so trying to import
8 | * ./src/** will thrown an error as Node 4.x does not support imports
9 | */
10 | import { getAllPaths, getAssetKey } from './src/utils.js';
11 |
12 | // This seems like potentiall a viable solution. Simply pass the nescessary data
13 | // in a `data` prop directly to the component. The type should likely be an
14 | // array, and it could be read from disk, for instance a list of markdown file
15 | // contents that you would read from disk
16 | // const posts = [
17 | // { title: 'Hey there', body: 'Some really short text' },
18 | // { title: 'Second post', body: 'I like to write blogs and stuff' },
19 | // ];
20 | //
21 | //
22 | //
23 | //
24 |
25 | const Layout = props => (
26 |
27 |
I'm the layout
28 |
29 |
30 | About
31 |
32 |
33 | {props.children}
34 |
35 | );
36 |
37 | const AppIndex = props => (
38 |
39 |
I'm the AppIndex
40 | No children here
41 |
42 | );
43 |
44 | const About = props => (
45 | About page bro
46 | );
47 |
48 | const Products = props => (
49 | Product are here
50 | );
51 |
52 | const Product = props => (
53 |
54 |
Product are here
55 |
This is the product description
56 |
57 | );
58 |
59 | const NotFound = props => (
60 | Nothing to see here
61 | );
62 |
63 | const Contact = props => (
64 |
71 | );
72 |
73 | test('getAssetKey', t => {
74 | t.is(getAssetKey('/'), 'index.html');
75 | t.is(getAssetKey('/about'), 'about.html');
76 | t.is(getAssetKey('/about/team'), 'about/team.html');
77 | t.is(getAssetKey('/about/team/bios/'), 'about/team/bios/index.html');
78 | });
79 |
80 | test('Ignores index routes when generating paths', t => {
81 | const routes = (
82 |
83 |
84 |
85 |
86 |
87 |
88 | );
89 |
90 | const paths = getAllPaths(routes);
91 |
92 | t.deepEqual(paths, [
93 | '/',
94 | '/about',
95 | '/products',
96 | '/contact',
97 | ]);
98 |
99 | t.deepEqual(paths.map(getAssetKey), [
100 | 'index.html',
101 | 'about.html',
102 | 'products.html',
103 | 'contact.html',
104 | ]);
105 | });
106 |
107 | test('Can get paths from plain routes', t => {
108 | const routes = {
109 | path: '/',
110 | component: Layout,
111 | childRoutes: [{
112 | path: 'about',
113 | component: About,
114 | }, {
115 | path: 'products',
116 | component: Products,
117 | childRoutes: [{
118 | path: 'zephyr',
119 | component: Product
120 | }, {
121 | path: 'sparkles',
122 | component: Product
123 | }, {
124 | path: 'jarvis',
125 | component: Product
126 | }]
127 | }, {
128 | path: 'contact',
129 | component: Contact
130 | }]
131 | };
132 |
133 | const paths = getAllPaths(routes);
134 |
135 | t.deepEqual(paths, [
136 | '/',
137 | '/about',
138 | '/products',
139 | '/products/zephyr',
140 | '/products/sparkles',
141 | '/products/jarvis',
142 | '/contact',
143 | ]);
144 |
145 | t.deepEqual(paths.map(getAssetKey), [
146 | 'index.html',
147 | 'about.html',
148 | 'products.html',
149 | 'products/zephyr.html',
150 | 'products/sparkles.html',
151 | 'products/jarvis.html',
152 | 'contact.html',
153 | ]);
154 | });
155 |
156 | test('Can get paths from nested routes', t => {
157 | const routes = (
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 | );
168 |
169 | const paths = getAllPaths(routes);
170 |
171 | t.deepEqual(paths, [
172 | '/',
173 | '/about',
174 | '/products',
175 | '/products/zephyr',
176 | '/products/sparkles',
177 | '/products/jarvis',
178 | '/contact',
179 | ]);
180 |
181 | t.deepEqual(paths.map(getAssetKey), [
182 | 'index.html',
183 | 'about.html',
184 | 'products.html',
185 | 'products/zephyr.html',
186 | 'products/sparkles.html',
187 | 'products/jarvis.html',
188 | 'contact.html',
189 | ]);
190 | });
191 |
192 | test('Can get deeply nested route paths', t => {
193 | const routes = (
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 | );
203 |
204 | const paths = getAllPaths(routes);
205 |
206 | t.deepEqual(paths, [
207 | '/',
208 | '/about',
209 | '/products',
210 | '/products/zephyr',
211 | '/products/zephyr/nomad',
212 | ]);
213 |
214 | t.deepEqual(paths.map(getAssetKey), [
215 | 'index.html',
216 | 'about.html',
217 | 'products.html',
218 | 'products/zephyr.html',
219 | 'products/zephyr/nomad.html',
220 | ]);
221 | });
222 |
223 | test('Ignores IndexRedirect', t => {
224 | const routes = (
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 | );
233 |
234 | const paths = getAllPaths(routes);
235 |
236 | t.deepEqual(paths, [
237 | '/', // This is not a real route, but it will be detected anyway b/c of Layout
238 | '/about',
239 | '/products',
240 | '/contact',
241 | ]);
242 |
243 | t.deepEqual(paths.map(getAssetKey), [
244 | 'index.html',
245 | 'about.html',
246 | 'products.html',
247 | 'contact.html',
248 | ]);
249 | });
250 |
251 |
252 | test('Can utilize not found route', t => {
253 | const routes = (
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 | );
262 |
263 | const paths = getAllPaths(routes);
264 |
265 | t.deepEqual(paths, [
266 | '/',
267 | '/about',
268 | '/code-of-conduct',
269 | '/sponsors',
270 | '/*',
271 | ]);
272 |
273 | t.deepEqual(paths.map(getAssetKey), [
274 | 'index.html',
275 | 'about.html',
276 | 'code-of-conduct.html',
277 | 'sponsors.html',
278 | '404.html',
279 | ]);
280 | });
281 |
--------------------------------------------------------------------------------
/utils.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 |
3 | export const compileWebpack = (options) => {
4 | return new Promise((resolve, reject) => {
5 | webpack(options, (err, stats) => {
6 | if (err) {
7 | return reject(err);
8 | } else if (stats.hasErrors()) {
9 | return reject(new Error(stats.toString()));
10 | }
11 |
12 | resolve(stats);
13 | });
14 | });
15 | };
16 |
--------------------------------------------------------------------------------