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! Home About
) ================================================ 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
Follow me at @patrickleet
) 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