17 Commits

Author SHA1 Message Date
Mars Hall
b65b1b2a2a Switch to React functional component w/ hooks 2019-04-01 16:20:09 -07:00
Mars Hall
d0478f2723 Upgrade to React 16.8 2019-04-01 15:47:41 -07:00
Mars Hall
23cddeae9c Upgrade to React 16.8 2019-04-01 15:45:43 -07:00
Mars Hall
09e155d625 Switch to Heroku Node.js auto-build 2019-04-01 15:37:45 -07:00
Mars Hall
440b1b2d29 Upgrade to react-dom@16.4.2 to mitigate security issue 2019-01-04 11:48:23 -08:00
Mars Hall
21e4a7bda2 Upgrade to react-scripts@2.1.2 to mitigate security issue 2019-01-04 10:02:54 -08:00
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 15025 additions and 9824 deletions

View File

@@ -10,14 +10,16 @@ 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` script in the Node server's [`./package.json`](package.json)
* module cache configured by `cacheDirectories`
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
@@ -38,7 +40,7 @@ This deployment will automatically:
* detect [Node buildpack](https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-nodejs) * detect [Node buildpack](https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-nodejs)
* build the app with * build the app with
* `npm install` for the Node server * `npm install` for the Node server
* `heroku-postbuild` for create-react-app * `npm run build` for create-react-app
* launch the web process with `npm start` * launch the web process with `npm start`
* serves `../react-ui/build/` as static files * serves `../react-ui/build/` as static files
* customize by adding API, proxy, or route handlers/redirectors * customize by adding API, proxy, or route handlers/redirectors
@@ -61,10 +63,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 +82,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 +99,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 +122,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
```

295
package-lock.json generated
View File

@@ -1,27 +1,54 @@
{ {
"name": "heroku-cra-node", "name": "heroku-cra-node",
"version": "1.0.0", "version": "3.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": "https://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,9 +184,9 @@
"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",
@@ -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

@@ -1,20 +1,20 @@
{ {
"name": "heroku-cra-node", "name": "heroku-cra-node",
"version": "1.0.0", "version": "3.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",
"heroku-postbuild": "cd react-ui/ && npm install && npm install --only=dev --no-shrinkwrap && npm run build" "build": "cd react-ui/ && npm install && npm install --only=dev --no-shrinkwrap && npm run build"
}, },
"cacheDirectories": [ "cacheDirectories": [
"node_modules", "node_modules",
"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

22886
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.8.6",
"react-dom": "^16.2.0" "react-dom": "^16.8.6",
"react-scripts": "^2.1.8"
}, },
"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);
}
} }

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

@@ -1,18 +1,14 @@
import React, { Component } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import logo from './logo.svg'; import logo from './logo.svg';
import './App.css'; import './App.css';
class App extends Component { function App() {
constructor(props) { const [message, setMessage] = useState(null);
super(props); const [isFetching, setIsFetching] = useState(false);
this.state = { const [url, setUrl] = useState('/api');
message: null,
fetching: true
};
}
componentDidMount() { const fetchData = useCallback(() => {
fetch('/api') fetch(url)
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
throw new Error(`status ${response.status}`); throw new Error(`status ${response.status}`);
@@ -20,39 +16,54 @@ class App extends Component {
return response.json(); return response.json();
}) })
.then(json => { .then(json => {
this.setState({ setMessage(json.message);
message: json.message, setIsFetching(false);
fetching: false
});
}).catch(e => { }).catch(e => {
this.setState({ setMessage(`API call failed: ${e}`);
message: `API call failed: ${e}`, setIsFetching(false);
fetching: false
});
}) })
} }, [url]);
render() { useEffect(() => {
return ( setIsFetching(true);
<div className="App"> fetchData();
<div className="App-header"> }, [fetchData]);
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2> return (
</div> <div className="App">
<p className="App-intro"> <header className="App-header">
{'This is '} <img src={logo} className="App-logo" alt="logo" />
<a href="https://github.com/mars/heroku-cra-node"> { process.env.NODE_ENV === 'production' ?
{'create-react-app with a custom Node/Express server'} <p>
</a><br/> This is a production build from create-react-app.
</p> </p>
<p className="App-intro"> : <p>
{this.state.fetching Edit <code>src/App.js</code> and save to reload.
</p>
}
<p>{'« '}<strong>
{isFetching
? 'Fetching message from API' ? 'Fetching message from API'
: this.state.message} : message}
</p> </strong>{' »'}</p>
</div> <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>
);
} }
export default App; export default App;

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}`);
});
}