├── .github └── workflows │ ├── main.yml │ └── pr.yml ├── .gitignore ├── .npmignore ├── README.md ├── cdk.json ├── docs ├── .gitignore ├── .npmrc ├── README.md ├── astro.config.mjs ├── images │ ├── domain-registrar.png │ ├── hosted-zone-id.png │ ├── name-servers.png │ └── undraw_To_the_stars_qhyy.svg ├── package-lock.json ├── package.json ├── public │ ├── favicon.svg │ ├── index.css │ └── theme.js ├── serverlessui.config.js ├── src │ ├── components │ │ ├── ArticleFooter.astro │ │ ├── DocSidebar.tsx │ │ ├── EditOnGithub.tsx │ │ ├── Note.astro │ │ ├── SiteSidebar.astro │ │ └── ThemeToggle.tsx │ ├── config.ts │ ├── layouts │ │ └── Main.astro │ └── pages │ │ ├── cli-reference.md │ │ ├── getting-started.md │ │ └── index.md └── yarn.lock ├── examples ├── gatsby │ ├── .gitignore │ ├── README.md │ ├── gatsby-config.js │ ├── package.json │ ├── serverlessui.config.js │ ├── src │ │ ├── images │ │ │ └── icon.png │ │ └── pages │ │ │ ├── 404.js │ │ │ ├── index.js │ │ │ └── page-2.js │ └── yarn.lock └── simple │ ├── README.md │ ├── dist │ └── index.html │ ├── functions │ ├── hello.ts │ └── helloAgain.ts │ ├── package.json │ └── yarn.lock ├── lerna.json ├── package.json ├── packages ├── cli │ ├── .gitignore │ ├── LICENSE │ ├── __tests__ │ │ └── cli-integration.test.ts │ ├── bin │ │ └── cli │ ├── docs │ │ ├── commands.md │ │ └── plugins.md │ ├── package.json │ ├── readme.md │ ├── rollup.config.js │ ├── src │ │ ├── cli.ts │ │ ├── commands │ │ │ ├── configure-domain.ts │ │ │ └── deploy.ts │ │ ├── extensions │ │ │ └── cli-extension.ts │ │ └── types.ts │ ├── tsconfig.json │ └── tslint.json ├── construct │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── domain-certificate.construct.ts │ │ ├── index.ts │ │ ├── serverless-ui.construct.ts │ │ └── utils.ts │ └── tsconfig.json ├── domain-application │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── domain.application.ts │ │ ├── index.ts │ │ └── stacks │ │ │ └── domain.stack.ts │ └── tsconfig.json └── serverless-application │ ├── package.json │ ├── rollup.config.js │ ├── src │ ├── index.ts │ ├── serverless-ui.application.ts │ └── stacks │ │ └── serverless-ui.stack.ts │ └── tsconfig.json ├── scripts └── aws.md ├── serverlessui.config.js └── yarn.lock /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | jobs: 6 | deploy-docs: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Use Node.js 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: "14.x" 15 | - name: Configure AWS Credentials 16 | uses: aws-actions/configure-aws-credentials@v1 17 | with: 18 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 19 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 20 | aws-region: us-west-2 21 | - run: npm install -g lerna 22 | - run: lerna bootstrap 23 | - run: lerna run prepack 24 | - run: cd docs && yarn install && yarn build 25 | - run: yarn sui deploy --compiled-build --dir="docs/dist" --prod 26 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Serverless UI Build & Deploy Example Sites 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | deploy-docs-preview: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Use Node.js 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: "14.x" 15 | - name: Configure AWS Credentials 16 | uses: aws-actions/configure-aws-credentials@v1 17 | with: 18 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 19 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 20 | aws-region: us-west-2 21 | - run: npm install -g lerna 22 | - run: lerna bootstrap 23 | - run: lerna run prepack 24 | - run: cd docs && yarn install && yarn build 25 | - run: yarn sui deploy --compiled-build --dir="docs/dist" 26 | - name: Add PR Comment 27 | uses: actions/github-script@v3 28 | with: 29 | github-token: ${{secrets.GITHUB_TOKEN}} 30 | script: | 31 | const outputs = require(`${process.env.GITHUB_WORKSPACE}/cdk.out/outputs.json`); 32 | const stackName = Object.keys(outputs).find((key) => 33 | key.startsWith("ServerlessUI") 34 | ); 35 | const baseUrlKey = Object.keys(outputs[stackName]).find((key) => 36 | key.startsWith("ServerlessUIBaseUrl") 37 | ); 38 | github.issues.createComment({ 39 | issue_number: context.issue.number, 40 | owner: context.repo.owner, 41 | repo: context.repo.repo, 42 | body: `✅ Your deploy preview is ready: ${outputs[stackName][baseUrlKey]}`, 43 | }); 44 | deploy-simple-preview: 45 | runs-on: ubuntu-latest 46 | 47 | steps: 48 | - uses: actions/checkout@v2 49 | - name: Use Node.js 50 | uses: actions/setup-node@v1 51 | with: 52 | node-version: "14.x" 53 | - name: Configure AWS Credentials 54 | uses: aws-actions/configure-aws-credentials@v1 55 | with: 56 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 57 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 58 | aws-region: us-west-2 59 | - run: npm install -g lerna 60 | - run: lerna bootstrap 61 | - run: lerna run prepack 62 | - run: cd examples/simple && yarn install 63 | - run: yarn sui deploy --compiled-build --dir="examples/simple/dist" --functions="examples/simple/functions" 64 | deploy-privateS3-preview: 65 | runs-on: ubuntu-latest 66 | 67 | steps: 68 | - uses: actions/checkout@v2 69 | - name: Use Node.js 70 | uses: actions/setup-node@v1 71 | with: 72 | node-version: "14.x" 73 | - name: Configure AWS Credentials 74 | uses: aws-actions/configure-aws-credentials@v1 75 | with: 76 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 77 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 78 | aws-region: us-west-2 79 | - run: npm install -g lerna 80 | - run: lerna bootstrap 81 | - run: lerna run prepack 82 | - run: cd examples/gatsby && yarn install && yarn build 83 | - run: yarn sui deploy --compiled-build --dir="examples/gatsby/public" 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !jest.config.js 2 | *.d.ts 3 | node_modules 4 | 5 | # CDK asset staging directory 6 | .cdk.staging 7 | cdk.out 8 | dist 9 | tsconfig.tsbuildinfo 10 | .rollup.cache 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Serverless UI 3 |

4 |

5 | Serverless UI 6 |

7 | 8 |

9 | 💻 🚀 ☁ 10 |

11 |

12 | Deploying Websites to AWS on Easy Mode 13 |

14 |

15 | Serverless UI is a free, open source command-line utility for quickly building and deploying serverless applications on AWS 16 |

17 | 18 | - **Bring your own UI** It doesn't matter if it's React, Vue, Svelte or JQuery. If it compiles down to static files, then it is supported. 19 | 20 | - **Serverless Functions** Your functions become endpoints, automatically. Serverless UI deploys each function in your `/functions` directory as a Node.js lambda behind a CDN and API Gateway for an optimal blend of performance and scalability. 21 | 22 | - **Deploy Previews** Automatically deploy each iteration of your application with a separate URL to continuously integrate and test with confidence. 23 | 24 | - **Custom Domains** Quickly configure a custom domain to take advantage of production deploys! 25 | 26 | - **TypeScript Support** Write your serverless functions in JavaScript or TypeScript. Either way, they'll be bundled down extremely quickly and deployed as Node.js 14 lambdas. 27 | 28 | - **Own your code** Skip the 3rd Party services — get all of the benefits and security of a hosted AWS application, without going through a middleman. Deploy to a new AWS account, or an existing account and get up and running in five minutes! 29 | 30 | ## What's in this Document 31 | 32 | - [What's in this Document](#whats-in-this-document) 33 | - [🚀 Get Up and Running in 5 Minutes](#-get-up-and-running-in-5-minutes) 34 | - [📖 CLI Reference](#-cli-reference) 35 | - [`deploy`](#deploy) 36 | - [Options](#options) 37 | - [Examples](#examples) 38 | - [`configure-domain`](#configure-domain) 39 | - [Options](#options-1) 40 | - [Examples](#examples-1) 41 | - [Additional Steps](#additional-steps) 42 | - [Continuous Integration](#continuous-integration) 43 | - [GitHub Actions](#github-actions) 44 | - [👩‍🔬 Experimental Features](#-experimental-features) 45 | - [\_\_experimental_privateS3](#__experimental_privates3) 46 | - [👩‍💻 Advanced Use Cases](#-advanced-use-cases) 47 | - [Serverless UI Advanced Example](#serverless-ui-advanced-example) 48 | - [FAQ](#faq) 49 | - [License](#license) 50 | 51 | ## 🚀 Get Up and Running in 5 Minutes 52 | 53 | You can get a new Serverless UI site deployed to you AWS account in just a few steps: 54 | 55 | 1. **AWS Prerequisites** 56 | 57 | In order to deploy to AWS, you'll have to configure your machine with local credentials. You'll find the best instructions [here](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html). 58 | 59 | 1. **Install the Serverless UI Command-Line Interface** 60 | 61 | ```shell 62 | npm install -g @serverlessui/cli 63 | ``` 64 | 65 | 1. **Deploy your static website** 66 | 67 | Finally, tell the Serverless UI where to find your website's static files. 68 | 69 | ```shell 70 | sui deploy --dir="dist" 71 | ``` 72 | 73 | ## 📖 CLI Reference 74 | 75 | 1. [deploy](#deploy) 76 | 2. [configure-domain](#configure-domain) 77 | 78 | ### `deploy` 79 | 80 | ```shell 81 | sui deploy 82 | ``` 83 | 84 | #### Options 85 | 86 | | Option | Description | Default | 87 | | :-----------: | ----------------------------------------------------- | :-----------: | 88 | | `--dir` | The directory of your website's static files | `"dist"` | 89 | | `--functions` | The directory of the functions to deploy as endpoints | `"functions"` | 90 | | `--prod` | Custom Domains only: `false` will deploy a preview | `false` | 91 | 92 | > Note: The `--dir` directory should be only static files. You may need to run a build step prior to deploying 93 | 94 | #### Examples 95 | 96 | - Deploy a preview of static website in a `build` directory with no functions 97 | 98 | ```shell 99 | sui deploy --dir="build" 100 | ... 101 | ❯ Website Url: https://xxxxx.cloudfront.net 102 | ``` 103 | 104 | - Deploy a preview of static website with serverless functions 105 | 106 | ```shell 107 | sui deploy --dir="build" --functions="lambdas" 108 | ... 109 | ❯ Website Url: https://xxxxx.cloudfront.net 110 | ❯ API Url: https://xxxxx.cloudfront.net/api/my-function-name 111 | ❯ API Url: https://xxxxx.cloudfront.net/api/my-other-function-name 112 | ``` 113 | 114 | - Production deploy 115 | > Note: A custom domain must be configured for production deploys. See [configure-domain](#configure-domain) 116 | 117 | ```shell 118 | sui deploy --prod --dir="build" --functions="lambdas" 119 | ... 120 | ❯ Website Url: https://www.my-domain.com 121 | ❯ API Url: https://www.my-domain.com/api/my-function-name 122 | ❯ API Url: https://www.my-domain.com/api/my-other-function-name 123 | ``` 124 | 125 | ### `configure-domain` 126 | 127 | This step only needs to be completed once, but it may take anywhere from 20 minutes - 48 hours to fully propogate 128 | 129 | ```shell 130 | sui configure-domain [--domain] 131 | ``` 132 | 133 | #### Options 134 | 135 | | Option | Description | Default | 136 | | :--------: | ------------------ | :-----: | 137 | | `--domain` | Your custom domain | None | 138 | 139 | #### Examples 140 | 141 | Deploy a Hosted Zone and Certificate to us-east-1 (required region for Cloudfront) 142 | 143 | ```shell 144 | sui configure-domain --domain="serverlessui.app" 145 | ``` 146 | 147 | #### Additional Steps 148 | 149 | A minute or two after running this command, the deploy will "hang" while trying to validate the domain prior to creating the wildcard certificate. 150 | 151 | 1. **Navigate to Route53** 152 | 153 | Find your Hosted Zone and take note of the Zone Id and Name Servers 154 | 155 |

156 | Serverless UI 157 |

158 |

159 | Serverless UI 160 |

161 | 162 | 2. **Update the Nameservers on your Domain Registrar's website (eg. Namecheap)** 163 | 164 |

165 | Serverless UI 166 |

167 | 168 | 3. **Wait** 169 | 170 | The DNS resolution can be as quick as 10 minutes or take up to 48 hours. After some time, the Serverless UI command may timeout, but running it again should pick up where it left off. 171 | 172 | 4. **Navigate to Certificate Manager** 173 | 174 | After the `configure-domain` command has completed successfully, navigate to Certificate Manager and take note of the Certificate Arn (eg. "arn:aws:acm:us-east-1:ACCOUNT_ID:certificate/xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx") 175 | 176 | 5. **Create a Serverless UI config file** 177 | 178 | Place the config file in the root of your project 179 | 180 | > serverlessui.config.js 181 | 182 | ```js 183 | module.exports = { 184 | domain: "serverlessui.app", 185 | zoneId: "Z10011111YYYYGGGRRR", 186 | certificateArn: 187 | "arn:aws:acm:us-east-1:ACCOUNT_ID:certificate/xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx", 188 | }; 189 | ``` 190 | 191 | ## Continuous Integration 192 | 193 | Since Serverless UI is a command-line tool available via npm, it will work in almost any CI environment. 194 | 195 | ### GitHub Actions 196 | 197 | > Note: Checkout the action in this repo for a live example https://github.com/JakePartusch/serverlessui/actions 198 | 199 | ```yaml 200 | name: Serverless UI Build & Deploy Preview 201 | 202 | on: [pull_request] 203 | 204 | jobs: 205 | deploy-pr-preview: 206 | runs-on: ubuntu-latest 207 | 208 | steps: 209 | - uses: actions/checkout@v2 210 | - name: Use Node.js 211 | uses: actions/setup-node@v1 212 | with: 213 | node-version: "12.x" 214 | - run: npm ci 215 | - run: npm run build 216 | - name: Configure AWS Credentials 217 | uses: aws-actions/configure-aws-credentials@v1 218 | with: 219 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 220 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 221 | aws-region: us-west-2 222 | - run: npm install -g @serverlessui/cli 223 | - run: sui deploy --dir="build" 224 | - name: Add PR Comment 225 | uses: actions/github-script@v3 226 | with: 227 | github-token: ${{secrets.GITHUB_TOKEN}} 228 | script: | 229 | const outputs = require(`${process.env.GITHUB_WORKSPACE}/cdk.out/outputs.json`); 230 | const stackName = Object.keys(outputs).find((key) => 231 | key.startsWith("ServerlessUI") 232 | ); 233 | const baseUrlKey = Object.keys(outputs[stackName]).find((key) => 234 | key.startsWith("ServerlessUIBaseUrl") 235 | ); 236 | github.issues.createComment({ 237 | issue_number: context.issue.number, 238 | owner: context.repo.owner, 239 | repo: context.repo.repo, 240 | body: `✅ Your deploy preview is ready: ${outputs[stackName][baseUrlKey]}`, 241 | }); 242 | ``` 243 | 244 | ## 👩‍🔬 Experimental Features 245 | 246 | In order to use experimental features, a `serverlessui.config.js` file must exist at the base of the project. 247 | 248 | ### \_\_experimental_privateS3 249 | 250 | This experimental feature allows the configuration of a private S3 bucket — which may be desired for enhanced security. This feature can be enabled in `serverlessui.config.js`: 251 | 252 | ```javascript 253 | module.exports = { 254 | __experimental_privateS3: true, 255 | }; 256 | ``` 257 | 258 | ## 👩‍💻 Advanced Use Cases 259 | 260 | For existing serverless projects or those that may have additional CloudFormation and/or CDK infrastructure, Serverless UI provides CDK constructs for each of the cli actions: 261 | 262 | ```javascript 263 | import { ServerlessUI, DomainCertificate } from '@serverlessui/construct; 264 | ``` 265 | 266 | ### Serverless UI Advanced Example 267 | 268 | For a full-featured example, check out: 269 | https://github.com/JakePartusch/serverlessui-advanced-example 270 | 271 | ```javascript 272 | const { functions } = new ServerlessUI(this, "ServerlessUI", { 273 | buildId: "advanced-example", 274 | uiSources: [Source.asset(`${__dirname}/../build`)], 275 | apiEntries: [`${__dirname}/../functions/graphql.ts`], 276 | apiEnvironment: { 277 | TABLE_NAME: table.tableName, 278 | }, 279 | domain: { 280 | domainName: "serverlessui.app", 281 | hostedZone: HostedZone.fromHostedZoneAttributes(this, "HostedZone", { 282 | hostedZoneId: "Z1XXXXXXXXXXXXX", 283 | zoneName: "serverlessui.app", 284 | }), 285 | certificate: Certificate.fromCertificateArn( 286 | this, 287 | "Certificate", 288 | "arn:aws:acm:us-east-1:xxxxxxxxxx:certificate/xxxxxx-xxxx-xxxx-xxxxxx" 289 | ), 290 | }, 291 | }); 292 | ``` 293 | 294 | ## FAQ 295 | 296 | - Q. How is this different than Netlify or Vercel? 297 | - Serverless UI allows you to enjoy the benefits of pre-configured infrastructure without going through a middleman. This allows for fewer accounts, tighter security and seamless integration with a wealth of AWS services. Additionally, you receive these benefits "at cost" since this is deployed directly to your AWS account. 298 | 299 | ## License 300 | 301 | Licensed under the [MIT License](./LICENSE). 302 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/application.ts", 3 | "context": { 4 | "@aws-cdk/core:enableStackNameDuplicates": "true", 5 | "aws-cdk:enableDiffNoFail": "true", 6 | "@aws-cdk/core:stackRelativeExports": "true", 7 | "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true, 8 | "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true, 9 | "@aws-cdk/aws-kms:defaultKeyPolicies": true, 10 | "@aws-cdk/aws-s3:grantWriteWithoutAcl": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist 3 | 4 | # dependencies 5 | node_modules/ 6 | .snowpack/ 7 | 8 | # logs 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # environment variables 14 | .env 15 | .env.production 16 | 17 | # macOS-specific files 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /docs/.npmrc: -------------------------------------------------------------------------------- 1 | ## force pnpm to hoist 2 | shamefully-hoist = true -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Astro Starter Kit: Docs Site 2 | 3 | ``` 4 | npm init astro -- --template docs 5 | ``` 6 | 7 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 8 | 9 | Features: 10 | 11 | - ✅ CSS Grid Layout 12 | - ✅ Full Markdown support 13 | - ✅ Automatic header navigation sidebar 14 | - ✅ Dark mode enabled by default 15 | 16 | ## 🧞 Commands 17 | 18 | All commands are run from the root of the project, from a terminal: 19 | 20 | | Command | Action | 21 | |:----------------|:--------------------------------------------| 22 | | `npm install` | Installs dependencies | 23 | | `npm start` | Starts local dev server at `localhost:3000` | 24 | | `npm run build` | Build your production site to `./dist/` | 25 | 26 | ## 👀 Want to learn more? 27 | 28 | Feel free to check [our documentation](https://github.com/snowpackjs/astro) or jump into our [Discord server](https://astro.build/chat). 29 | -------------------------------------------------------------------------------- /docs/astro.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | // projectRoot: '.', // Where to resolve all URLs relative to. Useful if you have a monorepo project. 3 | pages: "./src/pages", // Path to Astro components, pages, and data 4 | dist: "./dist", // When running `astro build`, path to final static output 5 | public: "./public", // A folder of static files Astro will copy to the root. Useful for favicons, images, and other files that don’t need processing. 6 | buildOptions: { 7 | site: "https://www.serverlessui.app/", // Your public domain, e.g.: https://my-site.dev/. Used to generate sitemaps and canonical URLs. 8 | sitemap: true, // Generate sitemap (set to "false" to disable) 9 | }, 10 | devOptions: { 11 | // port: 3000, // The port to run the dev server on. 12 | // tailwindConfig: '', // Path to tailwind.config.js if used, e.g. './tailwind.config.js' 13 | }, 14 | renderers: ["@astrojs/renderer-preact"], 15 | }; 16 | -------------------------------------------------------------------------------- /docs/images/domain-registrar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JakePartusch/serverlessui/c28e57a72e4428af50dc42997f47a90f5f0dffb7/docs/images/domain-registrar.png -------------------------------------------------------------------------------- /docs/images/hosted-zone-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JakePartusch/serverlessui/c28e57a72e4428af50dc42997f47a90f5f0dffb7/docs/images/hosted-zone-id.png -------------------------------------------------------------------------------- /docs/images/name-servers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JakePartusch/serverlessui/c28e57a72e4428af50dc42997f47a90f5f0dffb7/docs/images/name-servers.png -------------------------------------------------------------------------------- /docs/images/undraw_To_the_stars_qhyy.svg: -------------------------------------------------------------------------------- 1 | To the stars -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/docs", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "astro dev", 7 | "build": "astro build" 8 | }, 9 | "devDependencies": { 10 | "astro": "^0.17.3", 11 | "@astrojs/renderer-preact": "^0.1.3" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/public/index.css: -------------------------------------------------------------------------------- 1 | /* @import "./theme"; */ 2 | @import "./code"; 3 | 4 | * { 5 | box-sizing: border-box; 6 | margin: 0; 7 | } 8 | 9 | :root { 10 | --user-font-scale: 1rem - 16px; 11 | --max-width: calc(100% - 2rem); 12 | --font-fallback: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, 13 | sans-serif, Apple Color Emoji, Segoe UI Emoji; 14 | --font-body: system-ui, var(--font-fallback); 15 | --font-mono: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 16 | monospace; 17 | 18 | --color-white: #fff; 19 | --color-black: #000014; 20 | 21 | --color-gray-50: #f9fafb; 22 | --color-gray-100: #f3f4f6; 23 | --color-gray-200: #e5e7eb; 24 | --color-gray-300: #d1d5db; 25 | --color-gray-400: #9ca3af; 26 | --color-gray-500: #6b7280; 27 | --color-gray-600: #4b5563; 28 | --color-gray-700: #374151; 29 | --color-gray-800: #1f2937; 30 | --color-gray-900: #111827; 31 | 32 | --color-blue: #3894ff; 33 | --color-blue-rgb: 56, 148, 255; 34 | --color-green: #17c083; 35 | --color-green-rgb: 23, 192, 131; 36 | --color-orange: #ff5d01; 37 | --color-orange-rgb: 255, 93, 1; 38 | --color-purple: #882de7; 39 | --color-purple-rgb: 136, 45, 231; 40 | --color-red: #ff1639; 41 | --color-red-rgb: 255, 22, 57; 42 | --color-yellow: #ffbe2d; 43 | --color-yellow-rgb: 255, 190, 45; 44 | } 45 | 46 | :root { 47 | color-scheme: light; 48 | --theme-accent: var(--color-blue); 49 | --theme-accent-rgb: var(--color-blue-rgb); 50 | --theme-accent-opacity: 0.1; 51 | --theme-divider: var(--color-gray-100); 52 | --theme-text: var(--color-gray-800); 53 | --theme-text-light: var(--color-gray-600); 54 | --theme-text-lighter: var(--color-gray-400); 55 | --theme-bg: var(--color-white); 56 | --theme-bg-offset: var(--color-gray-100); 57 | --theme-bg-accent: rgba(var(--theme-accent-rgb), var(--theme-accent-opacity)); 58 | --theme-code-inline-bg: var(--color-gray-100); 59 | --theme-code-text: var(--color-gray-100); 60 | --theme-code-bg: var(--color-gray-700); 61 | } 62 | 63 | @media (min-width: 50em) { 64 | :root { 65 | --max-width: 48em; 66 | } 67 | } 68 | 69 | body { 70 | display: flex; 71 | flex-direction: column; 72 | min-height: 100vh; 73 | font-family: var(--font-body); 74 | font-size: 1rem; 75 | font-size: clamp( 76 | 0.875rem, 77 | 0.4626rem + 1.0309vw + var(--user-font-scale), 78 | 1.125rem 79 | ); 80 | line-height: 1.625; 81 | background: var(--theme-bg); 82 | color: var(--theme-text); 83 | } 84 | 85 | :root.theme-dark { 86 | color-scheme: dark; 87 | --theme-accent-opacity: 0.3; 88 | --theme-divider: var(--color-gray-900); 89 | --theme-text: var(--color-gray-200); 90 | --theme-text-light: var(--color-gray-400); 91 | --theme-text-lighter: var(--color-gray-600); 92 | --theme-bg: var(--color-black); 93 | --theme-bg-offset: var(--color-gray-900); 94 | --theme-code-inline-bg: var(--color-gray-800); 95 | --theme-code-text: var(--color-gray-200); 96 | --theme-code-bg: var(--color-gray-900); 97 | } 98 | 99 | ::selection { 100 | color: var(--theme-accent); 101 | background-color: rgba(var(--theme-accent-rgb), var(--theme-accent-opacity)); 102 | } 103 | 104 | nav ul { 105 | list-style: none; 106 | padding: 0; 107 | } 108 | 109 | .content main > * + * { 110 | margin-top: 1rem; 111 | } 112 | 113 | /* Typography */ 114 | :is(h1, h2, h3, h4, h5, h6) { 115 | margin-bottom: 1.38rem; 116 | font-weight: 400; 117 | line-height: 1.3; 118 | } 119 | 120 | :is(h1, h2) { 121 | max-width: 40ch; 122 | } 123 | 124 | :is(h2, h3):not(:first-child) { 125 | margin-top: 3rem; 126 | } 127 | 128 | h1 { 129 | font-size: clamp(2.488rem, 1.924rem + 1.41vw, 3.052rem); 130 | } 131 | 132 | h2 { 133 | font-size: clamp(2.074rem, 1.707rem + 0.9175vw, 2.441rem); 134 | } 135 | 136 | h3 { 137 | font-size: clamp(1.728rem, 1.503rem + 0.5625vw, 1.953rem); 138 | } 139 | 140 | h4 { 141 | font-size: clamp(1.44rem, 1.317rem + 0.3075vw, 1.563rem); 142 | } 143 | 144 | h5 { 145 | font-size: clamp(1.2rem, 1.15rem + 0.125vw, 1.25rem); 146 | } 147 | 148 | p { 149 | color: var(--theme-text-light); 150 | } 151 | 152 | small, 153 | .text_small { 154 | font-size: 0.833rem; 155 | } 156 | 157 | a { 158 | color: var(--theme-accent); 159 | font-weight: 400; 160 | text-underline-offset: 0.08em; 161 | text-decoration: none; 162 | display: inline-flex; 163 | align-items: center; 164 | gap: 0.5rem; 165 | } 166 | 167 | a > code:not([class*="language"]) { 168 | position: relative; 169 | color: var(--theme-accent); 170 | background: transparent; 171 | text-underline-offset: var(--padding-block); 172 | } 173 | 174 | a > code:not([class*="language"])::before { 175 | content: ""; 176 | position: absolute; 177 | top: 0; 178 | right: 0; 179 | bottom: 0; 180 | left: 0; 181 | display: block; 182 | background: var(--theme-accent); 183 | opacity: var(--theme-accent-opacity); 184 | border-radius: var(--border-radius); 185 | } 186 | 187 | a:hover, 188 | a:focus { 189 | text-decoration: underline; 190 | } 191 | 192 | a:focus { 193 | outline: 2px solid currentColor; 194 | outline-offset: 0.25em; 195 | } 196 | 197 | strong { 198 | font-weight: 600; 199 | color: inherit; 200 | } 201 | 202 | /* Supporting Content */ 203 | 204 | code:not([class*="language"]) { 205 | --border-radius: 3px; 206 | --padding-block: 0.2rem; 207 | --padding-inline: 0.33rem; 208 | 209 | font-family: var(--font-mono); 210 | font-size: 0.85em; 211 | color: inherit; 212 | background-color: var(--theme-code-inline-bg); 213 | padding: var(--padding-block) var(--padding-inline); 214 | margin: calc(var(--padding-block) * -1) -0.125em; 215 | border-radius: var(--border-radius); 216 | } 217 | 218 | pre > code:not([class*="language"]) { 219 | background-color: transparent; 220 | padding: 0; 221 | margin: 0; 222 | border-radius: 0; 223 | color: inherit; 224 | } 225 | 226 | pre { 227 | position: relative; 228 | background-color: var(--theme-code-bg); 229 | color: var(--theme-code-text); 230 | --padding-block: 1rem; 231 | --padding-inline: 2rem; 232 | padding: var(--padding-block) var(--padding-inline); 233 | padding-right: calc(var(--padding-inline) * 2); 234 | margin-left: calc(50vw - var(--padding-inline)); 235 | transform: translateX(-50vw); 236 | 237 | line-height: 1.414; 238 | width: calc(100vw + 4px); 239 | max-width: calc(100% + (var(--padding-inline) * 2)); 240 | overflow-y: hidden; 241 | overflow-x: auto; 242 | } 243 | 244 | @media (min-width: 37.75em) { 245 | pre { 246 | --padding-inline: 1.25rem; 247 | border-radius: 8px; 248 | } 249 | } 250 | 251 | blockquote { 252 | margin: 2rem 0; 253 | padding: 0.5em 1rem; 254 | border-left: 3px solid rgba(0, 0, 0, 0.35); 255 | background-color: rgba(0, 0, 0, 0.05); 256 | border-radius: 0 0.25rem 0.25rem 0; 257 | } 258 | 259 | .flex { 260 | display: flex; 261 | align-items: center; 262 | } 263 | 264 | header button { 265 | background-color: var(--theme-bg); 266 | } 267 | header button:hover, 268 | header button:focus { 269 | background: var(--theme-text); 270 | color: var(--theme-bg); 271 | } 272 | 273 | button { 274 | display: flex; 275 | align-items: center; 276 | justify-items: center; 277 | gap: 0.25em; 278 | padding: 0.33em 0.67em; 279 | border: 0; 280 | background: var(--theme-bg); 281 | display: flex; 282 | font-size: 1rem; 283 | align-items: center; 284 | gap: 0.25em; 285 | border-radius: 99em; 286 | background-color: var(--theme-bg); 287 | } 288 | button:hover { 289 | } 290 | 291 | #theme-toggle { 292 | display: flex; 293 | align-items: center; 294 | gap: 0.25em; 295 | padding: 0.33em 0.67em; 296 | margin-left: -0.67em; 297 | margin-right: -0.67em; 298 | border-radius: 99em; 299 | background-color: var(--theme-bg); 300 | } 301 | 302 | #theme-toggle > label:focus-within { 303 | outline: 2px solid transparent; 304 | box-shadow: 0 0 0 0.08em var(--theme-accent), 0 0 0 0.12em white; 305 | } 306 | 307 | #theme-toggle > label { 308 | position: relative; 309 | display: flex; 310 | align-items: center; 311 | justify-content: center; 312 | font-size: 1.5rem; 313 | width: 1.5rem; 314 | height: 1.5rem; 315 | opacity: 0.5; 316 | transition: transform 120ms ease-out, opacity 120ms ease-out; 317 | } 318 | 319 | #theme-toggle > label:hover, 320 | #theme-toggle > label:focus { 321 | transform: scale(1.125); 322 | opacity: 1; 323 | } 324 | 325 | #theme-toggle .checked { 326 | color: var(--theme-accent); 327 | transform: scale(1.125); 328 | opacity: 1; 329 | } 330 | 331 | input[name="theme-toggle"] { 332 | position: absolute; 333 | opacity: 0; 334 | top: 0; 335 | right: 0; 336 | bottom: 0; 337 | left: 0; 338 | z-index: -1; 339 | } 340 | 341 | nav h4 { 342 | font-weight: 400; 343 | font-size: 1.25rem; 344 | margin: 0; 345 | margin-bottom: 1em; 346 | } 347 | 348 | .edit-on-github, 349 | .header-link { 350 | font-size: 1rem; 351 | padding-left: 1rem; 352 | border-left: 4px solid var(--theme-divider); 353 | } 354 | 355 | .edit-on-github:hover, 356 | .edit-on-github:focus, 357 | .header-link:hover, 358 | .header-link:focus { 359 | color: var(--theme-text-light); 360 | border-left-color: var(--theme-text-lighter); 361 | } 362 | 363 | .header-link:focus-within { 364 | color: var(--theme-text-light); 365 | border-left-color: var(--theme-text-lighter); 366 | } 367 | 368 | .header-link.active { 369 | border-left-color: var(--theme-accent); 370 | color: var(--theme-accent); 371 | } 372 | 373 | .header-link.depth-2 { 374 | font-weight: 600; 375 | } 376 | 377 | .header-link.depth-3 { 378 | padding-left: 2rem; 379 | } 380 | .header-link.depth-4 { 381 | padding-left: 3rem; 382 | } 383 | 384 | .edit-on-github, 385 | .header-link a { 386 | font: inherit; 387 | color: inherit; 388 | text-decoration: none; 389 | } 390 | 391 | .edit-on-github { 392 | margin-top: 2rem; 393 | text-decoration: none; 394 | } 395 | .edit-on-github > * { 396 | text-decoration: none; 397 | } 398 | 399 | .nav-link { 400 | font-size: 1rem; 401 | margin-bottom: 0; 402 | transform: translateX(0); 403 | transition: 120ms transform ease-out; 404 | } 405 | 406 | .nav-link:hover, 407 | .nav-link:focus { 408 | color: var(--theme-text-lighter); 409 | transform: translateX(0.25em); 410 | } 411 | 412 | .nav-link:focus-within { 413 | color: var(--theme-text-lighter); 414 | transform: translateX(0.25em); 415 | } 416 | 417 | .nav-link a { 418 | font: inherit; 419 | color: inherit; 420 | text-decoration: none; 421 | } 422 | 423 | .nav-groups { 424 | padding-bottom: 2rem; 425 | padding-right: 2rem; 426 | max-height: calc(100% - 3rem); 427 | overflow-y: auto; 428 | overflow-x: hidden; 429 | } 430 | 431 | .nav-groups > li + li { 432 | margin-top: 2rem; 433 | } 434 | 435 | /* Scrollbar */ 436 | 437 | /* Firefox */ 438 | body { 439 | scrollbar-width: thin; 440 | scrollbar-color: var(--theme-text-lighter) var(--theme-divider); 441 | } 442 | 443 | /* width */ 444 | ::-webkit-scrollbar { 445 | width: 0.5rem; 446 | } 447 | 448 | /* Track */ 449 | ::-webkit-scrollbar-track { 450 | background: var(--theme-divider); 451 | border-radius: 1rem; 452 | } 453 | 454 | /* Handle */ 455 | ::-webkit-scrollbar-thumb { 456 | background: var(--theme-text-lighter); 457 | border-radius: 1rem; 458 | } 459 | 460 | /* Handle on hover */ 461 | ::-webkit-scrollbar-thumb:hover { 462 | background: var(--theme-text-light); 463 | } 464 | 465 | /* Buttons */ 466 | ::-webkit-scrollbar-button { 467 | display: none; 468 | } 469 | /* Scrollbar - End */ 470 | 471 | .language-css > code, 472 | .language-sass > code, 473 | .language-scss > code { 474 | color: #fd9170; 475 | } 476 | 477 | [class*="language-"] .namespace { 478 | opacity: 0.7; 479 | } 480 | 481 | .token.atrule { 482 | color: #c792ea; 483 | } 484 | 485 | .token.attr-name { 486 | color: #ffcb6b; 487 | } 488 | 489 | .token.attr-value { 490 | color: #a5e844; 491 | } 492 | 493 | .token.attribute { 494 | color: #a5e844; 495 | } 496 | 497 | .token.boolean { 498 | color: #c792ea; 499 | } 500 | 501 | .token.builtin { 502 | color: #ffcb6b; 503 | } 504 | 505 | .token.cdata { 506 | color: #80cbc4; 507 | } 508 | 509 | .token.char { 510 | color: #80cbc4; 511 | } 512 | 513 | .token.class { 514 | color: #ffcb6b; 515 | } 516 | 517 | .token.class-name { 518 | color: #f2ff00; 519 | } 520 | 521 | .token.comment { 522 | color: #616161; 523 | } 524 | 525 | .token.constant { 526 | color: #c792ea; 527 | } 528 | 529 | .token.deleted { 530 | color: #ff6666; 531 | } 532 | 533 | .token.doctype { 534 | color: #616161; 535 | } 536 | 537 | .token.entity { 538 | color: #ff6666; 539 | } 540 | 541 | .token.function { 542 | color: #c792ea; 543 | } 544 | 545 | .token.hexcode { 546 | color: #f2ff00; 547 | } 548 | 549 | .token.id { 550 | color: #c792ea; 551 | font-weight: bold; 552 | } 553 | 554 | .token.important { 555 | color: #c792ea; 556 | font-weight: bold; 557 | } 558 | 559 | .token.inserted { 560 | color: #80cbc4; 561 | } 562 | 563 | .token.keyword { 564 | color: #c792ea; 565 | } 566 | 567 | .token.number { 568 | color: #fd9170; 569 | } 570 | 571 | .token.operator { 572 | color: #89ddff; 573 | } 574 | 575 | .token.prolog { 576 | color: #616161; 577 | } 578 | 579 | .token.property { 580 | color: #80cbc4; 581 | } 582 | 583 | .token.pseudo-class { 584 | color: #a5e844; 585 | } 586 | 587 | .token.pseudo-element { 588 | color: #a5e844; 589 | } 590 | 591 | .token.punctuation { 592 | color: #89ddff; 593 | } 594 | 595 | .token.regex { 596 | color: #f2ff00; 597 | } 598 | 599 | .token.selector { 600 | color: #ff6666; 601 | } 602 | 603 | .token.string { 604 | color: #a5e844; 605 | } 606 | 607 | .token.symbol { 608 | color: #c792ea; 609 | } 610 | 611 | .token.tag { 612 | color: #ff6666; 613 | } 614 | 615 | .token.unit { 616 | color: #fd9170; 617 | } 618 | 619 | .token.url { 620 | color: #ff6666; 621 | } 622 | 623 | .token.variable { 624 | color: #ff6666; 625 | } 626 | -------------------------------------------------------------------------------- /docs/public/theme.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const root = document.documentElement; 3 | if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { 4 | root.classList.add('theme-dark'); 5 | } else { 6 | root.classList.remove('theme-dark'); 7 | } 8 | })(); 9 | -------------------------------------------------------------------------------- /docs/serverlessui.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | domain: "serverlessui.app", 3 | zoneId: "Z10061011Y616GGTRN1OW", 4 | certificateArn: 5 | "arn:aws:acm:us-east-1:857786057494:certificate/be831128-bb68-4fcd-b903-4d112d8fd2cd", 6 | }; 7 | -------------------------------------------------------------------------------- /docs/src/components/ArticleFooter.astro: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /docs/src/components/DocSidebar.tsx: -------------------------------------------------------------------------------- 1 | import type { FunctionalComponent } from 'preact'; 2 | import { h } from 'preact'; 3 | import { useState, useEffect, useRef } from 'preact/hooks'; 4 | import EditOnGithub from './EditOnGithub'; 5 | 6 | const DocSidebar: FunctionalComponent<{ headers: any[]; editHref: string }> = ({ headers = [], editHref }) => { 7 | const itemOffsets = useRef([]); 8 | const [activeId, setActiveId] = useState(undefined); 9 | 10 | useEffect(() => { 11 | const getItemOffsets = () => { 12 | const titles = document.querySelectorAll('article :is(h2, h3, h4)'); 13 | itemOffsets.current = Array.from(titles).map((title) => ({ 14 | id: title.id, 15 | topOffset: title.getBoundingClientRect().top + window.scrollY, 16 | })); 17 | }; 18 | 19 | const onScroll = () => { 20 | const itemIndex = itemOffsets.current.findIndex((item) => item.topOffset > window.scrollY + window.innerHeight / 3); 21 | if (itemIndex === 0) { 22 | setActiveId(undefined); 23 | } else if (itemIndex === -1) { 24 | setActiveId(itemOffsets.current[itemOffsets.current.length - 1].id); 25 | } else { 26 | setActiveId(itemOffsets.current[itemIndex - 1].id); 27 | } 28 | }; 29 | 30 | getItemOffsets(); 31 | window.addEventListener('resize', getItemOffsets); 32 | window.addEventListener('scroll', onScroll); 33 | 34 | return () => { 35 | window.removeEventListener('resize', getItemOffsets); 36 | window.removeEventListener('scroll', onScroll); 37 | }; 38 | }, []); 39 | 40 | return ( 41 | 58 | ); 59 | }; 60 | 61 | export default DocSidebar; 62 | -------------------------------------------------------------------------------- /docs/src/components/EditOnGithub.tsx: -------------------------------------------------------------------------------- 1 | import type { FunctionalComponent } from 'preact'; 2 | import { h } from 'preact'; 3 | 4 | const EditOnGithub: FunctionalComponent<{ href: string }> = ({ href }) => { 5 | return ( 6 | 7 | 17 | 18 | 19 | 20 | 21 | Edit on GitHub 22 | 23 | ); 24 | }; 25 | 26 | export default EditOnGithub; 27 | -------------------------------------------------------------------------------- /docs/src/components/Note.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export interface Props { 3 | title: string; 4 | type?: 'tip' | 'warning' | 'error' 5 | } 6 | const { type = 'tip', title } = Astro.props; 7 | --- 8 | 9 | 13 | 14 | 53 | -------------------------------------------------------------------------------- /docs/src/components/SiteSidebar.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { sidebar } from '../config.ts'; 3 | --- 4 | 5 | 21 | -------------------------------------------------------------------------------- /docs/src/components/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | import type { FunctionalComponent } from 'preact'; 2 | import { h, Fragment } from 'preact'; 3 | import { useState, useEffect } from 'preact/hooks'; 4 | 5 | const themes = ['system', 'light', 'dark']; 6 | 7 | const icons = [ 8 | 9 | 14 | , 15 | 16 | 21 | , 22 | 23 | 24 | , 25 | ]; 26 | 27 | const ThemeToggle: FunctionalComponent = () => { 28 | const [theme, setTheme] = useState(themes[0]); 29 | 30 | useEffect(() => { 31 | const user = localStorage.getItem('theme'); 32 | if (!user) return; 33 | setTheme(user); 34 | }, []); 35 | 36 | useEffect(() => { 37 | const root = document.documentElement; 38 | if (theme === 'system') { 39 | localStorage.removeItem('theme'); 40 | if (window.matchMedia('(prefers-color-scheme: dark)').matches) { 41 | root.classList.add('theme-dark'); 42 | } else { 43 | root.classList.remove('theme-dark'); 44 | } 45 | } else { 46 | localStorage.setItem('theme', theme); 47 | if (theme === 'light') { 48 | root.classList.remove('theme-dark'); 49 | } else { 50 | root.classList.add('theme-dark'); 51 | } 52 | } 53 | }, [theme]); 54 | 55 | return ( 56 |
57 | {themes.map((t, i) => { 58 | const icon = icons[i]; 59 | const checked = t === theme; 60 | return ( 61 | 65 | ); 66 | })} 67 |
68 | ); 69 | }; 70 | 71 | export default ThemeToggle; 72 | -------------------------------------------------------------------------------- /docs/src/config.ts: -------------------------------------------------------------------------------- 1 | export const sidebar = [ 2 | { 3 | text: "Introduction", 4 | link: "", // No leading slash needed, so this links to the homepage 5 | children: [ 6 | { text: "Getting Started", link: "getting-started" }, 7 | { text: "CLI Reference", link: "cli-reference" }, 8 | ], 9 | }, 10 | ]; 11 | -------------------------------------------------------------------------------- /docs/src/layouts/Main.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // Component Imports 3 | import ArticleFooter from '../components/ArticleFooter.astro'; 4 | import SiteSidebar from '../components/SiteSidebar.astro'; 5 | import ThemeToggle from '../components/ThemeToggle.tsx'; 6 | import DocSidebar from '../components/DocSidebar.tsx'; 7 | 8 | // Component Script: 9 | // You can write any JavaScript/TypeScript that you'd like here. 10 | // It will run during the build, but never in the browser. 11 | // All variables are available to use in the HTML template below. 12 | const { content } = Astro.props; 13 | const headers = content.astro.headers; 14 | const currentPage = Astro.request.url.pathname; 15 | const currentFile = currentPage === '/' ? 'src/pages/index.md' : `src/pages${currentPage.replace(/\/$/, "")}.md`; 16 | const githubEditUrl = `https://github.com/jakepartusch/serverlessui/blob/main/docs/${currentFile}` 17 | 18 | // Full Astro Component Syntax: 19 | // https://docs.astro.build/core-concepts/astro-components/ 20 | --- 21 | 22 | 23 | {content.title} 24 | 25 | 26 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/simple/functions/hello.ts: -------------------------------------------------------------------------------- 1 | import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; 2 | 3 | export interface ProxyResponse { 4 | data: string; 5 | } 6 | 7 | export const handler = async ( 8 | event: APIGatewayProxyEvent 9 | ): Promise => { 10 | console.log(JSON.stringify(event, null, 2)); 11 | const response: ProxyResponse = { 12 | data: "Hello World!", 13 | }; 14 | return { 15 | statusCode: 200, 16 | body: JSON.stringify(response), 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /examples/simple/functions/helloAgain.ts: -------------------------------------------------------------------------------- 1 | import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; 2 | 3 | export interface ProxyResponse { 4 | data: string; 5 | } 6 | 7 | export const handler = async ( 8 | event: APIGatewayProxyEvent 9 | ): Promise => { 10 | console.log(JSON.stringify(event, null, 2)); 11 | const response: ProxyResponse = { 12 | data: "Hello World Again!", 13 | }; 14 | return { 15 | statusCode: 200, 16 | body: JSON.stringify(response), 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /examples/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@types/aws-lambda": "^8.10.71", 13 | "typescript": "^4.1.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/simple/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/aws-lambda@^8.10.71": 6 | version "8.10.72" 7 | resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.72.tgz#af2a6eeaf39be9674e3856f1870d9d15cf75e2e0" 8 | integrity sha512-jOrTwAhSiUtBIN/QsWNKlI4+4aDtpZ0sr2BRvKW6XQZdspgHUSHPcuzxbzCRiHUiDQ+0026u5TSE38VyIhNnfA== 9 | 10 | typescript@^4.1.4: 11 | version "4.2.3" 12 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3" 13 | integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw== 14 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "yarn", 3 | "useWorkspaces": true, 4 | "packages": [ 5 | "packages/*" 6 | ], 7 | "version": "0.12.0" 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverlessui", 3 | "version": "0.0.1", 4 | "private": "true", 5 | "workspaces": [ 6 | "./packages/*" 7 | ], 8 | "scripts": {}, 9 | "devDependencies": { 10 | "lerna": "^4.0.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/cli/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | coverage 5 | .nyc_output 6 | dist 7 | build 8 | .vscode 9 | -------------------------------------------------------------------------------- /packages/cli/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 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 | -------------------------------------------------------------------------------- /packages/cli/__tests__/cli-integration.test.ts: -------------------------------------------------------------------------------- 1 | const { system, filesystem } = require('gluegun') 2 | 3 | const src = filesystem.path(__dirname, '..') 4 | 5 | const cli = async (cmd: string) => 6 | system.run('node ' + filesystem.path(src, 'bin', 'cli') + ` ${cmd}`) 7 | 8 | test('outputs version', async () => { 9 | const output = await cli('--version') 10 | expect(output).toContain('0.0.2') 11 | }) 12 | 13 | test('outputs help', async () => { 14 | const output = await cli('--help') 15 | expect(output).toContain('0.0.2') 16 | }) 17 | 18 | test('deploy', async () => { 19 | const output = await cli('deploy --domain=foo.com') 20 | expect(output).toContain('foo.com') 21 | }) 22 | -------------------------------------------------------------------------------- /packages/cli/bin/cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* tslint:disable */ 4 | // check if we're running in dev mode 5 | var devMode = require('fs').existsSync(`${__dirname}/../src`) 6 | // or want to "force" running the compiled version with --compiled-build 7 | var wantsCompiled = process.argv.indexOf('--compiled-build') >= 0 8 | 9 | if (wantsCompiled || !devMode) { 10 | // this runs from the compiled javascript source 11 | require(`${__dirname}/../dist/cli`).run(process.argv) 12 | } else { 13 | // this runs from the typescript source (for dev only) 14 | // hook into ts-node so we can run typescript on the fly 15 | require('ts-node').register({ project: `${__dirname}/../tsconfig.json` }) 16 | // run the CLI with the current process arguments 17 | require(`${__dirname}/../src/cli`).run(process.argv) 18 | } 19 | -------------------------------------------------------------------------------- /packages/cli/docs/commands.md: -------------------------------------------------------------------------------- 1 | # Command Reference for cli 2 | 3 | TODO: Add your command reference here 4 | -------------------------------------------------------------------------------- /packages/cli/docs/plugins.md: -------------------------------------------------------------------------------- 1 | # Plugin guide for cli 2 | 3 | Plugins allow you to add features to cli, such as commands and 4 | extensions to the `toolbox` object that provides the majority of the functionality 5 | used by cli. 6 | 7 | Creating a cli plugin is easy. Just create a repo with two folders: 8 | 9 | ``` 10 | commands/ 11 | extensions/ 12 | ``` 13 | 14 | A command is a file that looks something like this: 15 | 16 | ```js 17 | // commands/foo.js 18 | 19 | module.exports = { 20 | run: (toolbox) => { 21 | const { print, filesystem } = toolbox 22 | 23 | const desktopDirectories = filesystem.subdirectories(`~/Desktop`) 24 | print.info(desktopDirectories) 25 | } 26 | } 27 | ``` 28 | 29 | An extension lets you add additional features to the `toolbox`. 30 | 31 | ```js 32 | // extensions/bar-extension.js 33 | 34 | module.exports = (toolbox) => { 35 | const { print } = toolbox 36 | 37 | toolbox.bar = () => { print.info('Bar!') } 38 | } 39 | ``` 40 | 41 | This is then accessible in your plugin's commands as `toolbox.bar`. 42 | 43 | # Loading a plugin 44 | 45 | To load a particular plugin (which has to start with `cli-*`), 46 | install it to your project using `npm install --save-dev cli-PLUGINNAME`, 47 | and cli will pick it up automatically. 48 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverlessui/cli", 3 | "version": "0.12.0", 4 | "description": "Serverless UI command-line utility for deploying serverless applications to AWS", 5 | "author": "Jake Partusch ", 6 | "license": "MIT", 7 | "homepage": "https://github.com/JakePartusch/serverlessui/tree/main#README.md", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/JakePartusch/serverlessui.git", 11 | "directory": "packages/cli" 12 | }, 13 | "keywords": [ 14 | "cli", 15 | "serverless" 16 | ], 17 | "bugs": { 18 | "url": "https://github.com/JakePartusch/serverlessui/issues" 19 | }, 20 | "types": "dist/types/src/cli.d.ts", 21 | "bin": { 22 | "sui": "bin/cli" 23 | }, 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "scripts": { 28 | "format": "prettier --write **/*.{js,ts,tsx,json}", 29 | "lint": "tslint -p .", 30 | "build": "rollup -c", 31 | "rimraf": "rm -rf ./dist", 32 | "prepack": "yarn rimraf && yarn build", 33 | "test": "jest" 34 | }, 35 | "files": [ 36 | "tsconfig.json", 37 | "tslint.json", 38 | "dist", 39 | "LICENSE", 40 | "readme.md", 41 | "docs", 42 | "bin" 43 | ], 44 | "dependencies": { 45 | "@serverlessui/domain-app": "^0.12.0", 46 | "@serverlessui/serverless-app": "^0.12.0", 47 | "aws-cdk": "1.137.0", 48 | "cosmiconfig": "^7.0.0", 49 | "esbuild": "^0.12.15", 50 | "gluegun": "^4.6.1", 51 | "tiny-glob": "^0.2.8" 52 | }, 53 | "devDependencies": { 54 | "@rollup/plugin-typescript": "^8.2.1", 55 | "@types/jest": "^26.0.23", 56 | "@types/node": "^14.14.42", 57 | "jest": "^27.0.6", 58 | "prettier": "^2.2.1", 59 | "rollup": "^2.45.2", 60 | "ts-jest": "^27.0.4", 61 | "ts-node": "^10.1.0", 62 | "tslint": "^6.1.3", 63 | "tslint-config-prettier": "^1.17.0", 64 | "tslint-config-standard": "^9.0.0", 65 | "typescript": "^4.2.4" 66 | }, 67 | "jest": { 68 | "preset": "ts-jest", 69 | "testEnvironment": "node" 70 | }, 71 | "prettier": { 72 | "semi": false, 73 | "singleQuote": true 74 | }, 75 | "gitHead": "4a136cf3777ae864939139dcaee277e9213fe925" 76 | } 77 | -------------------------------------------------------------------------------- /packages/cli/readme.md: -------------------------------------------------------------------------------- 1 |

2 | Serverless UI 3 |

4 |

5 | Serverless UI 6 |

7 | 8 |

9 | 💻 🚀 ☁ 10 |

11 |

12 | Deploying Websites to AWS on Easy Mode 13 |

14 |

15 | Serverless UI is a free, open source command-line utility for quickly building and deploying serverless applications on AWS 16 |

17 | 18 | ## 📖 CLI Reference 19 | 20 | 1. [deploy](#deploy) 21 | 2. [configure-domain](#configure-domain) 22 | 23 | ### `deploy` 24 | 25 | ```shell 26 | sui deploy 27 | ``` 28 | 29 | #### Options 30 | 31 | | Option | Description | Default | 32 | | :-----------: | ----------------------------------------------------- | :-----------: | 33 | | `--dir` | The directory of your website's static files | `"dist"` | 34 | | `--functions` | The directory of the functions to deploy as endpoints | `"functions"` | 35 | | `--prod` | Custom Domains only: `false` will deploy a preview | `false` | 36 | 37 | > Note: The `--dir` directory should be only static files. You may need to run a build step prior to deploying 38 | 39 | #### Examples 40 | 41 | - Deploy a preview of static website in a `build` directory with no functions 42 | 43 | ```shell 44 | sui deploy --dir="build" 45 | ... 46 | ❯ Website Url: https://xxxxx.cloudfront.net 47 | ``` 48 | 49 | - Deploy a preview of static website with serverless functions 50 | 51 | ```shell 52 | sui deploy --dir="build" --functions="lambdas" 53 | ... 54 | ❯ Website Url: https://xxxxx.cloudfront.net 55 | ❯ API Url: https://xxxxx.cloudfront.net/api/my-function-name 56 | ❯ API Url: https://xxxxx.cloudfront.net/api/my-other-function-name 57 | ``` 58 | 59 | - Production deploy 60 | > Note: A custom domain must be configured for production deploys. See [configure-domain](#configure-domain) 61 | 62 | ```shell 63 | sui deploy --prod --dir="build" --functions="lambdas" 64 | ... 65 | ❯ Website Url: https://www.my-domain.com 66 | ❯ API Url: https://www.my-domain.com/api/my-function-name 67 | ❯ API Url: https://www.my-domain.com/api/my-other-function-name 68 | ``` 69 | 70 | ### `configure-domain` 71 | 72 | This step only needs to be completed once, but it may take anywhere from 20 minutes - 48 hours to fully propogate 73 | 74 | ```shell 75 | sui configure-domain [--domain] 76 | ``` 77 | 78 | #### Options 79 | 80 | | Option | Description | Default | 81 | | :--------: | ------------------ | :-----: | 82 | | `--domain` | Your custom domain | None | 83 | 84 | #### Additional Steps 85 | 86 | A minute or two after running this command, the deploy will "hang" while trying to validate the domain prior to creating the wildcard certificate. 87 | 88 | 1. **Navigate to Route53** 89 | 90 | Find your Hosted Zone and take note of the Zone Id and Name Servers 91 | 92 |

93 | Serverless UI 94 |

95 |

96 | Serverless UI 97 |

98 | 99 | 2. **Update the Nameservers on your Domain Registrar's website (eg. Namecheap)** 100 | 101 |

102 | Serverless UI 103 |

104 | 105 | 3. **Wait** 106 | 107 | The DNS resolution can be as quick as 10 minutes or take up to 48 hours. After some time, the Serverless UI command may timeout, but running it again should pick up where it left off. 108 | 109 | 4. **Navigate to Certificate Manager** 110 | 111 | After the `configure-domain` command has completed successfully, navigate to Certificate Manager and take note of the Certificate Arn (eg. "arn:aws:acm:us-east-1:ACCOUNT_ID:certificate/xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx") 112 | 113 | 5. **Create a Serverless UI config file** 114 | 115 | Place the config file in the root of your project 116 | 117 | > serverlessui.config.js 118 | 119 | ```js 120 | module.exports = { 121 | domain: 'serverlessui.app', 122 | zoneId: 'Z10011111YYYYGGGRRR', 123 | certificateArn: 124 | 'arn:aws:acm:us-east-1:ACCOUNT_ID:certificate/xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx' 125 | } 126 | ``` 127 | 128 | ## License 129 | 130 | Licensed under the [MIT License](./LICENSE). 131 | -------------------------------------------------------------------------------- /packages/cli/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript' 2 | 3 | export default { 4 | input: 'src/cli.ts', 5 | output: { 6 | dir: 'dist', 7 | format: 'cjs' 8 | }, 9 | plugins: [typescript()] 10 | } 11 | -------------------------------------------------------------------------------- /packages/cli/src/cli.ts: -------------------------------------------------------------------------------- 1 | import { build } from 'gluegun' 2 | import { command as DeployCommand } from './commands/deploy' 3 | import { command as ConfigureDomainCommand } from './commands/configure-domain' 4 | 5 | /** 6 | * Create the cli and kick it off 7 | */ 8 | export async function run(argv?: string) { 9 | // create a CLI runtime 10 | const cli = build() 11 | .brand('cli') 12 | .src(__dirname) 13 | .plugins('./node_modules', { matching: 'cli-*', hidden: true }) 14 | .command(DeployCommand) 15 | .command(ConfigureDomainCommand) 16 | .help() // provides default for help, h, --help, -h 17 | .version() // provides default for version, v, --version, -v 18 | .create() 19 | // enable the following method if you'd like to skip loading one of these core extensions 20 | // this can improve performance if they're not necessary for your project: 21 | // .exclude(['meta', 'strings', 'print', 'filesystem', 'semver', 'system', 'prompt', 'http', 'template', 'patching', 'package-manager']) 22 | // and run it 23 | const toolbox = await cli.run(argv) 24 | 25 | // send it back (for testing, mostly) 26 | return toolbox 27 | } 28 | -------------------------------------------------------------------------------- /packages/cli/src/commands/configure-domain.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun' 2 | import * as child_process from 'child_process' 3 | const domainApplicationPath = require.resolve('@serverlessui/domain-app') 4 | 5 | export const command: GluegunCommand = { 6 | name: 'configure-domain', 7 | description: 'Create a Route53 Zone and Wildcard Certificate', 8 | run: async toolbox => { 9 | const { parameters } = toolbox 10 | const { options } = parameters 11 | const { domain } = options 12 | 13 | child_process.execSync( 14 | `cdk synth -c domainName=${domain} -a "node ${domainApplicationPath}" --quiet`, 15 | { 16 | stdio: 'inherit' 17 | } 18 | ) 19 | 20 | child_process.execSync( 21 | `cdk deploy -c domainName=${domain} -a "node ${domainApplicationPath}" --require-approval never`, 22 | { 23 | stdio: 'inherit' 24 | } 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/cli/src/commands/deploy.ts: -------------------------------------------------------------------------------- 1 | import { GluegunCommand } from 'gluegun' 2 | import glob from 'tiny-glob' 3 | import * as child_process from 'child_process' 4 | import { cosmiconfigSync } from 'cosmiconfig' 5 | const serverlessApplicationPath = require.resolve( 6 | '@serverlessui/serverless-app' 7 | ) 8 | 9 | const readFunctionFiles = async (functionsDir: string): Promise => { 10 | try { 11 | const files = await glob(`${functionsDir}/**/*.{js,ts}`) 12 | return files.join(',') 13 | } catch (e) { 14 | return '' 15 | } 16 | } 17 | 18 | export const command: GluegunCommand = { 19 | name: 'deploy', 20 | alias: 'd', 21 | description: 'Deploy your website and serverless functions', 22 | run: async (toolbox) => { 23 | const { parameters } = toolbox 24 | 25 | const { options } = parameters 26 | 27 | const { functions = './functions', dir = './dist', prod = false } = options 28 | 29 | const apiFiles = await readFunctionFiles(functions) 30 | const explorerSync = cosmiconfigSync('serverlessui') 31 | const configResult = explorerSync.search() 32 | 33 | const prodCli = prod ? '-c prod=true' : '' 34 | 35 | const isPrivateS3Cli = configResult?.config?.__experimental_privateS3 36 | ? '-c isPrivateS3=true' 37 | : '' 38 | 39 | if (isPrivateS3Cli) { 40 | toolbox.print.info( 41 | 'Using experimental feature - Private S3 Bucket with Origin Access Identity' 42 | ) 43 | } 44 | 45 | if (apiFiles.length === 0) { 46 | toolbox.print.info(`No functions found in directory ${functions}`) 47 | } 48 | if (prod) { 49 | toolbox.print.highlight('Deploying Production Stack...') 50 | } else { 51 | toolbox.print.info('Deploying Preview Stack...') 52 | } 53 | if (!configResult?.isEmpty) { 54 | toolbox.print.info('Config file found, overriding defaults') 55 | } 56 | 57 | let domainConfigCli = '' 58 | if ( 59 | configResult?.config?.zoneId && 60 | configResult?.config?.certificateArn && 61 | configResult?.config?.domain 62 | ) { 63 | toolbox.print.info( 64 | 'Using Zone ID, Certificate Arn, and Domain from config file' 65 | ) 66 | domainConfigCli = `-c zoneId="${configResult?.config?.zoneId}" -c certificateArn="${configResult?.config?.certificateArn}" -c domainName="${configResult?.config?.domain}"` 67 | } else { 68 | toolbox.print.warning( 69 | 'Zone ID, Certificate Arn and Domain not specified, defaulting to cloudfront.net' 70 | ) 71 | } 72 | 73 | toolbox.print.highlight( 74 | `npx cdk bootstrap ${prodCli} ${domainConfigCli} -c apiEntries="${apiFiles}" -c uiEntry="${dir}" -a "node ${serverlessApplicationPath}"` 75 | ) 76 | child_process.execSync( 77 | `npx cdk bootstrap ${prodCli} ${domainConfigCli} -c apiEntries="${apiFiles}" -c uiEntry="${dir}" -a "node ${serverlessApplicationPath}"`, 78 | { 79 | stdio: 'inherit', 80 | } 81 | ) 82 | 83 | toolbox.print.highlight( 84 | `npx cdk synth ${prodCli} ${domainConfigCli} -c apiEntries="${apiFiles}" -c uiEntry="${dir}" ${isPrivateS3Cli} -a "node ${serverlessApplicationPath}" --quiet` 85 | ) 86 | child_process.execSync( 87 | `npx cdk synth ${prodCli} ${domainConfigCli} -c apiEntries="${apiFiles}" -c uiEntry="${dir}" ${isPrivateS3Cli} -a "node ${serverlessApplicationPath}" --quiet`, 88 | { 89 | stdio: 'inherit', 90 | } 91 | ) 92 | 93 | toolbox.print.highlight( 94 | `npx cdk deploy ${prodCli} ${domainConfigCli} -c apiEntries="${apiFiles}" -c uiEntry=${dir} ${isPrivateS3Cli} -a "node ${serverlessApplicationPath}" --require-approval never --outputs-file cdk.out/outputs.json` 95 | ) 96 | 97 | child_process.execSync( 98 | `npx cdk deploy ${prodCli} ${domainConfigCli} -c apiEntries="${apiFiles}" -c uiEntry=${dir} ${isPrivateS3Cli} -a "node ${serverlessApplicationPath}" --require-approval never --outputs-file cdk.out/outputs.json`, 99 | { 100 | stdio: 'inherit', 101 | } 102 | ) 103 | }, 104 | } 105 | -------------------------------------------------------------------------------- /packages/cli/src/extensions/cli-extension.ts: -------------------------------------------------------------------------------- 1 | import { GluegunToolbox } from 'gluegun' 2 | 3 | // add your CLI-specific functionality here, which will then be accessible 4 | // to your commands 5 | module.exports = (toolbox: GluegunToolbox) => { 6 | // enable this if you want to read configuration in from 7 | // the current folder's package.json (in a "cli" property), 8 | // cli.config.json, etc. 9 | // toolbox.config = { 10 | // ...toolbox.config, 11 | // ...toolbox.config.loadConfig("cli", process.cwd()) 12 | // } 13 | } 14 | -------------------------------------------------------------------------------- /packages/cli/src/types.ts: -------------------------------------------------------------------------------- 1 | // export types 2 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node10/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declarationDir": "dist/types", 6 | "declaration": true, 7 | "composite": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/cli/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-standard", "tslint-config-prettier"], 3 | "rules": { 4 | "strict-type-predicates": false 5 | }, 6 | "env": { 7 | "jest": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/construct/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverlessui/construct", 3 | "version": "0.12.0", 4 | "description": "CDK constructs to support Serverless UI", 5 | "license": "MIT", 6 | "homepage": "https://github.com/JakePartusch/serverlessui/tree/main#README.md", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/JakePartusch/serverlessui.git", 10 | "directory": "packages/construct" 11 | }, 12 | "keywords": [ 13 | "cli", 14 | "serverless", 15 | "cdk" 16 | ], 17 | "bugs": { 18 | "url": "https://github.com/JakePartusch/serverlessui/issues" 19 | }, 20 | "main": "dist/index.js", 21 | "types": "dist/types/src/index.d.ts", 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "scripts": { 26 | "build": "rollup -c", 27 | "rimraf": "rm -rf ./dist", 28 | "prepack": "yarn rimraf && yarn build" 29 | }, 30 | "dependencies": { 31 | "@aws-cdk/aws-apigateway": "1.137.0", 32 | "@aws-cdk/aws-apigatewayv2": "1.137.0", 33 | "@aws-cdk/aws-apigatewayv2-integrations": "1.137.0", 34 | "@aws-cdk/aws-certificatemanager": "1.137.0", 35 | "@aws-cdk/aws-cloudfront": "1.137.0", 36 | "@aws-cdk/aws-cloudfront-origins": "1.137.0", 37 | "@aws-cdk/aws-lambda-nodejs": "1.137.0", 38 | "@aws-cdk/aws-route53": "1.137.0", 39 | "@aws-cdk/aws-route53-targets": "1.137.0", 40 | "@aws-cdk/aws-s3": "1.137.0", 41 | "@aws-cdk/aws-s3-deployment": "1.137.0", 42 | "@aws-cdk/core": "1.137.0", 43 | "deepmerge": "^4.2.2" 44 | }, 45 | "files": [ 46 | "tsconfig.json", 47 | "dist", 48 | "LICENSE", 49 | "readme.md" 50 | ], 51 | "devDependencies": { 52 | "@rollup/plugin-typescript": "^8.2.1", 53 | "@tsconfig/node10": "^1.0.7", 54 | "rollup": "^2.45.2", 55 | "typescript": "^4.2.4" 56 | }, 57 | "gitHead": "4a136cf3777ae864939139dcaee277e9213fe925" 58 | } 59 | -------------------------------------------------------------------------------- /packages/construct/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | 3 | export default { 4 | input: "src/index.ts", 5 | output: { 6 | dir: "dist", 7 | format: "cjs", 8 | }, 9 | plugins: [typescript()], 10 | }; 11 | -------------------------------------------------------------------------------- /packages/construct/src/domain-certificate.construct.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Certificate, 3 | CertificateValidation, 4 | ICertificate, 5 | } from "@aws-cdk/aws-certificatemanager"; 6 | import { IHostedZone, PublicHostedZone } from "@aws-cdk/aws-route53"; 7 | import { Construct } from "@aws-cdk/core"; 8 | 9 | interface DomainCertificateProps { 10 | /** 11 | * Domain to be used to generate the Route53 zone and wildcard certificate 12 | * Ex: google.com 13 | */ 14 | domainName: string; 15 | } 16 | 17 | export class DomainCertificate extends Construct { 18 | readonly hostedZone: IHostedZone; 19 | readonly certificate: ICertificate; 20 | constructor(scope: Construct, id: string, props: DomainCertificateProps) { 21 | super(scope, id); 22 | 23 | const hostedZone = new PublicHostedZone(this, "HostedZone", { 24 | zoneName: props.domainName, 25 | }); 26 | 27 | const certificate = new Certificate(this, "Certificate", { 28 | domainName: `*.${props.domainName}`, 29 | validation: CertificateValidation.fromDns(hostedZone), 30 | }); 31 | 32 | this.hostedZone = hostedZone; 33 | this.certificate = certificate; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/construct/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./serverless-ui.construct"; 2 | export * from "./domain-certificate.construct"; 3 | -------------------------------------------------------------------------------- /packages/construct/src/serverless-ui.construct.ts: -------------------------------------------------------------------------------- 1 | import { ICertificate } from "@aws-cdk/aws-certificatemanager"; 2 | import { 3 | ARecord, 4 | AaaaRecord, 5 | RecordTarget, 6 | IHostedZone, 7 | } from "@aws-cdk/aws-route53"; 8 | import { CloudFrontTarget } from "@aws-cdk/aws-route53-targets"; 9 | import { 10 | IDistribution, 11 | Distribution, 12 | ViewerProtocolPolicy, 13 | AllowedMethods, 14 | CachePolicy, 15 | DistributionProps, 16 | Function, 17 | FunctionCode, 18 | FunctionEventType, 19 | } from "@aws-cdk/aws-cloudfront"; 20 | import { IFunction, Runtime } from "@aws-cdk/aws-lambda"; 21 | import { NodejsFunction } from "@aws-cdk/aws-lambda-nodejs"; 22 | import { BucketDeployment, ISource } from "@aws-cdk/aws-s3-deployment"; 23 | import { CfnOutput, Construct, RemovalPolicy, Stack } from "@aws-cdk/core"; 24 | import { Bucket, IBucket } from "@aws-cdk/aws-s3"; 25 | import { PolicyStatement, Effect, AnyPrincipal } from "@aws-cdk/aws-iam"; 26 | import * as path from "path"; 27 | import { HttpOrigin, S3Origin } from "@aws-cdk/aws-cloudfront-origins"; 28 | import { HttpLambdaIntegration } from "@aws-cdk/aws-apigatewayv2-integrations"; 29 | import { HttpApi, IHttpApi } from "@aws-cdk/aws-apigatewayv2"; 30 | import { overrideProps } from "./utils"; 31 | 32 | interface Domain { 33 | /** 34 | * The custom domain name for this deployment 35 | */ 36 | domainName: string; 37 | /** 38 | * The hosted zone associated with the custom domain 39 | */ 40 | hostedZone: IHostedZone; 41 | /** 42 | * The wildcard certificate for this custom domain 43 | */ 44 | certificate: ICertificate; 45 | } 46 | 47 | interface ServerlessUIProps { 48 | /** 49 | * The unique id to use in generating the infrastructure and domain. Only used with custom domains 50 | * Ex. https://{buildId}.my-domain.com 51 | */ 52 | buildId?: string; 53 | /** 54 | * The custom domain to use for this deployment 55 | */ 56 | domain?: Domain; 57 | /** 58 | * Paths to the entry files (JavaScript or TypeScript). 59 | */ 60 | apiEntries: string[]; 61 | /** 62 | * The sources from which to deploy the contents of the bucket. 63 | */ 64 | uiSources: ISource[]; 65 | /** 66 | * Key-value pairs that Lambda caches and makes available for your Lambda functions. 67 | * 68 | * Use environment variables to apply configuration changes, such 69 | * as test and production environment configurations, without changing your 70 | * Lambda function source code. 71 | * 72 | * @default - No environment variables. 73 | */ 74 | readonly apiEnvironment?: { 75 | [key: string]: string; 76 | }; 77 | /** 78 | * Optional user provided props to merge with the default props for CloudFront Distribution 79 | */ 80 | cloudFrontDistributionProps?: Partial; 81 | /** 82 | * Experimental - Make the S3 bucket private and use a Cloudfront function to rewrite website URLs 83 | */ 84 | isPrivateS3?: boolean; 85 | } 86 | 87 | export class ServerlessUI extends Construct { 88 | /** 89 | * The s3 bucket the website is deployed to 90 | */ 91 | readonly websiteBucket: IBucket; 92 | /** 93 | * The API Gateway API for the function deployments 94 | */ 95 | readonly httpApi: IHttpApi; 96 | /** 97 | * The Node.js Lambda Functions deployed 98 | */ 99 | readonly functions: IFunction[]; 100 | /** 101 | * The Cloudfront web distribution for the website and API Gateways 102 | */ 103 | readonly distribution: IDistribution; 104 | 105 | constructor(scope: Construct, id: string, props: ServerlessUIProps) { 106 | super(scope, id); 107 | 108 | const websiteBucket = new Bucket(this, "WebsiteBucket", { 109 | removalPolicy: RemovalPolicy.DESTROY, 110 | autoDeleteObjects: true, 111 | ...(!props.isPrivateS3 112 | ? { 113 | publicReadAccess: true, 114 | websiteIndexDocument: "index.html", 115 | } 116 | : {}), 117 | }); 118 | 119 | if (props.isPrivateS3) { 120 | // Apply bucket policy to enforce encryption of data in transit 121 | websiteBucket.addToResourcePolicy( 122 | new PolicyStatement({ 123 | sid: "HttpsOnly", 124 | resources: [`${websiteBucket.bucketArn}/*`], 125 | actions: ["*"], 126 | principals: [new AnyPrincipal()], 127 | effect: Effect.DENY, 128 | conditions: { 129 | Bool: { 130 | "aws:SecureTransport": "false", 131 | }, 132 | }, 133 | }) 134 | ); 135 | } 136 | 137 | /** 138 | * URL rewrite to append index.html to the URI for single page applications 139 | */ 140 | const createRewriteFunction = () => { 141 | return new Function(this, "CloudFrontFunction", { 142 | code: FunctionCode.fromInline(` 143 | function handler(event) { 144 | var request = event.request; 145 | var uri = request.uri; 146 | 147 | // Check whether the URI is missing a file name. 148 | if (uri.endsWith('/')) { 149 | request.uri += 'index.html'; 150 | } 151 | // Check whether the URI is missing a file extension. 152 | else if (!uri.includes('.')) { 153 | request.uri += '/index.html'; 154 | } 155 | 156 | return request; 157 | }`), 158 | }); 159 | }; 160 | 161 | const functionFiles = props.apiEntries.map((apiEntry) => ({ 162 | name: path.basename(apiEntry).split(".")[0], 163 | entry: apiEntry, 164 | })); 165 | 166 | const lambdas = functionFiles.map((functionFile) => { 167 | return new NodejsFunction(this, `NodejsFunction-${functionFile.name}`, { 168 | entry: functionFile.entry, 169 | handler: "handler", 170 | runtime: Runtime.NODEJS_14_X, 171 | bundling: { 172 | externalModules: [ 173 | "aws-sdk", // Use the 'aws-sdk' available in the Lambda runtime 174 | ], 175 | }, 176 | environment: { 177 | ...props.apiEnvironment, 178 | }, 179 | }); 180 | }); 181 | 182 | const httpApi = new HttpApi(this, "HttpApi"); 183 | 184 | lambdas.forEach((lambda, i) => { 185 | const lambdaFileName = functionFiles[i].name; 186 | const lambdaProxyIntegration = new HttpLambdaIntegration( 187 | `LambdaIntegration${i}`, 188 | lambda 189 | ); 190 | 191 | httpApi.addRoutes({ 192 | path: `/api/${lambdaFileName}`, 193 | integration: lambdaProxyIntegration, 194 | }); 195 | }); 196 | 197 | /** 198 | * Build a Cloudfront behavior for each api function that allows all HTTP Methods and has caching disabled. 199 | */ 200 | const additionalBehaviors = { 201 | "/api/*": { 202 | origin: new HttpOrigin( 203 | `${httpApi.apiId}.execute-api.${Stack.of(this).region}.amazonaws.com` 204 | ), 205 | cachePolicy: CachePolicy.CACHING_DISABLED, 206 | allowedMethods: AllowedMethods.ALLOW_ALL, 207 | viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 208 | }, 209 | }; 210 | 211 | const defaultDistributionProps = { 212 | defaultBehavior: { 213 | origin: new S3Origin(websiteBucket), 214 | allowedMethods: AllowedMethods.ALLOW_ALL, 215 | viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 216 | cachePolicy: CachePolicy.CACHING_OPTIMIZED, 217 | compress: true, 218 | functionAssociations: [ 219 | ...(props.isPrivateS3 220 | ? [ 221 | { 222 | function: createRewriteFunction(), 223 | eventType: FunctionEventType.VIEWER_REQUEST, 224 | }, 225 | ] 226 | : []), 227 | ], 228 | }, 229 | defaultRootObject: "index.html", 230 | additionalBehaviors, 231 | certificate: props.domain?.certificate, 232 | domainNames: props.domain 233 | ? [ 234 | props.buildId 235 | ? `${props.buildId}.${props.domain.domainName}` 236 | : `www.${props.domain.domainName}`, 237 | ] 238 | : undefined, 239 | enableLogging: true, 240 | }; 241 | 242 | const mergedDistributionProps = overrideProps( 243 | defaultDistributionProps, 244 | props.cloudFrontDistributionProps ?? {} 245 | ); 246 | 247 | /** 248 | * Creating a Cloudfront distribution for the website bucket with an aggressive caching policy 249 | */ 250 | const distribution = new Distribution( 251 | this, 252 | "Distribution", 253 | mergedDistributionProps 254 | ); 255 | 256 | new BucketDeployment(this, "BucketDeployment", { 257 | sources: props.uiSources, 258 | destinationBucket: websiteBucket!, 259 | distribution: distribution, 260 | retainOnDelete: false, 261 | }); 262 | 263 | if (props.domain) { 264 | new ARecord(this, "IPv4 AliasRecord", { 265 | zone: props.domain.hostedZone, 266 | recordName: props.buildId ?? "www", 267 | target: RecordTarget.fromAlias(new CloudFrontTarget(distribution)), 268 | }); 269 | 270 | new AaaaRecord(this, "IPv6 AliasRecord", { 271 | zone: props.domain.hostedZone, 272 | recordName: props.buildId ?? "www", 273 | target: RecordTarget.fromAlias(new CloudFrontTarget(distribution)), 274 | }); 275 | } 276 | if (!props.domain) { 277 | new CfnOutput(this, "Base Url", { 278 | value: `https://${distribution.distributionDomainName}`, 279 | }); 280 | } else { 281 | new CfnOutput(this, "Base Url", { 282 | value: props.buildId 283 | ? `https://${props.buildId}.${props.domain.domainName}` 284 | : `https://www.${props.domain.domainName}`, 285 | }); 286 | } 287 | 288 | functionFiles.map((apiEntry) => { 289 | if (props.domain) { 290 | new CfnOutput(this, `Function Path - ${apiEntry.name}`, { 291 | value: props.buildId 292 | ? `https://${props.buildId}.${props.domain.domainName}/api/${apiEntry.name}` 293 | : `https://www.${props.domain.domainName}/api`, 294 | }); 295 | } else { 296 | new CfnOutput(this, `Function Path - ${apiEntry.name}`, { 297 | value: `https://${distribution.distributionDomainName}/api/${apiEntry.name}`, 298 | }); 299 | } 300 | }); 301 | 302 | this.websiteBucket = websiteBucket; 303 | this.httpApi = httpApi; 304 | this.functions = lambdas; 305 | this.distribution = distribution; 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /packages/construct/src/utils.ts: -------------------------------------------------------------------------------- 1 | import deepmerge from "deepmerge"; 2 | 3 | function isObject(val: object) { 4 | return ( 5 | val != null && 6 | typeof val === "object" && 7 | Object.prototype.toString.call(val) === "[object Object]" 8 | ); 9 | } 10 | 11 | function isPlainObject(o: object) { 12 | if (Array.isArray(o) === true) { 13 | return true; 14 | } 15 | 16 | if (isObject(o) === false) { 17 | return false; 18 | } 19 | 20 | // If has modified constructor 21 | const ctor = o.constructor; 22 | if (typeof ctor !== "function") { 23 | return false; 24 | } 25 | 26 | // If has modified prototype 27 | const prot = ctor.prototype; 28 | if (isObject(prot) === false) { 29 | return false; 30 | } 31 | 32 | // If constructor does not have an Object-specific method 33 | if (prot.hasOwnProperty("isPrototypeOf") === false) { 34 | return false; 35 | } 36 | 37 | // Most likely a plain Object 38 | return true; 39 | } 40 | 41 | function combineMerge(target: any[], source: any[]) { 42 | return target.concat(source); 43 | } 44 | 45 | function overwriteMerge(target: any[], source: any[]) { 46 | target = source; 47 | return target; 48 | } 49 | 50 | export function overrideProps( 51 | DefaultProps: object, 52 | userProps: object, 53 | concatArray: boolean = false 54 | ): any { 55 | // Override the sensible defaults with user provided props 56 | if (concatArray) { 57 | return deepmerge(DefaultProps, userProps, { 58 | arrayMerge: combineMerge, 59 | isMergeableObject: isPlainObject, 60 | }); 61 | } else { 62 | return deepmerge(DefaultProps, userProps, { 63 | arrayMerge: overwriteMerge, 64 | isMergeableObject: isPlainObject, 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/construct/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node10/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declarationDir": "dist/types", 6 | "declaration": true, 7 | "composite": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/domain-application/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverlessui/domain-app", 3 | "version": "0.12.0", 4 | "description": "CDK Application to deploy a stack with a Hosted Zone and Wildcard Certificate", 5 | "license": "MIT", 6 | "homepage": "https://github.com/JakePartusch/serverlessui/tree/main#README.md", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/JakePartusch/serverlessui.git", 10 | "directory": "packages/domain-application" 11 | }, 12 | "keywords": [ 13 | "cli", 14 | "serverless", 15 | "cdk" 16 | ], 17 | "bugs": { 18 | "url": "https://github.com/JakePartusch/serverlessui/issues" 19 | }, 20 | "main": "dist/index.js", 21 | "types": "dist/types/src/index.d.ts", 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "scripts": { 26 | "build": "rollup -c", 27 | "rimraf": "rm -rf ./dist", 28 | "prepack": "yarn rimraf && yarn build" 29 | }, 30 | "dependencies": { 31 | "@aws-cdk/core": "1.137.0", 32 | "@serverlessui/construct": "^0.12.0" 33 | }, 34 | "devDependencies": { 35 | "@rollup/plugin-typescript": "^8.2.1", 36 | "@tsconfig/node10": "^1.0.7", 37 | "rollup": "^2.45.2", 38 | "typescript": "^4.2.4" 39 | }, 40 | "files": [ 41 | "tsconfig.json", 42 | "dist", 43 | "LICENSE", 44 | "readme.md" 45 | ], 46 | "gitHead": "4a136cf3777ae864939139dcaee277e9213fe925" 47 | } 48 | -------------------------------------------------------------------------------- /packages/domain-application/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | 3 | export default { 4 | input: "src/index.ts", 5 | output: { 6 | dir: "dist", 7 | format: "cjs", 8 | }, 9 | plugins: [typescript()], 10 | }; 11 | -------------------------------------------------------------------------------- /packages/domain-application/src/domain.application.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import { DomainStack } from "./stacks/domain.stack"; 3 | 4 | const app = new cdk.App(); 5 | 6 | const domainName = app.node.tryGetContext("domainName"); 7 | 8 | const domainNameStackName = `ServerlessUIDomain-${domainName 9 | .split(".") 10 | .join("dot")}`; 11 | 12 | new DomainStack(app, domainNameStackName, { 13 | domainName, 14 | //Force the certificate to be created in us-east-1: https://github.com/JakePartusch/serverlessui/issues/20 15 | env: { 16 | region: "us-east-1", 17 | }, 18 | }); 19 | 20 | export const DomainApplication = app; 21 | -------------------------------------------------------------------------------- /packages/domain-application/src/index.ts: -------------------------------------------------------------------------------- 1 | import { DomainApplication } from "./domain.application"; 2 | export default DomainApplication; 3 | -------------------------------------------------------------------------------- /packages/domain-application/src/stacks/domain.stack.ts: -------------------------------------------------------------------------------- 1 | import { App, Stack, StackProps } from "@aws-cdk/core"; 2 | import { DomainCertificate } from "@serverlessui/construct"; 3 | 4 | interface DomainStackProps extends StackProps { 5 | domainName: string; 6 | } 7 | 8 | export class DomainStack extends Stack { 9 | constructor(scope: App, id: string, props: DomainStackProps) { 10 | super(scope, id, props); 11 | 12 | new DomainCertificate(this, "DomainCertificate", { 13 | domainName: props.domainName, 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/domain-application/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node10/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declarationDir": "dist/types", 6 | "declaration": true, 7 | "composite": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/serverless-application/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverlessui/serverless-app", 3 | "version": "0.12.0", 4 | "description": "CDK Application to deploy a stack with a Hosted Zone and Wildcard Certificate", 5 | "license": "MIT", 6 | "homepage": "https://github.com/JakePartusch/serverlessui/tree/main#README.md", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/JakePartusch/serverlessui.git", 10 | "directory": "packages/domain-application" 11 | }, 12 | "keywords": [ 13 | "cli", 14 | "serverless", 15 | "cdk" 16 | ], 17 | "bugs": { 18 | "url": "https://github.com/JakePartusch/serverlessui/issues" 19 | }, 20 | "main": "dist/index.js", 21 | "types": "dist/types/src/index.d.ts", 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "scripts": { 26 | "build": "rollup -c", 27 | "rimraf": "rm -rf ./dist", 28 | "prepack": "yarn rimraf && yarn build" 29 | }, 30 | "dependencies": { 31 | "@aws-cdk/aws-certificatemanager": "1.137.0", 32 | "@aws-cdk/aws-route53": "1.137.0", 33 | "@aws-cdk/aws-s3-deployment": "1.137.0", 34 | "@aws-cdk/core": "1.137.0", 35 | "@serverlessui/construct": "^0.12.0", 36 | "nanoid": "^3.1.22" 37 | }, 38 | "devDependencies": { 39 | "@rollup/plugin-typescript": "^8.2.1", 40 | "@tsconfig/node10": "^1.0.7", 41 | "rollup": "^2.45.2", 42 | "typescript": "^4.2.4" 43 | }, 44 | "files": [ 45 | "tsconfig.json", 46 | "dist", 47 | "LICENSE", 48 | "readme.md" 49 | ], 50 | "gitHead": "4a136cf3777ae864939139dcaee277e9213fe925" 51 | } 52 | -------------------------------------------------------------------------------- /packages/serverless-application/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | 3 | export default { 4 | input: "src/index.ts", 5 | output: { 6 | dir: "dist", 7 | format: "cjs", 8 | }, 9 | plugins: [typescript()], 10 | }; 11 | -------------------------------------------------------------------------------- /packages/serverless-application/src/index.ts: -------------------------------------------------------------------------------- 1 | import { ServerlessUIApplication } from "./serverless-ui.application"; 2 | export default ServerlessUIApplication; 3 | -------------------------------------------------------------------------------- /packages/serverless-application/src/serverless-ui.application.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import { customAlphabet } from "nanoid"; 3 | import { ServerlessUIStack } from "./stacks/serverless-ui.stack"; 4 | 5 | const app = new cdk.App(); 6 | 7 | const domainName = app.node.tryGetContext("domainName"); 8 | const apiEntries = app.node.tryGetContext("apiEntries"); 9 | const uiEntry = app.node.tryGetContext("uiEntry"); 10 | const prod = app.node.tryGetContext("prod"); 11 | const zoneId = app.node.tryGetContext("zoneId"); 12 | const certificateArn = app.node.tryGetContext("certificateArn"); 13 | const isPrivateS3 = app.node.tryGetContext("isPrivateS3"); 14 | 15 | const nanoid = customAlphabet("0123456789abcdef", 8); 16 | const id = nanoid(); 17 | 18 | const stackName = prod 19 | ? "ServerlessUIAppProduction" 20 | : `ServerlessUIAppPreview${id}`; 21 | 22 | new ServerlessUIStack(app, stackName, { 23 | buildId: prod ? undefined : id, 24 | domainName, 25 | zoneId, 26 | certificateArn, 27 | apiEntries: apiEntries.split(",").filter((entry: string) => entry), 28 | uiEntry, 29 | isPrivateS3, 30 | }); 31 | 32 | export const ServerlessUIApplication = app; 33 | -------------------------------------------------------------------------------- /packages/serverless-application/src/stacks/serverless-ui.stack.ts: -------------------------------------------------------------------------------- 1 | import { App, Stack, StackProps } from "@aws-cdk/core"; 2 | import { Certificate } from "@aws-cdk/aws-certificatemanager"; 3 | import { HostedZone } from "@aws-cdk/aws-route53"; 4 | import { Source } from "@aws-cdk/aws-s3-deployment"; 5 | import { ServerlessUI } from "@serverlessui/construct"; 6 | 7 | interface ServerlessUIStackProps extends StackProps { 8 | buildId?: string; 9 | domainName?: string; 10 | zoneId?: string; 11 | certificateArn?: string; 12 | apiEntries: string[]; 13 | uiEntry: string; 14 | isPrivateS3: boolean; 15 | } 16 | 17 | export class ServerlessUIStack extends Stack { 18 | constructor(scope: App, id: string, props: ServerlessUIStackProps) { 19 | super(scope, id, props); 20 | 21 | const domain = 22 | props.domainName && props.zoneId && props.certificateArn 23 | ? { 24 | domainName: props.domainName, 25 | hostedZone: HostedZone.fromHostedZoneAttributes( 26 | this, 27 | "HostedZone", 28 | { 29 | hostedZoneId: props.zoneId, 30 | zoneName: props.domainName, 31 | } 32 | ), 33 | certificate: Certificate.fromCertificateArn( 34 | this, 35 | "Certificate", 36 | props.certificateArn 37 | ), 38 | } 39 | : undefined; 40 | 41 | new ServerlessUI(this, "ServerlessUI", { 42 | ...props, 43 | uiSources: [Source.asset(props.uiEntry)], 44 | domain, 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/serverless-application/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node10/tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declarationDir": "dist/types", 6 | "declaration": true, 7 | "composite": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /scripts/aws.md: -------------------------------------------------------------------------------- 1 | Remove all buckets for Preview builds 2 | 3 | ``` 4 | aws s3api list-buckets \ 5 | --query 'Buckets[?starts_with(Name, `serverlessuiapppreview`) == `true`].[Name]' \ 6 | --output text | xargs -I {} aws s3 rb s3://{} --force 7 | ``` 8 | -------------------------------------------------------------------------------- /serverlessui.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | domain: 'serverlessui.app', 3 | zoneId: 'Z10061011Y616GGTRN1OW', 4 | certificateArn: 5 | 'arn:aws:acm:us-east-1:857786057494:certificate/be831128-bb68-4fcd-b903-4d112d8fd2cd', 6 | }; 7 | --------------------------------------------------------------------------------