Repository: patrickleet/streaming-ssr-react-styled-components
Branch: master
Commit: dc497fc7c466
Files: 46
Total size: 29.3 KB
Directory structure:
gitextract_yyu56on6/
├── .babelrc
├── .dockerignore
├── .eslintignore
├── .gitignore
├── .prettierignore
├── .travis.yml
├── Dockerfile
├── Makefile
├── README.md
├── __tests__/
│ ├── setup.js
│ └── unit/
│ ├── app/
│ │ ├── App.jsx
│ │ ├── client.js
│ │ ├── components/
│ │ │ ├── Header.jsx
│ │ │ └── Page.jsx
│ │ ├── pages/
│ │ │ ├── About.jsx
│ │ │ ├── Error.jsx
│ │ │ ├── Home.jsx
│ │ │ └── Loading.jsx
│ │ └── styles.js
│ └── server/
│ ├── index.js
│ └── lib/
│ ├── client.js
│ ├── server.js
│ └── ssr.js
├── app/
│ ├── App.jsx
│ ├── client.js
│ ├── components/
│ │ ├── Header.jsx
│ │ └── Page.jsx
│ ├── imported.js
│ ├── index.html
│ ├── pages/
│ │ ├── About.jsx
│ │ ├── Error.jsx
│ │ ├── Home.jsx
│ │ └── Loading.jsx
│ └── styles.js
├── docker-compose.builder.yml
├── docker-compose.yml
├── jest.json
├── nginx/
│ ├── Dockerfile
│ ├── entrypoint.sh
│ └── nginx.conf.template
├── package.json
├── renovate.json
└── server/
├── index.js
└── lib/
├── client.js
├── server.js
└── ssr.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"env": {
"test": {
"presets":[
["@babel/preset-env"],
["@babel/preset-react"],
],
"plugins": [
["@babel/plugin-syntax-dynamic-import"]
]
},
"server": {
"plugins": ["react-imported-component/babel", "@babel/plugin-syntax-dynamic-import"]
},
"client": {
"plugins": [
["react-imported-component/babel"]
]
}
}
}
================================================
FILE: .dockerignore
================================================
.cache
coverage
dist
node_modules
================================================
FILE: .eslintignore
================================================
app/imported.js
dist
coverage
node_modules
================================================
FILE: .gitignore
================================================
node_modules
*.log
.cache
dist
coverage
================================================
FILE: .prettierignore
================================================
app/imported.js
dist
coverage
node_modules
================================================
FILE: .travis.yml
================================================
jobs:
include:
- language: node_js
node_js:
- 12
before_script:
- npm run build
after_success:
- npm i -g codecov
- codecov
- services:
- docker
script:
- docker build . -t ssr
- services:
- docker
script:
- docker build . -f nginx/Dockerfile -t nginx
================================================
FILE: Dockerfile
================================================
FROM node:12.22.1-alpine AS build
RUN apk add --update --no-cache \
python \
make \
g++ \
git
WORKDIR /src
COPY ./package* ./
RUN npm ci
COPY . .
RUN npm run lint
RUN npm run build
RUN npm run test
RUN npm prune --production
FROM node:12.22.1-alpine
RUN apk add --update --no-cache curl
EXPOSE 1234
WORKDIR /usr/src/service
COPY --from=build /src/node_modules node_modules
COPY --from=build /src/dist dist
HEALTHCHECK --interval=5s \
--timeout=5s \
--retries=6 \
CMD curl -fs http://localhost:1234/ || exit 1
USER node
CMD ["node", "./dist/server/index.js"]
================================================
FILE: Makefile
================================================
setup:
docker volume create nodemodules
install:
docker-compose -f docker-compose.builder.yml run --rm install
dev:
docker-compose up
down:
docker-compose down
================================================
FILE: README.md
================================================
# streaming-ssr-react-styled-components
Learn how this boilerplate was created with my tutorials on HackerNoon!
* [Part 1: Move over Next.js and Webpack!!](https://hackernoon.com/move-over-next-js-and-webpack-ba367f07545)
* [Part 2: A Better Way to Develop Node.js with Docker And Keep Your Hot Code Reloading](https://hackernoon.com/a-better-way-to-develop-node-js-with-docker-cd29d3a0093)
* [Part 3: Enforcing Code Quality for Node.js - Using Linting, Formatting, and Unit Testing with Code Coverage to Enforce Quality Standards](https://hackernoon.com/enforcing-code-quality-for-node-js-c3b837d7ae17)
* [Part 4: The 100% Code Coverage Myth](https://hackernoon.com/the-100-code-coverage-myth-900b83d20d3d)
* [Part 5: A Tale of Two (Docker Multi-Stage Build) Layers - Production Ready Dockerfiles for Node.js using SSR or Nginx](https://hackernoon.com/a-tale-of-two-docker-multi-stage-build-layers-85348a409c84)
* [Part 6: Bring In The Bots, And Let Them Maintain Our Code!](https://hackernoon.com/bring-in-the-bots-and-let-them-maintain-our-code-gh3s33n9)
UPDATE: Sorry everyone, the Hackernoon formatting got messed up in the import, and I THINK they are all fixed now - can you please point out to me if something is not?
Just in case...
You can find the original versions of the articles on Medium with the original formatting at the following links:
* https://medium.com/hackernoon/move-over-next-js-and-webpack-ba367f07545
* https://medium.com/hackernoon/a-better-way-to-develop-node-js-with-docker-cd29d3a0093
* https://medium.com/hackernoon/enforcing-code-quality-for-node-js-c3b837d7ae17
* https://medium.com/hackernoon/the-100-code-coverage-myth-900b83d20d3d
* https://medium.com/hackernoon/a-tale-of-two-docker-multi-stage-build-layers-85348a409c84
Sorry for the inconvenience in tracking these down!
## tl;dr;
### Developing with Docker
```
# setup only needs to be run once
make setup
make install
make dev
```
### Developing Locally
```
npm i
npm run dev
```
================================================
FILE: __tests__/setup.js
================================================
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
================================================
FILE: __tests__/unit/app/App.jsx
================================================
import React from 'react'
import { shallow } from 'enzyme'
import App, { renderAboutPage } from 'app/App.jsx'
describe('app/App.jsx', () => {
it('renders App', () => {
expect(App).toBeDefined()
const tree = shallow()
expect(tree).not.toBeNull()
})
it('#renderAboutPage', () => {
const tree = shallow(renderAboutPage())
expect(tree).not.toBeNull()
})
})
================================================
FILE: __tests__/unit/app/client.js
================================================
import React from 'react'
import fs from 'fs'
import path from 'path'
import { start, hydrate } from 'app/client'
import { JSDOM } from 'jsdom'
jest.mock('react-dom')
jest.mock('react-imported-component')
jest.mock('app/imported.js')
// mock DOM with actual index.html contents
const pathToIndex = path.join(process.cwd(), 'app', 'index.html')
const indexHTML = fs.readFileSync(pathToIndex).toString()
const DOM = new JSDOM(indexHTML)
const document = DOM.window.document
// this doesn't contribute to coverage, but we
// should know if it changes as it would
// cause our app to break
describe('app/index.html', () => {
it('has element with id "app"', () => {
const element = document.getElementById('app')
expect(element.id).toBe('app')
})
})
describe('app/client.js', () => {
// Reset counts of mock calls after each test
afterEach(() => {
jest.clearAllMocks()
})
describe('#start', () => {
it('renders when in development and accepts hot module reloads', () => {
// this is mocked above, so require gets the mock version
// so we can see if its functions are called
const ReactDOM = require('react-dom')
// mock module.hot
const module = {
hot: {
accept: jest.fn()
}
}
// mock options
const options = {
isProduction: false,
module,
document
}
start(options)
expect(ReactDOM.render).toBeCalled()
expect(module.hot.accept).toBeCalled()
})
it('hydrates when in production does not accept hot module reloads', () => {
const ReactDOM = require('react-dom')
const importedComponent = require('react-imported-component')
importedComponent.rehydrateMarks.mockImplementation(() =>
Promise.resolve()
)
// mock module.hot
const module = {}
// mock rehydrate function
const hydrate = jest.fn()
// mock options
const options = {
isProduction: true,
module,
document,
hydrate
}
start(options)
expect(ReactDOM.render).not.toBeCalled()
expect(hydrate).toBeCalled()
})
})
describe('#hydrate', () => {
it('uses ReactDOM to hydrate given element with an app', () => {
const ReactDOM = require('react-dom')
const element = document.getElementById('app')
const app =
const doHydrate = hydrate(app, element)
expect(typeof doHydrate).toBe('function')
doHydrate()
expect(ReactDOM.hydrate).toBeCalledWith(app, element)
})
})
})
================================================
FILE: __tests__/unit/app/components/Header.jsx
================================================
import React from 'react'
import { shallow } from 'enzyme'
import Header from 'app/components/Header.jsx'
describe('app/components/Header.jsx', () => {
it('renders Header component', () => {
expect(Header).toBeDefined()
const tree = shallow()
expect(tree.find('Page')).toBeDefined()
expect(tree.text()).toContain('Stream things!')
})
})
================================================
FILE: __tests__/unit/app/components/Page.jsx
================================================
import React from 'react'
import { shallow } from 'enzyme'
import Page from 'app/components/Page.jsx'
describe('app/components/Page.jsx', () => {
it('renders Page component', () => {
expect(Page).toBeDefined()
const tree = shallow()
expect(tree).toBeDefined()
})
})
================================================
FILE: __tests__/unit/app/pages/About.jsx
================================================
import React from 'react'
import { shallow } from 'enzyme'
import About from 'app/pages/About.jsx'
describe('app/pages/About.jsx', () => {
it('renders About page', () => {
expect(About).toBeDefined()
const tree = shallow()
expect(tree.find('Page')).toBeDefined()
expect(
tree
.find('Helmet')
.find('title')
.text()
).toEqual('About Page')
expect(tree.find('div').text()).toEqual('This is the about page')
})
})
================================================
FILE: __tests__/unit/app/pages/Error.jsx
================================================
import React from 'react'
import { shallow } from 'enzyme'
import Error from 'app/pages/Error.jsx'
describe('app/pages/Error.jsx', () => {
it('renders Error page', () => {
expect(Error).toBeDefined()
const tree = shallow()
expect(tree.find('Page')).toBeDefined()
expect(tree.text()).toEqual('Error!')
})
})
================================================
FILE: __tests__/unit/app/pages/Home.jsx
================================================
import React from 'react'
import { shallow } from 'enzyme'
import Home from 'app/pages/Home.jsx'
describe('app/pages/Home.jsx', () => {
it('renders Home page', () => {
expect(Home).toBeDefined()
const tree = shallow()
expect(tree.find('Page')).toBeDefined()
expect(
tree
.find('Helmet')
.find('title')
.text()
).toEqual('Home Page')
expect(tree.find('div').text()).toEqual('Follow me at @patrickleet')
expect(
tree
.find('div')
.find('a')
.text()
).toEqual('@patrickleet')
})
})
================================================
FILE: __tests__/unit/app/pages/Loading.jsx
================================================
import React from 'react'
import { shallow } from 'enzyme'
import Loading from 'app/pages/Loading.jsx'
describe('app/pages/Loading.jsx', () => {
it('renders Loading page', () => {
expect(Loading).toBeDefined()
const tree = shallow()
expect(tree.find('Page')).toBeDefined()
expect(tree.text()).toEqual('Loading...')
})
})
================================================
FILE: __tests__/unit/app/styles.js
================================================
import React from 'react'
import { shallow } from 'enzyme'
import { GlobalStyles } from 'app/styles.js'
describe('app/styles.js', () => {
it('renders styles page', () => {
expect(GlobalStyles).toBeDefined()
const tree = shallow()
expect(tree).not.toBeNull()
})
})
================================================
FILE: __tests__/unit/server/index.js
================================================
import { onListen } from 'server/index'
jest.mock('llog')
jest.mock('server/lib/server', () => ({
server: {
use: jest.fn(),
get: jest.fn(),
listen: jest.fn()
},
serveStatic: jest.fn(() => "static/path")
}))
jest.mock('server/lib/ssr')
describe('server/index.js', () => {
it('main', () => {
const { server, serveStatic } = require('server/lib/server')
expect(server.use).toBeCalledWith('/dist/client', "static/path")
expect(serveStatic).toBeCalledWith(`${process.cwd()}/dist/client`)
expect(server.get).toBeCalledWith('/*', expect.any(Function))
expect(server.listen).toBeCalledWith(1234, expect.any(Function))
})
it('onListen', () => {
const log = require('llog')
onListen(4000)()
expect(log.info).toBeCalledWith('Listening on port 4000...')
})
})
================================================
FILE: __tests__/unit/server/lib/client.js
================================================
import { getHTMLFragments } from 'server/lib/client.js'
describe('client', () => {
it('exists', () => {
const drainHydrateMarks = ''
const [start, end] = getHTMLFragments({ drainHydrateMarks })
expect(start).toContain('')
expect(start).toContain(drainHydrateMarks)
expect(end).toContain('script id="js-entrypoint"')
})
})
================================================
FILE: __tests__/unit/server/lib/server.js
================================================
import express from 'express'
import { server, serveStatic } from 'server/lib/server.js'
describe('server/lib/server', () => {
it('should provide server APIs to use', () => {
expect(server).toBeDefined()
expect(server.use).toBeDefined()
expect(server.get).toBeDefined()
expect(server.listen).toBeDefined()
expect(serveStatic).toEqual(express.static)
})
})
================================================
FILE: __tests__/unit/server/lib/ssr.js
================================================
/**
* @jest-environment node
*/
import defaultSSR, { ssr, write, end } from 'server/lib/ssr.js'
jest.mock('llog')
const mockReq = {
originalUrl: '/'
}
const mockRes = {
redirect: jest.fn(),
status: jest.fn(),
end: jest.fn(),
write: jest.fn(),
on: jest.fn(),
removeListener: jest.fn(),
emit: jest.fn()
}
describe('server/lib/ssr.js', () => {
describe('ssr', () => {
it('redirects when context.url is set', () => {
const req = Object.assign({}, mockReq)
const res = Object.assign({}, mockRes)
const getApplicationStream = jest.fn((originalUrl, context) => {
context.url = '/redirect'
})
const doSSR = ssr(getApplicationStream)
expect(typeof doSSR).toBe('function')
doSSR(req, res)
expect(res.redirect).toBeCalledWith(301, '/redirect')
})
it('catches error and logs before returning 500', () => {
const log = require('llog')
const req = Object.assign({}, mockReq)
const res = Object.assign({}, mockRes)
const getApplicationStream = jest.fn((originalUrl, context) => {
throw new Error('test')
})
const doSSR = ssr(getApplicationStream)
expect(typeof doSSR).toBe('function')
doSSR(req, res)
expect(log.error).toBeCalledWith(Error('test'))
expect(res.status).toBeCalledWith(500)
expect(res.end).toBeCalled()
})
})
describe('defaultSSR', () => {
it('renders app with default SSR', () => {
const req = Object.assign({}, mockReq)
const res = Object.assign({}, mockRes)
defaultSSR(req, res)
expect(res.status).toBeCalledWith(200)
expect(res.write.mock.calls[0][0]).toContain('')
expect(res.write.mock.calls[0][0]).toContain(
'window.___REACT_DEFERRED_COMPONENT_MARKS'
)
})
})
describe('#write', () => {
it('write queues data', () => {
const context = {
queue: jest.fn()
}
const buffer = new Buffer.from('hello')
write.call(context, buffer)
expect(context.queue).toBeCalledWith(buffer)
})
})
describe('#end', () => {
it('end queues endingFragment and then null to end stream', () => {
const context = {
queue: jest.fn()
}
const endingFragment = ''
const doEnd = end(endingFragment)
doEnd.call(context)
expect(context.queue).toBeCalledWith(endingFragment)
expect(context.queue).toBeCalledWith(null)
})
})
})
================================================
FILE: app/App.jsx
================================================
import React from 'react'
import { Switch, Route, Redirect } from 'react-router-dom'
import { lazy, LazyBoundary } from 'react-imported-component'
import { GlobalStyles } from './styles'
import Header from './components/Header'
import Home from './pages/Home'
import LoadingComponent from './pages/Loading'
const About = lazy(() => import('./pages/About'))
export const renderAboutPage = () => (
}>
)
const App = () => (
<>
>
)
export default App
================================================
FILE: app/client.js
================================================
import React from 'react'
import ReactDOM from 'react-dom'
import { HelmetProvider } from 'react-helmet-async'
import { BrowserRouter } from 'react-router-dom'
import { rehydrateMarks } from 'react-imported-component'
import App from './App'
export const hydrate = (app, element) => () => {
ReactDOM.hydrate(app, element)
}
export const start = ({ isProduction, document, module, hydrate }) => {
const element = document.getElementById('app')
const app = (
)
// In production, we want to hydrate instead of render
// because of the server-rendering
if (isProduction) {
// rehydrate the bundle marks from imported-components,
// then rehydrate the react app
rehydrateMarks().then(hydrate(app, element))
} else {
ReactDOM.render(app, element)
}
// Enable Hot Module Reloading
if (module.hot) {
module.hot.accept()
}
}
const options = {
isProduction: process.env.NODE_ENV === 'production',
document: document,
module: module,
hydrate
}
start(options)
================================================
FILE: app/components/Header.jsx
================================================
import React from 'react'
import styled from 'styled-components'
import { NavLink } from 'react-router-dom'
const Header = styled.header`
z-index: 100;
position: fixed;
top: 0;
left: 0;
right: 0;
max-width: 90vw;
margin: 0 auto;
padding: 1em 0;
display: flex;
justify-content: space-between;
align-items: center;
`
const Brand = styled.h1`
font-size: var(--step-up-1);
`
const Menu = styled.ul`
display: flex;
justify-content: flex-end;
align-items: center;
width: 50vw;
`
const MenuLink = styled.li`
margin-left: 2em;
text-decoration: none;
`
export default () => (
Stream things!
)
================================================
FILE: app/components/Page.jsx
================================================
import styled from 'styled-components'
const Page = styled.div`
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
`
export default Page
================================================
FILE: app/imported.js
================================================
/* eslint-disable */
/* tslint:disable */
// generated by react-imported-component, DO NOT EDIT
import {assignImportedComponents} from 'react-imported-component/macro';
// all your imports are defined here
// all, even the ones you tried to hide in comments (that's the cost of making a very fast parser)
// to remove any import from here
// 1) use magic comment `import(/* client-side */ './myFile')` - and it will disappear
// 2) use file filter to ignore specific locations (refer to the README)
const applicationImports = assignImportedComponents([
[() => import('./pages/About'), '', './app/pages/About', false],
]);
export default applicationImports;
// @ts-ignore
if (module.hot) {
// these imports would make this module a parent for the imported modules.
// but this is just a helper - so ignore(and accept!) all updates
// @ts-ignore
module.hot.accept(() => null);
}
================================================
FILE: app/index.html
================================================
================================================
FILE: app/pages/About.jsx
================================================
import React from 'react'
import { Helmet } from 'react-helmet-async'
import Page from '../components/Page'
const About = () => (
About Page
This is the about page
)
export default About
================================================
FILE: app/pages/Error.jsx
================================================
import React from 'react'
import Page from '../components/Page'
const Error = () => Error!
export default Error
================================================
FILE: app/pages/Home.jsx
================================================
import React from 'react'
import { Helmet } from 'react-helmet-async'
import Page from '../components/Page'
const Home = () => (
Home Page
)
export default Home
================================================
FILE: app/pages/Loading.jsx
================================================
import React from 'react'
import Page from '../components/Page'
const Loading = () => Loading...
export default Loading
================================================
FILE: app/styles.js
================================================
import { createGlobalStyle } from 'styled-components'
export const GlobalStyles = createGlobalStyle`
/* Base 10 typography scale courtesty of @wesbos 1.6rem === 16px */
html {
font-size: 10px;
}
body {
font-size: 1.6rem;
}
/* Relative Type Scale */
/* https://blog.envylabs.com/responsive-typographic-scales-in-css-b9f60431d1c4 */
:root {
--step-up-5: 2em;
--step-up-4: 1.7511em;
--step-up-3: 1.5157em;
--step-up-2: 1.3195em;
--step-up-1: 1.1487em;
/* baseline: 1em */
--step-down-1: 0.8706em;
--step-down-2: 0.7579em;
--step-down-3: 0.6599em;
--step-down-4: 0.5745em;
--step-down-5: 0.5em;
/* Colors */
--header: rgb(0,0,0);
}
/* https://css-tricks.com/snippets/css/system-font-stack/ */
/* Define the "system" font family */
/* Fastest loading font - the one native to their device */
@font-face {
font-family: system;
font-style: normal;
font-weight: 300;
src: local(".SFNSText-Light"), local(".HelveticaNeueDeskInterface-Light"), local(".LucidaGrandeUI"), local("Ubuntu Light"), local("Segoe UI Light"), local("Roboto-Light"), local("DroidSans"), local("Tahoma");
}
/* Modern CSS Reset */
/* https://alligator.io/css/minimal-css-reset/ */
body, h1, h2, h3, h4, h5, h6, p, ol, ul, input[type=text], input[type=email], button {
margin: 0;
padding: 0;
font-weight: normal;
}
body, h1, h2, h3, h4, h5, h6, p, ol, ul, input[type=text], input[type=email], button {
font-family: "system"
}
*, *:before, *:after {
box-sizing: inherit;
}
ol, ul {
list-style: none;
}
img {
max-width: 100%;
height: auto;
}
/* Links */
a {
text-decoration: underline;
color: inherit;
&.active {
text-decoration: none;
}
}
`
================================================
FILE: docker-compose.builder.yml
================================================
version: '3.4'
x-base:
&base
image: node:12
volumes:
- nodemodules:/usr/src/service/node_modules
- .:/usr/src/service/
working_dir: /usr/src/service/
services:
install:
<< : *base
command: npm i
build:
<< : *base
command: npm run build
create-bundles:
<< : *base
command: npm run create-bundles
volumes:
nodemodules:
external: true
================================================
FILE: docker-compose.yml
================================================
version: '3'
services:
dev:
image: node:12
volumes:
- nodemodules:/usr/src/service/node_modules
- .:/usr/src/service
environment:
- NODE_ENV=development
- CHOKIDAR_USEPOLLING=true
working_dir: /usr/src/service
command: npm run dev
ports:
- 1234:1234
- 1235:1235
volumes:
nodemodules:
external: true
================================================
FILE: jest.json
================================================
{
"roots": ["/__tests__/unit"],
"modulePaths": [
"",
"/node_modules/"
],
"moduleFileExtensions": [
"js",
"jsx"
],
"transform": {
"^.+\\.jsx?$": "babel-jest"
},
"transformIgnorePatterns": ["/node_modules/"],
"coverageThreshold": {
"global": {
"branches": 100,
"functions": 100,
"lines": 100,
"statements": 100
}
},
"collectCoverage": true,
"collectCoverageFrom": [
"**/*.{js,jsx}"
],
"coveragePathIgnorePatterns": [
"/app/imported.js",
"/node_modules/"
],
"setupFilesAfterEnv": ["/__tests__/setup.js"]
}
================================================
FILE: nginx/Dockerfile
================================================
FROM node:12.22.1-alpine AS build
RUN apk add --update --no-cache \
python \
make \
g++ \
git
COPY . /src
WORKDIR /src
RUN npm ci
RUN npm run lint
RUN npm run build:nginx
RUN npm run test
RUN npm prune --production
FROM nginx:1.19.10-alpine
RUN apk add --update --no-cache curl
WORKDIR /usr/src/service
COPY --from=build /src/dist ./dist
COPY --from=build /src/nginx ./nginx
HEALTHCHECK --interval=5s \
--timeout=5s \
--retries=6 \
CMD curl -fs http://localhost:1234/ || exit 1
RUN ["chmod", "+x", "./nginx/entrypoint.sh"]
ENTRYPOINT [ "ash", "./nginx/entrypoint.sh" ]
================================================
FILE: nginx/entrypoint.sh
================================================
#!/bin/bash
# This script can be used when you have webpack or parcel builds that
# insert env variables at build time, usually as build args.
# Just set the build args to an a unique string for replacement,
# and do it post build instead. Uncomment `echo` through `done` and modify
# to match your env variables
# --- Start Insert ENV to JS bundle ---
# echo "Inserting env variables"
# for file in ./dist/**/*.js
# do
# echo "env sub for $file"
# sed -i "s/REPLACE_MIXPANEL_TOKEN/${MIXPANEL_TOKEN}/g" $file
# done
# --- End Insert ENV to JS bundle ---
# And if you need env variables in Nginx, use this instead of `cp`
# --- Start Insert ENV to Nginx---
# echo "Injecting Nginx ENV Vars..."
# envsubst '${GRAPHQL_URL}' < nginx/nginx.conf.template > /etc/nginx/nginx.conf
# --- End Insert ENV to Nginx---
cp nginx/nginx.conf.template /etc/nginx/nginx.conf
echo "Using config:"
cat /etc/nginx/nginx.conf
echo "Starting nginx..."
nginx -c '/etc/nginx/nginx.conf' -g 'daemon off;'
================================================
FILE: nginx/nginx.conf.template
================================================
events {
worker_connections 1024;
}
http {
server {
include /etc/nginx/mime.types;
listen 1234;
root /usr/src/service/dist/client;
index index.html;
gzip on;
gzip_min_length 1000;
gzip_buffers 4 32k;
gzip_proxied any;
gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css;
gzip_vary on;
location ~* \.(?:css|js|eot|woff|woff2|ttf|svg|otf) {
# Enable GZip for static files
gzip_static on;
# Indefinite caching for static files
expires max;
add_header Cache-Control "public";
}
}
}
================================================
FILE: package.json
================================================
{
"name": "stream-all-the-things",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "npm run generate-imported-components && parcel app/index.html --hmr-port 1235",
"dev:server": "nodemon -e js,jsx,html --ignore dist --ignore app/imported.js --exec \"npm run build && npm run start\"",
"lint": "prettier-standard 'app/**/*.js' 'app/**/*.jsx' 'server/**/*.js' --lint",
"lint:fix": "prettier-standard 'app/**/*.js' 'app/**/*.jsx' 'server/**/*.js' --lint --fix",
"format": "prettier-standard 'app/**/*.js' 'app/**/*.jsx' 'server/**/*.js' --format",
"build": "rimraf dist && npm run generate-imported-components && npm run create-bundles",
"build:nginx": "rimraf dist && npm run generate-imported-components && npm run create-bundle:nginx",
"create-bundles": "concurrently \"npm run create-bundle:client\" \"npm run create-bundle:server\"",
"create-bundle:client": "cross-env BABEL_ENV=client parcel build app/index.html -d dist/client --public-url /dist/client",
"create-bundle:nginx": "cross-env BABEL_ENV=client parcel build app/index.html -d dist/client --public-url .",
"create-bundle:server": "cross-env BABEL_ENV=server parcel build server/index.js -d dist/server --public-url /dist --target=node",
"generate-imported-components": "imported-components app app/imported.js",
"start": "node dist/server",
"test": "cross-env BABEL_ENV=test jest --config jest.json",
"test:watch": "cross-env BABEL_ENV=test jest --config jest.json --watch"
},
"lint-staged": {
"*": [
"prettier-standard --lint",
"git add"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged && npm run test"
}
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"llog": "^0.2.0",
"pino": "^6.0.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-helmet-async": "^1.0.4",
"react-imported-component": "^6.2.1",
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
"styled-components": "^5.0.1",
"through": "^2.3.8"
},
"devDependencies": {
"@babel/core": "7.13.15",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/polyfill": "7.12.1",
"@babel/preset-env": "7.13.15",
"@babel/preset-react": "7.13.13",
"babel-jest": "26.6.3",
"concurrently": "5.3.0",
"cross-env": "7.0.3",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.6",
"husky": "4.3.8",
"jest": "26.6.3",
"lint-staged": "10.5.4",
"nodemon": "2.0.7",
"parcel-bundler": "1.12.5",
"prettier-standard": "16.4.1",
"react-hot-loader": "4.13.0",
"rimraf": "3.0.2"
}
}
================================================
FILE: renovate.json
================================================
{
"extends": [
"config:base"
],
"automerge": true,
"major": {
"automerge": false
}
}
================================================
FILE: server/index.js
================================================
import path from 'path'
import log from 'llog'
import { server, serveStatic } from './lib/server'
import ssr from './lib/ssr'
// Expose the public directory as /dist and point to the browser version
server.use(
'/dist/client',
serveStatic(path.resolve(process.cwd(), 'dist', 'client'))
)
// Anything unresolved is serving the application and let
// react-router do the routing!
server.get('/*', ssr)
// Check for PORT environment variable, otherwise fallback on Parcel default port
const port = process.env.PORT || 1234
export const onListen = port => () => {
log.info(`Listening on port ${port}...`)
}
server.listen(port, onListen(port))
================================================
FILE: server/lib/client.js
================================================
import fs from 'fs'
import path from 'path'
const htmlPath = path.join(process.cwd(), 'dist', 'client', 'index.html')
const rawHTML = fs.readFileSync(htmlPath).toString()
const appString = ''
const splitter = '###SPLIT###'
const [startingRawHTMLFragment, endingRawHTMLFragment] = rawHTML
.replace(appString, `${appString}${splitter}`)
.split(splitter)
export const getHTMLFragments = ({ drainHydrateMarks }) => {
const startingHTMLFragment = `${startingRawHTMLFragment}${drainHydrateMarks}`
return [startingHTMLFragment, endingRawHTMLFragment]
}
================================================
FILE: server/lib/server.js
================================================
import express from 'express'
export const server = express()
export const serveStatic = express.static
================================================
FILE: server/lib/ssr.js
================================================
import React from 'react'
import { renderToNodeStream } from 'react-dom/server'
import { HelmetProvider } from 'react-helmet-async'
import { StaticRouter } from 'react-router-dom'
import { ServerStyleSheet } from 'styled-components'
import { printDrainHydrateMarks } from 'react-imported-component'
import log from 'llog'
import through from 'through'
import App from '../../app/App'
import { getHTMLFragments } from './client'
// import { getDataFromTree } from 'react-apollo';
const getApplicationStream = (originalUrl, context) => {
const helmetContext = {}
const app = (
)
const sheet = new ServerStyleSheet()
return sheet.interleaveWithNodeStream(
renderToNodeStream(sheet.collectStyles(app))
)
}
export function write (data) {
this.queue(data)
}
export const end = endingHTMLFragment =>
function end () {
this.queue(endingHTMLFragment)
this.queue(null)
}
export const ssr = getApplicationStream => (req, res) => {
try {
// If you were using Apollo, you could fetch data with this
// await getDataFromTree(app);
const context = {}
const stream = getApplicationStream(req.originalUrl, context)
if (context.url) {
return res.redirect(301, context.url)
}
const [startingHTMLFragment, endingHTMLFragment] = getHTMLFragments({
drainHydrateMarks: printDrainHydrateMarks()
})
res.status(200)
res.write(startingHTMLFragment)
stream.pipe(through(write, end(endingHTMLFragment))).pipe(res)
} catch (e) {
log.error(e)
res.status(500)
res.end()
}
}
const defaultSSR = ssr(getApplicationStream)
export default defaultSSR