├── .babelrc ├── .env.example ├── .gitattributes ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .serverless_plugins └── serverless-patches.js ├── .travis.yml ├── Dockerfile ├── Makefile ├── README.md ├── config └── webpack │ ├── webpack.config.server.js │ └── webpack.config.static.js ├── docker-compose.yml ├── handler.ts ├── package.json ├── scripts └── ci │ ├── build.sh │ ├── check.sh │ ├── deploy.sh │ └── install.sh ├── serverless-react-ssr.code-workspace ├── serverless.yml ├── src ├── app │ ├── Root.tsx │ ├── addons │ │ └── withTracker.tsx │ ├── components │ │ ├── AspectRatio.tsx │ │ ├── Grid.tsx │ │ ├── Link.tsx │ │ ├── List.tsx │ │ ├── Loadable.tsx │ │ ├── Table.tsx │ │ ├── Typography.tsx │ │ └── index.ts │ ├── containers │ │ ├── Home │ │ │ └── index.tsx │ │ ├── Page1 │ │ │ └── index.tsx │ │ └── Page2 │ │ │ └── index.tsx │ ├── reducer.ts │ ├── routes.ts │ ├── schema │ │ ├── fragments │ │ │ └── index.ts │ │ ├── mutations │ │ │ └── index.ts │ │ └── queries │ │ │ └── index.ts │ ├── styled │ │ ├── index.ts │ │ └── media.ts │ └── theme.ts └── platforms │ ├── browser │ ├── apollo.ts │ ├── components │ │ └── App.tsx │ ├── hydrate.ts │ └── main.ts │ └── lambda │ ├── apollo.ts │ ├── cache.ts │ ├── components │ └── App.tsx │ ├── document.ts │ ├── fragment-matcher.ts │ ├── middleware │ ├── robots.ts │ └── sitemap.ts │ ├── render.ts │ └── server.ts ├── tsconfig.json ├── tslint.json ├── typings └── typings.d.ts └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "presets": [ 5 | ["@babel/env"], 6 | ["@babel/react"], 7 | ["@babel/typescript"] 8 | ], 9 | "plugins": [ 10 | ["@loadable/babel-plugin"], 11 | ["module-resolver", { "extensions": [".ts", ".tsx"], "root": ["./src"]}], 12 | ["styled-components", { "displayName": false }] 13 | ] 14 | }, 15 | "production": { 16 | "presets": [ 17 | ["@babel/env", { "targets": { "browsers": ["last 2 versions"] }}], 18 | ["@babel/react"], 19 | ["@babel/typescript"] 20 | ], 21 | "plugins": [ 22 | ["@loadable/babel-plugin"], 23 | ["module-resolver", { "extensions": [".ts", ".tsx"], "root": ["./src"]}], 24 | ["styled-components", { "displayName": false }] 25 | ] 26 | }, 27 | "serverless": { 28 | "presets": [ 29 | ["@babel/env", { "targets": { "node": "8.10" }}], 30 | ["@babel/react"], 31 | ["@babel/typescript"] 32 | ], 33 | "plugins": [ 34 | ["@loadable/babel-plugin"], 35 | ["module-resolver", { "extensions": [".ts", ".tsx"], "root": ["./src"]}], 36 | ["styled-components", { "displayName": false }] 37 | ] 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | AWS_ACCESS_KEY_ID=<> 2 | AWS_SECRET_ACCESS_KEY=<> 3 | 4 | ARTIFACTS_BUCKET=<> 5 | 6 | API_DOMAIN_NAME=<> 7 | APP_DOMAIN_NAME=<> 8 | APP_HOSTED_ZONE=<> 9 | APP_SSL_CERT_ARN=<> 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | yarn.lock binary 2 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | - run: npm test 30 | env: 31 | CI: true 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | # typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | .env 64 | .env.* 65 | !.env.example 66 | 67 | .DS_Store 68 | 69 | .serverless 70 | .webpack 71 | dist 72 | -------------------------------------------------------------------------------- /.serverless_plugins/serverless-patches.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class ServerlessPlugin { 4 | constructor(serverless, options) { 5 | this.serverless = serverless 6 | this.options = options 7 | 8 | this.hooks = { 9 | 'before:package:finalize': this.beforePackageFinalize.bind(this), 10 | } 11 | } 12 | 13 | beforePackageFinalize () { 14 | const Resources = this.serverless.service.provider.compiledCloudFormationTemplate.Resources 15 | 16 | // Add caching to ApiGatewayMethodProxyVarGet 17 | Object.keys(Resources).map(key => { 18 | if (key === 'ApiGatewayMethodProxyVarGet') { 19 | Resources[key].Properties = { 20 | ...Resources[key].Properties, 21 | Integration: { 22 | ...Resources[key].Properties.Integration, 23 | CacheKeyParameters: ['method.request.path.proxy'], 24 | CacheNamespace: 'ApiGatewayMethodProxyVarGetCacheNS', 25 | RequestParameters: { 26 | 'integration.request.path.proxy': 'method.request.path.proxy' 27 | } 28 | }, 29 | RequestParameters: { 30 | ...Resources[key].Properties.RequestParameters, 31 | 'method.request.path.proxy': true 32 | } 33 | } 34 | } 35 | 36 | // Add S3 Proxy API Method to the API Gateway Deployment dependencies 37 | if (key.indexOf('ApiGatewayDeployment') > -1) { 38 | Resources[key].DependsOn = [ 39 | ...Resources[key].DependsOn, 40 | 'ApiGatewayMethodStaticProxyVarGet' 41 | ] 42 | } 43 | }) 44 | } 45 | } 46 | 47 | module.exports = ServerlessPlugin 48 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: bash 3 | services: 4 | - docker 5 | 6 | env: 7 | global: 8 | - CONTAINER_NAME=serverless-react/travis 9 | 10 | matrix: 11 | fast_finish: true 12 | 13 | cache: 14 | directories: 15 | - $TRAVIS_BUILD_DIR/node_modules 16 | 17 | before_install: 18 | - docker build -t $CONTAINER_NAME . 19 | 20 | install: 21 | - travis_retry ./scripts/ci/install.sh 22 | 23 | stages: 24 | - build 25 | - test 26 | - deploy 27 | 28 | jobs: 29 | include: 30 | - script: 31 | - ./scripts/ci/check.sh 32 | - ./scripts/ci/build.sh 33 | stage: build 34 | - if: branch IN (master) 35 | script: 36 | - travis_retry ./scripts/ci/deploy.sh 37 | stage: deploy 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest@sha256:737b3a051de3db388aac1d4ef2e7cf6b96e6dcceb3e1f700c01e8c250d7d5500 2 | 3 | # install the latest version of yarn 4 | RUN curl -o- -L https://yarnpkg.com/install.sh | bash 5 | ENV PATH /root/.yarn/bin:/root/.config/yarn/global/node_modules/.bin:$PATH 6 | 7 | # check versions 8 | RUN yarn --version 9 | 10 | RUN yarn global add apollo 11 | RUN yarn global add serverless 12 | 13 | # expose development ports 14 | EXPOSE 3000:3000 15 | EXPOSE 3001:3001 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Load the environment variables 2 | ifdef ENV 3 | export ENV_FILE = .env.$(ENV) 4 | else 5 | export ENV_FILE = .env 6 | endif 7 | 8 | # Include the envionment variables in this Makefile 9 | include $(ENV_FILE) 10 | 11 | # Export these variables for docker-compose usage 12 | export CONTAINER_NAME = serverless-react/devbox 13 | export NODE_CONTAINER = \ 14 | --interactive \ 15 | --rm \ 16 | --tty \ 17 | --volume $(shell pwd):/var/task \ 18 | --workdir /var/task \ 19 | $(CONTAINER_NAME) 20 | 21 | build-server: 22 | @docker run --env-file $(ENV_FILE) $(NODE_CONTAINER) yarn run build:server 23 | 24 | build-static: 25 | @docker run --env-file $(ENV_FILE) $(NODE_CONTAINER) yarn run build:static 26 | 27 | deploy-master: 28 | @make stop-docker 29 | @docker run --env-file .env.master $(NODE_CONTAINER) yarn run tsc 30 | @docker run --env-file .env.master $(NODE_CONTAINER) yarn run tslint 31 | @docker run \ 32 | --env-file .env.master \ 33 | --env "BABEL_ENV=production" \ 34 | --env "NODE_ENV=production" \ 35 | $(NODE_CONTAINER) yarn run build:static 36 | @docker run \ 37 | --env-file .env.master \ 38 | --env "BABEL_ENV=serverless" \ 39 | --env "NODE_ENV=production" \ 40 | $(NODE_CONTAINER) serverless deploy --stage master 41 | @docker run \ 42 | --env-file .env.master \ 43 | $(NODE_CONTAINER) serverless client deploy --no-confirm --stage master 44 | 45 | check: 46 | @make tsc 47 | @make tslint 48 | 49 | dev: 50 | @docker run --env-file $(ENV_FILE) $(NODE_CONTAINER) /bin/bash 51 | 52 | devbox: 53 | @docker build --no-cache --tag $(CONTAINER_NAME) . 54 | 55 | install: 56 | @docker run $(NODE_CONTAINER) yarn install 57 | 58 | profile: 59 | @docker run --env-file $(ENV_FILE) $(NODE_CONTAINER) yarn run profile 60 | 61 | start: 62 | @docker-compose -f docker-compose.yml down --remove-orphans --volumes 63 | @docker-compose -f docker-compose.yml up -d --no-recreate --remove-orphans 64 | 65 | start-hard: 66 | rm -rf node_modules 67 | rm -f yarn.lock 68 | make devbox 69 | make install 70 | make start 71 | 72 | stop: 73 | @docker-compose -f docker-compose.yml down --remove-orphans --volumes 74 | 75 | stop-docker: 76 | @docker ps -aq | xargs docker stop 77 | @docker ps -aq | xargs docker rm 78 | 79 | tsc: 80 | @docker run --env-file $(ENV_FILE) $(NODE_CONTAINER) yarn run tsc 81 | 82 | tslint: 83 | @docker run --env-file $(ENV_FILE) $(NODE_CONTAINER) yarn run tslint 84 | 85 | tslint-fix: 86 | @docker run --env-file $(ENV_FILE) $(NODE_CONTAINER) yarn run tslint:fix 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Serverless React

2 |

3 | 4 | Travis Badge 5 | 6 | 7 | Standard Code Style Badge 8 | 9 | 10 | Renovate App Badge 11 | 12 |

13 | 14 | AWS Serverless React Project Template 15 | -------------------------------------------------------------------------------- /config/webpack/webpack.config.server.js: -------------------------------------------------------------------------------- 1 | const LoadablePlugin = require('@loadable/webpack-plugin') 2 | const CircularDependencyPlugin = require('circular-dependency-plugin') 3 | const path = require('path') 4 | const slsw = require('serverless-webpack') 5 | const webpack = require('webpack') 6 | const nodeExternals = require('webpack-node-externals') 7 | 8 | const config = { 9 | mode: slsw.lib.webpack.isLocal ? 'development' : 'production', 10 | target: 'node', 11 | entry: slsw.lib.entries, 12 | externals: [nodeExternals()], 13 | output: { 14 | filename: '[name].js', 15 | libraryTarget: 'commonjs2', 16 | path: path.resolve(process.cwd(), '.webpack'), 17 | publicPath: '/static/' 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | enforce: 'pre', 23 | exclude: /node_modules/, 24 | test: /\.(ts|tsx)$/, 25 | use: [ 26 | { loader: require.resolve('tslint-loader') } 27 | ] 28 | }, 29 | { 30 | exclude: /node_modules/, 31 | test: /\.(graphql|gql)$/, 32 | use: [ 33 | { loader: require.resolve('graphql-tag/loader') } 34 | ] 35 | }, 36 | { 37 | exclude: /node_modules/, 38 | test: /\.(ts|tsx)$/, 39 | use: [ 40 | { loader: require.resolve('babel-loader') }, 41 | ], 42 | }, 43 | ], 44 | }, 45 | plugins: [ 46 | new webpack.NamedModulesPlugin(), 47 | new webpack.NoEmitOnErrorsPlugin(), 48 | new CircularDependencyPlugin({ exclude: /node_modules/, failOnError: true }), 49 | new LoadablePlugin() 50 | ], 51 | resolve: { 52 | extensions: [ '.wasm', '.mjs', '.js', '.json', '.jsx', '.graphql', '.ts', '.tsx' ], 53 | modules: [path.resolve(process.cwd(), 'node_modules'), 'node_modules'] 54 | } 55 | } 56 | 57 | // patch serverless-offline, lambda always returns :UTC for the TZ and is a reserved variable 58 | if (slsw.lib.webpack.isLocal) { 59 | config.plugins.push(new webpack.DefinePlugin({ 'process.env.TZ': JSON.stringify(':UTC') })) 60 | } 61 | 62 | module.exports = config 63 | -------------------------------------------------------------------------------- /config/webpack/webpack.config.static.js: -------------------------------------------------------------------------------- 1 | const LoadablePlugin = require('@loadable/webpack-plugin') 2 | const CircularDependencyPlugin = require('circular-dependency-plugin') 3 | const path = require('path') 4 | const webpack = require('webpack') 5 | 6 | const pkg = require('../../package.json') 7 | 8 | module.exports = { 9 | mode: process.env.NODE_ENV || 'development', 10 | devtool: 'eval-source-map', 11 | target: 'web', 12 | entry: [ 13 | path.resolve(process.cwd(), 'src/platforms/browser/main.ts') 14 | ], 15 | externals: [], 16 | optimization: { 17 | // https://webpack.js.org/plugins/split-chunks-plugin/#split-chunks-example-2 18 | splitChunks: { 19 | cacheGroups: { 20 | commons: { 21 | chunks: 'all', 22 | name: 'vendors', 23 | test: /[\\/]node_modules[\\/]/ 24 | } 25 | } 26 | } 27 | }, 28 | output: { 29 | filename: '[name].js', 30 | path: path.resolve(process.cwd(), 'dist'), 31 | publicPath: '/static/' 32 | }, 33 | module: { 34 | rules: [ 35 | { 36 | enforce: 'pre', 37 | exclude: /node_modules/, 38 | test: /\.(ts|tsx)$/, 39 | use: [ 40 | { loader: require.resolve('tslint-loader') } 41 | ] 42 | }, 43 | { 44 | exclude: /node_modules/, 45 | test: /\.(graphql|gql)$/, 46 | use: [ 47 | { loader: require.resolve('graphql-tag/loader') } 48 | ] 49 | }, 50 | { 51 | exclude: /node_modules/, 52 | test: /\.(ts|tsx)$/, 53 | use: [ 54 | { loader: require.resolve('babel-loader') }, 55 | ], 56 | }, 57 | ], 58 | }, 59 | plugins: [ 60 | new webpack.DefinePlugin([ 61 | 'API_DOMAIN_NAME', 62 | 'APP_DOMAIN_NAME' 63 | ].reduce((obj, key) => ({ ...obj, [`process.env.${key}`]: JSON.stringify(process.env[key]) }), {}), { 64 | 'process.env.PACKAGE_VERSION': pkg.version 65 | }), 66 | new webpack.NamedModulesPlugin(), 67 | new webpack.NoEmitOnErrorsPlugin(), 68 | new CircularDependencyPlugin({ exclude: /node_modules/, failOnError: true }), 69 | new LoadablePlugin() 70 | ], 71 | resolve: { 72 | extensions: ['.wasm', '.mjs', '.js', '.json', '.jsx', '.graphql', '.ts', '.tsx'], 73 | modules: [path.resolve(process.cwd(), 'node_modules'), 'node_modules'], 74 | }, 75 | devServer: { 76 | host: process.env.HOST || '0.0.0.0', 77 | port: 3000, 78 | proxy: { 79 | // This proxies the docker website.server service when running `make start` 80 | '/': 'http://website.server:3001' 81 | }, 82 | publicPath: '/static/' 83 | }, 84 | }; 85 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | website.server: 4 | container_name: serverless-react-ssr.server 5 | command: yarn run start:server 6 | env_file: 7 | - ${ENV_FILE} 8 | image: ${CONTAINER_NAME} 9 | ports: 10 | - 3001:3001 11 | restart: always 12 | volumes: 13 | - .:/var/task 14 | working_dir: /var/task 15 | 16 | website.static: 17 | container_name: serverless-react-ssr.static 18 | command: yarn run start:static 19 | env_file: 20 | - ${ENV_FILE} 21 | image: ${CONTAINER_NAME} 22 | ports: 23 | - 3000:3000 24 | restart: always 25 | volumes: 26 | - .:/var/task 27 | working_dir: /var/task 28 | -------------------------------------------------------------------------------- /handler.ts: -------------------------------------------------------------------------------- 1 | export { default as website } from './src/platforms/lambda/server' 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-react-ssr", 3 | "version": "0.1.0", 4 | "description": "Serverless React Project Template", 5 | "main": "handler.ts", 6 | "scripts": { 7 | "build:server": "BABEL_ENV=serverless NODE_ENV=production sls webpack", 8 | "build:static": "BABEL_ENV=production NODE_ENV=production webpack --config config/webpack/webpack.config.static.js", 9 | 10 | "profile": "BABEL_ENV=production NODE_ENV=production webpack --config config/webpack/webpack.config.static.js --profile --json > dist/stats.json && webpack-bundle-analyzer dist/stats.json dist --mode static --no-open --report dist/report.html", 11 | "start:server": "BABEL_ENV=serverless NODE_ENV=development sls offline start --dontPrintOutput --host 0.0.0.0 --port 3001", 12 | "start:static": "BABEL_ENV=development NODE_ENV=development webpack-dev-server --config config/webpack/webpack.config.static.js", 13 | "test": "NODE_ENV=test jest", 14 | "tsc": "tsc --project tsconfig.json", 15 | "tslint": "tslint --project tsconfig.json", 16 | "tslint:fix": "tslint --fix --project tsconfig.json" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/jgeschwendt/serverless-react-ssr.git" 21 | }, 22 | "keywords": [ 23 | "aws", 24 | "serverless", 25 | "react" 26 | ], 27 | "author": "jlg", 28 | "license": "SEE LICENSE IN LICENSE", 29 | "bugs": { 30 | "url": "https://github.com/jgeschwendt/serverless-react-ssr/issues" 31 | }, 32 | "homepage": "https://github.com/jgeschwendt/serverless-react-ssr#readme", 33 | "private": true, 34 | "resolutions": { 35 | "graphql": "14.5.8" 36 | }, 37 | "dependencies": { 38 | "@loadable/component": "5.11.0", 39 | "@loadable/server": "5.11.0", 40 | "apollo-cache-inmemory": "1.6.3", 41 | "apollo-client": "2.6.4", 42 | "apollo-link-http": "1.5.16", 43 | "apollo-link-schema": "1.2.4", 44 | "graphql": "14.5.8", 45 | "graphql-tag": "2.10.1", 46 | "isomorphic-fetch": "2.2.1", 47 | "react": "16.12.0", 48 | "react-apollo": "2.5.8", 49 | "react-dom": "16.12.0", 50 | "react-ga": "2.7.0", 51 | "react-helmet": "5.2.1", 52 | "react-redux": "5.1.2", 53 | "react-router": "5.1.2", 54 | "react-router-config": "5.1.1", 55 | "react-router-dom": "5.1.2", 56 | "redux": "4.0.4", 57 | "redux-form":"7.4.2", 58 | "sitemap": "2.2.0", 59 | "styled-components": "4.4.1" 60 | }, 61 | "devDependencies": { 62 | "@babel/core": "7.7.5", 63 | "@babel/preset-env": "7.7.5", 64 | "@babel/preset-react": "7.7.4", 65 | "@babel/preset-typescript": "7.7.4", 66 | "@loadable/babel-plugin": "5.11.0", 67 | "@loadable/webpack-plugin": "5.7.1", 68 | "@types/react": "16.9.15", 69 | "@types/react-dom": "16.9.4", 70 | "aws-sdk": "2.585.0", 71 | "babel-loader": "8.0.6", 72 | "babel-plugin-module-resolver": "3.2.0", 73 | "babel-plugin-styled-components": "1.10.6", 74 | "circular-dependency-plugin": "5.2.0", 75 | "jest": "24.9.0", 76 | "npm-run-all": "4.1.5", 77 | "serverless": "1.66.0", 78 | "serverless-finch": "2.4.3", 79 | "serverless-offline": "4.10.6", 80 | "serverless-webpack": "5.3.1", 81 | "tslint": "5.20.0", 82 | "tslint-config-standard": "8.0.1", 83 | "tslint-loader": "3.6.0", 84 | "tslint-react": "3.6.0", 85 | "typescript": "3.7.3", 86 | "webpack": "4.41.2", 87 | "webpack-bundle-analyzer": "3.6.0", 88 | "webpack-cli": "3.3.10", 89 | "webpack-dev-server": "3.9.0", 90 | "webpack-node-externals": "1.7.2" 91 | }, 92 | "peerDependencies": { 93 | "aws-sdk": "*" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /scripts/ci/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -u -e -o pipefail 4 | 5 | docker run \ 6 | --env ARTIFACTS_BUCKET=$ARTIFACTS_BUCKET \ 7 | --env API_DOMAIN_NAME=$API_DOMAIN_NAME \ 8 | --env APP_DOMAIN_NAME=$APP_DOMAIN_NAME \ 9 | --env APP_HOSTED_ZONE=$APP_HOSTED_ZONE \ 10 | --env APP_SSL_CERT_ARN=$APP_SSL_CERT_ARN \ 11 | --rm \ 12 | --tty \ 13 | --volume $TRAVIS_BUILD_DIR:/var/task \ 14 | --workdir /var/task \ 15 | $CONTAINER_NAME yarn run build:static 16 | 17 | docker run \ 18 | --env ARTIFACTS_BUCKET=$ARTIFACTS_BUCKET \ 19 | --env API_DOMAIN_NAME=$API_DOMAIN_NAME \ 20 | --env APP_DOMAIN_NAME=$APP_DOMAIN_NAME \ 21 | --env APP_HOSTED_ZONE=$APP_HOSTED_ZONE \ 22 | --env APP_SSL_CERT_ARN=$APP_SSL_CERT_ARN \ 23 | --rm \ 24 | --tty \ 25 | --volume $TRAVIS_BUILD_DIR:/var/task \ 26 | --workdir /var/task \ 27 | $CONTAINER_NAME yarn run build:server 28 | 29 | exit 0 30 | -------------------------------------------------------------------------------- /scripts/ci/check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -u -e -o pipefail 4 | 5 | docker run \ 6 | --rm \ 7 | --tty \ 8 | --volume $TRAVIS_BUILD_DIR:/var/task \ 9 | --workdir /var/task \ 10 | $CONTAINER_NAME yarn run tsc 11 | 12 | docker run \ 13 | --rm \ 14 | --tty \ 15 | --volume $TRAVIS_BUILD_DIR:/var/task \ 16 | --workdir /var/task \ 17 | $CONTAINER_NAME yarn run tslint 18 | 19 | exit 0 20 | -------------------------------------------------------------------------------- /scripts/ci/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -u -e -o pipefail 4 | 5 | # Don't deploy if this is a PR build 6 | if [[ ${TRAVIS_PULL_REQUEST:-} != "false" ]]; then 7 | echo "Skipping deploy, this is a PR build." 8 | exit 0 9 | fi 10 | 11 | if [ ${TRAVIS_BRANCH:-} == "master" ]; then 12 | docker run \ 13 | --env ARTIFACTS_BUCKET=$ARTIFACTS_BUCKET \ 14 | --env API_DOMAIN_NAME=$API_DOMAIN_NAME \ 15 | --env APP_DOMAIN_NAME=$APP_DOMAIN_NAME \ 16 | --env APP_HOSTED_ZONE=$APP_HOSTED_ZONE \ 17 | --env APP_SSL_CERT_ARN=$APP_SSL_CERT_ARN \ 18 | --env BABEL_ENV=production \ 19 | --env NODE_ENV=production \ 20 | --rm \ 21 | --tty \ 22 | --volume $TRAVIS_BUILD_DIR:/var/task \ 23 | --workdir /var/task \ 24 | $CONTAINER_NAME yarn run build:static 25 | 26 | docker run \ 27 | --env AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \ 28 | --env AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \ 29 | --env ARTIFACTS_BUCKET=$ARTIFACTS_BUCKET \ 30 | --env API_DOMAIN_NAME=$API_DOMAIN_NAME \ 31 | --env APP_DOMAIN_NAME=$APP_DOMAIN_NAME \ 32 | --env APP_HOSTED_ZONE=$APP_HOSTED_ZONE \ 33 | --env APP_SSL_CERT_ARN=$APP_SSL_CERT_ARN \ 34 | --env BABEL_ENV=serverless \ 35 | --env NODE_ENV=production \ 36 | --rm \ 37 | --tty \ 38 | --volume $TRAVIS_BUILD_DIR:/var/task \ 39 | --workdir /var/task \ 40 | $CONTAINER_NAME serverless deploy --stage master 41 | 42 | docker run \ 43 | --env AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \ 44 | --env AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \ 45 | --env ARTIFACTS_BUCKET=$ARTIFACTS_BUCKET \ 46 | --env API_DOMAIN_NAME=$API_DOMAIN_NAME \ 47 | --env APP_DOMAIN_NAME=$APP_DOMAIN_NAME \ 48 | --env APP_HOSTED_ZONE=$APP_HOSTED_ZONE \ 49 | --env APP_SSL_CERT_ARN=$APP_SSL_CERT_ARN \ 50 | --rm \ 51 | --tty \ 52 | --volume $TRAVIS_BUILD_DIR:/var/task \ 53 | --workdir /var/task \ 54 | $CONTAINER_NAME serverless client deploy --no-confirm --stage master 55 | 56 | exit 0 57 | fi 58 | 59 | exit 0 60 | -------------------------------------------------------------------------------- /scripts/ci/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -u -e -o pipefail 4 | 5 | docker run --rm --tty --volume $TRAVIS_BUILD_DIR:/var/task --workdir /var/task $CONTAINER_NAME yarn 6 | 7 | exit 0 8 | -------------------------------------------------------------------------------- /serverless-react-ssr.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-react-ssr 2 | 3 | frameworkVersion: '>=1.9.0 <2.0.0' 4 | 5 | custom: 6 | stage: ${opt:stage, self:provider.stage} 7 | client: 8 | bucketName: ${env:APP_DOMAIN_NAME} 9 | distributionFolder: dist 10 | webpack: 11 | includeModules: 12 | forceInclude: 13 | - react-dom # required through react-apollo (renderToStringWithData) 14 | packager: yarn 15 | webpackConfig: ./config/webpack/webpack.config.server.js 16 | 17 | package: 18 | individually: true 19 | 20 | plugins: 21 | - serverless-finch 22 | - serverless-webpack # https://github.com/serverless-heaven/serverless-webpack#usage-with-serverless-offline 23 | - serverless-offline 24 | - serverless-patches 25 | 26 | provider: 27 | name: aws 28 | region: us-east-1 29 | runtime: nodejs8.10 30 | deploymentBucket: ${env:ARTIFACTS_BUCKET} 31 | environment: 32 | API_DOMAIN_NAME: ${env:API_DOMAIN_NAME} 33 | STAGE: ${self:custom.stage} 34 | 35 | functions: 36 | website: 37 | name: serverless-react-${self:custom.stage} 38 | handler: handler.website 39 | events: 40 | - http: 41 | cors: true 42 | integration: lambda-proxy 43 | method: get 44 | path: / 45 | - http: 46 | cors: true 47 | integration: lambda-proxy 48 | method: get 49 | path: /{proxy+} 50 | 51 | resources: 52 | Resources: 53 | ApiGatewayBasePathMapping: 54 | DependsOn: ApiGatewayDomainName 55 | Type: AWS::ApiGateway::BasePathMapping 56 | Properties: 57 | BasePath: '' 58 | DomainName: ${env:APP_DOMAIN_NAME} 59 | RestApiId: 60 | Ref: ApiGatewayRestApi 61 | Stage: ${self:custom.stage} 62 | 63 | ApiGatewayDomainName: 64 | Type: AWS::ApiGateway::DomainName 65 | Properties: 66 | CertificateArn: ${env:APP_SSL_CERT_ARN} 67 | DomainName: ${env:APP_DOMAIN_NAME} 68 | 69 | ApiGatewayDNSRecords: 70 | Type: AWS::Route53::RecordSetGroup 71 | Properties: 72 | HostedZoneName: ${env:APP_HOSTED_ZONE}. 73 | RecordSets: 74 | - Name: ${env:APP_DOMAIN_NAME}. 75 | Type: A 76 | AliasTarget: 77 | DNSName: {'Fn::GetAtt': ApiGatewayDomainName.DistributionDomainName} 78 | HostedZoneId: Z2FDTNDATAQYW2 79 | - Name: ${env:APP_DOMAIN_NAME}. 80 | Type: AAAA 81 | AliasTarget: 82 | DNSName: {'Fn::GetAtt': ApiGatewayDomainName.DistributionDomainName} 83 | HostedZoneId: Z2FDTNDATAQYW2 84 | 85 | ApiGatewayStaticBucket: 86 | Type: AWS::S3::Bucket 87 | Properties: 88 | BucketName: ${env:APP_DOMAIN_NAME} 89 | 90 | ApiGatewayResourceStatic: 91 | Type: AWS::ApiGateway::Resource 92 | Properties: 93 | ParentId: {'Fn::GetAtt': [ApiGatewayRestApi, RootResourceId]} 94 | PathPart: 'static' 95 | RestApiId: {Ref: ApiGatewayRestApi} 96 | 97 | ApiGatewayResourceStaticProxyVar: 98 | Type: AWS::ApiGateway::Resource 99 | Properties: 100 | ParentId: {Ref: ApiGatewayResourceStatic} 101 | PathPart: '{proxy+}' 102 | RestApiId: {Ref: ApiGatewayRestApi} 103 | 104 | ApiGatewayIamRoleExecution: 105 | Type: AWS::IAM::Role 106 | Properties: 107 | AssumeRolePolicyDocument: 108 | Statement: 109 | Effect: Allow 110 | Principal: 111 | Service: [apigateway.amazonaws.com] 112 | Action: [sts:AssumeRole] 113 | Path: '/' 114 | Policies: 115 | - PolicyName: AllowApiGatewayS3Access 116 | PolicyDocument: 117 | Statement: 118 | Effect: Allow 119 | Resource: {'Fn::Join': ['', ['arn:aws:s3:::', {Ref: ApiGatewayStaticBucket}, '/*']]} 120 | Action: 121 | - s3:Get* 122 | - s3:List* 123 | 124 | ApiGatewayMethodStaticProxyVarGet: 125 | Type: AWS::ApiGateway::Method 126 | Properties: 127 | AuthorizationType: NONE 128 | HttpMethod: GET 129 | Integration: 130 | CacheKeyParameters: 131 | - method.request.path.proxy 132 | CacheNamespace: ApiGatewayMethodStaticProxyVarGetCacheNS 133 | Credentials: {'Fn::Join': ['', ['arn:aws:iam::', {Ref: AWS::AccountId}, ':role/', {Ref: ApiGatewayIamRoleExecution}]]} 134 | IntegrationHttpMethod: GET 135 | IntegrationResponses: 136 | - StatusCode: 200 137 | ResponseParameters: 138 | method.response.header.Timestamp: integration.response.header.Date 139 | method.response.header.Content-Length: integration.response.header.Content-Length 140 | method.response.header.Content-Type: integration.response.header.Content-Type 141 | - StatusCode: 400 142 | SelectionPattern: '4\d{2}' 143 | ResponseParameters: 144 | method.response.header.Content-Length: integration.response.header.Content-Length 145 | method.response.header.Content-Type: integration.response.header.Content-Type 146 | - StatusCode: 500 147 | SelectionPattern: '5\d{2}' 148 | ResponseParameters: 149 | method.response.header.Content-Length: integration.response.header.Content-Length 150 | method.response.header.Content-Type: integration.response.header.Content-Type 151 | PassthroughBehavior: WHEN_NO_MATCH 152 | RequestParameters: 153 | integration.request.path.proxy: method.request.path.proxy 154 | Type: AWS 155 | Uri: {'Fn::Join': ['', ['arn:aws:apigateway:', {Ref: AWS::Region}, ':s3:path/${env:APP_DOMAIN_NAME}', '/{proxy}']]} 156 | MethodResponses: 157 | - StatusCode: 200 158 | ResponseParameters: 159 | method.response.header.Timestamp: integration.response.header.Date 160 | method.response.header.Content-Length: integration.response.header.Content-Length 161 | method.response.header.Content-Type: integration.response.header.Content-Type 162 | - StatusCode: 400 163 | ResponseParameters: 164 | method.response.header.Content-Length: integration.response.header.Content-Length 165 | method.response.header.Content-Type: integration.response.header.Content-Type 166 | - StatusCode: 500 167 | ResponseParameters: 168 | method.response.header.Content-Length: integration.response.header.Content-Length 169 | method.response.header.Content-Type: integration.response.header.Content-Type 170 | RequestParameters: 171 | method.request.path.proxy: true 172 | ResourceId: {Ref: ApiGatewayResourceStaticProxyVar} 173 | RestApiId: {Ref: ApiGatewayRestApi} 174 | -------------------------------------------------------------------------------- /src/app/Root.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styled from 'styled-components' 3 | import { renderRoutes } from 'react-router-config' 4 | import { Link } from 'react-router-dom' 5 | 6 | const Nav = styled.nav` 7 | border: 1px solid #ddd; 8 | display: block; 9 | padding: 1rem; 10 | ` 11 | 12 | const A = styled(Link)` 13 | color: blue; 14 | display: inline-block; 15 | padding: 1rem; 16 | text-decoration: none; 17 | ` 18 | 19 | export default (props: any) => ( 20 |
21 |
22 | 27 |
28 |
{renderRoutes(props.route.routes)}
29 |
30 | ) 31 | -------------------------------------------------------------------------------- /src/app/addons/withTracker.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | // import * as ReactGA from 'react-ga' 3 | 4 | // ReactGA.initialize(process.env.GOOGLE_ANALYTICS_TRACKING_ID) 5 | 6 | export default (Component, options = {}) => { 7 | const trackPage = page => { 8 | console.log('[TRACKING] ', page) 9 | // ReactGA.set({ page, ...options }) 10 | // ReactGA.pageview(page) 11 | } 12 | 13 | return class extends React.Component { 14 | componentDidMount () { 15 | const { pathname, search, hash } = this.props.location 16 | trackPage(`${pathname}${search}${hash}`) 17 | } 18 | 19 | componentWillReceiveProps (nextProps) { 20 | const currentPage = this.props.location.pathname 21 | const nextPage = nextProps.location.pathname 22 | 23 | if (currentPage !== nextPage) { 24 | trackPage(nextPage) 25 | } 26 | } 27 | 28 | render () { 29 | return 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/components/AspectRatio.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlg-archive/serverless-react-ssr/e42159096e57f59bdd01e59bf3877ba5196c2eb8/src/app/components/AspectRatio.tsx -------------------------------------------------------------------------------- /src/app/components/Grid.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlg-archive/serverless-react-ssr/e42159096e57f59bdd01e59bf3877ba5196c2eb8/src/app/components/Grid.tsx -------------------------------------------------------------------------------- /src/app/components/Link.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlg-archive/serverless-react-ssr/e42159096e57f59bdd01e59bf3877ba5196c2eb8/src/app/components/Link.tsx -------------------------------------------------------------------------------- /src/app/components/List.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlg-archive/serverless-react-ssr/e42159096e57f59bdd01e59bf3877ba5196c2eb8/src/app/components/List.tsx -------------------------------------------------------------------------------- /src/app/components/Loadable.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export default () => ( 4 |
@loadable/component
5 | ) 6 | -------------------------------------------------------------------------------- /src/app/components/Table.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlg-archive/serverless-react-ssr/e42159096e57f59bdd01e59bf3877ba5196c2eb8/src/app/components/Table.tsx -------------------------------------------------------------------------------- /src/app/components/Typography.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlg-archive/serverless-react-ssr/e42159096e57f59bdd01e59bf3877ba5196c2eb8/src/app/components/Typography.tsx -------------------------------------------------------------------------------- /src/app/components/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlg-archive/serverless-react-ssr/e42159096e57f59bdd01e59bf3877ba5196c2eb8/src/app/components/index.ts -------------------------------------------------------------------------------- /src/app/containers/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export default () => ( 4 |
5 |

Home

6 |
7 | ) 8 | -------------------------------------------------------------------------------- /src/app/containers/Page1/index.tsx: -------------------------------------------------------------------------------- 1 | import loadable from '@loadable/component' 2 | import * as React from 'react' 3 | 4 | const Loadable = loadable(() => import('../../components/Loadable')) 5 | 6 | export default () => ( 7 |
8 |

Page 1

9 | 10 |
11 | ) 12 | -------------------------------------------------------------------------------- /src/app/containers/Page2/index.tsx: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag' 2 | import * as React from 'react' 3 | import { graphql } from 'react-apollo' 4 | 5 | export default graphql(gql` 6 | query Feed { 7 | feed { 8 | author { 9 | name 10 | } 11 | title 12 | text 13 | } 14 | } 15 | `)(props => ( 16 |
17 |

Page 2

18 |
{JSON.stringify(props, null, 2)}
19 |
20 | )) 21 | -------------------------------------------------------------------------------- /src/app/reducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { reducer as form } from 'redux-form' 3 | 4 | export default combineReducers({ 5 | form 6 | }) 7 | -------------------------------------------------------------------------------- /src/app/routes.ts: -------------------------------------------------------------------------------- 1 | import loadable from '@loadable/component' 2 | import withTracker from './addons/withTracker' 3 | import Main from './Root' 4 | 5 | import Home from './containers/Home' 6 | import Page1 from './containers/Page1' 7 | import Page2 from './containers/Page2' 8 | 9 | // const Home = loadable(() => import('./containers/Home')) 10 | // const Page1 = loadable(() => import('./containers/Page1')) 11 | // const Page2 = loadable(() => import('./containers/Page2')) 12 | 13 | const routes = [{ 14 | component: Main, 15 | routes: [ 16 | { 17 | component: withTracker(Home), 18 | exact: true, 19 | path: '/' 20 | }, 21 | { 22 | component: withTracker(Page1), 23 | exact: true, 24 | path: '/page1' 25 | }, 26 | { 27 | component: withTracker(Page2), 28 | exact: true, 29 | path: '/page2' 30 | }, 31 | { 32 | component: withTracker(Home), 33 | path: '/*' 34 | } 35 | ] 36 | }] 37 | 38 | export default routes 39 | -------------------------------------------------------------------------------- /src/app/schema/fragments/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlg-archive/serverless-react-ssr/e42159096e57f59bdd01e59bf3877ba5196c2eb8/src/app/schema/fragments/index.ts -------------------------------------------------------------------------------- /src/app/schema/mutations/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlg-archive/serverless-react-ssr/e42159096e57f59bdd01e59bf3877ba5196c2eb8/src/app/schema/mutations/index.ts -------------------------------------------------------------------------------- /src/app/schema/queries/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlg-archive/serverless-react-ssr/e42159096e57f59bdd01e59bf3877ba5196c2eb8/src/app/schema/queries/index.ts -------------------------------------------------------------------------------- /src/app/styled/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlg-archive/serverless-react-ssr/e42159096e57f59bdd01e59bf3877ba5196c2eb8/src/app/styled/index.ts -------------------------------------------------------------------------------- /src/app/styled/media.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlg-archive/serverless-react-ssr/e42159096e57f59bdd01e59bf3877ba5196c2eb8/src/app/styled/media.ts -------------------------------------------------------------------------------- /src/app/theme.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | color: '#996699' 3 | } 4 | -------------------------------------------------------------------------------- /src/platforms/browser/apollo.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient } from 'apollo-client' 2 | import { HttpLink } from 'apollo-link-http' 3 | import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory' 4 | import fetch from 'isomorphic-fetch' 5 | 6 | export default class extends ApolloClient { 7 | constructor ({ uri }) { 8 | super({ 9 | cache: new InMemoryCache({ 10 | fragmentMatcher: new IntrospectionFragmentMatcher({ 11 | introspectionQueryResultData: window.__APOLLO_SCHEMA__ 12 | }) 13 | }).restore(window.__APOLLO_STATE__), 14 | link: new HttpLink({ fetch, uri }), 15 | ssrForceFetchDelay: 100, 16 | connectToDevTools: true 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/platforms/browser/components/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Provider } from 'react-redux' 3 | import { ApolloProvider } from 'react-apollo' 4 | import { renderRoutes } from 'react-router-config' 5 | import { BrowserRouter } from 'react-router-dom' 6 | import { ThemeProvider } from 'styled-components' 7 | 8 | export default ({ client, location, routes, store, theme }) => ( 9 | 10 | 11 | 12 | 13 | {renderRoutes(routes)} 14 | 15 | 16 | 17 | 18 | ) 19 | -------------------------------------------------------------------------------- /src/platforms/browser/hydrate.ts: -------------------------------------------------------------------------------- 1 | import { loadableReady } from '@loadable/component' 2 | import { createElement } from 'react' 3 | import { hydrate } from 'react-dom' 4 | import { compose, createStore } from 'redux' 5 | 6 | import ApolloClient from './apollo' 7 | 8 | import reducer from '../../app/reducer' 9 | import routes from '../../app/routes' 10 | import theme from '../../app/theme' 11 | 12 | import App from './components/App' 13 | 14 | const store = createStore(reducer , window.__REDUX_STATE__, (window['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__'] || compose)()) 15 | 16 | export default (element) => { 17 | loadableReady(() => { 18 | hydrate(createElement(App, { 19 | client: new ApolloClient({ uri: `https:${process.env.API_DOMAIN_NAME}` }), 20 | location: window.location.pathname || '/', 21 | routes, 22 | store, 23 | theme 24 | }), element) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /src/platforms/browser/main.ts: -------------------------------------------------------------------------------- 1 | import { default as Hydrate } from './hydrate' 2 | 3 | Hydrate(document.getElementById('root')) 4 | -------------------------------------------------------------------------------- /src/platforms/lambda/apollo.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient } from 'apollo-client' 2 | import { HttpLink } from 'apollo-link-http' 3 | import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory' 4 | import fetch from 'isomorphic-fetch' 5 | 6 | export default class extends ApolloClient { 7 | constructor ({ introspectionQueryResultData, uri }) { 8 | super({ 9 | cache: new InMemoryCache({ 10 | fragmentMatcher: new IntrospectionFragmentMatcher({ 11 | introspectionQueryResultData 12 | }) 13 | }), 14 | link: new HttpLink({ fetch, uri }), 15 | ssrMode: true 16 | }) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/platforms/lambda/cache.ts: -------------------------------------------------------------------------------- 1 | import ApolloClient from './apollo' 2 | import fetchFragmentMatcher from './fragment-matcher' 3 | 4 | const cache = async () => { 5 | const uri = `https://${process.env.API_DOMAIN_NAME}` 6 | 7 | const introspectionQueryResultData = await fetchFragmentMatcher({ uri }) 8 | const client = new ApolloClient({ introspectionQueryResultData, uri }) 9 | 10 | return { 11 | client, 12 | introspectionQueryResultData 13 | } 14 | } 15 | 16 | export default cache() 17 | -------------------------------------------------------------------------------- /src/platforms/lambda/components/App.tsx: -------------------------------------------------------------------------------- 1 | import { ChunkExtractorManager } from '@loadable/server' 2 | import * as React from 'react' 3 | import { ApolloProvider } from 'react-apollo' 4 | import { Provider } from 'react-redux' 5 | import { StaticRouter } from 'react-router-dom' 6 | import { renderRoutes } from 'react-router-config' 7 | import { StyleSheetManager, ThemeProvider } from 'styled-components' 8 | 9 | export default ({ 10 | client, 11 | extractor, 12 | location, 13 | routes, 14 | sheet, 15 | store, 16 | theme 17 | }) => { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | {renderRoutes(routes)} 26 | 27 | 28 | 29 | 30 | 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/platforms/lambda/document.ts: -------------------------------------------------------------------------------- 1 | export default ({ 2 | content, 3 | extractor, 4 | helmet, 5 | introspectionQueryResultData, 6 | sheet, 7 | state, 8 | store 9 | }) => ` 10 | 11 | 12 | 13 | 14 | ${helmet.title.toString()} 15 | ${helmet.meta.toString()} 16 | 17 | ${helmet.link.toString()} 18 | ${sheet.getStyleTags()} 19 | 20 | 21 |
${content}
22 | 27 | ${extractor.getScriptTags()} 28 | 29 | 30 | 31 | 32 | ` 33 | -------------------------------------------------------------------------------- /src/platforms/lambda/fragment-matcher.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch' 2 | 3 | const query = `{ 4 | __schema { 5 | types { 6 | kind 7 | name 8 | possibleTypes { 9 | name 10 | } 11 | } 12 | } 13 | }` 14 | 15 | // https://www.apollographql.com/docs/react/advanced/fragments.html 16 | export default ({ uri }) => fetch(uri, { 17 | body: JSON.stringify({ query }), 18 | headers: { 19 | 'Content-Type': 'application/json' 20 | }, 21 | method: 'POST' 22 | }) 23 | .then(result => result.json()) 24 | .then(result => { 25 | if (result && result.data && result.data.__schema && result.data.__schema.types) { 26 | // here we're filtering out any type information unrelated to unions or interfaces 27 | result.data.__schema.types = result.data.__schema.types.filter( 28 | type => type.possibleTypes !== null 29 | ) 30 | return result.data 31 | } 32 | return null 33 | }) 34 | -------------------------------------------------------------------------------- /src/platforms/lambda/middleware/robots.ts: -------------------------------------------------------------------------------- 1 | export default async (event, context, callback) => { 2 | callback(null, { 3 | body: process.env.STAGE === 'prod' 4 | ? 'User-agent: *\nDisallow:\nSitemap: https://' + process.env.APP_DOMAIN_NAME + '/sitemap.xml\n' 5 | : 'User-agent: *\nDisallow: /\n', 6 | headers: { 7 | 'Access-Control-Allow-Origin': '*' 8 | }, 9 | statusCode: 200 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /src/platforms/lambda/middleware/sitemap.ts: -------------------------------------------------------------------------------- 1 | import * as sitemap from 'sitemap' 2 | 3 | const sitemapDocument = async () => new Promise((resolve, reject) => { 4 | const mapping = sitemap.createSitemap({ 5 | cacheTime: 600000, 6 | hostname: process.env.API_DOMAIN_NAME, 7 | urls: [ 8 | { url: '/' } 9 | ] 10 | }) 11 | 12 | mapping.toXML((error, xml) => { 13 | if (error) { 14 | reject(error) 15 | } else { 16 | resolve(xml) 17 | } 18 | }) 19 | }) 20 | 21 | export default async (event, context, callback) => { 22 | callback(null, { 23 | statusCode: 200, 24 | headers: { 25 | 'Content-Type': 'application/xml', 26 | 'Access-Control-Allow-Origin': '*' 27 | }, 28 | body: await sitemapDocument() 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /src/platforms/lambda/render.ts: -------------------------------------------------------------------------------- 1 | import { ChunkExtractor } from '@loadable/server' 2 | import { resolve } from 'path' 3 | import { createElement } from 'react' 4 | import { renderToStringWithData } from 'react-apollo' 5 | import { Helmet } from 'react-helmet' 6 | import { createStore } from 'redux' 7 | import { ServerStyleSheet } from 'styled-components' 8 | 9 | import LambdaContainerCache from './cache' 10 | 11 | import App from './components/App' 12 | import HTML from './document' 13 | 14 | import reducer from '../../app/reducer' 15 | import routes from '../../app/routes' 16 | import theme from '../../app/theme' 17 | 18 | export default async ({ location }) => { 19 | const { client, introspectionQueryResultData } = await LambdaContainerCache 20 | const statsFile = process.env.IS_OFFLINE 21 | ? resolve('.webpack/service/loadable-stats.json') 22 | : resolve('loadable-stats.json') 23 | 24 | const store = createStore(reducer) 25 | await client.resetStore() // the cache persists between lambda requests 26 | 27 | const extractor = new ChunkExtractor({ entrypoints: [], statsFile }) 28 | 29 | const sheet = new ServerStyleSheet() 30 | 31 | const content = await renderToStringWithData( 32 | createElement(App, { 33 | client, 34 | extractor, 35 | location, 36 | routes, 37 | sheet: sheet.instance, 38 | store, 39 | theme 40 | }) 41 | ) 42 | 43 | const helmet = Helmet.renderStatic() 44 | const state = client.extract() 45 | 46 | return HTML({ 47 | content, 48 | extractor, 49 | introspectionQueryResultData, 50 | helmet, 51 | sheet, 52 | state, 53 | store 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /src/platforms/lambda/server.ts: -------------------------------------------------------------------------------- 1 | import render from './render' 2 | import robots from './middleware/robots' 3 | import sitemap from './middleware/sitemap' 4 | 5 | const server = async (event, context, callback) => { 6 | switch (event.path) { 7 | case '/sitemap.xml': await sitemap(event, context, callback); return 8 | case '/robots.txt': await robots(event, context, callback); return 9 | default: 10 | const html = await render({ location: event.path }) 11 | const body = `${html}` 12 | 13 | callback(null, { 14 | body, 15 | statusCode: 200, 16 | headers: { 17 | 'Access-Control-Allow-Origin': '*', 18 | 'Content-Length': body.length, 19 | 'Content-Type': 'text/html' 20 | } 21 | }) 22 | } 23 | } 24 | 25 | export default (event, context, callback) => { 26 | server(event, context, callback) 27 | .catch(error => console.error('Error:\n\n', error, '\n\n')) 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "jsx": "preserve", 7 | "lib": [ 8 | "dom", 9 | "esnext" 10 | ], 11 | "module": "commonjs", 12 | "moduleResolution": "node", 13 | "noEmit": true, 14 | "pretty": true, 15 | "skipLibCheck": true, 16 | "target": "esnext", 17 | }, 18 | "include": [ 19 | "src/**/*", 20 | "typings/**/*" 21 | ], 22 | "exclude": [ 23 | "node_modules" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-react", 4 | "tslint-config-standard" 5 | ], 6 | "rules": { 7 | "await-promise": false, 8 | "no-floating-promises": false, 9 | "no-implicit-dependencies": false, 10 | "no-unnecessary-qualifier": false, 11 | "no-unnecessary-type-assertion": false, 12 | "no-unused-variable": false, 13 | "no-use-before-declare": false, 14 | "return-undefined": false, 15 | "strict-type-predicates": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /typings/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.gql' { 2 | const content: any 3 | export default content 4 | } 5 | 6 | declare module '*.graphql' { 7 | const content: any 8 | export default content 9 | } 10 | 11 | declare module '*.json' { 12 | const content: any 13 | export default content 14 | } 15 | 16 | interface Window { 17 | __APOLLO_SCHEMA__: any 18 | __APOLLO_STATE__: any 19 | __REDUX_STATE__: any 20 | } 21 | --------------------------------------------------------------------------------