11 Commits

Author SHA1 Message Date
Mars Hall
65fd0a393b Re-implement Node API example in React app 2018-12-16 11:57:34 -08:00
Mars Hall
d261bdfb76 Improve dev vs production distinctions 2018-12-16 11:35:55 -08:00
Mars Hall
8e0f4d44c6 Upgrade to Express 4.16.4 & Heroku deployment Node version to 10.x 2018-12-16 10:55:44 -08:00
Mars Hall
1afb09546e Upgrade to Create React App 2.1.1 & React 16.6.3 2018-12-16 10:51:40 -08:00
Mars Hall
f821f3878f 📚 tip for switching 2018-08-13 17:41:33 -07:00
Mars Hall
c77492f74d Update all packages 2018-07-10 14:38:05 -07:00
Mars Hall
a507d52db4 Enable Node.js Language Metrics 2018-04-30 11:43:41 -07:00
Mars Hall
91c10ad95b 📚 clarify how this is "two npm projects" 2018-04-18 10:13:56 -07:00
Mars Hall
047761bfd5 📚 fix Markdown foible 2018-03-17 17:55:34 -07:00
Mars Hall
a86ec2778d 📚 Update "Switching from create-react-app-buildpack" directions 2018-03-17 17:53:46 -07:00
Mars Hall
3e5ed66d1d Multi-process (#17)
* Multi-process across all CPU cores

* 📚 multi-processing w/ Node Cluster API
2018-02-05 22:06:59 -08:00
17 changed files with 13337 additions and 9736 deletions

View File

@@ -10,14 +10,17 @@ To deploy a frontend-only React app, use the static-site optimized
## Design Points ## Design Points
A combo of two npm projects, the backend server and the frontend UI. So there are two `package.json` configs. A combo of two npm projects, the backend server and the frontend UI. So there are two `package.json` configs and thereforce [two places to run `npm` commands](#user-content-local-development):
1. [`package.json`](package.json) for [Node server](server/) & [Heroku deploy](https://devcenter.heroku.com/categories/deployment) 1. [**Node server**](server/): [`./package.json`](package.json)
* `heroku-postbuild` script compiles the webpack bundle during deploy * [deployed automatically](https://devcenter.heroku.com/categories/deployment) via heroku/nodejs buildpack
* `cacheDirectories` includes `react-ui/node_modules/` to optimize build time 2. [**React UI**](react-ui/): [`react-ui/package.json`](react-ui/package.json)
2. [`react-ui/package.json`](react-ui/package.json) for [React web UI](react-ui/)
* generated by [create-react-app](https://github.com/facebookincubator/create-react-app) * generated by [create-react-app](https://github.com/facebookincubator/create-react-app)
* deployed via build hooks in the Node server's [`./package.json`](package.json)
* module cache configured by `cacheDirectories`
* bundling via `heroku-postbuild` hook
Includes a minimal [Node Cluster](https://nodejs.org/docs/latest-v8.x/api/cluster.html) [implementation](server/index.js) to parallelize the single-threaded Node process across the available CPU cores.
## Demo ## Demo
@@ -61,10 +64,13 @@ If an app was previously deployed with [create-react-app-buildpack](https://gith
```bash ```bash
mkdir react-ui mkdir react-ui
git mv [!react-ui]* react-ui/ git mv -k [!react-ui]* react-ui/
# You'll see "fatal: Not a git repository"; let's fix that error mv node_modules react-ui/
# If you see "fatal: Not a git repository", then fix that error
mv react-ui/.git ./ mv react-ui/.git ./
``` ```
⚠️ *Some folks have reported problems with these commands. Using the `bash` shell will probably allow them to work. Sorry if they do not work for you, know that the point is to move **everything** in the repo into the `react-ui/` subdirectory. Except for `.git/` which should remain at the root level.* 
1. Create a root [`package.json`](package.json), [`server/`](server/), & [`.gitignore`](.gitignore) modeled after the code in this repo 1. Create a root [`package.json`](package.json), [`server/`](server/), & [`.gitignore`](.gitignore) modeled after the code in this repo
1. Commit and deploy ♻️ 1. Commit and deploy ♻️
@@ -77,7 +83,12 @@ If an app was previously deployed with [create-react-app-buildpack](https://gith
## Local Development ## Local Development
### Run the API Server Because this app is made of two npm projects, there are two places to run `npm` commands:
1. **Node API server** at the root `./`
1. **React UI** in `react-ui/` directory.
### Run the API server
In a terminal: In a terminal:
@@ -89,6 +100,12 @@ npm install
npm start npm start
``` ```
#### Install new npm packages for Node
```bash
npm install package-name --save
```
### Run the React UI ### Run the React UI
@@ -106,3 +123,12 @@ npm install
# Start the server # Start the server
npm start npm start
``` ```
#### Install new npm packages for React UI
```bash
# Always change directory, first
cd react-ui/
npm install package-name --save
```

297
package-lock.json generated
View File

@@ -2,26 +2,53 @@
"name": "heroku-cra-node", "name": "heroku-cra-node",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true,
"dependencies": { "dependencies": {
"accepts": { "accepts": {
"version": "1.3.3", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
"integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=" "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
"requires": {
"mime-types": "~2.1.18",
"negotiator": "0.6.1"
}
}, },
"array-flatten": { "array-flatten": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "resolved": "http://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
}, },
"body-parser": {
"version": "1.18.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
"integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
"requires": {
"bytes": "3.0.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "~1.6.3",
"iconv-lite": "0.4.23",
"on-finished": "~2.3.0",
"qs": "6.5.2",
"raw-body": "2.3.3",
"type-is": "~1.6.16"
}
},
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
},
"content-disposition": { "content-disposition": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
"integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
}, },
"content-type": { "content-type": {
"version": "1.0.2", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha1-t9ETrueo3Se9IRM8TcJSnfFyHu0=" "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
}, },
"cookie": { "cookie": {
"version": "0.3.1", "version": "0.3.1",
@@ -34,14 +61,17 @@
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
}, },
"debug": { "debug": {
"version": "2.6.7", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha1-krrR9tBbu2u6Isyoi80OyJTChh4=" "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
}, },
"depd": { "depd": {
"version": "1.1.0", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=" "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
}, },
"destroy": { "destroy": {
"version": "1.0.4", "version": "1.0.4",
@@ -54,9 +84,9 @@
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
}, },
"encodeurl": { "encodeurl": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
}, },
"escape-html": { "escape-html": {
"version": "1.0.3", "version": "1.0.3",
@@ -64,34 +94,89 @@
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
}, },
"etag": { "etag": {
"version": "1.8.0", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.0.tgz", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-b2Ma7zNtbEY2K1F2QETOIWvjwFE=" "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
}, },
"express": { "express": {
"version": "4.15.3", "version": "4.16.4",
"resolved": "https://registry.npmjs.org/express/-/express-4.15.3.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
"integrity": "sha1-urZdDwOqgMNYQIly/HAPkWlEtmI=" "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==",
"requires": {
"accepts": "~1.3.5",
"array-flatten": "1.1.1",
"body-parser": "1.18.3",
"content-disposition": "0.5.2",
"content-type": "~1.0.4",
"cookie": "0.3.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.1.1",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.2",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.4",
"qs": "6.5.2",
"range-parser": "~1.2.0",
"safe-buffer": "5.1.2",
"send": "0.16.2",
"serve-static": "1.13.2",
"setprototypeof": "1.1.0",
"statuses": "~1.4.0",
"type-is": "~1.6.16",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
}
}, },
"finalhandler": { "finalhandler": {
"version": "1.0.3", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.0.3.tgz", "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
"integrity": "sha1-70fneVDpmXgOhgIqVg4yF+DQzIk=" "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.2",
"statuses": "~1.4.0",
"unpipe": "~1.0.0"
}
}, },
"forwarded": { "forwarded": {
"version": "0.1.0", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-Ge+YdMSuHCl7zweP3mOgm2aoQ2M=" "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
}, },
"fresh": { "fresh": {
"version": "0.5.0", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.0.tgz", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-9HTKXmqSRtb9jglTz6m5yAWvp44=" "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
}, },
"http-errors": { "http-errors": {
"version": "1.6.1", "version": "1.6.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.1.tgz", "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"integrity": "sha1-X4uO2YrKVFZWv1cplzh/kEpyIlc=" "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.0",
"statuses": ">= 1.4.0 < 2"
}
},
"iconv-lite": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
"integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
}, },
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
@@ -99,13 +184,13 @@
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
}, },
"ipaddr.js": { "ipaddr.js": {
"version": "1.3.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.3.0.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
"integrity": "sha1-HgOlL9rYOou7KyXL9JmLTP/NPew=" "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4="
}, },
"media-typer": { "media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
}, },
"merge-descriptors": { "merge-descriptors": {
@@ -119,19 +204,22 @@
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
}, },
"mime": { "mime": {
"version": "1.3.4", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
"integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=" "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
}, },
"mime-db": { "mime-db": {
"version": "1.27.0", "version": "1.37.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
"integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=" "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg=="
}, },
"mime-types": { "mime-types": {
"version": "2.1.15", "version": "2.1.21",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz",
"integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=" "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==",
"requires": {
"mime-db": "~1.37.0"
}
}, },
"ms": { "ms": {
"version": "2.0.0", "version": "2.0.0",
@@ -146,12 +234,15 @@
"on-finished": { "on-finished": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=" "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
}, },
"parseurl": { "parseurl": {
"version": "1.3.1", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
"integrity": "sha1-yKuMkiO6NIiKpkopeyiFO+wY2lY=" "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
}, },
"path-to-regexp": { "path-to-regexp": {
"version": "0.1.7", "version": "0.1.7",
@@ -159,44 +250,94 @@
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
}, },
"proxy-addr": { "proxy-addr": {
"version": "1.1.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.1.4.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
"integrity": "sha1-J+VF9pYKRKYn2bREZ+NcG2tM4vM=" "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==",
"requires": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.8.0"
}
}, },
"qs": { "qs": {
"version": "6.4.0", "version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
}, },
"range-parser": { "range-parser": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
"integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
}, },
"raw-body": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
"integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
"requires": {
"bytes": "3.0.0",
"http-errors": "1.6.3",
"iconv-lite": "0.4.23",
"unpipe": "1.0.0"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"send": { "send": {
"version": "0.15.3", "version": "0.16.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.15.3.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
"integrity": "sha1-UBP5+ZAj31DRvZiSwZ4979HVMwk=" "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.6.2",
"mime": "1.4.1",
"ms": "2.0.0",
"on-finished": "~2.3.0",
"range-parser": "~1.2.0",
"statuses": "~1.4.0"
}
}, },
"serve-static": { "serve-static": {
"version": "1.12.3", "version": "1.13.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.12.3.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
"integrity": "sha1-n0uhni8wMMVH+K+ZEHg47DjVseI=" "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.2",
"send": "0.16.2"
}
}, },
"setprototypeof": { "setprototypeof": {
"version": "1.0.3", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
"integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
}, },
"statuses": { "statuses": {
"version": "1.3.1", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
"integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
}, },
"type-is": { "type-is": {
"version": "1.6.15", "version": "1.6.16",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
"integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=" "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.18"
}
}, },
"unpipe": { "unpipe": {
"version": "1.0.0", "version": "1.0.0",
@@ -204,14 +345,14 @@
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
}, },
"utils-merge": { "utils-merge": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=" "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
}, },
"vary": { "vary": {
"version": "1.1.1", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.1.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-Z1Neu2lMHVIldFeYRmUyP1h+jTc=" "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
} }
} }
} }

View File

@@ -3,7 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"description": "How to use create-react-app with a custom Node API on Heroku", "description": "How to use create-react-app with a custom Node API on Heroku",
"engines": { "engines": {
"node": "8.9.x" "node": "10.x"
}, },
"scripts": { "scripts": {
"start": "node server", "start": "node server",
@@ -14,7 +14,7 @@
"react-ui/node_modules" "react-ui/node_modules"
], ],
"dependencies": { "dependencies": {
"express": "^4.14.1" "express": "^4.16.4"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

20
react-ui/.gitignore vendored
View File

@@ -1,15 +1,23 @@
# See http://help.github.com/ignore-files/ for more about ignoring files. # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies # dependencies
node_modules /node_modules
/.pnp
.pnp.js
# testing # testing
coverage /coverage
# production # production
build /build
# misc # misc
.DS_Store .DS_Store
.env .env.local
npm-debug.log .env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

File diff suppressed because it is too large Load Diff

21161
react-ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,18 +2,25 @@
"name": "react-ui", "name": "react-ui",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"devDependencies": {
"react-scripts": "^1.1.0"
},
"dependencies": { "dependencies": {
"react": "^16.2.0", "react": "^16.6.3",
"react-dom": "^16.2.0" "react-dom": "^16.6.3",
"react-scripts": "2.1.1"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test --env=jsdom", "test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
],
"proxy": "http://localhost:5000" "proxy": "http://localhost:5000"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1,11 +1,17 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!-- <!--
Notice the use of %PUBLIC_URL% in the tag above. manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build. It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML. Only files inside the `public` folder can be referenced from the HTML.
@@ -16,6 +22,9 @@
<title>React App</title> <title>React App</title>
</head> </head>
<body> <body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div> <div id="root"></div>
<!-- <!--
This HTML file is a template. This HTML file is a template.
@@ -24,8 +33,8 @@
You can add webfonts, meta tags, or analytics to this file. You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag. The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start`. To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build`. To create a production bundle, use `npm run build` or `yarn build`.
--> -->
</body> </body>
</html> </html>

View File

@@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -4,25 +4,29 @@
.App-logo { .App-logo {
animation: App-logo-spin infinite 20s linear; animation: App-logo-spin infinite 20s linear;
height: 80px; height: 40vmin;
} }
.App-header { .App-header {
background-color: #222; background-color: #282c34;
height: 150px; min-height: 100vh;
padding: 20px; display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white; color: white;
} }
.App-intro { .App-link {
font-size: large; color: #61dafb;
}
.App a {
color: #f60;
} }
@keyframes App-logo-spin { @keyframes App-logo-spin {
from { transform: rotate(0deg); } from {
to { transform: rotate(360deg); } transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
} }

43
react-ui/src/App.js vendored
View File

@@ -35,21 +35,36 @@ class App extends Component {
render() { render() {
return ( return (
<div className="App"> <div className="App">
<div className="App-header"> <header className="App-header">
<img src={logo} className="App-logo" alt="logo" /> <img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2> { process.env.NODE_ENV === 'production' ?
</div> <p>
<p className="App-intro"> This is a production build from create-react-app.
{'This is '} </p>
<a href="https://github.com/mars/heroku-cra-node"> : <p>
{'create-react-app with a custom Node/Express server'} Edit <code>src/App.js</code> and save to reload.
</a><br/> </p>
</p> }
<p className="App-intro"> <p>{'« '}<strong>
{this.state.fetching {this.state.fetching
? 'Fetching message from API' ? 'Fetching message from API'
: this.state.message} : this.state.message}
</p> </strong>{' »'}</p>
<p><a
className="App-link"
href="https://github.com/mars/heroku-cra-node"
>
React + Node deployment on Heroku
</a></p>
<p><a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a></p>
</header>
</div> </div>
); );
} }

View File

@@ -5,4 +5,5 @@ import App from './App';
it('renders without crashing', () => { it('renders without crashing', () => {
const div = document.createElement('div'); const div = document.createElement('div');
ReactDOM.render(<App />, div); ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
}); });

View File

@@ -1,5 +1,18 @@
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
p + p {
margin-top: 0;
} }

13
react-ui/src/index.js vendored
View File

@@ -1,9 +1,12 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import App from './App';
import './index.css'; import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render( ReactDOM.render(<App />, document.getElementById('root'));
<App />,
document.getElementById('root') // If you want your app to work offline and load faster, you can change
); // unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();

135
react-ui/src/serviceWorker.js vendored Normal file
View File

@@ -0,0 +1,135 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read http://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit http://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See http://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

View File

@@ -1,23 +1,42 @@
const express = require('express'); const express = require('express');
const path = require('path'); const path = require('path');
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
const app = express(); const isDev = process.env.NODE_ENV !== 'production';
const PORT = process.env.PORT || 5000; const PORT = process.env.PORT || 5000;
// Priority serve any static files. // Multi-process to utilize all CPU cores.
app.use(express.static(path.resolve(__dirname, '../react-ui/build'))); if (!isDev && cluster.isMaster) {
console.error(`Node cluster master ${process.pid} is running`);
// Answer API requests. // Fork workers.
app.get('/api', function (req, res) { for (let i = 0; i < numCPUs; i++) {
res.set('Content-Type', 'application/json'); cluster.fork();
res.send('{"message":"Hello from the custom server!"}'); }
});
// All remaining requests return the React app, so it can handle routing. cluster.on('exit', (worker, code, signal) => {
app.get('*', function(request, response) { console.error(`Node cluster worker ${worker.process.pid} exited: code ${code}, signal ${signal}`);
response.sendFile(path.resolve(__dirname, '../react-ui/build', 'index.html')); });
});
app.listen(PORT, function () { } else {
console.log(`Listening on port ${PORT}`); const app = express();
});
// Priority serve any static files.
app.use(express.static(path.resolve(__dirname, '../react-ui/build')));
// Answer API requests.
app.get('/api', function (req, res) {
res.set('Content-Type', 'application/json');
res.send('{"message":"Hello from the custom server!"}');
});
// All remaining requests return the React app, so it can handle routing.
app.get('*', function(request, response) {
response.sendFile(path.resolve(__dirname, '../react-ui/build', 'index.html'));
});
app.listen(PORT, function () {
console.error(`Node ${isDev ? 'dev server' : 'cluster worker '+process.pid}: listening on port ${PORT}`);
});
}