├── .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 |
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 |
157 |
158 |
159 |
160 |
161 |
162 | 2. **Update the Nameservers on your Domain Registrar's website (eg. Namecheap)**
163 |
164 |
165 |
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 |
42 |
43 |
Contents
44 |
45 | {headers
46 | .filter(({ depth }) => depth > 1 && depth < 5)
47 | .map((header) => (
48 |
51 | ))}
52 |
53 |
54 |
55 |
56 |
57 |
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 |
10 | {title && {title} }
11 |
12 |
13 |
14 |
53 |
--------------------------------------------------------------------------------
/docs/src/components/SiteSidebar.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { sidebar } from '../config.ts';
3 | ---
4 |
5 |
6 |
7 | {sidebar.map(category => (
8 |
9 |
10 |
11 |
12 | {category.children.map(child => (
13 | {child.text}
14 | ))}
15 |
16 |
17 |
18 | ))}
19 |
20 |
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 |
62 | {icon}
63 | setTheme(t)} />
64 |
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 |
27 |
30 |
31 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
225 |
226 |
227 |
228 | {content.title}
229 |
230 |
231 |
232 |
233 |
234 |
237 |
238 |
239 |
240 |
--------------------------------------------------------------------------------
/docs/src/pages/cli-reference.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: CLI Reference
3 | layout: ../layouts/Main.astro
4 | ---
5 |
6 | ## CLI Reference
7 |
8 | 1. [deploy](#deploy)
9 | 2. [configure-domain](#configure-domain)
10 |
11 | ### `deploy`
12 |
13 | ```shell
14 | sui deploy
15 | ```
16 |
17 | #### Options
18 |
19 | | Option | Description | Default |
20 | | :-----------: | ----------------------------------------------------- | :-----------: |
21 | | `--dir` | The directory of your static files | `"dist"` |
22 | | `--functions` | The directory of the functions to deploy as endpoints | `"functions"` |
23 | | `--prod` | Custom Domains only: `false` will deploy a preview | `false` |
24 |
25 | > Note: The `--dir` directory should be only static files. You may need to run a build step prior to deploying
26 |
27 | #### Examples
28 |
29 | - Deploy a preview of static website in a `build` directory with no functions
30 |
31 | ```shell
32 | sui deploy --dir="build"
33 | ...
34 | Website Url: https://xxxxx.cloudfront.net
35 | ```
36 |
37 | - Deploy a preview of static website with serverless functions
38 |
39 | ```shell
40 | sui deploy --dir="build" --functions="lambdas"
41 | ...
42 | Website Url: https://xxxxx.cloudfront.net
43 | API Url: https://xxxxx.cloudfront.net/api/my-function-name
44 | API Url: https://xxxxx.cloudfront.net/api/my-other-function-name
45 | ```
46 |
47 | - Production deploy
48 | > Note: A custom domain must be configured for production deploys. See [configure-domain](#configure-domain)
49 |
50 | ```shell
51 | sui deploy --prod --dir="build" --functions="lambdas"
52 | ...
53 | Website Url: https://www.my-domain.com
54 | API Url: https://www.my-domain.com/api/my-function-name
55 | API Url: https://www.my-domain.com/api/my-other-function-name
56 | ```
57 |
58 | ### `configure-domain`
59 |
60 | This step only needs to be completed once, but it may take anywhere from 20 minutes - 48 hours to fully propogate
61 |
62 | ```shell
63 | sui configure-domain [--domain]
64 | ```
65 |
66 | #### Options
67 |
68 | | Option | Description | Default |
69 | | :--------: | ------------------ | :-----: |
70 | | `--domain` | Your custom domain | None |
71 |
72 | #### Examples
73 |
74 | Deploy a Hosted Zone and Certificate to us-east-1 (required region for Cloudfront)
75 |
76 | ```shell
77 | sui configure-domain --domain="serverlessui.app"
78 | ```
79 |
80 | #### Additional Steps
81 |
82 | A minute or two after running this command, the deploy will "hang" while trying to validate the domain prior to creating the wildcard certificate.
83 |
84 | 1. **Navigate to Route53**
85 |
86 | Find your Hosted Zone and take note of the Zone Id and Name Servers
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | 2. **Update the Nameservers on your Domain Registrar website (eg. Namecheap)**
96 |
97 |
98 |
99 |
100 |
101 | 3. **Wait**
102 |
103 | 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.
104 |
105 | 4. **Navigate to Certificate Manager**
106 |
107 | 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")
108 |
109 | 5. **Create a Serverless UI config file**
110 |
111 | Place the config file in the root of your project
112 |
113 | > serverlessui.config.js
114 |
115 | ```js
116 | module.exports = {
117 | domain: "serverlessui.app",
118 | zoneId: "Z10011111YYYYGGGRRR",
119 | certificateArn:
120 | "arn:aws:acm:us-east-1:ACCOUNT_ID:certificate/xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx",
121 | };
122 | ```
123 |
--------------------------------------------------------------------------------
/docs/src/pages/getting-started.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Getting Started
3 | layout: ../layouts/Main.astro
4 | ---
5 |
6 | ## Get Up and Running in 5 Minutes
7 |
8 | You can get a new Serverless UI site deployed to you AWS account in just a few steps:
9 |
10 | 1. **AWS Prerequisites**
11 |
12 | In order to deploy to AWS, you will have to configure your machine with local credentials. You will find the best instructions [here](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html).
13 |
14 | 1. **Install the Serverless UI Command-Line Interface**
15 |
16 | ```shell
17 | npm install -g @serverlessui/cli
18 | ```
19 |
20 | 1. **Deploy your static website**
21 |
22 | Finally, tell the Serverless UI where to find your static files.
23 |
24 | ```shell
25 | sui deploy --dir="dist"
26 | ```
27 |
--------------------------------------------------------------------------------
/docs/src/pages/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Serverless UI - Introduction
3 | layout: ../layouts/Main.astro
4 | ---
5 |
6 |
7 |
8 | ## What is Serverless UI?
9 |
10 | Serverless UI is a free, open source command-line utility for quickly building and deploying serverless applications on AWS. Serverless UI supports a variety of features, such as deploy previews, custom domains and serverless functions.
11 |
12 | ## Features
13 |
14 | - **Bring your own UI** It does not matter if it is React, Vue, Svelte or JQuery. If it compiles down to static files, then it is supported.
15 |
16 | - **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.
17 |
18 | - **Deploy Previews** Automatically deploy each iteration of your application with a separate URL to continuously integrate and test with confidence.
19 |
20 | - **Custom Domains** Quickly configure a custom domain to take advantage of production deploys!
21 |
22 | - **TypeScript Support** Write your serverless functions in JavaScript or TypeScript. Either way, they will be bundled down extremely quickly and deployed as Node.js 14 lambdas.
23 |
24 | - **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!
25 |
26 | ## Project Status
27 |
28 | **Serverless UI is still an early beta, missing features and bugs are to be expected!** If you can stomach it, then Astro-built sites are production ready and several production websites built with Astro already exist in the wild. We will update this note once we get closer to a stable, v1.0 release.
29 |
30 | ## Quick Start
31 |
32 | ```bash
33 | # In order to deploy to AWS, you will have to configure your machine with local credentials. You will find the best instructions [here](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html).
34 |
35 | # Install the Serverless UI Command-Line Interface
36 | npm install -g @serverlessui/cli
37 |
38 | # Tell the Serverless UI where to find your website's static files.
39 | sui deploy --dir="dist"
40 | ```
41 |
--------------------------------------------------------------------------------
/examples/gatsby/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .cache/
3 | public
4 |
--------------------------------------------------------------------------------
/examples/gatsby/README.md:
--------------------------------------------------------------------------------
1 | # Gatsby Example Website for Serverless UI
2 |
3 | This is simple Gatsby website that can be deployed with Serverless UI. We use this website for integration tests in this repository.
4 |
5 | Try it yourself with:
6 |
7 | ```shell
8 | npm install -g @serverlessui/cli
9 | npm install
10 | npm run build
11 | sui deploy --dir="public"
12 | ```
13 |
--------------------------------------------------------------------------------
/examples/gatsby/gatsby-config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | siteMetadata: {
3 | title: "gatsby",
4 | },
5 | plugins: [
6 | "gatsby-plugin-image",
7 | "gatsby-plugin-react-helmet",
8 | {
9 | resolve: "gatsby-plugin-manifest",
10 | options: {
11 | icon: "src/images/icon.png",
12 | },
13 | },
14 | "gatsby-plugin-sharp",
15 | "gatsby-transformer-sharp",
16 | {
17 | resolve: "gatsby-source-filesystem",
18 | options: {
19 | name: "images",
20 | path: "./src/images/",
21 | },
22 | __key: "images",
23 | },
24 | ],
25 | };
26 |
--------------------------------------------------------------------------------
/examples/gatsby/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gatsby",
3 | "version": "1.0.0",
4 | "private": true,
5 | "description": "gatsby",
6 | "author": "Jake Partusch",
7 | "keywords": [
8 | "gatsby"
9 | ],
10 | "scripts": {
11 | "develop": "gatsby develop",
12 | "start": "gatsby develop",
13 | "build": "gatsby build",
14 | "serve": "gatsby serve",
15 | "clean": "gatsby clean"
16 | },
17 | "dependencies": {
18 | "gatsby": "^3.0.1",
19 | "gatsby-plugin-image": "^1.0.0",
20 | "gatsby-plugin-manifest": "^3.0.0",
21 | "gatsby-plugin-react-helmet": "^4.0.0",
22 | "gatsby-plugin-sharp": "^3.0.0",
23 | "gatsby-plugin-sitemap": "^3.0.0",
24 | "gatsby-source-filesystem": "^3.0.0",
25 | "gatsby-transformer-sharp": "^3.0.0",
26 | "react": "^17.0.1",
27 | "react-dom": "^17.0.1",
28 | "react-helmet": "^6.1.0"
29 | },
30 | "devDependencies": {}
31 | }
32 |
--------------------------------------------------------------------------------
/examples/gatsby/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 | __experimental_privateS3: true,
7 | };
8 |
--------------------------------------------------------------------------------
/examples/gatsby/src/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JakePartusch/serverlessui/c28e57a72e4428af50dc42997f47a90f5f0dffb7/examples/gatsby/src/images/icon.png
--------------------------------------------------------------------------------
/examples/gatsby/src/pages/404.js:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Link } from "gatsby"
3 |
4 | // styles
5 | const pageStyles = {
6 | color: "#232129",
7 | padding: "96px",
8 | fontFamily: "-apple-system, Roboto, sans-serif, serif",
9 | }
10 | const headingStyles = {
11 | marginTop: 0,
12 | marginBottom: 64,
13 | maxWidth: 320,
14 | }
15 |
16 | const paragraphStyles = {
17 | marginBottom: 48,
18 | }
19 | const codeStyles = {
20 | color: "#8A6534",
21 | padding: 4,
22 | backgroundColor: "#FFF4DB",
23 | fontSize: "1.25rem",
24 | borderRadius: 4,
25 | }
26 |
27 | // markup
28 | const NotFoundPage = () => {
29 | return (
30 |
31 | Not found
32 | Page not found
33 |
34 | Sorry{" "}
35 |
36 | 😔
37 | {" "}
38 | we couldn’t find what you were looking for.
39 |
40 | {process.env.NODE_ENV === "development" ? (
41 | <>
42 |
43 | Try creating a page in src/pages/
.
44 |
45 | >
46 | ) : null}
47 |
48 | Go home.
49 |
50 |
51 | )
52 | }
53 |
54 | export default NotFoundPage
55 |
--------------------------------------------------------------------------------
/examples/gatsby/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Link } from "gatsby";
3 |
4 | // styles
5 | const pageStyles = {
6 | color: "#232129",
7 | padding: 96,
8 | fontFamily: "-apple-system, Roboto, sans-serif, serif",
9 | };
10 | const headingStyles = {
11 | marginTop: 0,
12 | marginBottom: 64,
13 | maxWidth: 320,
14 | };
15 | const headingAccentStyles = {
16 | color: "#663399",
17 | };
18 | const paragraphStyles = {
19 | marginBottom: 48,
20 | };
21 | const codeStyles = {
22 | color: "#8A6534",
23 | padding: 4,
24 | backgroundColor: "#FFF4DB",
25 | fontSize: "1.25rem",
26 | borderRadius: 4,
27 | };
28 | const listStyles = {
29 | marginBottom: 96,
30 | paddingLeft: 0,
31 | };
32 | const listItemStyles = {
33 | fontWeight: 300,
34 | fontSize: 24,
35 | maxWidth: 560,
36 | marginBottom: 30,
37 | };
38 |
39 | const linkStyle = {
40 | color: "#8954A8",
41 | fontWeight: "bold",
42 | fontSize: 16,
43 | verticalAlign: "5%",
44 | };
45 |
46 | const docLinkStyle = {
47 | ...linkStyle,
48 | listStyleType: "none",
49 | marginBottom: 24,
50 | };
51 |
52 | const descriptionStyle = {
53 | color: "#232129",
54 | fontSize: 14,
55 | marginTop: 10,
56 | marginBottom: 0,
57 | lineHeight: 1.25,
58 | };
59 |
60 | const docLink = {
61 | text: "Documentation",
62 | url: "https://www.gatsbyjs.com/docs/",
63 | color: "#8954A8",
64 | };
65 |
66 | const badgeStyle = {
67 | color: "#fff",
68 | backgroundColor: "#088413",
69 | border: "1px solid #088413",
70 | fontSize: 11,
71 | fontWeight: "bold",
72 | letterSpacing: 1,
73 | borderRadius: 4,
74 | padding: "4px 6px",
75 | display: "inline-block",
76 | position: "relative",
77 | top: -2,
78 | marginLeft: 10,
79 | lineHeight: 1,
80 | };
81 |
82 | // data
83 | const links = [
84 | {
85 | text: "Tutorial",
86 | url: "https://www.gatsbyjs.com/docs/tutorial/",
87 | description:
88 | "A great place to get started if you're new to web development. Designed to guide you through setting up your first Gatsby site.",
89 | color: "#E95800",
90 | },
91 | {
92 | text: "How to Guides",
93 | url: "https://www.gatsbyjs.com/docs/how-to/",
94 | description:
95 | "Practical step-by-step guides to help you achieve a specific goal. Most useful when you're trying to get something done.",
96 | color: "#1099A8",
97 | },
98 | {
99 | text: "Reference Guides",
100 | url: "https://www.gatsbyjs.com/docs/reference/",
101 | description:
102 | "Nitty-gritty technical descriptions of how Gatsby works. Most useful when you need detailed information about Gatsby's APIs.",
103 | color: "#BC027F",
104 | },
105 | {
106 | text: "Conceptual Guides",
107 | url: "https://www.gatsbyjs.com/docs/conceptual/",
108 | description:
109 | "Big-picture explanations of higher-level Gatsby concepts. Most useful for building understanding of a particular topic.",
110 | color: "#0D96F2",
111 | },
112 | {
113 | text: "Plugin Library",
114 | url: "https://www.gatsbyjs.com/plugins",
115 | description:
116 | "Add functionality and customize your Gatsby site or app with thousands of plugins built by our amazing developer community.",
117 | color: "#8EB814",
118 | },
119 | {
120 | text: "Build and Host",
121 | url: "https://www.gatsbyjs.com/cloud",
122 | badge: true,
123 | description:
124 | "Now you’re ready to show the world! Give your Gatsby site superpowers: Build and host on Gatsby Cloud. Get started for free!",
125 | color: "#663399",
126 | },
127 | ];
128 |
129 | // markup
130 | const IndexPage = () => {
131 | return (
132 |
133 | Home Page
134 |
135 | Congratulations
136 |
137 | — you just made a Gatsby site!
138 |
139 | 🎉🎉🎉
140 |
141 |
142 | Go to Page 2
143 |
144 | Edit src/pages/index.js
to see this page
145 | update in real-time.{" "}
146 |
147 | 😎
148 |
149 |
150 |
178 |
182 |
183 | );
184 | };
185 |
186 | export default IndexPage;
187 |
--------------------------------------------------------------------------------
/examples/gatsby/src/pages/page-2.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | const Page2 = () => (
3 |
4 | Page 2
5 |
6 | );
7 | export default Page2;
8 |
--------------------------------------------------------------------------------
/examples/simple/README.md:
--------------------------------------------------------------------------------
1 | # Simple Example Website for Serverless UI
2 |
3 | This is _probably_ the most basic website that can be deployed with Serverless UI. We use this website for integration tests in this repository.
4 |
5 | Try it yourself with:
6 |
7 | ```shell
8 | npm install -g @serverlessui/cli
9 | npm install
10 | sui deploy
11 | ```
12 |
--------------------------------------------------------------------------------
/examples/simple/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Serverless UI Example
6 |
7 |
8 |
9 |
10 |
11 |
12 |
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 |
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 |
94 |
95 |
96 |
97 |
98 |
99 | 2. **Update the Nameservers on your Domain Registrar's website (eg. Namecheap)**
100 |
101 |
102 |
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 |
--------------------------------------------------------------------------------