├── src ├── .gitignore ├── public │ └── favicon.ico ├── tsconfig.json ├── app │ ├── routes │ │ ├── index.css │ │ ├── 404.tsx │ │ ├── 500.tsx │ │ └── index.tsx │ ├── global.css │ ├── tsconfig.json │ ├── entry-browser.tsx │ ├── App.tsx │ └── entry-server.tsx ├── loaders │ ├── global.ts │ ├── tsconfig.json │ └── routes │ │ └── index.ts ├── config │ └── shared-tsconfig.json ├── server.js └── remix.config.js ├── .npmignore ├── .npmrc ├── jest.config.js ├── .gitignore ├── cdk.json ├── cdk ├── app │ └── stack.ts └── pipeline.ts ├── test └── remix-cdk-starter.test.ts ├── bin └── app.ts ├── tsconfig.json ├── README.md └── package.json /src/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | //npm.remix.run/:_authToken=${REMIX_TOKEN} 2 | @remix-run:registry=https://npm.remix.run 3 | -------------------------------------------------------------------------------- /src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shortjared/remix-cdk-starter/HEAD/src/public/favicon.ico -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "app" }, 5 | { "path": "loaders" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/test'], 3 | testMatch: ['**/*.test.ts'], 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest' 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/app/routes/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | * when the user visits this page, this style will apply, when they leave, it 3 | * will get unloaded, so don't worry so much about conflicting styles between 4 | * pages! 5 | */ 6 | -------------------------------------------------------------------------------- /src/loaders/global.ts: -------------------------------------------------------------------------------- 1 | import type { DataLoader } from "@remix-run/core"; 2 | 3 | let loader: DataLoader = async () => { 4 | return { 5 | date: new Date() 6 | }; 7 | }; 8 | 9 | export = loader; 10 | -------------------------------------------------------------------------------- /src/loaders/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../config/shared-tsconfig.json", 3 | "include": ["**/*"], 4 | "compilerOptions": { 5 | "outDir": "../build/loaders", 6 | "module": "commonjs" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !src/* 3 | !jest.config.js 4 | *.d.ts 5 | node_modules 6 | 7 | # CDK asset staging directory 8 | .cdk.staging 9 | cdk.out 10 | 11 | # Parcel default cache directory 12 | .parcel-cache 13 | -------------------------------------------------------------------------------- /src/loaders/routes/index.ts: -------------------------------------------------------------------------------- 1 | import type { DataLoader } from "@remix-run/core"; 2 | 3 | let loader: DataLoader = async () => { 4 | return { 5 | message: "this is awesome 😎" 6 | }; 7 | }; 8 | 9 | export = loader; 10 | -------------------------------------------------------------------------------- /src/app/global.css: -------------------------------------------------------------------------------- 1 | :focus:not(:focus-visible) { 2 | outline: none; 3 | } 4 | 5 | body { 6 | font-family: sans-serif; 7 | } 8 | 9 | footer { 10 | text-align: center; 11 | color: #ccc; 12 | padding-top: 80px; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/routes/404.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function meta() { 4 | return { title: "Ain't nothing here" }; 5 | } 6 | 7 | export default function FourOhFour() { 8 | return ( 9 |
10 |

404

11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../config/shared-tsconfig.json", 3 | "compilerOptions": { 4 | // The Remix compiler takes care of compiling everything in the `app` 5 | // directory, so we don't need to emit anything with `tsc`. 6 | "noEmit": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node bin/app.ts", 3 | "context": { 4 | "@aws-cdk/core:enableStackNameDuplicates": "true", 5 | "aws-cdk:enableDiffNoFail": "true", 6 | "@aws-cdk/core:stackRelativeExports": "true", 7 | "@aws-cdk/core:newStyleStackSynthesis": true 8 | } 9 | } -------------------------------------------------------------------------------- /src/config/shared-tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react", 5 | "moduleResolution": "node", 6 | "target": "es2019", 7 | "strict": true, 8 | "skipLibCheck": true 9 | }, 10 | "exclude": ["node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /cdk/app/stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from '@aws-cdk/core'; 2 | 3 | export class RemixAppStack extends cdk.Stack { 4 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 5 | super(scope, id, props); 6 | 7 | // The code that defines your stack goes here 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/routes/500.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function meta() { 4 | return { title: "Shoot..." }; 5 | } 6 | 7 | export default function FiveHundred() { 8 | console.error("Check your server terminal output"); 9 | 10 | return ( 11 |
12 |

500

13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /test/remix-cdk-starter.test.ts: -------------------------------------------------------------------------------- 1 | import { expect as expectCDK, matchTemplate, MatchStyle } from '@aws-cdk/assert'; 2 | import * as cdk from '@aws-cdk/core'; 3 | import * as RemixAppStack from '../cdk/app/stack'; 4 | 5 | test('Empty Stack', () => { 6 | const app = new cdk.App(); 7 | // WHEN 8 | const stack = new RemixAppStack.RemixAppStack(app, 'MyTestStack'); 9 | // THEN 10 | expectCDK(stack).to(matchTemplate({ 11 | "Resources": {} 12 | }, MatchStyle.EXACT)) 13 | }); 14 | -------------------------------------------------------------------------------- /bin/app.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from '@aws-cdk/core'; 4 | import * as project from '../package.json'; 5 | 6 | import { RemixAppStack } from '../cdk/app/stack'; 7 | import { RemixPipelineStack } from '../cdk/pipeline'; 8 | 9 | const app = new cdk.App(); 10 | 11 | new RemixAppStack(app, 'RemixApp', {}); 12 | new RemixPipelineStack(app, 'RemixAppPipeline', { 13 | env: { 14 | account: project.aws.pipeline.account, 15 | region: project.aws.pipeline.region 16 | } 17 | }); -------------------------------------------------------------------------------- /src/app/entry-browser.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import Remix from "@remix-run/react/browser"; 4 | 5 | import App from "./App"; 6 | 7 | ReactDOM.hydrate( 8 | // @types/react-dom says the 2nd argument to ReactDOM.hydrate() must be a 9 | // `Element | DocumentFragment | null` but React 16 allows you to pass the 10 | // `document` object as well. This is a bug in @types/react-dom that we can 11 | // safely ignore for now. 12 | // @ts-ignore 13 | 14 | 15 | , 16 | document 17 | ); 18 | -------------------------------------------------------------------------------- /src/app/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Meta, Scripts, Styles, Routes, useGlobalData } from "@remix-run/react"; 3 | 4 | export default function App() { 5 | let data = useGlobalData(); 6 | 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

This page was rendered at {data.date.toLocaleString()}

19 |
20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRouteData } from "@remix-run/react"; 3 | 4 | export function meta() { 5 | return { 6 | title: "Remix Starter", 7 | description: "Welcome to remix!" 8 | }; 9 | } 10 | 11 | export default function Index() { 12 | let data = useRouteData(); 13 | 14 | return ( 15 |
16 |

Welcome to Remix!

17 |

18 | Check out the docs to get 19 | started. 20 |

21 |

Message from the loader: {data.message}

22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const morgan = require("morgan"); 3 | const { createRequestHandler } = require("@remix-run/express"); 4 | 5 | let app = express(); 6 | 7 | if (process.env.NODE_ENV === "development") { 8 | app.use(morgan("dev")); 9 | } 10 | 11 | app.use(express.static("public")); 12 | 13 | app.get( 14 | "*", 15 | createRequestHandler({ 16 | getLoadContext() { 17 | // Whatever you return here will be passed as `context` to your loaders. 18 | } 19 | }) 20 | ); 21 | 22 | let port = process.env.PORT || 3000; 23 | 24 | app.listen(port, () => { 25 | console.log(`Express server started on http://localhost:${port}`); 26 | }); 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "resolveJsonModule": true, 21 | "typeRoots": ["./node_modules/@types"] 22 | }, 23 | "exclude": ["cdk.out", "src"] 24 | } 25 | -------------------------------------------------------------------------------- /src/app/entry-server.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOMServer from "react-dom/server"; 3 | import type { EntryContext } from "@remix-run/core"; 4 | import Remix from "@remix-run/react/server"; 5 | 6 | import App from "./App"; 7 | 8 | export default function handleRequest( 9 | request: Request, 10 | responseStatusCode: number, 11 | responseHeaders: Headers, 12 | remixContext: EntryContext 13 | ) { 14 | let markup = ReactDOMServer.renderToString( 15 | 16 | 17 | 18 | ); 19 | 20 | return new Response("" + markup, { 21 | status: responseStatusCode, 22 | headers: { 23 | ...Object.fromEntries(responseHeaders), 24 | "Content-Type": "text/html" 25 | } 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remix && AWS CDK == ❤️ 2 | 3 | Git clone this repo, `npm install` get to hacking with `npm run dev`. Make sure you have our `REMIX_TOKEN` (license) exported in your env somewhere so you can download the remix resources from the private npm registry. 4 | 5 | ## Getting started 6 | 7 | Update the repository information in `package.json`. We use that to make a couple other things easier. 8 | 9 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 10 | 11 | ## Useful commands 12 | 13 | `npm run dev` start remix server process and watchers 14 | `npm run deploy:dev` deploys the full app stack (including remix) for the dev environment 15 | 16 | 17 | ## CI / CD Pipeline 18 | 19 | One of the great features of the CDK is the pipelines construct to build a pretty thourough experience to flow your CDK app from a source provider through to all your different envs. 20 | 21 | Make sure you've configured the package.json `aws.piplines` section with the correct account and region. Then you'll need to boostrap things. (Only necessary for the first CDK app in this account / region.) 22 | 23 | `npm run cdk bootstrap` 24 | 25 | Add a `GITHUB_TOKEN` in Secrets Manager 26 | 27 | Then once, and only once, you'll need to run `npm run deploy:pipeline`. Why only once? The pipeline stack is self-mutating on pushes to you main branch. 28 | 29 | Everything is Infrastructure as Code (IaC) and self-contained in this repo. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remix-cdk-starter", 3 | "version": "0.1.0", 4 | "bin": { 5 | "remix-cdk-starter": "bin/remix-cdk-starter.js" 6 | }, 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/shortjared/remix-cdk-starter.git" 10 | }, 11 | "aws": { 12 | "pipeline": { 13 | "account": "241707393264", 14 | "region": "us-east-1" 15 | } 16 | }, 17 | "scripts": { 18 | "build": "tsc", 19 | "cdk": "cdk", 20 | "deploy": "npm:remix:build && cdk deploy RemixApp -O .stack-outputs.json", 21 | "deploy:pipeline": "cdk deploy RemixAppPipeline", 22 | "dev": "concurrently npm:remix:run npm:remix:tsc npm:remix:nodemon", 23 | "remix:build": "cd src && cross-env NODE_ENV=production remix build && tsc -b", 24 | "remix:nodemon": "cd src && nodemon --ignore app server.js", 25 | "remix:run": "cd src && remix run", 26 | "remix:tsc": "cd src && tsc -b -w", 27 | "test": "jest", 28 | "update:cdk": "ncu -u /.*aws-cdk.*/", 29 | "watch": "tsc -w" 30 | }, 31 | "dependencies": { 32 | "@aws-cdk/core": "1.71.0", 33 | "@remix-run/cli": "0.6.2", 34 | "@remix-run/express": "0.6.2", 35 | "@remix-run/loader": "0.6.2", 36 | "@remix-run/react": "0.6.2", 37 | "express": "4.17.1", 38 | "morgan": "1.10.0", 39 | "react": "16.14.0", 40 | "react-dom": "16.14.0", 41 | "react-router": "6.0.0-beta.0", 42 | "react-router-dom": "6.0.0-beta.0", 43 | "source-map-support": "0.5.16" 44 | }, 45 | "devDependencies": { 46 | "@aws-cdk/assert": "1.71.0", 47 | "@aws-cdk/pipelines": "^1.71.0", 48 | "@types/jest": "26.0.10", 49 | "@types/node": "10.17.27", 50 | "@types/react": "16.9.53", 51 | "@types/react-dom": "16.9.8", 52 | "aws-cdk": "1.71.0", 53 | "concurrently": "5.3.0", 54 | "cross-env": "7.0.2", 55 | "jest": "26.4.2", 56 | "nodemon": "2.0.5", 57 | "ts-jest": "26.2.0", 58 | "ts-node": "8.1.0", 59 | "typescript": "4.0.3" 60 | }, 61 | "engines": { 62 | "node": "12" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/remix.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * The path to the `app` directory, relative to remix.config.js. Defaults to 4 | * "app". All code in this directory is part of your app and will be compiled 5 | * by Remix. 6 | * 7 | */ 8 | appDirectory: "app", 9 | 10 | /** 11 | * A hook for defining custom routes based on your own file conventions. This 12 | * is not required, but may be useful if you have custom/advanced routing 13 | * requirements. 14 | */ 15 | // routes(defineRoutes) { 16 | // return defineRoutes(route => { 17 | // route( 18 | // // The URL path for this route. 19 | // "/pages/one", 20 | // // The path to this route's component file, relative to `appDirectory`. 21 | // "pages/one", 22 | // // Options: 23 | // { 24 | // // The path to this route's data loader, relative to `loadersDirectory`. 25 | // loader: "...", 26 | // // The path to this route's styles file, relative to `appDirectory`. 27 | // styles: "..." 28 | // } 29 | // ); 30 | // }); 31 | // }, 32 | 33 | /** 34 | * The path to the `loaders` directory, relative to remix.config.js. Defaults 35 | * to "loaders". The loaders directory contains "data loaders" for your 36 | * routes. 37 | */ 38 | loadersDirectory: "build/loaders", 39 | 40 | /** 41 | * The path to the browser build, relative to remix.config.js. Defaults to 42 | * `public/build`. The browser build contains all public JavaScript and CSS 43 | * files that are created when building your routes. 44 | */ 45 | browserBuildDirectory: "public/build", 46 | 47 | /** 48 | * The URL prefix of the browser build with a trailing slash. Defaults to 49 | * `/build/`. 50 | */ 51 | publicPath: "/build/", 52 | 53 | /** 54 | * The path to the server build directory, relative to remix.config.js. 55 | * Defaults to `build`. The server build is a collection of JavaScript modules 56 | * that are created from building your routes. They are used on the server to 57 | * generate HTML. 58 | */ 59 | serverBuildDirectory: "build/app", 60 | 61 | /** 62 | * The port to use when running `remix run`. Defaults to 8002. 63 | */ 64 | devServerPort: 8002 65 | }; 66 | -------------------------------------------------------------------------------- /cdk/pipeline.ts: -------------------------------------------------------------------------------- 1 | import { RemixAppStack } from "./app/stack" 2 | 3 | import { Construct, Stage, Stack, StackProps, StageProps } from '@aws-cdk/core'; 4 | import { CdkPipeline, SimpleSynthAction } from '@aws-cdk/pipelines'; 5 | import * as codepipeline from '@aws-cdk/aws-codepipeline'; 6 | import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; 7 | import * as cdk from "@aws-cdk/core" 8 | 9 | 10 | // pull repo information from package.json! 11 | import * as project from "../package.json" 12 | const repoOwner = project.repository.url.split('/')[3]; 13 | const repoName = project.repository.url.split('/')[4]; 14 | 15 | class RemixApplication extends Stage { 16 | constructor(scope: Construct, id: string, props?: StageProps) { 17 | super(scope, id, props); 18 | 19 | const remixStack = new RemixAppStack(this, 'RemixApp'); 20 | } 21 | } 22 | 23 | export class RemixPipelineStack extends Stack { 24 | constructor(scope: Construct, id: string, props?: StackProps) { 25 | super(scope, id, props); 26 | 27 | const sourceArtifact = new codepipeline.Artifact(); 28 | const cloudAssemblyArtifact = new codepipeline.Artifact(); 29 | 30 | const pipeline = new CdkPipeline(this, 'RemixPipeline', { 31 | // Only required to be set to true for Multi-Account Deployments 32 | // https://docs.aws.amazon.com/cdk/api/latest/docs/pipelines-readme.html#a-note-on-cost 33 | cloudAssemblyArtifact, 34 | crossAccountKeys: false, 35 | sourceAction: new codepipeline_actions.GitHubSourceAction({ 36 | actionName: 'GitHub', 37 | output: sourceArtifact, 38 | oauthToken: cdk.SecretValue.secretsManager('GITHUB_TOKEN'), 39 | // Replace these with your actual GitHub project name 40 | owner: repoOwner, 41 | repo: repoName, 42 | branch: 'main', // default: 'master' 43 | }), 44 | 45 | synthAction: SimpleSynthAction.standardNpmSynth({ 46 | sourceArtifact, 47 | cloudAssemblyArtifact, 48 | 49 | // Optionally specify a VPC in which the action runs 50 | // vpc: new ec2.Vpc(this, 'NpmSynthVpc'), 51 | 52 | // Use this if you need a build step (if you're not using ts-node 53 | // or if you have TypeScript Lambdas that need to be compiled). 54 | buildCommand: 'npm run build', 55 | }) 56 | }); 57 | 58 | 59 | // Do this as many times as necessary with any account and region 60 | // Account and region may different from the pipeline's. 61 | pipeline.addApplicationStage(new RemixApplication(this, 'Dev', { 62 | env: { 63 | account: process.env.AWS_ACCOUNT_ID, 64 | region: 'us-east-1', 65 | } 66 | })); 67 | 68 | pipeline.addApplicationStage(new RemixApplication(this, 'Prod', { 69 | env: { 70 | account: process.env.AWS_ACCOUNT_ID, 71 | region: 'us-east-1', 72 | } 73 | })); 74 | } 75 | } 76 | --------------------------------------------------------------------------------