├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── architecture.png ├── infrastructure ├── .gitignore ├── .npmignore ├── bin │ └── infrastructure.ts ├── cdk.json ├── jest.config.js ├── lib │ └── remix-stack.ts ├── package.json └── tsconfig.json ├── package.json ├── remix-aws-cdk-example.drawio ├── remix ├── .gitignore ├── app.arc ├── app │ ├── entry.client.tsx │ ├── entry.server.tsx │ ├── root.tsx │ ├── routes │ │ ├── demos │ │ │ ├── about.tsx │ │ │ ├── about │ │ │ │ ├── index.tsx │ │ │ │ └── whoa.tsx │ │ │ ├── actions.tsx │ │ │ ├── correct.tsx │ │ │ ├── params.tsx │ │ │ └── params │ │ │ │ ├── $id.tsx │ │ │ │ └── index.tsx │ │ └── index.tsx │ └── styles │ │ ├── dark.css │ │ ├── demos │ │ └── about.css │ │ └── global.css ├── concurrently.js ├── package.json ├── preferences.arc ├── public │ └── favicon.ico ├── remix.config.js ├── remix.env.d.ts ├── server │ ├── config.arc │ └── index.js └── tsconfig.json ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | cdk.out 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Florian Wiech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 💽 remix-aws-cdk-example 2 | 3 | This example is based on the `remix-run/architect` template and uses the `arc sandbox` for local development. 4 | The deployment is done with the `aws-cdk v2`. 5 | 6 | ## Architecture 7 | 8 | ![architecture](architecture.png) 9 | 10 | ## 🧰 Setup 11 | 12 | This repository uses [yarn workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces/). 13 | To set up all use the following command: 14 | 15 | ```shell 16 | yarn install 17 | ``` 18 | 19 | ## ⌨️ Local Development 20 | 21 | ```shell 22 | yarn start 23 | ``` 24 | 25 | Open up [http://localhost:3333](http://localhost:3333) and you should be ready to go! 26 | 27 | ## 🚀 AWS Deployment 28 | 29 | If you want to have a look at the synthesized CloudFormation template, you can run `yarn synth` and find the template in `infrastructure/RemixStack.yml`. 30 | 31 | You can deploy remix via the following commands: 32 | 33 | ```shell 34 | yarn build 35 | yarn deploy 36 | ``` 37 | 38 | ## 🗑 Clean Up 39 | 40 | Delete the CloudFormation stack named "RemixStack" created by this project. 41 | 42 | ## 🔗 Related Links 43 | 44 | - [Remix Docs](https://remix.run/docs) 45 | - [aws-cdk GitHub](https://github.com/aws/aws-cdk) 46 | - [aws-cdk API Reference](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-construct-library.html) 47 | -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwiech/remix-aws-cdk-example/37574fe1085d53e55eeb626d604d2a8a51b149e5/architecture.png -------------------------------------------------------------------------------- /infrastructure/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | RemixStack.yml 11 | RemixStack.json 12 | -------------------------------------------------------------------------------- /infrastructure/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /infrastructure/bin/infrastructure.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "source-map-support/register"; 3 | import * as cdk from "aws-cdk-lib"; 4 | import { RemixStack } from "../lib/remix-stack"; 5 | 6 | const env: cdk.Environment = { 7 | account: process.env.CDK_DEFAULT_ACCOUNT, 8 | region: process.env.CDK_DEFAULT_REGION, 9 | }; 10 | 11 | const app = new cdk.App(); 12 | 13 | new RemixStack(app, "RemixStack", { env }); 14 | -------------------------------------------------------------------------------- /infrastructure/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/infrastructure.ts", 3 | "watch": { 4 | "include": ["**"], 5 | "exclude": [ 6 | "README.md", 7 | "cdk*.json", 8 | "**/*.d.ts", 9 | "**/*.js", 10 | "tsconfig.json", 11 | "package*.json", 12 | "yarn.lock", 13 | "node_modules", 14 | "test" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 19 | "@aws-cdk/core:stackRelativeExports": true, 20 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 21 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 22 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 23 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 24 | "@aws-cdk/core:target-partitions": ["aws", "aws-cn"] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /infrastructure/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: "node", 3 | roots: ["/test"], 4 | testMatch: ["**/*.test.ts"], 5 | transform: { 6 | "^.+\\.tsx?$": "ts-jest", 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /infrastructure/lib/remix-stack.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "source-map-support/register"; 3 | import { Construct } from "constructs"; 4 | import { join } from "path"; 5 | import * as cdk from "aws-cdk-lib"; 6 | import * as ssm from "aws-cdk-lib/aws-ssm"; 7 | import * as s3 from "aws-cdk-lib/aws-s3"; 8 | import * as s3deploy from "aws-cdk-lib/aws-s3-deployment"; 9 | import * as lambda from "aws-cdk-lib/aws-lambda"; 10 | import * as logs from "aws-cdk-lib/aws-logs"; 11 | import * as cloudfront from "aws-cdk-lib/aws-cloudfront"; 12 | import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"; 13 | import * as origin from "aws-cdk-lib/aws-cloudfront-origins"; 14 | import * as api from "@aws-cdk/aws-apigatewayv2-alpha"; 15 | import { HttpLambdaIntegration } from "@aws-cdk/aws-apigatewayv2-integrations-alpha"; 16 | 17 | export class RemixStack extends cdk.Stack { 18 | readonly distributionUrlParameterName = "/remix/distribution/url"; 19 | 20 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 21 | super(scope, id, props); 22 | 23 | const bucket = new s3.Bucket(this, "StaticAssetsBucket"); 24 | 25 | new s3deploy.BucketDeployment(this, "DeployStaticAssets", { 26 | sources: [s3deploy.Source.asset(join(__dirname, "../../remix/public"))], 27 | destinationBucket: bucket, 28 | destinationKeyPrefix: "_static", 29 | }); 30 | 31 | const fn = new NodejsFunction(this, "RequestHandler", { 32 | runtime: lambda.Runtime.NODEJS_14_X, 33 | handler: "handler", 34 | entry: join(__dirname, "../../remix/server/index.js"), 35 | environment: { 36 | NODE_ENV: "production", 37 | }, 38 | bundling: { 39 | nodeModules: ["@remix-run/architect", "react", "react-dom"], 40 | }, 41 | timeout: cdk.Duration.seconds(10), 42 | logRetention: logs.RetentionDays.THREE_DAYS, 43 | tracing: lambda.Tracing.ACTIVE, 44 | }); 45 | 46 | const integration = new HttpLambdaIntegration("RequestHandlerIntegration", fn, { 47 | payloadFormatVersion: api.PayloadFormatVersion.VERSION_2_0, 48 | }); 49 | 50 | const httpApi = new api.HttpApi(this, "WebsiteApi", { 51 | defaultIntegration: integration, 52 | }); 53 | 54 | const httpApiUrl = `${httpApi.httpApiId}.execute-api.${cdk.Stack.of(this).region}.${cdk.Stack.of(this).urlSuffix}`; 55 | 56 | const requestHandlerOrigin = new origin.HttpOrigin(httpApiUrl); 57 | const originRequestPolicy = new cloudfront.OriginRequestPolicy(this, "RequestHandlerPolicy", { 58 | originRequestPolicyName: "website-request-handler", 59 | queryStringBehavior: cloudfront.OriginRequestQueryStringBehavior.all(), 60 | cookieBehavior: cloudfront.OriginRequestCookieBehavior.all(), 61 | // https://stackoverflow.com/questions/65243953/pass-query-params-from-cloudfront-to-api-gateway 62 | headerBehavior: cloudfront.OriginRequestHeaderBehavior.none(), 63 | }); 64 | const requestHandlerBehavior: cloudfront.AddBehaviorOptions = { 65 | allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, 66 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 67 | cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, 68 | originRequestPolicy, 69 | }; 70 | 71 | const assetOrigin = new origin.S3Origin(bucket); 72 | const assetBehaviorOptions = { 73 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 74 | }; 75 | 76 | const distribution = new cloudfront.Distribution(this, "CloudFront", { 77 | defaultBehavior: { 78 | origin: requestHandlerOrigin, 79 | ...requestHandlerBehavior, 80 | }, 81 | priceClass: cloudfront.PriceClass.PRICE_CLASS_100, 82 | }); 83 | 84 | distribution.addBehavior("/_static/*", assetOrigin, assetBehaviorOptions); 85 | 86 | new ssm.StringParameter(this, "DistributionUrlParameter", { 87 | parameterName: this.distributionUrlParameterName, 88 | stringValue: distribution.distributionDomainName, 89 | tier: ssm.ParameterTier.STANDARD, 90 | }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /infrastructure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@remix-aws-cdk-example/infrastructure", 4 | "version": "0.0.0", 5 | "bin": { 6 | "infrastructure": "bin/infrastructure.js" 7 | }, 8 | "scripts": { 9 | "build": "tsc", 10 | "watch": "tsc -w", 11 | "test": "jest", 12 | "cdk": "cdk", 13 | "synth": "cdk synth -e RemixStack > ./RemixStack.yml", 14 | "deploy": "cdk deploy 'RemixStack' --outputs-file ./RemixStack.json" 15 | }, 16 | "devDependencies": { 17 | "@types/jest": "^27.4.1", 18 | "@types/node": "^14.10.1", 19 | "aws-cdk": "2.14.0", 20 | "jest": "^27.5.1", 21 | "ts-jest": "^27.1.3", 22 | "ts-node": "^10.5.0", 23 | "typescript": "~4.6.2" 24 | }, 25 | "dependencies": { 26 | "@aws-cdk/aws-apigatewayv2-alpha": "^2.14.0-alpha.0", 27 | "@aws-cdk/aws-apigatewayv2-integrations-alpha": "^2.14.0-alpha.0", 28 | "@remix-run/architect": "^1.2.3", 29 | "aws-cdk-lib": "2.14.0", 30 | "constructs": "^10.0.75", 31 | "esbuild": "^0.14.23", 32 | "react": "^17.0.2", 33 | "react-dom": "^17.0.2", 34 | "source-map-support": "^0.5.21" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /infrastructure/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 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["node_modules", "cdk.out"] 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@remix-aws-cdk-example/root", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "postinstall": "yarn --cwd remix postinstall", 7 | "start": "yarn --cwd remix start", 8 | "build": "yarn --cwd remix build", 9 | "synth": "yarn --cwd infrastructure synth", 10 | "deploy": "yarn --cwd infrastructure deploy", 11 | "format:check": "prettier . --check --ignore-unknown", 12 | "format:write": "prettier . --write --ignore-unknown" 13 | }, 14 | "workspaces": [ 15 | "infrastructure", 16 | "remix" 17 | ], 18 | "devDependencies": { 19 | "prettier": "^2.5.1", 20 | "typescript": "^4.6.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /remix-aws-cdk-example.drawio: -------------------------------------------------------------------------------- 1 | 7VnbcuI4EP0av2wVlG1hGx7BhMxskarsZmt39imlYNkoI1uMLALM12/LlvFFniTskEvVJJDgPpZalz593AoWCtP9pcCb9RWPCLNcO9pbaG657tiZwF8FHErAtzWQCBqVkFMDN/Q70aCt0S2NSN5qKDlnkm7a4IpnGVnJFoaF4Lt2s5iz9qgbnBADuFlhZqL/0Eiu9bLcoMY/EZqsq5EdX68vxVVjvZJ8jSO+a0DowkKh4FyWV+k+JEztXbUvZb/FD+4eJyZIJp/T4WqO8tXff93zP/h++e3qdxTfLweOdvOA2Vav+E+SUJ7pOctDtRF8KxnNSHjcZ9tCs0TgiML4IWdcAJbxDJrP1jJlYDlwuVtTSW42eKXc7IAigMU8kzrSjlvZeiTlFXZqo67TfaI4NcS7fDRMBN9uiiE/Q6x7796KcurgQQr+lVSzslw0DibOxFdjUcY6s30gQlKI+JTRRDmWXI2DtcVILJVHWALNkmVhzZGtp903RITzNYn0+s0oVTsOo5J9A9JRuyQ8JVIcoEl119cM0inkIG3vakJ6FcvWDTK6gQaxToLk6LvmCVxoqpxCG4M1Nwjs2Xb1lUiDOhtOM1lMwZvBGyYVlr8e3A0VMnS9HrAPC0zQMZvBh9M3QhfswwITdMxmyqpm3Qb7sMAzZ9zt7fT0djq94Y1mz8pEIKRvTxEKGvfmVECHIkOA+0Jxp5UP0McNApCwvgyKi58mve1G7izxHWHXPKfa/R2XkqdPJtcKZkVEWzGeUgecb8plx3Sv5tEvF4LkfCtWpBSLGZh9spGj86ToyO6kqG+maDA2M7TCzp6gvpGEJILnmjZV7HnCM8wuanQGEppFx8jWbZZcxayIzT2R8qCDg7eStyMH+ycOX3T/wvhXGcBpbc73zZvzQ9O6JoLC2hUbSnBP5ZfKMVyXrlxPm7UrZRwaRtfRD8NbEuSRTUS63sAiIfJpNVQ7/ChZBGFY0od2ZdEX+qLrVAh8aDTQKlp7vlZAzUHUeUy43rhdDzzR3plMOrQrZ1CT8LiU/8/LwHhwWK7P9AM2azHW/7ZVpVGR/4MYp5TBwFNLPeh8nCoV0Q1SkjHeBYt2oarDeMbzov5wSzbYkNNyoBVI+Sue8Mfh4CopPxe3uYR4reDqt2qWcF1OtGrUk2WFFrYzw9C7riymNIrKJCQ5/Y7vjmrXfnbOe/n8mAIYGnYslfUgrWq0T9vsoTNyJ22qlNZpfDYIOEAdr6O2Bx7Hua4mFudl4eRN1LFX0ILTBa1Td8a2er2K0HnvU+gmwUlCh5zAenGhq86vLaVTOvL6gvF8vjyWLD8vJAN7OEJjvxWLwZmkxB233U5eS0qQEeaQ8W20EDz7OAi9k4PQZOTNF+5pByFviuyZ98schFaKtHFBWkMwFrZ6neeg5HYOSi4av+1BaWTk7xKndxEG7BPOQGLFRxa/jyxeBOMLe3RaFs9tL3SCXyaLWUnds2Sq162ygjfO1PFbFO3nr7W9Z9bao3PX2j+1+Z4hk9PrzwBcYkl2+PChke9DIz8qnSc1Em/obaJZ+yL/+31BoQSz/r6wPMHUX7qii/8A -------------------------------------------------------------------------------- /remix/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /server/build 5 | /public/build 6 | 7 | sam.json 8 | sam.yaml 9 | 10 | # CDK asset staging directory 11 | .cdk.staging 12 | cdk.out 13 | artifacts 14 | 15 | .idea 16 | -------------------------------------------------------------------------------- /remix/app.arc: -------------------------------------------------------------------------------- 1 | @app 2 | remix-architect-app 3 | 4 | @http 5 | /* 6 | method any 7 | src server 8 | 9 | @static 10 | 11 | # @aws 12 | # profile default 13 | # region eu-central-1 14 | -------------------------------------------------------------------------------- /remix/app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { hydrate } from "react-dom"; 2 | import { RemixBrowser } from "remix"; 3 | 4 | hydrate(, document); 5 | -------------------------------------------------------------------------------- /remix/app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import { renderToString } from "react-dom/server"; 2 | import { RemixServer } from "remix"; 3 | import type { EntryContext } from "remix"; 4 | 5 | export default function handleRequest( 6 | request: Request, 7 | responseStatusCode: number, 8 | responseHeaders: Headers, 9 | remixContext: EntryContext 10 | ) { 11 | let markup = renderToString(); 12 | 13 | responseHeaders.set("Content-Type", "text/html"); 14 | 15 | return new Response("" + markup, { 16 | status: responseStatusCode, 17 | headers: responseHeaders, 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /remix/app/root.tsx: -------------------------------------------------------------------------------- 1 | import { Link, Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useCatch } from "remix"; 2 | import type { LinksFunction } from "remix"; 3 | 4 | import globalStylesUrl from "~/styles/global.css"; 5 | import darkStylesUrl from "~/styles/dark.css"; 6 | 7 | // https://remix.run/api/app#links 8 | export let links: LinksFunction = () => { 9 | return [ 10 | { rel: "stylesheet", href: globalStylesUrl }, 11 | { 12 | rel: "stylesheet", 13 | href: darkStylesUrl, 14 | media: "(prefers-color-scheme: dark)", 15 | }, 16 | ]; 17 | }; 18 | 19 | // https://remix.run/api/conventions#default-export 20 | // https://remix.run/api/conventions#route-filenames 21 | export default function App() { 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | // https://remix.run/docs/en/v1/api/conventions#errorboundary 32 | export function ErrorBoundary({ error }: { error: Error }) { 33 | console.error(error); 34 | return ( 35 | 36 | 37 |
38 |

There was an error

39 |

{error.message}

40 |
41 |

Hey, developer, you should replace this with what you want your users to see.

42 |
43 |
44 |
45 | ); 46 | } 47 | 48 | // https://remix.run/docs/en/v1/api/conventions#catchboundary 49 | export function CatchBoundary() { 50 | let caught = useCatch(); 51 | 52 | let message; 53 | switch (caught.status) { 54 | case 401: 55 | message =

Oops! Looks like you tried to visit a page that you do not have access to.

; 56 | break; 57 | case 404: 58 | message =

Oops! Looks like you tried to visit a page that does not exist.

; 59 | break; 60 | 61 | default: 62 | throw new Error(caught.data || caught.statusText); 63 | } 64 | 65 | return ( 66 | 67 | 68 |

69 | {caught.status}: {caught.statusText} 70 |

71 | {message} 72 |
73 |
74 | ); 75 | } 76 | 77 | function Document({ children, title }: { children: React.ReactNode; title?: string }) { 78 | return ( 79 | 80 | 81 | 82 | 83 | 84 | {title ? {title} : null} 85 | 86 | 87 | 88 | 89 | {children} 90 | 91 | 92 | {process.env.NODE_ENV === "development" && } 93 | 94 | 95 | ); 96 | } 97 | 98 | function Layout({ children }: { children: React.ReactNode }) { 99 | return ( 100 |
101 |
102 |
103 | 104 | 105 | 106 | 119 |
120 |
121 |
122 |
{children}
123 |
124 |
125 |
126 |

© You!

127 |
128 |
129 |
130 | ); 131 | } 132 | 133 | function RemixLogo() { 134 | return ( 135 | 146 | Remix Logo 147 | 148 | 149 | 150 | 151 | 152 | 153 | ); 154 | } 155 | -------------------------------------------------------------------------------- /remix/app/routes/demos/about.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "remix"; 2 | import type { MetaFunction, LinksFunction } from "remix"; 3 | 4 | import stylesUrl from "~/styles/demos/about.css"; 5 | 6 | export let meta: MetaFunction = () => { 7 | return { 8 | title: "About Remix", 9 | }; 10 | }; 11 | 12 | export let links: LinksFunction = () => { 13 | return [{ rel: "stylesheet", href: stylesUrl }]; 14 | }; 15 | 16 | export default function Index() { 17 | return ( 18 |
19 |
20 |

About Us

21 |

22 | Ok, so this page isn't really about us, but we did want to show you a few more things Remix can do. 23 |

24 |

25 | Did you notice that things look a little different on this page? The CSS that we import in the route file and 26 | include in its links export is only included on this route and its children. 27 |

28 |

29 | Wait a sec...its children? To understand what we mean by this,{" "} 30 | read all about nested routes in the docs. 31 |

32 |
33 | 34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /remix/app/routes/demos/about/index.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "remix"; 2 | 3 | export default function AboutIndex() { 4 | return ( 5 |
6 |

7 | You are looking at the index route for the /about URL segment, but there are nested routes as well! 8 |

9 |

10 | 11 | Check out one of them here. 12 | 13 |

14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /remix/app/routes/demos/about/whoa.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "remix"; 2 | 3 | export default function AboutIndex() { 4 | return ( 5 |
6 |

7 | Whoa, this is a nested route! We render the /about layout route component, and its{" "} 8 | Outlet renders our route component. 🤯 9 |

10 |

11 | 12 | 13 | Go back to the /about index. 14 | 15 | 16 |

17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /remix/app/routes/demos/actions.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import type { ActionFunction } from "remix"; 3 | import { Form, json, useActionData, redirect } from "remix"; 4 | 5 | export function meta() { 6 | return { title: "Actions Demo" }; 7 | } 8 | 9 | // When your form sends a POST, the action is called on the server. 10 | // - https://remix.run/api/conventions#action 11 | // - https://remix.run/guides/data-updates 12 | export let action: ActionFunction = async ({ request }) => { 13 | let formData = await request.formData(); 14 | let answer = formData.get("answer"); 15 | 16 | // Typical action workflows start with validating the form data that just came 17 | // over the network. Clientside validation is fine, but you definitely need it 18 | // server side. If there's a problem, return the the data and the component 19 | // can render it. 20 | if (typeof answer !== "string") { 21 | return json("Come on, at least try!", { status: 400 }); 22 | } 23 | 24 | if (answer !== "egg") { 25 | return json(`Sorry, ${answer} is not right.`, { status: 400 }); 26 | } 27 | 28 | // Finally, if the data is valid, you'll typically write to a database or send or 29 | // email or log the user in, etc. It's recommended to redirect after a 30 | // successful action, even if it's to the same place so that non-JavaScript workflows 31 | // from the browser doesn't repost the data if the user clicks back. 32 | return redirect("/demos/correct"); 33 | }; 34 | 35 | export default function ActionsDemo() { 36 | // https://remix.run/api/remix#useactiondata 37 | let actionMessage = useActionData(); 38 | let answerRef = useRef(null); 39 | 40 | // This form works without JavaScript, but when we have JavaScript we can make 41 | // the experience better by selecting the input on wrong answers! Go ahead, disable 42 | // JavaScript in your browser and see what happens. 43 | useEffect(() => { 44 | if (actionMessage && answerRef.current) { 45 | answerRef.current.select(); 46 | } 47 | }, [actionMessage]); 48 | 49 | return ( 50 |
51 |
52 |

Actions!

53 |

54 | This form submission will send a post request that we handle in our `action` export. Any route can export an 55 | action to handle data mutations. 56 |

57 |
58 |

Post an Action

59 |

60 | What is more useful when it is broken? 61 |

62 | 66 |
67 | 68 |
69 | {actionMessage ? ( 70 |

71 | {actionMessage} 72 |

73 | ) : null} 74 |
75 |
76 | 77 | 94 |
95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /remix/app/routes/demos/correct.tsx: -------------------------------------------------------------------------------- 1 | export default function NiceWork() { 2 | return

You got it right!

; 3 | } 4 | -------------------------------------------------------------------------------- /remix/app/routes/demos/params.tsx: -------------------------------------------------------------------------------- 1 | import { useCatch, Link, json, useLoaderData, Outlet } from "remix"; 2 | 3 | export function meta() { 4 | return { title: "Boundaries Demo" }; 5 | } 6 | 7 | export default function Boundaries() { 8 | return ( 9 |
10 |
11 | 12 |
13 | 14 | 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /remix/app/routes/demos/params/$id.tsx: -------------------------------------------------------------------------------- 1 | import { useCatch, Link, json, useLoaderData } from "remix"; 2 | import type { LoaderFunction, MetaFunction } from "remix"; 3 | 4 | // The `$` in route filenames becomes a pattern that's parsed from the URL and 5 | // passed to your loaders so you can look up data. 6 | // - https://remix.run/api/conventions#loader-params 7 | export let loader: LoaderFunction = async ({ params }) => { 8 | // pretend like we're using params.id to look something up in the db 9 | 10 | if (params.id === "this-record-does-not-exist") { 11 | // If the record doesn't exist we can't render the route normally, so 12 | // instead we throw a 404 reponse to stop running code here and show the 13 | // user the catch boundary. 14 | throw new Response("Not Found", { status: 404 }); 15 | } 16 | 17 | // now pretend like the record exists but the user just isn't authorized to 18 | // see it. 19 | if (params.id === "shh-its-a-secret") { 20 | // Again, we can't render the component if the user isn't authorized. You 21 | // can even put data in the response that might help the user rectify the 22 | // issue! Like emailing the webmaster for access to the page. (Oh, right, 23 | // `json` is just a Response helper that makes it easier to send JSON 24 | // responses). 25 | throw json({ webmasterEmail: "hello@remix.run" }, { status: 401 }); 26 | } 27 | 28 | // Sometimes your code just blows up and you never anticipated it. Remix will 29 | // automatically catch it and send the UI to the error boundary. 30 | if (params.id === "kaboom") { 31 | lol(); 32 | } 33 | 34 | // but otherwise the record was found, user has access, so we can do whatever 35 | // else we needed to in the loader and return the data. (This is boring, we're 36 | // just gonna return the params.id). 37 | return { param: params.id }; 38 | }; 39 | 40 | export default function ParamDemo() { 41 | let data = useLoaderData(); 42 | return ( 43 |

44 | The param is {data.param} 45 |

46 | ); 47 | } 48 | 49 | // https://remix.run/api/conventions#catchboundary 50 | // https://remix.run/api/remix#usecatch 51 | // https://remix.run/api/guides/not-found 52 | export function CatchBoundary() { 53 | let caught = useCatch(); 54 | 55 | let message: React.ReactNode; 56 | switch (caught.status) { 57 | case 401: 58 | message = ( 59 |

60 | Looks like you tried to visit a page that you do not have access to. Maybe ask the webmaster ( 61 | {caught.data.webmasterEmail}) for access. 62 |

63 | ); 64 | case 404: 65 | message =

Looks like you tried to visit a page that does not exist.

; 66 | default: 67 | message = ( 68 |

69 | There was a problem with your request! 70 |
71 | {caught.status} {caught.statusText} 72 |

73 | ); 74 | } 75 | 76 | return ( 77 | <> 78 |

Oops!

79 |

{message}

80 |

81 | (Isn't it cool that the user gets to stay in context and try a different link in the parts of the UI that didn't 82 | blow up?) 83 |

84 | 85 | ); 86 | } 87 | 88 | // https://remix.run/api/conventions#errorboundary 89 | // https://remix.run/api/guides/not-found 90 | export function ErrorBoundary({ error }: { error: Error }) { 91 | console.error(error); 92 | return ( 93 | <> 94 |

Error!

95 |

{error.message}

96 |

97 | (Isn't it cool that the user gets to stay in context and try a different link in the parts of the UI that didn't 98 | blow up?) 99 |

100 | 101 | ); 102 | } 103 | 104 | export let meta: MetaFunction = ({ data }) => { 105 | return { 106 | title: data ? `Param: ${data.param}` : "Oops...", 107 | }; 108 | }; 109 | -------------------------------------------------------------------------------- /remix/app/routes/demos/params/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCatch, Link, json, useLoaderData, Outlet } from "remix"; 2 | import type { LoaderFunction } from "remix"; 3 | 4 | export default function Boundaries() { 5 | return ( 6 | <> 7 |

Params

8 |

9 | When you name a route segment with $ like routes/users/$userId.js, the $ segment will be parsed 10 | from the URL and sent to your loaders and actions by the same name. 11 |

12 |

Errors

13 |

14 | When a route throws and error in it's action, loader, or component, Remix automatically catches it, won't even 15 | try to render the component, but it will render the route's ErrorBoundary instead. If the route doesn't have 16 | one, it will bubble up to the routes above it until it hits the root. 17 |

18 |

So be as granular as you want with your error handling.

19 |

Not Found

20 |

21 | (and other{" "} 22 | client errors) 23 |

24 |

25 | Loaders and Actions can throw a Response instead of an error and Remix will render the 26 | CatchBoundary instead of the component. This is great when loading data from a database isn't found. As soon as 27 | you know you can't render the component normally, throw a 404 response and send your app into the catch 28 | boundary. Just like error boundaries, catch boundaries bubble, too. 29 |

30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /remix/app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import type { MetaFunction, LoaderFunction } from "remix"; 2 | import { useLoaderData, json, Link } from "remix"; 3 | 4 | type IndexData = { 5 | resources: Array<{ name: string; url: string }>; 6 | demos: Array<{ name: string; to: string }>; 7 | }; 8 | 9 | // Loaders provide data to components and are only ever called on the server, so 10 | // you can connect to a database or run any server side code you want right next 11 | // to the component that renders it. 12 | // https://remix.run/api/conventions#loader 13 | export let loader: LoaderFunction = () => { 14 | let data: IndexData = { 15 | resources: [ 16 | { 17 | name: "Remix Docs", 18 | url: "https://remix.run/docs", 19 | }, 20 | { 21 | name: "React Router Docs", 22 | url: "https://reactrouter.com/docs", 23 | }, 24 | { 25 | name: "Remix Discord", 26 | url: "https://discord.gg/VBePs6d", 27 | }, 28 | ], 29 | demos: [ 30 | { 31 | to: "demos/actions", 32 | name: "Actions", 33 | }, 34 | { 35 | to: "demos/about", 36 | name: "Nested Routes, CSS loading/unloading", 37 | }, 38 | { 39 | to: "demos/params", 40 | name: "URL Params and Error Boundaries", 41 | }, 42 | ], 43 | }; 44 | 45 | // https://remix.run/api/remix#json 46 | return json(data); 47 | }; 48 | 49 | // https://remix.run/api/conventions#meta 50 | export let meta: MetaFunction = () => { 51 | return { 52 | title: "Remix Starter", 53 | description: "Welcome to remix!", 54 | }; 55 | }; 56 | 57 | // https://remix.run/guides/routing#index-routes 58 | export default function Index() { 59 | let data = useLoaderData(); 60 | 61 | return ( 62 |
63 |
64 |

Welcome to Remix!

65 |

We're stoked that you're here. 🥳

66 |

67 | Feel free to take a look around the code to see how Remix does things, it might be a bit different than what 68 | you’re used to. When you're ready to dive deeper, we've got plenty of resources to get you up-and-running 69 | quickly. 70 |

71 |

72 | Check out all the demos in this starter, and then just delete the app/routes/demos and{" "} 73 | app/styles/demos folders when you're ready to turn this into your next project. 74 |

75 |
76 | 96 |
97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /remix/app/styles/dark.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-foreground: hsl(0, 0%, 100%); 3 | --color-background: hsl(0, 0%, 7%); 4 | --color-links: hsl(213, 100%, 73%); 5 | --color-links-hover: hsl(213, 100%, 80%); 6 | --color-border: hsl(0, 0%, 25%); 7 | } 8 | -------------------------------------------------------------------------------- /remix/app/styles/demos/about.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Whoa whoa whoa, wait a sec...why are we overriding global CSS selectors? 3 | * Isn't that kind of scary? How do we know this won't have side effects? 4 | * 5 | * In Remix, CSS that is included in a route file will *only* show up on that 6 | * route (and for nested routes, its children). When the user navigates away 7 | * from that route the CSS files linked from those routes will be automatically 8 | * unloaded, making your styles much easier to predict and control. 9 | * 10 | * Read more about styling routes in the docs: 11 | * https://remix.run/guides/styling 12 | */ 13 | 14 | :root { 15 | --color-foreground: hsl(0, 0%, 7%); 16 | --color-background: hsl(56, 100%, 50%); 17 | --color-links: hsl(345, 56%, 39%); 18 | --color-links-hover: hsl(345, 51%, 49%); 19 | --color-border: rgb(184, 173, 20); 20 | --font-body: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace; 21 | } 22 | 23 | .about__intro { 24 | max-width: 500px; 25 | } 26 | -------------------------------------------------------------------------------- /remix/app/styles/global.css: -------------------------------------------------------------------------------- 1 | /* 2 | * You can just delete everything here or keep whatever you like, it's just a 3 | * quick baseline! 4 | */ 5 | :root { 6 | --color-foreground: hsl(0, 0%, 7%); 7 | --color-background: hsl(0, 0%, 100%); 8 | --color-links: hsl(213, 100%, 52%); 9 | --color-links-hover: hsl(213, 100%, 43%); 10 | --color-border: hsl(0, 0%, 82%); 11 | --font-body: -apple-system, "Segoe UI", Helvetica Neue, Helvetica, Roboto, Arial, sans-serif, system-ui, 12 | "Apple Color Emoji", "Segoe UI Emoji"; 13 | } 14 | 15 | html { 16 | box-sizing: border-box; 17 | } 18 | 19 | *, 20 | *::before, 21 | *::after { 22 | box-sizing: inherit; 23 | } 24 | 25 | :-moz-focusring { 26 | outline: auto; 27 | } 28 | 29 | :focus { 30 | outline: var(--color-links) solid 2px; 31 | outline-offset: 2px; 32 | } 33 | 34 | html, 35 | body { 36 | padding: 0; 37 | margin: 0; 38 | background-color: var(--color-background); 39 | color: var(--color-foreground); 40 | } 41 | 42 | body { 43 | font-family: var(--font-body); 44 | line-height: 1.5; 45 | } 46 | 47 | a { 48 | color: var(--color-links); 49 | text-decoration: none; 50 | } 51 | 52 | a:hover { 53 | color: var(--color-links-hover); 54 | text-decoration: underline; 55 | } 56 | 57 | hr { 58 | display: block; 59 | height: 1px; 60 | border: 0; 61 | background-color: var(--color-border); 62 | margin-top: 2rem; 63 | margin-bottom: 2rem; 64 | } 65 | 66 | input:where([type="text"]), 67 | input:where([type="search"]) { 68 | display: block; 69 | border: 1px solid var(--color-border); 70 | width: 100%; 71 | font: inherit; 72 | line-height: 1; 73 | height: calc(1ch + 1.5em); 74 | padding-right: 0.5em; 75 | padding-left: 0.5em; 76 | background-color: hsl(0 0% 100% / 20%); 77 | color: var(--color-foreground); 78 | } 79 | 80 | .sr-only { 81 | position: absolute; 82 | width: 1px; 83 | height: 1px; 84 | padding: 0; 85 | margin: -1px; 86 | overflow: hidden; 87 | clip: rect(0, 0, 0, 0); 88 | white-space: nowrap; 89 | border-width: 0; 90 | } 91 | 92 | .container { 93 | --gutter: 16px; 94 | width: 1024px; 95 | max-width: calc(100% - var(--gutter) * 2); 96 | margin-right: auto; 97 | margin-left: auto; 98 | } 99 | 100 | .remix-app { 101 | display: flex; 102 | flex-direction: column; 103 | min-height: 100vh; 104 | min-height: calc(100vh - env(safe-area-inset-bottom)); 105 | } 106 | 107 | .remix-app > * { 108 | width: 100%; 109 | } 110 | 111 | .remix-app__header { 112 | padding-top: 1rem; 113 | padding-bottom: 1rem; 114 | border-bottom: 1px solid var(--color-border); 115 | } 116 | 117 | .remix-app__header-content { 118 | display: flex; 119 | justify-content: space-between; 120 | align-items: center; 121 | } 122 | 123 | .remix-app__header-home-link { 124 | width: 106px; 125 | height: 30px; 126 | color: var(--color-foreground); 127 | } 128 | 129 | .remix-app__header-nav ul { 130 | list-style: none; 131 | margin: 0; 132 | display: flex; 133 | align-items: center; 134 | gap: 1.5em; 135 | } 136 | 137 | .remix-app__header-nav li { 138 | font-weight: bold; 139 | } 140 | 141 | .remix-app__main { 142 | flex: 1 1 100%; 143 | } 144 | 145 | .remix-app__footer { 146 | padding-top: 1rem; 147 | padding-bottom: 1rem; 148 | border-top: 1px solid var(--color-border); 149 | } 150 | 151 | .remix-app__footer-content { 152 | display: flex; 153 | justify-content: center; 154 | align-items: center; 155 | } 156 | 157 | .remix__page { 158 | --gap: 1rem; 159 | --space: 2rem; 160 | display: grid; 161 | grid-auto-rows: min-content; 162 | gap: var(--gap); 163 | padding-top: var(--space); 164 | padding-bottom: var(--space); 165 | } 166 | 167 | @media print, screen and (min-width: 640px) { 168 | .remix__page { 169 | --gap: 2rem; 170 | grid-auto-rows: unset; 171 | grid-template-columns: repeat(2, 1fr); 172 | } 173 | } 174 | 175 | @media screen and (min-width: 1024px) { 176 | .remix__page { 177 | --gap: 4rem; 178 | } 179 | } 180 | 181 | .remix__page > main > :first-child { 182 | margin-top: 0; 183 | } 184 | 185 | .remix__page > main > :last-child { 186 | margin-bottom: 0; 187 | } 188 | 189 | .remix__page > aside { 190 | margin: 0; 191 | padding: 1.5ch 2ch; 192 | border: solid 1px var(--color-border); 193 | border-radius: 0.5rem; 194 | } 195 | 196 | .remix__page > aside > :first-child { 197 | margin-top: 0; 198 | } 199 | 200 | .remix__page > aside > :last-child { 201 | margin-bottom: 0; 202 | } 203 | 204 | .remix__form { 205 | display: flex; 206 | flex-direction: column; 207 | gap: 1rem; 208 | padding: 1rem; 209 | border: 1px solid var(--color-border); 210 | border-radius: 0.5rem; 211 | } 212 | 213 | .remix__form > * { 214 | margin-top: 0; 215 | margin-bottom: 0; 216 | } 217 | -------------------------------------------------------------------------------- /remix/concurrently.js: -------------------------------------------------------------------------------- 1 | const concurrently = require("concurrently"); 2 | 3 | concurrently( 4 | [ 5 | { command: "yarn dev", name: "remix dev" }, 6 | { command: "yarn arc:sandbox", name: "sandbox" }, 7 | ], 8 | { 9 | prefix: "name", 10 | killOthers: ["failure", "success"], 11 | restartTries: 3, 12 | } 13 | ); 14 | -------------------------------------------------------------------------------- /remix/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@remix-aws-cdk-example/remix", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "postinstall": "remix setup node", 7 | "start": "node concurrently.js", 8 | "dev": "remix watch", 9 | "arc:sandbox": "arc sandbox", 10 | "build": "remix build" 11 | }, 12 | "dependencies": { 13 | "@remix-run/architect": "^1.2.3", 14 | "@remix-run/react": "^1.2.3", 15 | "aws-sdk": "2.1083.0", 16 | "remix": "^1.2.3" 17 | }, 18 | "devDependencies": { 19 | "@architect/architect": "^10.0.2", 20 | "@remix-run/dev": "^1.2.3", 21 | "@types/react": "^17.0.39", 22 | "@types/react-dom": "^17.0.11", 23 | "concurrently": "^7.0.0", 24 | "typescript": "^4.6.2" 25 | }, 26 | "engines": { 27 | "node": ">=14" 28 | }, 29 | "sideEffects": false 30 | } 31 | -------------------------------------------------------------------------------- /remix/preferences.arc: -------------------------------------------------------------------------------- 1 | # The @env pragma is synced (and overwritten) by running arc env 2 | @env 3 | testing 4 | REMIX_ENV development 5 | 6 | staging 7 | REMIX_ENV production 8 | 9 | production 10 | REMIX_ENV production 11 | -------------------------------------------------------------------------------- /remix/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwiech/remix-aws-cdk-example/37574fe1085d53e55eeb626d604d2a8a51b149e5/remix/public/favicon.ico -------------------------------------------------------------------------------- /remix/remix.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@remix-run/dev/config').AppConfig} 3 | */ 4 | module.exports = { 5 | appDirectory: "app", 6 | assetsBuildDirectory: "public/build", 7 | publicPath: "/_static/build/", 8 | serverBuildDirectory: "server/build", 9 | devServerPort: 8002, 10 | }; 11 | -------------------------------------------------------------------------------- /remix/remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /remix/server/config.arc: -------------------------------------------------------------------------------- 1 | @aws 2 | # runtime nodejs14.x 3 | # memory 1152 4 | # timeout 30 5 | # concurrency 1 6 | -------------------------------------------------------------------------------- /remix/server/index.js: -------------------------------------------------------------------------------- 1 | const { createRequestHandler } = require("@remix-run/architect"); 2 | exports.handler = createRequestHandler({ 3 | build: require("./build"), 4 | }); 5 | -------------------------------------------------------------------------------- /remix/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2019"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "jsx": "react-jsx", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2019", 11 | "strict": true, 12 | "baseUrl": ".", 13 | "paths": { 14 | "~/*": ["./app/*"] 15 | }, 16 | 17 | // Remix takes care of building everything in `remix build`. 18 | "noEmit": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [{ "path": "infrastructure" }, { "path": "remix" }] 3 | } 4 | --------------------------------------------------------------------------------