├── .changeset ├── README.md └── config.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ └── feature-request.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── cd-dev.yml │ ├── cd-prod.yml │ ├── ci.yml │ ├── deploy.yml │ └── stale.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.cjs ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── apps ├── docs │ ├── .gitignore │ ├── README.md │ ├── astro.config.ts │ ├── package.json │ ├── public │ │ ├── favicon.svg │ │ └── load-cwr.js │ ├── scripts │ │ └── copy-package-readmes.sh │ ├── src │ │ ├── assets │ │ │ ├── architecture-diagram.png │ │ │ ├── astro-aws-hero-tilted.webp │ │ │ ├── astro-aws-hero.png │ │ │ ├── astro-aws-hero.webp │ │ │ ├── astro-aws-rounded.webp │ │ │ ├── astro-aws.jpg │ │ │ ├── astro-aws.png │ │ │ ├── astro-aws.svg │ │ │ ├── astro-aws.webp │ │ │ └── houston.webp │ │ ├── content │ │ │ ├── config.ts │ │ │ └── docs │ │ │ │ ├── guides │ │ │ │ ├── 01-getting-started.md │ │ │ │ ├── 02-query-parameters.md │ │ │ │ ├── 03-cookies.md │ │ │ │ └── 04-advanced.md │ │ │ │ ├── index.mdx │ │ │ │ └── reference │ │ │ │ ├── architecture.md │ │ │ │ └── packages │ │ │ │ ├── adapter.md │ │ │ │ └── constructs.md │ │ ├── env.d.ts │ │ └── styles │ │ │ └── base.css │ └── tsconfig.json └── infra │ ├── .gitignore │ ├── README.md │ ├── cdk.context.json │ ├── cdk.json │ ├── package.json │ ├── scripts │ └── deploy.sh │ ├── src │ ├── bin │ │ └── infra.ts │ └── lib │ │ ├── constants │ │ └── environments.ts │ │ ├── constructs │ │ ├── basic-graph-widget.ts │ │ ├── cross-region-certificate.ts │ │ └── distribution-metric.ts │ │ ├── stacks │ │ ├── certificate-stack.ts │ │ ├── github-oidc-stack.ts │ │ ├── github-users-stack.ts │ │ ├── monitoring-stack.ts │ │ ├── redirect-stack.ts │ │ └── website-stack.ts │ │ └── types │ │ └── astro-aws-stack-props.ts │ ├── support │ ├── cross-region-certificate │ │ └── index.mjs │ └── redirect │ │ └── index.js │ ├── tsconfig.build.json │ └── tsconfig.json ├── bun.lock ├── examples └── base │ ├── .gitignore │ ├── README.md │ ├── astro.config.ts │ ├── package.json │ ├── public │ └── favicon.svg │ ├── src │ ├── components │ │ └── home │ │ │ └── PersonRow.astro │ ├── env.d.ts │ ├── layouts │ │ └── Layout.astro │ ├── middleware.ts │ ├── mocks │ │ └── person.ts │ └── pages │ │ ├── 404.astro │ │ ├── index.astro │ │ ├── prerender.astro │ │ └── rerender.astro │ ├── tailwind.config.js │ └── tsconfig.json ├── package.json ├── packages ├── adapter │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ └── index.test.ts │ │ ├── args.ts │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── lambda │ │ │ ├── constants.ts │ │ │ ├── handlers │ │ │ │ ├── edge.ts │ │ │ │ ├── ssr-stream.ts │ │ │ │ └── ssr.ts │ │ │ ├── helpers.ts │ │ │ ├── middleware.ts │ │ │ └── types.ts │ │ ├── log.ts │ │ └── shared.ts │ ├── tsconfig.build.json │ └── tsconfig.json └── constructs │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── constructs │ │ ├── astro-aws-cloudfront-distribution.ts │ │ ├── astro-aws-origin.ts │ │ ├── astro-aws-s3-bucket-deployment.ts │ │ ├── astro-aws-s3-bucket.ts │ │ └── astro-aws.ts │ ├── index.ts │ └── types │ │ ├── astro-aws-construct.ts │ │ └── partial-by.ts │ └── tsconfig.json ├── scripts ├── CHANGELOG.md ├── bin.js ├── package.json ├── src │ ├── cmds │ │ └── build.ts │ ├── index.ts │ └── utils │ │ ├── arg-util.ts │ │ ├── config-util.ts │ │ ├── esbuild-util.ts │ │ ├── pkg-util.ts │ │ ├── shell-util.ts │ │ └── ts-util.ts └── tsconfig.json ├── tsconfig.base.json ├── tsconfig.json └── turbo.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json", 3 | "access": "public", 4 | "baseBranch": "main", 5 | "changelog": ["@changesets/changelog-git", { "repo": "lukeshay/astro-aws" }], 6 | "commit": false, 7 | "fixed": [], 8 | "ignore": ["@astro-aws/infra", "@astro-aws/docs", "@astro-aws/examples-base"], 9 | "linked": [["@astro-aws/adapter", "@astro-aws/constructs"]], 10 | "updateInternalDependencies": "patch" 11 | } 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 2 | - lukeshay 3 | custom: 4 | - https://account.venmo.com/u/LukeShay 5 | - https://paypal.me/rlshay 6 | open_collective: astro-aws 7 | # patreon: # Replace with a single Patreon username 8 | # ko_fi: # Replace with a single Ko-fi username 9 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 10 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 11 | # liberapay: # Replace with a single Liberapay username 12 | # issuehunt: # Replace with a single IssueHunt username 13 | # otechie: # Replace with a single Otechie username 14 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: "🐛 Bug Report" 2 | description: "If something isn't working as expected 🤔." 3 | title: "[Bug]: " 4 | labels: ["needs triage", "bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: Thanks for taking the time to file a bug report! Please fill out this form as completely as possible. 9 | 10 | - type: checkboxes 11 | id: input1 12 | attributes: 13 | label: "💻" 14 | options: 15 | - label: Would you like to work on a fix? 16 | 17 | - type: textarea 18 | attributes: 19 | label: Input code 20 | description: You must write here the minimal input code necessary to reproduce the bug. 21 | placeholder: | 22 | ```js 23 | new Stack(app, "Stack", {}); 24 | ``` 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | attributes: 30 | label: Current and expected behavior 31 | description: A clear and concise description of what Babel is doing and what you would expect. 32 | validations: 33 | required: true 34 | 35 | - type: textarea 36 | attributes: 37 | label: Environment 38 | placeholder: | 39 | - Astro AWS version(s): 40 | - Astro version(s): 41 | - Node: 42 | - pnpm/npm/yarn version: [e.g. npm 7/Yarn 2.3] 43 | - AWS CDK version(s): 44 | validations: 45 | required: true 46 | 47 | - type: textarea 48 | attributes: 49 | label: Possible solution 50 | description: "If you have suggestions on a fix for the bug." 51 | 52 | - type: textarea 53 | attributes: 54 | label: Additional context 55 | description: "Add any other context about the problem here. Or a screenshot if applicable." 56 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 🗣 Ask a Question, Discuss 4 | url: https://github.com/lukeshay/astro-aws/discussions 5 | - name: 🤗 Support the Project 6 | url: https://github.com/sponsors/astro-aws 7 | about: Support Astro AWS financially. 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: "🚀 Feature Request" 2 | description: "I have a specific suggestion for Babel!" 3 | labels: ["needs triage", "enhancement"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: Thanks for taking the time to suggest a new feature! Please fill out this form as completely as possible. 8 | 9 | - type: checkboxes 10 | id: input1 11 | attributes: 12 | label: "💻" 13 | description: | 14 | Check this if you would like to implement a PR, we are more than happy to help you go through the process 15 | options: 16 | - label: Would you like to work on this feature? 17 | 18 | - type: textarea 19 | attributes: 20 | label: What problem are you trying to solve? 21 | description: | 22 | A concise description of what the problem is. 23 | placeholder: | 24 | I have an issue when [...] 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | attributes: 30 | label: Describe the solution you'd like 31 | validations: 32 | required: true 33 | 34 | - type: textarea 35 | attributes: 36 | label: Describe alternatives you've considered 37 | validations: 38 | required: true 39 | 40 | - type: textarea 41 | attributes: 42 | label: Documentation, Adoption, Migration Strategy 43 | description: | 44 | If you can, explain how users will be able to use this and how it might be documented. Maybe a mock-up? 45 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | | Q                       | A | 6 | | ----------------------- | ---------------------------------------------------------------------------------------------------------- | 7 | | Fixed Issues? | `Fixes #1, Fixes #2` | 8 | | Patch: Bug Fix? | 9 | | Major: Breaking Change? | 10 | | Minor: New Feature? | 11 | | Tests Added + Pass? | Yes | 12 | | Documentation PR Link | | 13 | | Any Dependency Changes? | 14 | | License | MIT | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/cd-dev.yml: -------------------------------------------------------------------------------- 1 | name: CD - Dev 2 | 3 | on: 4 | workflow_run: 5 | workflows: 6 | - CI 7 | branches: 8 | - main 9 | types: 10 | - completed 11 | 12 | env: 13 | CI: true 14 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 15 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 16 | 17 | permissions: 18 | id-token: write 19 | contents: read 20 | deployments: write 21 | 22 | jobs: 23 | deploy-dev: 24 | name: Deploy Dev 25 | concurrency: 26 | group: ${{ format('{0}-{1}', github.workflow, github.job) }} 27 | uses: ./.github/workflows/deploy.yml 28 | with: 29 | environment_name: development 30 | stack_environment: DEV 31 | secrets: inherit 32 | -------------------------------------------------------------------------------- /.github/workflows/cd-prod.yml: -------------------------------------------------------------------------------- 1 | name: CD - Prod 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | CI: true 8 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 9 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 10 | 11 | permissions: 12 | id-token: write 13 | contents: read 14 | deployments: write 15 | 16 | jobs: 17 | deploy-prod: 18 | name: Deploy Prod 19 | concurrency: 20 | group: ${{ format('{0}-{1}', github.workflow, github.job) }} 21 | uses: ./.github/workflows/deploy.yml 22 | with: 23 | environment_name: production 24 | environment_url: https://www.docs.astro-aws.org/ 25 | stack_environment: PROD 26 | secrets: inherit 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request_target: 5 | push: 6 | branches: 7 | - "main" 8 | 9 | env: 10 | CI: true 11 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 12 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 13 | 14 | permissions: 15 | id-token: write 16 | contents: read 17 | 18 | jobs: 19 | check: 20 | name: Check 21 | runs-on: ubuntu-22.04 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | node_version: 26 | - 20 27 | - 22 28 | cmd: 29 | - build 30 | - test 31 | - format 32 | - synth 33 | steps: 34 | - uses: actions/checkout@v3 35 | - uses: oven-sh/setup-bun@v1 36 | - uses: actions/setup-node@v4 37 | with: 38 | node-version: ${{ matrix.node_version }} 39 | - uses: aws-actions/configure-aws-credentials@v4 40 | with: 41 | role-to-assume: arn:aws:iam::738697399292:role/GitHubOIDCReadOnlyRole 42 | aws-region: us-west-2 43 | - run: bun install 44 | - run: bun run ${{ matrix.cmd }} 45 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | node_version: 7 | description: The version of node to use 8 | required: false 9 | type: string 10 | default: "20" 11 | stack_environment: 12 | description: The stack environment to deploy 13 | required: true 14 | type: string 15 | environment_name: 16 | description: The GitHub environment name 17 | required: true 18 | type: string 19 | environment_url: 20 | description: The GitHub environment url 21 | required: false 22 | type: string 23 | aws_session_name: 24 | description: AWS session name 25 | required: false 26 | type: string 27 | aws_region: 28 | description: The AWS region 29 | required: false 30 | type: string 31 | default: us-east-2 32 | ref: 33 | description: The git ref to checkout 34 | required: false 35 | type: string 36 | repository: 37 | description: The git repository to checkout 38 | required: false 39 | type: string 40 | secrets: 41 | AWS_ROLE_TO_ASSUME: 42 | required: true 43 | AWS_ACCOUNT: 44 | required: true 45 | TURBO_TOKEN: 46 | required: false 47 | TURBO_TEAM: 48 | required: false 49 | 50 | env: 51 | CI: true 52 | 53 | permissions: 54 | id-token: write 55 | contents: read 56 | deployments: write 57 | 58 | jobs: 59 | deploy: 60 | name: Deploy 61 | runs-on: ubuntu-22.04 62 | env: 63 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 64 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 65 | environment: 66 | name: ${{ inputs.environment_name }} 67 | url: ${{ inputs.environment_url }} 68 | steps: 69 | - run: echo "::add-mask::${{ secrets.AWS_ACCOUNT }}" 70 | - uses: actions/checkout@v3 71 | with: 72 | ref: ${{ inputs.ref }} 73 | repository: ${{ inputs.repository }} 74 | - name: Configure AWS Credentials 75 | uses: aws-actions/configure-aws-credentials@v4 76 | with: 77 | aws-region: ${{ inputs.aws_region }} 78 | role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} 79 | - uses: oven-sh/setup-bun@v1 80 | - uses: actions/setup-node@v4 81 | with: 82 | node-version: ${{ inputs.node_version }} 83 | - run: bun install 84 | - run: bun run deploy:one infra -- ${{ inputs.stack_environment }} 85 | env: 86 | AWS_ACCOUNT: ${{ secrets.AWS_ACCOUNT }} 87 | # - run: | 88 | # export TIME="$(TZ=GMT date +'%Y-%m-%d_%H-%M-%S')" 89 | # git tag "${{ inputs.stack_environment }}_latest" --force 90 | # git tag "${{ inputs.stack_environment }}_${TIME}" 91 | # git push origin "${{ inputs.stack_environment }}_latest" --force 92 | # git push origin "${{ inputs.stack_environment }}_${TIME}" 93 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: "Stale issue handler" 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "40 23 * * *" 6 | 7 | permissions: 8 | issues: write 9 | 10 | jobs: 11 | stale: 12 | runs-on: ubuntu-22.04 13 | if: github.repository_owner == 'lukeshay' 14 | steps: 15 | - uses: actions/stale@v7 16 | name: "Close stale issues with no reproduction" 17 | with: 18 | close-issue-message: "This issue has been automatically closed because it received no activity. If you think this was closed by accident, please leave a comment. If you are running into a similar issue, please open a new issue with a reproduction. Thank you." 19 | days-before-issue-close: 7 20 | days-before-issue-stale: 7 21 | days-before-pr-close: -1 22 | days-before-pr-stale: -1 23 | exempt-issue-labels: "blocked,keep,needs triage" 24 | only-labels: "needs more info" 25 | operations-per-run: 300 26 | stale-issue-label: "stale" 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cdk.staging 2 | .DS_Store 3 | .env* 4 | .turbo 5 | *.log* 6 | apps/docs/src/content/docs/references/packages/* 7 | cdk.out/ 8 | dist/ 9 | node_modules/ 10 | apps/docs/public/cwr.js 11 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cdk.staging 2 | .DS_Store 3 | .env* 4 | .turbo 5 | *.log* 6 | apps/docs/src/content/docs/reference/packages/ 7 | cdk.out/ 8 | dist/ 9 | node_modules/ 10 | .astro 11 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require("@lshay/prettier-config") 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "astro-build.astro-vscode", 4 | "arcanis.vscode-zipfs", 5 | "dbaeumer.vscode-eslint", 6 | "esbenp.prettier-vscode" 7 | ], 8 | "unwantedRecommendations": [] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["astro", "awslambda", "powertools", "streamify"], 3 | "files.associations": { 4 | "*.mdx": "markdown" 5 | }, 6 | "search.exclude": { 7 | "**/.pnp.*": true, 8 | "**/.yarn": true 9 | }, 10 | "typescript.enablePromptUseWorkspaceTsdk": true, 11 | "typescript.tsdk": "node_modules/typescript/lib" 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Luke Shay 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # astro-aws 2 | 3 | [![CI](https://github.com/lukeshay/astro-aws/actions/workflows/ci.yml/badge.svg)](https://github.com/lukeshay/astro-aws/actions/workflows/ci.yml) [![CD](https://github.com/lukeshay/astro-aws/actions/workflows/cd-dev.yml/badge.svg)](https://github.com/lukeshay/astro-aws/actions/workflows/cd-dev.yml) 4 | 5 | An SSR adapter for Astro. Deploy your Astro application to AWS Lambda, Cloudfront, and S3 using the AWS CDK. 6 | -------------------------------------------------------------------------------- /apps/docs/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /apps/docs/README.md: -------------------------------------------------------------------------------- 1 | # Starlight Starter Kit: Basics 2 | 3 | ``` 4 | npm create astro@latest -- --template starlight 5 | ``` 6 | 7 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics) 8 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics) 9 | 10 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 11 | 12 | ## 🚀 Project Structure 13 | 14 | Inside of your Astro + Starlight project, you'll see the following folders and files: 15 | 16 | ``` 17 | . 18 | ├── public/ 19 | ├── src/ 20 | │ ├── assets/ 21 | │ ├── content/ 22 | │ │ ├── docs/ 23 | │ │ └── config.ts 24 | │ └── env.d.ts 25 | ├── astro.config.mjs 26 | ├── package.json 27 | └── tsconfig.json 28 | ``` 29 | 30 | Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name. 31 | 32 | Images can be added to `src/assets/` and embedded in Markdown with a relative link. 33 | 34 | Static assets, like favicons, can be placed in the `public/` directory. 35 | 36 | ## 🧞 Commands 37 | 38 | All commands are run from the root of the project, from a terminal: 39 | 40 | | Command | Action | 41 | | :------------------------ | :----------------------------------------------- | 42 | | `npm install` | Installs dependencies | 43 | | `npm run dev` | Starts local dev server at `localhost:4321` | 44 | | `npm run build` | Build your production site to `./dist/` | 45 | | `npm run preview` | Preview your build locally, before deploying | 46 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 47 | | `npm run astro -- --help` | Get help using the Astro CLI | 48 | 49 | ## 👀 Want to learn more? 50 | 51 | Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat). 52 | -------------------------------------------------------------------------------- /apps/docs/astro.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config" 2 | import starlight from "@astrojs/starlight" 3 | import starlightLinksValidator from "starlight-links-validator" 4 | 5 | // https://astro.build/config 6 | export default defineConfig({ 7 | integrations: [ 8 | starlight({ 9 | customCss: ["./src/styles/base.css"], 10 | pagination: true, 11 | sidebar: [ 12 | { 13 | autogenerate: { 14 | directory: "guides", 15 | }, 16 | label: "Guides", 17 | }, 18 | { 19 | autogenerate: { 20 | directory: "reference", 21 | }, 22 | label: "Reference", 23 | }, 24 | ], 25 | social: { 26 | github: "https://github.com/lukeshay/astro-aws", 27 | }, 28 | head: [ 29 | { 30 | tag: "script", 31 | attrs: { 32 | src: "/load-cwr.js", 33 | }, 34 | }, 35 | ], 36 | tagline: "AWS CDK constructs for Astro", 37 | title: "Astro AWS", 38 | plugins: [starlightLinksValidator()], 39 | }), 40 | ], 41 | output: "static", 42 | site: "https://www.astro-aws.org/", 43 | }) 44 | -------------------------------------------------------------------------------- /apps/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@astro-aws/docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "homepage": "https://www.astro-aws.org/", 6 | "repository": { 7 | "type": "git", 8 | "url": "ssh://git@github.com/lukeshay/astro-aws.git", 9 | "directory": "apps/www" 10 | }, 11 | "license": "MIT", 12 | "type": "module", 13 | "files": [ 14 | "dist" 15 | ], 16 | "scripts": { 17 | "astro": "astro", 18 | "build": "pnpm run clean && ./scripts/copy-package-readmes.sh && astro build", 19 | "check": "astro check && tsc", 20 | "clean": "rimraf dist", 21 | "dev": "astro dev", 22 | "preview": "astro preview", 23 | "release": "pnpm run build && pnpm run package", 24 | "start": "astro dev" 25 | }, 26 | "dependencies": { 27 | "@astrojs/starlight": "^0.25.1", 28 | "astro": "^4.11.6", 29 | "sharp": "^0.33.4" 30 | }, 31 | "devDependencies": { 32 | "@astro-aws/adapter": "workspace:^", 33 | "@types/node": "^18.18.0", 34 | "prettier": "^3.3.3", 35 | "rimraf": "^6.0.1", 36 | "starlight-links-validator": "^0.9.1" 37 | }, 38 | "engines": { 39 | "node": "20.x || 22.x" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /apps/docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /apps/docs/public/load-cwr.js: -------------------------------------------------------------------------------- 1 | ;(function (n, i, v, r, s, c, x, z) { 2 | x = window.AwsRumClient = { q: [], n: n, i: i, v: v, r: r, c: c } 3 | window[n] = function (c, p) { 4 | x.q.push({ c: c, p: p }) 5 | } 6 | z = document.createElement("script") 7 | z.async = true 8 | z.src = s 9 | document.head.insertBefore(z, document.head.getElementsByTagName("script")[0]) 10 | })( 11 | "cwr", 12 | "fbe0adac-0c2a-4780-9bc3-73e032005599", 13 | "1.0.0", 14 | "us-east-1", 15 | "/cwr.js", 16 | { 17 | sessionSampleRate: 1, 18 | identityPoolId: "us-east-1:a32bcdfa-cf9b-4010-9a9d-790b5d92f52a", 19 | endpoint: "https://dataplane.rum.us-east-1.amazonaws.com", 20 | telemetries: ["performance", "errors", "http"], 21 | allowCookies: true, 22 | enableXRay: true, 23 | }, 24 | ) 25 | -------------------------------------------------------------------------------- /apps/docs/scripts/copy-package-readmes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | create_frontmatter() { 6 | NAME="${1}" 7 | 8 | echo "---" 9 | echo "title: \"@astro-aws/${NAME}\"" 10 | echo "description: \"NPM package @astro-aws/${NAME}\"" 11 | echo "---" 12 | echo "" 13 | } 14 | 15 | create_more() { 16 | echo "## More" 17 | echo "" 18 | echo "For more information, see the [documentation website](https://astro-aws.org/)" 19 | } 20 | 21 | copy_readme() { 22 | NAME="${1}" 23 | 24 | README_FILE="../../packages/${NAME}/README.md" 25 | DOCS_FILE="./src/content/docs/reference/packages/${NAME}.md" 26 | 27 | if [ -f "${README_FILE}" ]; then 28 | echo "Copying README for ${NAME}..." 29 | 30 | create_frontmatter "${NAME}" > "${DOCS_FILE}" 31 | cat "${README_FILE}" | grep -v "# @astro-aws" >> "${DOCS_FILE}" 32 | create_more >> "${DOCS_FILE}" 33 | fi 34 | } 35 | 36 | mkdir -p src/content/docs/reference/packages 37 | 38 | copy_readme "adapter" 39 | copy_readme "constructs" 40 | -------------------------------------------------------------------------------- /apps/docs/src/assets/architecture-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeshay/astro-aws/01ecac3f3fae6a9b5ca6deb5f4ba4b706aab4ce0/apps/docs/src/assets/architecture-diagram.png -------------------------------------------------------------------------------- /apps/docs/src/assets/astro-aws-hero-tilted.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeshay/astro-aws/01ecac3f3fae6a9b5ca6deb5f4ba4b706aab4ce0/apps/docs/src/assets/astro-aws-hero-tilted.webp -------------------------------------------------------------------------------- /apps/docs/src/assets/astro-aws-hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeshay/astro-aws/01ecac3f3fae6a9b5ca6deb5f4ba4b706aab4ce0/apps/docs/src/assets/astro-aws-hero.png -------------------------------------------------------------------------------- /apps/docs/src/assets/astro-aws-hero.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeshay/astro-aws/01ecac3f3fae6a9b5ca6deb5f4ba4b706aab4ce0/apps/docs/src/assets/astro-aws-hero.webp -------------------------------------------------------------------------------- /apps/docs/src/assets/astro-aws-rounded.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeshay/astro-aws/01ecac3f3fae6a9b5ca6deb5f4ba4b706aab4ce0/apps/docs/src/assets/astro-aws-rounded.webp -------------------------------------------------------------------------------- /apps/docs/src/assets/astro-aws.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeshay/astro-aws/01ecac3f3fae6a9b5ca6deb5f4ba4b706aab4ce0/apps/docs/src/assets/astro-aws.jpg -------------------------------------------------------------------------------- /apps/docs/src/assets/astro-aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeshay/astro-aws/01ecac3f3fae6a9b5ca6deb5f4ba4b706aab4ce0/apps/docs/src/assets/astro-aws.png -------------------------------------------------------------------------------- /apps/docs/src/assets/astro-aws.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeshay/astro-aws/01ecac3f3fae6a9b5ca6deb5f4ba4b706aab4ce0/apps/docs/src/assets/astro-aws.webp -------------------------------------------------------------------------------- /apps/docs/src/assets/houston.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeshay/astro-aws/01ecac3f3fae6a9b5ca6deb5f4ba4b706aab4ce0/apps/docs/src/assets/houston.webp -------------------------------------------------------------------------------- /apps/docs/src/content/config.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | import { defineCollection } from "astro:content" 3 | import { docsSchema, i18nSchema } from "@astrojs/starlight/schema" 4 | 5 | export const collections = { 6 | docs: defineCollection({ 7 | schema: docsSchema(), 8 | }), 9 | i18n: defineCollection({ 10 | schema: i18nSchema(), 11 | type: "data", 12 | }), 13 | } 14 | -------------------------------------------------------------------------------- /apps/docs/src/content/docs/guides/01-getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | description: Getting started guide 4 | --- 5 | 6 | ## What is Astro AWS 7 | 8 | Astro AWS is an [Astro](https://astro.build/) SSR adapter and constructs for deploying your Astro application to AWS. 9 | 10 | > **IMPORTANT NOTE:** These packages only provide the bare minimum AWS CDK configuration to get your application running. Everything that does not need to be configured uses the default values AWS provides. 11 | 12 | ## Start your first Astro project 13 | 14 | Create a new Astro project using the `create-astro` CLI then add the Astro AWS adapter. 15 | 16 | ### Using NPM 17 | 18 | ```sh 19 | npm create astro@latest 20 | npx astro add @astro-aws/adapter 21 | ``` 22 | 23 | ### Using Yarn 24 | 25 | ```sh 26 | yarn create astro@latest 27 | yarn astro add @astro-aws/adapter 28 | ``` 29 | 30 | ### Using PNPM 31 | 32 | ```sh 33 | pnpm create astro@latest 34 | pnpm astro add @astro-aws/adapter 35 | ``` 36 | 37 | ## Build your Astro project 38 | 39 | ```sh 40 | ### Using NPM 41 | npm run build 42 | 43 | # Using Yarn 44 | yarn build 45 | 46 | # Using PNPM 47 | pnpm run build 48 | ``` 49 | 50 | ## Start your first AWS CDK project 51 | 52 | ### Create a new AWS CDK project using the CDK cli. 53 | 54 | ```sh 55 | npm i -g aws-cdk 56 | 57 | mkdir my-cdk-project 58 | cd my-cdk-project 59 | 60 | cdk init app --language typescript 61 | ``` 62 | 63 | ### Add the `@astro-aws/constructs` package 64 | 65 | ```sh 66 | # Using NPM 67 | npm i @astro-aws/constructs 68 | 69 | # Using Yarn 70 | yarn add @astro-aws/constructs 71 | 72 | # Using PNPM 73 | pnpm i @astro-aws/constructs 74 | ``` 75 | 76 | ### Modify `lib/hello-cdk-stack.ts` to contain the following 77 | 78 | ```ts ins={4, 10-14} 79 | // lib/hello-cdk-stack.ts 80 | import { Stack } from "aws-cdk-lib/core" 81 | import type { StackProps } from "aws-cdk-lib/core" 82 | import { AstroAWS } from "@astro-aws/constructs" 83 | 84 | export interface HelloCdkStackProps extends StackProps {} 85 | 86 | export class HelloCdkStack extends Stack { 87 | public constructor(scope: Construct, id: string, props: HelloCdkStackProps) { 88 | super(scope, id, props) 89 | 90 | new AstroAWS(this, "AstroAWS", { 91 | websiteDir: "../my-astro-project", 92 | }) 93 | } 94 | } 95 | ``` 96 | 97 | ### Deploy your cdk project 98 | 99 | ```sh 100 | cdk deploy 101 | ``` 102 | -------------------------------------------------------------------------------- /apps/docs/src/content/docs/guides/02-query-parameters.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Query Parameters 3 | description: Describes how to access query parameters on the server. 4 | --- 5 | 6 | ## Setup 7 | 8 | Follow the [getting started guide](/guides/01-getting-started) to create a new Astro project with the Astro AWS adapter. 9 | 10 | ## Allowing Query Parameters 11 | 12 | In order to allow query parameters to be passed to your application, you must create a custom `CachePolicy` for the CloudFront distribution. The following example based on the getting started guide will allow all query parameters to be passed to your application. 13 | 14 | ```ts ins={16-18,21-31} 15 | // lib/hello-cdk-stack.ts 16 | import { Stack } from "aws-cdk-lib/core" 17 | import type { StackProps } from "aws-cdk-lib/core" 18 | import { AstroAWS } from "@astro-aws/constructs" 19 | import { 20 | CachePolicy, 21 | CacheQueryStringBehavior, 22 | } from "aws-cdk-lib/aws-cloudfront" 23 | 24 | export interface HelloCdkStackProps extends StackProps {} 25 | 26 | export class HelloCdkStack extends Stack { 27 | public constructor(scope: Construct, id: string, props: HelloCdkStackProps) { 28 | super(scope, id, props) 29 | 30 | const cachePolicy = new CachePolicy(this, "CachePolicy", { 31 | queryStringBehavior: CacheQueryStringBehavior.all(), 32 | }) 33 | 34 | new AstroAWS(this, "AstroAWS", { 35 | cdk: { 36 | // This configures all subpaths of /api. 37 | apiBehavior: { 38 | cachePolicy, 39 | }, 40 | // This configures everything excluding subpaths of /api. 41 | cloudfrontDistribution: { 42 | defaultBehavior: { 43 | cachePolicy, 44 | }, 45 | }, 46 | }, 47 | websiteDir: "../my-astro-project", 48 | }) 49 | } 50 | } 51 | ``` 52 | 53 | ``` 54 | 55 | ``` 56 | -------------------------------------------------------------------------------- /apps/docs/src/content/docs/guides/03-cookies.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Cookies 3 | description: Describes how to access cookies on the server. 4 | --- 5 | 6 | ## Setup 7 | 8 | Follow the [getting started guide](/guides/01-getting-started) to create a new Astro project with the Astro AWS adapter. 9 | 10 | ## Allowing Cookies 11 | 12 | In order to allow cookies to be passed to your application, you must create a custom `CachePolicy` for the CloudFront distribution. The following example based on the getting started guide will allow all cookies to be passed to your application. 13 | 14 | ```ts ins={13-15,18-28} 15 | // lib/hello-cdk-stack.ts 16 | import { Stack } from "aws-cdk-lib/core" 17 | import type { StackProps } from "aws-cdk-lib/core" 18 | import { AstroAWS } from "@astro-aws/constructs" 19 | import { CachePolicy, CacheCookieBehavior } from "aws-cdk-lib/aws-cloudfront" 20 | 21 | export interface HelloCdkStackProps extends StackProps {} 22 | 23 | export class HelloCdkStack extends Stack { 24 | public constructor(scope: Construct, id: string, props: HelloCdkStackProps) { 25 | super(scope, id, props) 26 | 27 | const cachePolicy = new CachePolicy(this, "CachePolicy", { 28 | cookieBehavior: CacheCookieBehavior.all(), 29 | }) 30 | 31 | new AstroAWS(this, "AstroAWS", { 32 | cdk: { 33 | // This configures all subpaths of /api. 34 | apiBehavior: { 35 | cachePolicy, 36 | }, 37 | // This configures everything excluding subpaths of /api. 38 | cloudfrontDistribution: { 39 | defaultBehavior: { 40 | cachePolicy, 41 | }, 42 | }, 43 | }, 44 | websiteDir: "../my-astro-project", 45 | }) 46 | } 47 | } 48 | ``` 49 | -------------------------------------------------------------------------------- /apps/docs/src/content/docs/guides/04-advanced.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Advanced 3 | description: Describes advanced usages of Astro AWS. 4 | --- 5 | 6 | ## Configuring Constructs 7 | 8 | All constructs can be configured using the `cdk` object. The following example shows how to configure the `AstroAWS` construct. 9 | 10 | ```ts ins={11} 11 | import { Stack } from "aws-cdk-lib/core" 12 | 13 | export interface HelloCdkStackProps extends StackProps {} 14 | 15 | export class HelloCdkStack extends Stack { 16 | public constructor(scope: Construct, id: string, props: HelloCdkStackProps) { 17 | super(scope, id, props) 18 | 19 | new AstroAWS(this, "AstroAWS", { 20 | websiteDir: "../my-astro-project", 21 | cdk: {}, 22 | }) 23 | } 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /apps/docs/src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Welcome to Astro AWS 3 | description: Get started building your docs site with Astro AWS. 4 | template: splash 5 | banner: 6 | content: | 7 | Consider contributing to this project or starring it on GitHub! 8 | Check it out here. 9 | hero: 10 | tagline: Thank you for considering using Astro AWS! 11 | image: 12 | file: ../../assets/astro-aws-hero.webp 13 | actions: 14 | - text: Read the guide 15 | link: /guides/01-getting-started/ 16 | icon: right-arrow 17 | variant: primary 18 | - text: Checkout the reference 19 | link: /reference/ 20 | --- 21 | -------------------------------------------------------------------------------- /apps/docs/src/content/docs/reference/architecture.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Architecture 3 | --- 4 | 5 | This requires very few resources to deploy. The diagram does not include the following resources: 6 | 7 | - Lambda execution role 8 | - Lambda function url 9 | - Lambda invoke function url permission 10 | - Lambda assets deployment bucket 11 | - Bucket deployment custom resources 12 | 13 |
14 | 15 | ![Diagram](../../../assets/architecture-diagram.png) 16 | -------------------------------------------------------------------------------- /apps/docs/src/content/docs/reference/packages/adapter.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@astro-aws/adapter" 3 | description: "NPM package @astro-aws/adapter" 4 | --- 5 | 6 | 7 | An [Astro](https://astro.build) adapter for building an SSR application and deploying it to AWS Lambda. 8 | 9 | ## Install 10 | 11 | ```sh 12 | # Using NPM 13 | npx astro add @astro-aws/adapter 14 | 15 | # Using Yarn 16 | yarn astro add @astro-aws/adapter 17 | 18 | # Using PNPM 19 | pnpm astro add @astro-aws/adapter 20 | 21 | # Using Bun 22 | bun x astro add @astro-aws/adapter 23 | ``` 24 | 25 | ### Manually 26 | 27 | 1. Install the package. 28 | 29 | ``` 30 | # Using NPM 31 | npm install -D @astro-aws/adapter 32 | 33 | # Using Yarn 34 | yarn add -D @astro-aws/adapter 35 | 36 | # Using PNPM 37 | pnpm add -D @astro-aws/adapter 38 | 39 | # Using Bun 40 | bun add -D @astro-aws/adapter 41 | ``` 42 | 43 | 2. Add the following to your `astro.config.mjs` file. 44 | 45 | ```js 46 | import { defineConfig } from "astro/config" 47 | import astroAws from "@astro-aws/adapter" 48 | 49 | export default defineConfig({ 50 | output: "server", 51 | adapter: astroAws(), 52 | }) 53 | ``` 54 | 55 | ### SSR Usage 56 | 57 | 1. Install the package. 58 | 59 | ``` 60 | # Using NPM 61 | npm install -D @astro-aws/adapter 62 | 63 | # Using Yarn 64 | yarn add -D @astro-aws/adapter 65 | 66 | # Using PNPM 67 | pnpm add -D @astro-aws/adapter 68 | 69 | # Using Bun 70 | bun add -D @astro-aws/adapter 71 | ``` 72 | 73 | 2. Add the following to your `astro.config.mjs` file. 74 | 75 | ```js 76 | import { defineConfig } from "astro/config" 77 | import astroAws from "@astro-aws/adapter" 78 | 79 | export default defineConfig({ 80 | output: "server", 81 | adapter: astroAws({ 82 | mode: "ssr", 83 | }), 84 | }) 85 | ``` 86 | 87 | ### SSR Stream Usage 88 | 89 | 1. Install the package. 90 | 91 | ``` 92 | # Using NPM 93 | npm install -D @astro-aws/adapter 94 | 95 | # Using Yarn 96 | yarn add -D @astro-aws/adapter 97 | 98 | # Using PNPM 99 | pnpm add -D @astro-aws/adapter 100 | 101 | # Using Bun 102 | bun add -D @astro-aws/adapter 103 | ``` 104 | 105 | 2. Add the following to your `astro.config.mjs` file. 106 | 107 | ```js 108 | import { defineConfig } from "astro/config" 109 | import astroAws from "@astro-aws/adapter" 110 | 111 | export default defineConfig({ 112 | output: "server", 113 | adapter: astroAws({ 114 | mode: "ssr-stream", 115 | }), 116 | }) 117 | ``` 118 | 119 | ### Edge Usage 120 | 121 | > **NOTE:** Environment variables are not supported in edge mode. Due to the limitations of AWS Lambda@Edge. 122 | 123 | 1. Install the package. 124 | 125 | ``` 126 | # Using NPM 127 | npm install -D @astro-aws/adapter 128 | 129 | # Using Yarn 130 | yarn add -D @astro-aws/adapter 131 | 132 | # Using PNPM 133 | pnpm add -D @astro-aws/adapter 134 | 135 | # Using Bun 136 | bun add -D @astro-aws/adapter 137 | ``` 138 | 139 | 2. Add the following to your `astro.config.mjs` file. 140 | 141 | ```js 142 | import { defineConfig } from "astro/config" 143 | import astroAws from "@astro-aws/adapter" 144 | 145 | export default defineConfig({ 146 | output: "server", 147 | adapter: astroAws({ 148 | mode: "edge", 149 | }), 150 | }) 151 | ``` 152 | 153 | ## Example 154 | 155 | See [the source code of this site](https://github.com/lukeshay/astro-aws/blob/main/apps/www/astro.config.ts) 156 | ## More 157 | 158 | For more information, see the [documentation website](https://astro-aws.org/) 159 | -------------------------------------------------------------------------------- /apps/docs/src/content/docs/reference/packages/constructs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@astro-aws/constructs" 3 | description: "NPM package @astro-aws/constructs" 4 | --- 5 | 6 | 7 | Constructs for deploying your [Astro](https://astro.build/) project that is built using [@astro-aws/adapter](https://www.npmjs.com/package/@astro-aws/adapter). 8 | 9 | ## Usage 10 | 11 | 1. Install this package and it's peer dependencies in your AWS CDK project. 12 | 13 | ```sh 14 | # Using NPM 15 | npm install @astro-aws/constructs constructs aws-cdk-lib 16 | 17 | # Using Yarn 18 | yarn add @astro-aws/constructs constructs aws-cdk-lib 19 | 20 | # Using PNPM 21 | pnpm add @astro-aws/constructs constructs aws-cdk-lib 22 | 23 | # Using Bun 24 | bun add @astro-aws/constructs constructs aws-cdk-lib 25 | ``` 26 | 27 | 2. Add the construct to your CDK stack. 28 | 29 | ```ts 30 | import { Stack } from "aws-cdk-lib/core" 31 | import type { StackProps } from "aws-cdk-lib/core" 32 | import { AstroAWS } from "@astro-aws/constructs" 33 | 34 | export interface MyAstroStackProps extends StackProps {} 35 | 36 | export class MyAstroStack extends Stack { 37 | public constructor(scope: Construct, id: string, props: MyAstroStackProps) { 38 | super(scope, id, props) 39 | 40 | new AstroAWS(this, "AstroAWS", { 41 | websitePath: "..", // Replace with the path to your website code. 42 | }) 43 | } 44 | } 45 | ``` 46 | 47 | ## Customization 48 | 49 | All the resources created by the `AstroAWS` construct can be customized. We expose every prop of the resources that is customizable. The props can be set by passing them in to the `cdk` field on the `AstroAWS` construct props. Depending on the deployment method, not all of the props will be used. The constructed can be access through the `cdk` field on the `AstroAWS` construct object. 50 | 51 | ```ts 52 | import { Stack, CfnOutput } from "aws-cdk-lib/core" 53 | import type { StackProps } from "aws-cdk-lib/core" 54 | import { AstroAWS } from "@astro-aws/constructs" 55 | 56 | export interface MyAstroStackProps extends StackProps {} 57 | 58 | export class MyAstroStack extends Stack { 59 | public constructor(scope: Construct, id: string, props: MyAstroStackProps) { 60 | super(scope, id, props) 61 | 62 | const astroAWS = new AstroAWS(this, "AstroAWS", { 63 | cdk: { 64 | lambdaFunction: { 65 | memorySize: 1024, 66 | }, 67 | }, 68 | websitePath: "..", // Replace with the path to your website code. 69 | }) 70 | 71 | new CfnOutput(this, "DistributionDomainName", { 72 | value: astroAWS.cdk.cloudfrontDistribution.distributionDomainName, 73 | }) 74 | } 75 | } 76 | ``` 77 | 78 | ## Example 79 | 80 | See [the source code of this site](https://github.com/lukeshay/astro-aws/blob/main/apps/infra/src/lib/stacks/website-stack.ts) 81 | ## More 82 | 83 | For more information, see the [documentation website](https://astro-aws.org/) 84 | -------------------------------------------------------------------------------- /apps/docs/src/env.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/triple-slash-reference 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /apps/docs/src/styles/base.css: -------------------------------------------------------------------------------- 1 | /* Dark mode colors. */ 2 | :root { 3 | --sl-color-accent-low: #201c49; 4 | --sl-color-accent: #623eee; 5 | --sl-color-accent-high: #c1c2fd; 6 | --sl-color-white: #ffffff; 7 | --sl-color-gray-1: #eceef2; 8 | --sl-color-gray-2: #c0c2c7; 9 | --sl-color-gray-3: #888b96; 10 | --sl-color-gray-4: #545861; 11 | --sl-color-gray-5: #353841; 12 | --sl-color-gray-6: #24272f; 13 | --sl-color-black: #17181c; 14 | } 15 | /* Light mode colors. */ 16 | :root[data-theme="light"] { 17 | --sl-color-accent-low: #d1d2ff; 18 | --sl-color-accent: #6441f0; 19 | --sl-color-accent-high: #2d236d; 20 | --sl-color-white: #17181c; 21 | --sl-color-gray-1: #24272f; 22 | --sl-color-gray-2: #353841; 23 | --sl-color-gray-3: #545861; 24 | --sl-color-gray-4: #888b96; 25 | --sl-color-gray-5: #c0c2c7; 26 | --sl-color-gray-6: #eceef2; 27 | --sl-color-gray-7: #f5f6f8; 28 | --sl-color-black: #ffffff; 29 | } 30 | -------------------------------------------------------------------------------- /apps/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strictest" 3 | } 4 | -------------------------------------------------------------------------------- /apps/infra/.gitignore: -------------------------------------------------------------------------------- 1 | .turbo/ 2 | cdk.out/ 3 | dist/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /apps/infra/README.md: -------------------------------------------------------------------------------- 1 | # infra 2 | -------------------------------------------------------------------------------- /apps/infra/cdk.context.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosted-zone:account=738697399292:domainName=astro-aws.org:region=us-west-2": { 3 | "Id": "/hostedzone/Z0584480MGUI8KRBPWM", 4 | "Name": "astro-aws.org." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /apps/infra/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "node --enable-source-maps dist/bin/infra.js", 3 | "context": { 4 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 5 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 6 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 7 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 8 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 9 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 10 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 11 | "@aws-cdk/aws-iam:minimizePolicies": true, 12 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 13 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 14 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 15 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 16 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 17 | "@aws-cdk/core:checkSecretUsage": true, 18 | "@aws-cdk/core:enablePartitionLiterals": true, 19 | "@aws-cdk/core:stackRelativeExports": true, 20 | "@aws-cdk/core:target-partitions": ["aws", "aws-cn"], 21 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true 22 | }, 23 | "watch": { 24 | "exclude": [ 25 | "README.md", 26 | "cdk*.json", 27 | "**/*.d.ts", 28 | "**/*.js", 29 | "tsconfig.json", 30 | "package*.json", 31 | "yarn.lock", 32 | "node_modules", 33 | "test" 34 | ], 35 | "include": ["**"] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /apps/infra/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@astro-aws/infra", 3 | "version": "0.0.0", 4 | "private": true, 5 | "homepage": "https://www.astro-aws.org/", 6 | "repository": { 7 | "type": "git", 8 | "url": "ssh://git@github.com/lukeshay/astro-aws.git", 9 | "directory": "apps/infra" 10 | }, 11 | "license": "MIT", 12 | "type": "module", 13 | "bin": { 14 | "astro-aws-cdk": "dist/bin/infra.js" 15 | }, 16 | "files": [ 17 | "dist", 18 | "cdk.out" 19 | ], 20 | "scripts": { 21 | "build": "scripts build", 22 | "deploy": "./scripts/deploy.sh", 23 | "synth": "cdk synth" 24 | }, 25 | "dependencies": { 26 | "@astro-aws/constructs": "workspace:^", 27 | "@astro-aws/docs": "workspace:^", 28 | "@aws-sdk/client-acm": "^3.614.0", 29 | "aws-cdk": "^2.149.0", 30 | "aws-cdk-lib": "^2.149.0", 31 | "constructs": "^10.3.0", 32 | "prettier": "^3.3.3", 33 | "typescript": "^5.5.3", 34 | "workspace-tools": "^0.36.4" 35 | }, 36 | "devDependencies": { 37 | "@astro-aws/scripts": "workspace:^", 38 | "@types/aws-lambda": "^8.10.141", 39 | "@types/node": "^18.18.4" 40 | }, 41 | "engines": { 42 | "node": "20.x || 22.x" 43 | }, 44 | "cli": { 45 | "clean": [ 46 | "dist", 47 | "cdk.out" 48 | ], 49 | "build": { 50 | "skipTsc": true 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /apps/infra/scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | ENVIRONMENT="${1:-DEV}" 6 | AWS_ARGS="${@:2}" 7 | 8 | bun run cdk deploy --asset-parallelism --concurrency 4 --require-approval never "AstroAWS-${ENVIRONMENT}-*" ${AWS_ARGS} 9 | -------------------------------------------------------------------------------- /apps/infra/src/bin/infra.ts: -------------------------------------------------------------------------------- 1 | import { App, Tags } from "aws-cdk-lib/core" 2 | 3 | import { 4 | Environments, 5 | ENVIRONMENT_PROPS, 6 | } from "../lib/constants/environments.js" 7 | import { WebsiteStack } from "../lib/stacks/website-stack.js" 8 | import { GitHubOIDCStack } from "../lib/stacks/github-oidc-stack.js" 9 | import { GitHubUsersStack } from "../lib/stacks/github-users-stack.js" 10 | import { RedirectStack } from "../lib/stacks/redirect-stack.js" 11 | 12 | const app = new App() 13 | 14 | const createStackName = ( 15 | environment: string, 16 | stack: string, 17 | runtime?: string, 18 | mode?: string, 19 | ) => ["AstroAWS", environment, stack, environment === "PROD" ? "nodejs20" : runtime, mode].filter(Boolean).join("-") 20 | 21 | Object.entries(ENVIRONMENT_PROPS).forEach(([environment, environmentProps]) => { 22 | environmentProps.websites.forEach((websiteProps) => { 23 | if (environment === Environments.DEV) { 24 | // eslint-disable-next-line no-param-reassign 25 | delete websiteProps.hostedZoneName 26 | // eslint-disable-next-line no-param-reassign 27 | delete websiteProps.aliases 28 | // eslint-disable-next-line no-param-reassign 29 | delete websiteProps.redirectAliases 30 | } 31 | 32 | new WebsiteStack( 33 | app, 34 | createStackName( 35 | environment, 36 | "Website", 37 | websiteProps.runtime, 38 | websiteProps.mode, 39 | ), 40 | { 41 | ...environmentProps, 42 | ...websiteProps, 43 | }, 44 | ) 45 | 46 | if (websiteProps.redirectAliases) { 47 | new RedirectStack( 48 | app, 49 | createStackName( 50 | environment, 51 | "Redirect", 52 | websiteProps.runtime, 53 | websiteProps.mode, 54 | ), 55 | { 56 | ...environmentProps, 57 | aliases: websiteProps.redirectAliases, 58 | hostedZoneName: websiteProps.hostedZoneName!, 59 | targetAlias: websiteProps.aliases![0], 60 | }, 61 | ) 62 | } 63 | }) 64 | 65 | if (environment === Environments.DEV) { 66 | new GitHubUsersStack( 67 | app, 68 | createStackName(environment, "GitHubUsers"), 69 | environmentProps, 70 | ) 71 | 72 | new GitHubOIDCStack( 73 | app, 74 | createStackName(environment, "GitHubOIDC"), 75 | environmentProps, 76 | ) 77 | } 78 | 79 | Tags.of(app).add("Project", "AstroAWS") 80 | Tags.of(app).add("Environment", environment) 81 | }) 82 | -------------------------------------------------------------------------------- /apps/infra/src/lib/constants/environments.ts: -------------------------------------------------------------------------------- 1 | import { env } from "node:process" 2 | 3 | import type { AstroAWSStackProps } from "../types/astro-aws-stack-props.js" 4 | import { type StaticWebsiteStackProps } from "../stacks/website-stack.js" 5 | 6 | const base = { 7 | analyticsReporting: false, 8 | crossRegionReferences: true, 9 | env: { 10 | account: env.AWS_ACCOUNT ?? String(env.CDK_DEFAULT_ACCOUNT), 11 | region: "us-west-2", 12 | }, 13 | terminationProtection: false, 14 | } 15 | 16 | const Environments = { 17 | DEV: "DEV", 18 | PERSONAL: "PERSONAL", 19 | PROD: "PROD", 20 | } as const 21 | 22 | type Environment = (typeof Environments)[keyof typeof Environments] 23 | 24 | type EnvironmentProps = AstroAWSStackProps & { 25 | websites: readonly (Partial & 26 | StaticWebsiteStackProps & { 27 | redirectAliases?: [string, ...string[]] 28 | })[] 29 | } 30 | 31 | const ENVIRONMENT_PROPS: Record = { 32 | [Environments.DEV]: { 33 | ...base, 34 | environment: Environments.DEV, 35 | websites: [ 36 | { 37 | aliases: ["www.static.dev", "static.dev"], 38 | hostedZoneName: "astro-aws.org", 39 | mode: "static", 40 | app: "apps/docs", 41 | runtime: "nodejs22", 42 | }, 43 | { 44 | aliases: ["www.ssr.nodejs20.dev", "ssr.nodejs20.dev"], 45 | hostedZoneName: "astro-aws.org", 46 | mode: "ssr", 47 | app: "examples/base", 48 | runtime: "nodejs20", 49 | }, 50 | { 51 | aliases: ["www.stream.nodejs20.dev", "stream.nodejs20.dev"], 52 | hostedZoneName: "astro-aws.org", 53 | mode: "ssr-stream", 54 | app: "examples/base", 55 | runtime: "nodejs20", 56 | }, 57 | { 58 | aliases: ["www.edge.nodejs20.dev", "edge.nodejs20.dev"], 59 | env: { 60 | ...base.env, 61 | region: "us-east-1", 62 | }, 63 | hostedZoneName: "astro-aws.org", 64 | mode: "edge", 65 | app: "examples/base", 66 | runtime: "nodejs20", 67 | }, 68 | { 69 | aliases: ["www.ssr.nodejs22.dev", "ssr.nodejs22.dev"], 70 | hostedZoneName: "astro-aws.org", 71 | mode: "ssr", 72 | app: "examples/base", 73 | runtime: "nodejs22", 74 | }, 75 | { 76 | aliases: ["www.stream.nodejs22.dev", "stream.nodejs22.dev"], 77 | hostedZoneName: "astro-aws.org", 78 | mode: "ssr-stream", 79 | app: "examples/base", 80 | runtime: "nodejs22", 81 | }, 82 | { 83 | aliases: ["www.edge.nodejs22.dev", "edge.nodejs22.dev"], 84 | env: { 85 | ...base.env, 86 | region: "us-east-1", 87 | }, 88 | hostedZoneName: "astro-aws.org", 89 | mode: "edge", 90 | app: "examples/base", 91 | runtime: "nodejs22", 92 | }, 93 | ], 94 | }, 95 | [Environments.PROD]: { 96 | ...base, 97 | environment: Environments.PROD, 98 | websites: [ 99 | { 100 | aliases: ["www"], 101 | hostedZoneName: "astro-aws.org", 102 | mode: "static", 103 | app: "apps/docs", 104 | redirectAliases: ["", "docs", "www.docs"], 105 | runtime: "nodejs22", 106 | }, 107 | ], 108 | }, 109 | [Environments.PERSONAL]: { 110 | ...base, 111 | environment: Environments.PERSONAL, 112 | websites: [ 113 | { 114 | mode: "static", 115 | app: "apps/docs", 116 | runtime: "nodejs22", 117 | }, 118 | { 119 | mode: "ssr", 120 | app: "examples/base", 121 | runtime: "nodejs22", 122 | }, 123 | { 124 | mode: "ssr-stream", 125 | app: "examples/base", 126 | runtime: "nodejs22", 127 | }, 128 | ], 129 | }, 130 | } 131 | 132 | export { Environments, ENVIRONMENT_PROPS, type Environment } 133 | -------------------------------------------------------------------------------- /apps/infra/src/lib/constructs/basic-graph-widget.ts: -------------------------------------------------------------------------------- 1 | import type { GraphWidgetProps, Metric } from "aws-cdk-lib/aws-cloudwatch" 2 | import { GRID_WIDTH, GraphWidget } from "aws-cdk-lib/aws-cloudwatch" 3 | 4 | export type BasicGraphWidgetProps = Omit< 5 | GraphWidgetProps, 6 | "left" | "right" | "title" 7 | > & { 8 | metric: Metric 9 | } 10 | 11 | export class BasicGraphWidget extends GraphWidget { 12 | public constructor(props: BasicGraphWidgetProps) { 13 | const { metric, ...graphWidgetProps } = props 14 | 15 | super({ 16 | width: GRID_WIDTH / 2, 17 | ...graphWidgetProps, 18 | left: [metric], 19 | title: metric.label, 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/infra/src/lib/constructs/cross-region-certificate.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line max-classes-per-file 2 | import * as path from "node:path" 3 | 4 | import { Stack, CustomResource, Duration } from "aws-cdk-lib/core" 5 | import { Provider } from "aws-cdk-lib/custom-resources" 6 | import { Construct } from "constructs" 7 | import { Code, Function, Runtime } from "aws-cdk-lib/aws-lambda" 8 | import { PolicyStatement } from "aws-cdk-lib/aws-iam" 9 | import { 10 | Certificate, 11 | type ICertificate, 12 | } from "aws-cdk-lib/aws-certificatemanager" 13 | 14 | class CrossRegionCertificateProvider extends Construct { 15 | private readonly provider: Provider 16 | 17 | public constructor(scope: Construct, id: string) { 18 | super(scope, id) 19 | 20 | const onEventHandler = new Function( 21 | this, 22 | "CrossRegionCertificateProviderEventHandler", 23 | { 24 | code: Code.fromAsset( 25 | path.resolve(".", "support", "cross-region-certificate"), 26 | ), 27 | handler: "index.onEvent", 28 | initialPolicy: [ 29 | new PolicyStatement({ 30 | actions: ["acm:RequestCertificate", "acm:DeleteCertificate"], 31 | resources: ["*"], 32 | }), 33 | ], 34 | runtime: Runtime.NODEJS_20_X, 35 | }, 36 | ) 37 | 38 | const isCompleteHandler = new Function( 39 | this, 40 | "CrossRegionCertificateProviderCompleteHandler", 41 | { 42 | code: Code.fromAsset( 43 | path.resolve(".", "support", "cross-region-certificate"), 44 | ), 45 | handler: "index.isComplete", 46 | initialPolicy: [ 47 | new PolicyStatement({ 48 | actions: ["acm:DescribeCertificate"], 49 | resources: ["*"], 50 | }), 51 | ], 52 | runtime: Runtime.NODEJS_20_X, 53 | }, 54 | ) 55 | 56 | this.provider = new Provider(this, "CrossRegionCertificateProvider", { 57 | isCompleteHandler, 58 | onEventHandler, 59 | queryInterval: Duration.seconds(15), 60 | totalTimeout: Duration.minutes(20), 61 | }) 62 | } 63 | 64 | public static getOrCreate(scope: Construct) { 65 | const stack = Stack.of(scope) 66 | const id = 67 | "org.astro-aws.cdk.custom-resources.cross-region-certificate-provider" 68 | const x = 69 | (stack.node.tryFindChild(id) as 70 | | CrossRegionCertificateProvider 71 | | undefined) ?? new CrossRegionCertificateProvider(stack, id) 72 | 73 | return x.provider.serviceToken 74 | } 75 | } 76 | 77 | type CerticateStatus = 78 | | "EXPIRED" 79 | | "FAILED" 80 | | "INACTIVE" 81 | | "ISSUED" 82 | | "PENDING_VALIDATION" 83 | | "REVOKED" 84 | | "VALIDATION_TIMED_OUT" 85 | | undefined 86 | 87 | type CrossRegionCertificateProperties = { 88 | domainName: string 89 | alternateNames?: string[] 90 | region?: string 91 | } 92 | 93 | class CrossRegionCertificate extends Construct { 94 | public readonly certificateArn: string 95 | public readonly domainName: string 96 | public readonly alternateNames: string[] 97 | public readonly region: string 98 | public readonly status: CerticateStatus 99 | public readonly certificate: ICertificate 100 | 101 | public constructor( 102 | scope: Construct, 103 | id: string, 104 | properties: CrossRegionCertificateProperties, 105 | ) { 106 | super(scope, id) 107 | 108 | const resource = new CustomResource(this, "Resource", { 109 | properties: { 110 | alternateNames: properties.alternateNames ?? [], 111 | domainName: properties.domainName, 112 | idempotenceToken: this.node.addr, 113 | region: properties.region ?? Stack.of(this).region, 114 | }, 115 | resourceType: "Custom::CrossRegionCertificate", 116 | serviceToken: CrossRegionCertificateProvider.getOrCreate(this), 117 | }) 118 | 119 | this.certificateArn = resource.getAttString("certificateArn") 120 | this.domainName = resource.getAttString("domainName") 121 | this.alternateNames = resource.getAtt("alternateNames").toStringList() 122 | this.region = resource.getAttString("region") 123 | this.status = resource.getAttString("status") as CerticateStatus 124 | this.certificate = Certificate.fromCertificateArn( 125 | this, 126 | "Certificate", 127 | this.certificateArn, 128 | ) 129 | } 130 | } 131 | 132 | export { 133 | type CrossRegionCertificateProperties, 134 | type CerticateStatus, 135 | CrossRegionCertificate, 136 | } 137 | -------------------------------------------------------------------------------- /apps/infra/src/lib/constructs/distribution-metric.ts: -------------------------------------------------------------------------------- 1 | import type { Distribution } from "aws-cdk-lib/aws-cloudfront" 2 | import type { MetricProps } from "aws-cdk-lib/aws-cloudwatch" 3 | import { Metric } from "aws-cdk-lib/aws-cloudwatch" 4 | 5 | export type DistributionMetricProps = Omit< 6 | MetricProps, 7 | "dimensionsMap" | "namespace" 8 | > & { 9 | distribution: Distribution 10 | } 11 | 12 | export class DistributionMetric extends Metric { 13 | public constructor(props: DistributionMetricProps) { 14 | const { distribution, ...metricProps } = props 15 | 16 | super({ 17 | ...metricProps, 18 | dimensionsMap: { 19 | DistributionId: distribution.distributionId, 20 | Region: "Global", 21 | }, 22 | namespace: "AWS/CloudFront", 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/infra/src/lib/stacks/certificate-stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack } from "aws-cdk-lib/core" 2 | import { 3 | Certificate, 4 | CertificateValidation, 5 | } from "aws-cdk-lib/aws-certificatemanager" 6 | import type { Construct } from "constructs" 7 | import { HostedZone, type IHostedZone } from "aws-cdk-lib/aws-route53" 8 | 9 | import type { AstroAWSStackProps } from "../types/astro-aws-stack-props.js" 10 | 11 | export type CertificateStackProps = AstroAWSStackProps & { 12 | aliases: readonly [string, ...string[]] 13 | hostedZoneName: string 14 | } 15 | 16 | export class CertificateStack extends Stack { 17 | public readonly certificate: Certificate 18 | public readonly hostedZone: IHostedZone 19 | 20 | public constructor( 21 | scope: Construct, 22 | id: string, 23 | props: CertificateStackProps, 24 | ) { 25 | super(scope, id, { 26 | ...props, 27 | env: { 28 | ...props.env, 29 | region: "us-east-1", 30 | }, 31 | }) 32 | 33 | const { hostedZoneName, aliases } = props 34 | const [domainName, ...restDomainNames] = aliases.map((alias) => 35 | [alias, hostedZoneName].filter(Boolean).join("."), 36 | ) as [string, ...string[]] 37 | 38 | this.hostedZone = HostedZone.fromLookup(this, "HostedZone", { 39 | domainName: hostedZoneName, 40 | }) 41 | 42 | this.certificate = new Certificate(this, "Certificate", { 43 | domainName, 44 | subjectAlternativeNames: restDomainNames, 45 | validation: CertificateValidation.fromDns(this.hostedZone), 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /apps/infra/src/lib/stacks/github-oidc-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib/core" 2 | import type { Construct } from "constructs" 3 | import { 4 | FederatedPrincipal, 5 | ManagedPolicy, 6 | OpenIdConnectProvider, 7 | Role, 8 | } from "aws-cdk-lib/aws-iam" 9 | 10 | import type { AstroAWSStackProps } from "../types/astro-aws-stack-props.js" 11 | import { Environments } from "../constants/environments.js" 12 | 13 | export type GitHubOIDCStackProps = AstroAWSStackProps 14 | 15 | export class GitHubOIDCStack extends cdk.Stack { 16 | public constructor( 17 | scope: Construct, 18 | id: string, 19 | props: GitHubOIDCStackProps, 20 | ) { 21 | super(scope, id, props) 22 | 23 | const gitHubOIDC = new OpenIdConnectProvider(this, "GitHubOIDC", { 24 | clientIds: ["sts.amazonaws.com", "https://github.com/lukeshay/astro-aws"], 25 | thumbprints: ["6938fd4d98bab03faadb97b34396831e3780aea1"], 26 | url: "https://token.actions.githubusercontent.com", 27 | }) 28 | 29 | new Role(this, "GitHubOIDCAdminRole", { 30 | assumedBy: new FederatedPrincipal( 31 | gitHubOIDC.openIdConnectProviderArn, 32 | { 33 | StringEquals: { 34 | "token.actions.githubusercontent.com:aud": "sts.amazonaws.com", 35 | }, 36 | StringLike: { 37 | "token.actions.githubusercontent.com:sub": 38 | "repo:lukeshay/astro-aws:*", 39 | }, 40 | }, 41 | "sts:AssumeRoleWithWebIdentity", 42 | ), 43 | managedPolicies: [ 44 | ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess"), 45 | ], 46 | roleName: "GitHubOIDCAdminRole", 47 | }) 48 | 49 | if (props.environment !== Environments.PROD) { 50 | new Role(this, "GitHubOIDCReadOnlyRole", { 51 | assumedBy: new FederatedPrincipal( 52 | gitHubOIDC.openIdConnectProviderArn, 53 | { 54 | StringEquals: { 55 | "token.actions.githubusercontent.com:aud": "sts.amazonaws.com", 56 | }, 57 | StringLike: { 58 | "token.actions.githubusercontent.com:sub": 59 | "repo:lukeshay/astro-aws:*", 60 | }, 61 | }, 62 | "sts:AssumeRoleWithWebIdentity", 63 | ), 64 | managedPolicies: [ 65 | ManagedPolicy.fromAwsManagedPolicyName("ReadOnlyAccess"), 66 | ], 67 | roleName: "GitHubOIDCReadOnlyRole", 68 | }) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /apps/infra/src/lib/stacks/github-users-stack.ts: -------------------------------------------------------------------------------- 1 | import type { Construct } from "constructs" 2 | import { 3 | ManagedPolicy, 4 | User, 5 | AccessKey, 6 | Policy, 7 | PolicyStatement, 8 | Effect, 9 | } from "aws-cdk-lib/aws-iam" 10 | import { SecretValue, Stack } from "aws-cdk-lib/core" 11 | import { Secret } from "aws-cdk-lib/aws-secretsmanager" 12 | 13 | import type { AstroAWSStackProps } from "../types/astro-aws-stack-props.js" 14 | 15 | export type GitHubUsersStackProps = AstroAWSStackProps 16 | 17 | export class GitHubUsersStack extends Stack { 18 | public constructor( 19 | scope: Construct, 20 | id: string, 21 | props: GitHubUsersStackProps, 22 | ) { 23 | super(scope, id, props) 24 | 25 | const adminUser = new User(this, "GitHubAdminUser", { 26 | managedPolicies: [ 27 | ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess"), 28 | ], 29 | path: "/astro-aws/users/", 30 | userName: "GitHubAdminUser", 31 | }) 32 | 33 | const readOnlyUser = new User(this, "GitHubReadOnlyUser", { 34 | managedPolicies: [ 35 | ManagedPolicy.fromAwsManagedPolicyName("ReadOnlyAccess"), 36 | ], 37 | path: "/astro-aws/users/", 38 | userName: "GitHubReadOnlyUser", 39 | }) 40 | 41 | const adminAccessKey = new AccessKey(this, "GitHubAdminAccessKey", { 42 | user: adminUser, 43 | }) 44 | 45 | const readOnlyAccessKey = new AccessKey(this, "GitHubReadOnlyAccessKey", { 46 | user: readOnlyUser, 47 | }) 48 | 49 | const adminAccessKeys = new Secret(this, "GitHubAdminUserAccessKeys", { 50 | secretName: "/astro-aws/users/github/GitHubAdminUser/access-keys", 51 | secretObjectValue: { 52 | accessKeyId: SecretValue.unsafePlainText(adminAccessKey.accessKeyId), 53 | secretAccessKey: adminAccessKey.secretAccessKey, 54 | }, 55 | }) 56 | 57 | const readOnlyAccessKeys = new Secret( 58 | this, 59 | "GitHubReadOnlyUserAccessKeys", 60 | { 61 | secretName: "/astro-aws/users/github/GitHubReadOnlyUser/access-keys", 62 | secretObjectValue: { 63 | accessKeyId: SecretValue.unsafePlainText( 64 | readOnlyAccessKey.accessKeyId, 65 | ), 66 | secretAccessKey: readOnlyAccessKey.secretAccessKey, 67 | }, 68 | }, 69 | ) 70 | 71 | const denyGetGitHubKeysPolicy = new Policy( 72 | this, 73 | "DenyGetGitHubKeysPolicy", 74 | { 75 | statements: [ 76 | new PolicyStatement({ 77 | actions: ["secretsmanager:GetSecretValue"], 78 | effect: Effect.DENY, 79 | resources: [ 80 | readOnlyAccessKeys.secretArn, 81 | adminAccessKeys.secretArn, 82 | ], 83 | }), 84 | ], 85 | }, 86 | ) 87 | 88 | adminUser.attachInlinePolicy(denyGetGitHubKeysPolicy) 89 | readOnlyUser.attachInlinePolicy(denyGetGitHubKeysPolicy) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /apps/infra/src/lib/stacks/monitoring-stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack } from "aws-cdk-lib/core" 2 | import { Dashboard } from "aws-cdk-lib/aws-cloudwatch" 3 | import type { Construct } from "constructs" 4 | 5 | import type { AstroAWSStackProps } from "../types/astro-aws-stack-props.js" 6 | 7 | export type MonitoringStackProps = AstroAWSStackProps 8 | 9 | export class MonitoringStack extends Stack { 10 | public readonly cloudwatchDashboard: Dashboard 11 | 12 | public constructor( 13 | scope: Construct, 14 | id: string, 15 | props: MonitoringStackProps, 16 | ) { 17 | super(scope, id, props) 18 | 19 | const { 20 | environment, 21 | env: { region }, 22 | } = props 23 | 24 | this.cloudwatchDashboard = new Dashboard( 25 | this, 26 | `AstroAWSDashboard-${environment}-${region}`, 27 | { 28 | dashboardName: `AstroAWS-${environment}-${region}`, 29 | }, 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/infra/src/lib/stacks/redirect-stack.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path" 2 | 3 | import { RemovalPolicy, Stack } from "aws-cdk-lib/core" 4 | import { 5 | CloudFrontWebDistribution, 6 | Distribution, 7 | Function, 8 | FunctionCode, 9 | FunctionEventType, 10 | OriginProtocolPolicy, 11 | PriceClass, 12 | ViewerCertificate, 13 | ViewerProtocolPolicy, 14 | } from "aws-cdk-lib/aws-cloudfront" 15 | import type { Construct } from "constructs" 16 | import { 17 | AaaaRecord, 18 | ARecord, 19 | HostedZone, 20 | RecordTarget, 21 | } from "aws-cdk-lib/aws-route53" 22 | import { CloudFrontTarget } from "aws-cdk-lib/aws-route53-targets" 23 | import { S3Origin } from "aws-cdk-lib/aws-cloudfront-origins" 24 | import { 25 | BlockPublicAccess, 26 | Bucket, 27 | BucketEncryption, 28 | RedirectProtocol, 29 | } from "aws-cdk-lib/aws-s3" 30 | 31 | import type { AstroAWSStackProps } from "../types/astro-aws-stack-props.js" 32 | import { CrossRegionCertificate } from "../constructs/cross-region-certificate.js" 33 | 34 | export type RedirectStackProps = AstroAWSStackProps & { 35 | hostedZoneName: string 36 | aliases: [string, ...string[]] 37 | targetAlias: string 38 | } 39 | 40 | export class RedirectStack extends Stack { 41 | public constructor(scope: Construct, id: string, props: RedirectStackProps) { 42 | super(scope, id, props) 43 | 44 | const { hostedZoneName, aliases } = props 45 | 46 | const domainNames = aliases.map((alias) => 47 | [alias, hostedZoneName].filter(Boolean).join("."), 48 | ) as [string, ...string[]] 49 | 50 | const wwwRedirectFunction = new Function(this, "WwwRedirectFunction", { 51 | code: FunctionCode.fromFile({ 52 | filePath: path.resolve(".", "support", "redirect", "index.js"), 53 | }), 54 | }) 55 | 56 | const [domainName, ...alternateNames] = domainNames 57 | const targetDomainName = [props.targetAlias, "astro-aws.org"] 58 | .filter(Boolean) 59 | .join(".") 60 | 61 | const { certificate } = new CrossRegionCertificate(this, "Certificate", { 62 | alternateNames, 63 | domainName, 64 | region: "us-east-1", 65 | }) 66 | 67 | const bucket = new Bucket(this, "RedirectBucket", { 68 | blockPublicAccess: BlockPublicAccess.BLOCK_ALL, 69 | encryption: BucketEncryption.S3_MANAGED, 70 | enforceSSL: true, 71 | removalPolicy: RemovalPolicy.DESTROY, 72 | versioned: true, 73 | websiteRedirect: { 74 | hostName: targetDomainName, 75 | protocol: RedirectProtocol.HTTPS, 76 | }, 77 | }) 78 | 79 | const distribution = new CloudFrontWebDistribution( 80 | this, 81 | "WWWRedirectDistribution", 82 | { 83 | defaultRootObject: "", 84 | originConfigs: [ 85 | { 86 | behaviors: [{ isDefaultBehavior: true }], 87 | customOriginSource: { 88 | domainName: bucket.bucketWebsiteDomainName, 89 | originProtocolPolicy: OriginProtocolPolicy.HTTP_ONLY, 90 | }, 91 | }, 92 | ], 93 | viewerCertificate: ViewerCertificate.fromAcmCertificate(certificate, { 94 | aliases: domainNames, 95 | }), 96 | comment: `Redirect to ${targetDomainName} from ${domainNames.join( 97 | ", ", 98 | )}`, 99 | priceClass: PriceClass.PRICE_CLASS_ALL, 100 | viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 101 | }, 102 | ) 103 | 104 | const hostedZone = HostedZone.fromLookup(this, "HostedZone", { 105 | domainName: hostedZoneName, 106 | }) 107 | 108 | domainNames.forEach((domain) => { 109 | new ARecord(this, `${domain}-ARecord`, { 110 | recordName: domain, 111 | target: RecordTarget.fromAlias(new CloudFrontTarget(distribution)), 112 | zone: hostedZone, 113 | }) 114 | 115 | new AaaaRecord(this, `${domain}-AaaaRecord`, { 116 | recordName: domain, 117 | target: RecordTarget.fromAlias(new CloudFrontTarget(distribution)), 118 | zone: hostedZone, 119 | }) 120 | }) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /apps/infra/src/lib/stacks/website-stack.ts: -------------------------------------------------------------------------------- 1 | import { cwd, env } from "node:process" 2 | 3 | import { CfnOutput, Duration, Stack } from "aws-cdk-lib/core" 4 | import type { ICertificate } from "aws-cdk-lib/aws-certificatemanager" 5 | import { 6 | CachePolicy, 7 | PriceClass, 8 | ResponseHeadersPolicy, 9 | ViewerProtocolPolicy, 10 | } from "aws-cdk-lib/aws-cloudfront" 11 | import { Architecture, Runtime, Tracing } from "aws-cdk-lib/aws-lambda" 12 | import type { Construct } from "constructs" 13 | import { AstroAWS } from "@astro-aws/constructs" 14 | import { LogQueryWidget } from "aws-cdk-lib/aws-cloudwatch" 15 | import type { ConcreteWidget, Dashboard } from "aws-cdk-lib/aws-cloudwatch" 16 | import { 17 | AaaaRecord, 18 | ARecord, 19 | HostedZone, 20 | RecordTarget, 21 | } from "aws-cdk-lib/aws-route53" 22 | import { CloudFrontTarget } from "aws-cdk-lib/aws-route53-targets" 23 | import { 24 | BlockPublicAccess, 25 | Bucket, 26 | BucketAccessControl, 27 | BucketEncryption, 28 | } from "aws-cdk-lib/aws-s3" 29 | 30 | import { DistributionMetric } from "../constructs/distribution-metric.js" 31 | import { BasicGraphWidget } from "../constructs/basic-graph-widget.js" 32 | import { Environments } from "../constants/environments.js" 33 | import type { AstroAWSStackProps } from "../types/astro-aws-stack-props.js" 34 | import { CrossRegionCertificate } from "../constructs/cross-region-certificate.js" 35 | import { resolve } from "node:path" 36 | 37 | type StaticWebsiteStackProps = { 38 | aliases?: readonly [string, ...string[]] 39 | mode: string 40 | hostedZoneName?: string 41 | app: string 42 | runtime: string 43 | } 44 | 45 | type WebsiteStackProps = AstroAWSStackProps & 46 | StaticWebsiteStackProps & { 47 | cloudwatchDashboard?: Dashboard 48 | } 49 | 50 | class WebsiteStack extends Stack { 51 | public constructor(scope: Construct, id: string, props: WebsiteStackProps) { 52 | super(scope, id, props) 53 | 54 | const { 55 | aliases, 56 | cloudwatchDashboard, 57 | mode, 58 | environment, 59 | hostedZoneName, 60 | runtime, 61 | } = props 62 | 63 | const hostedZone = hostedZoneName 64 | ? HostedZone.fromLookup(this, "HostedZone", { 65 | domainName: hostedZoneName, 66 | }) 67 | : undefined 68 | 69 | const distDir = mode === "static" ? "dist" : `dist/${mode}` 70 | const domainNames = aliases?.map((alias) => 71 | [alias, hostedZoneName].filter(Boolean).join("."), 72 | ) 73 | 74 | let certificate: ICertificate | undefined 75 | 76 | if (hostedZone && domainNames?.length) { 77 | const [domainName, ...alternateNames] = domainNames as [ 78 | string, 79 | ...string[], 80 | ] 81 | 82 | const crossRegionCertificate = new CrossRegionCertificate( 83 | this, 84 | "Certificate", 85 | { 86 | alternateNames, 87 | domainName, 88 | region: "us-east-1", 89 | }, 90 | ) 91 | 92 | certificate = crossRegionCertificate.certificate 93 | } 94 | 95 | const cachePolicy = new CachePolicy(this, "CachePolicy", { 96 | minTtl: Duration.days(365), 97 | }) 98 | 99 | const accessLogBucket = new Bucket(this, "AccessLogBucket", { 100 | accessControl: BucketAccessControl.LOG_DELIVERY_WRITE, 101 | blockPublicAccess: BlockPublicAccess.BLOCK_ALL, 102 | encryption: BucketEncryption.S3_MANAGED, 103 | }) 104 | 105 | const astroAwsConstruct = new AstroAWS(this, "AstroAWSConstruct", { 106 | cdk: { 107 | cloudfrontDistribution: { 108 | apiBehavior: { 109 | cachePolicy, 110 | viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 111 | }, 112 | certificate, 113 | comment: `${environment} - ${mode}`, 114 | defaultBehavior: { 115 | cachePolicy, 116 | responseHeadersPolicy: new ResponseHeadersPolicy( 117 | this, 118 | "ResponseHeadersPolicy", 119 | { 120 | securityHeadersBehavior: { 121 | contentSecurityPolicy: { 122 | contentSecurityPolicy: 123 | "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self' cognito-identity.us-east-1.amazonaws.com dataplane.rum.us-east-1.amazonaws.com; upgrade-insecure-requests", 124 | override: true, 125 | }, 126 | }, 127 | }, 128 | ), 129 | viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 130 | }, 131 | domainNames, 132 | errorResponses: [ 133 | { 134 | httpStatus: 403, 135 | responsePagePath: "/403", 136 | }, 137 | ], 138 | logBucket: accessLogBucket, 139 | logFilePrefix: "cloudfront/", 140 | priceClass: 141 | environment === Environments.PROD 142 | ? PriceClass.PRICE_CLASS_ALL 143 | : PriceClass.PRICE_CLASS_100, 144 | webAclId: 145 | env.WEB_ACL_ARN && env.WEB_ACL_ARN.length > 0 146 | ? env.WEB_ACL_ARN 147 | : undefined, 148 | }, 149 | lambdaFunction: { 150 | architecture: Architecture.ARM_64, 151 | environment: { 152 | DOMAIN: String(domainNames?.[0]), 153 | }, 154 | runtime: new Runtime(`${runtime}.x`), 155 | tracing: Tracing.ACTIVE, 156 | }, 157 | s3Bucket: { 158 | serverAccessLogsBucket: accessLogBucket, 159 | serverAccessLogsPrefix: "s3/", 160 | }, 161 | }, 162 | outDir: resolve(cwd(), "..", "..", props.app, distDir), 163 | }) 164 | 165 | if (hostedZone && domainNames?.length) { 166 | domainNames.forEach((domainName) => { 167 | new ARecord(this, `ARecord-${domainName}`, { 168 | recordName: domainName, 169 | target: RecordTarget.fromAlias( 170 | new CloudFrontTarget(astroAwsConstruct.cdk.cloudfrontDistribution), 171 | ), 172 | zone: hostedZone, 173 | }) 174 | 175 | new AaaaRecord(this, `AaaaRecord-${domainName}`, { 176 | recordName: domainName, 177 | target: RecordTarget.fromAlias( 178 | new CloudFrontTarget(astroAwsConstruct.cdk.cloudfrontDistribution), 179 | ), 180 | zone: hostedZone, 181 | }) 182 | }) 183 | } 184 | 185 | const distribution5xxErrorRateMetric = new DistributionMetric({ 186 | distribution: astroAwsConstruct.cdk.cloudfrontDistribution, 187 | label: `${mode.toUpperCase()} - CloudFront 5xx error rate`, 188 | metricName: "5xxErrorRate", 189 | period: Duration.minutes(5), 190 | statistic: "Sum", 191 | }) 192 | 193 | const distributionRequestsMetric = new DistributionMetric({ 194 | distribution: astroAwsConstruct.cdk.cloudfrontDistribution, 195 | label: `${mode.toUpperCase()} - CloudFront requests`, 196 | metricName: "Requests", 197 | period: Duration.minutes(5), 198 | statistic: "Sum", 199 | }) 200 | 201 | const widgets: ConcreteWidget[] = [ 202 | new BasicGraphWidget({ metric: distribution5xxErrorRateMetric }), 203 | new BasicGraphWidget({ metric: distributionRequestsMetric }), 204 | ] 205 | 206 | if (astroAwsConstruct.cdk.lambdaFunction) { 207 | const lambdaLogQueryWidget = new LogQueryWidget({ 208 | height: 12, 209 | logGroupNames: [ 210 | astroAwsConstruct.cdk.lambdaFunction.logGroup.logGroupName, 211 | ], 212 | queryLines: ["fields @timestamp, @message", "sort @timestamp desc"], 213 | title: `${mode.toUpperCase()} - Lambda logs`, 214 | width: 24, 215 | }) 216 | 217 | const lambdaFailureRateMetric = 218 | astroAwsConstruct.cdk.lambdaFunction.metricErrors({ 219 | label: `${mode.toUpperCase()} - Lambda failure rate`, 220 | period: Duration.minutes(5), 221 | statistic: "sum", 222 | }) 223 | 224 | const lambdaInvocationsMetric = 225 | astroAwsConstruct.cdk.lambdaFunction.metricInvocations({ 226 | label: `${mode.toUpperCase()} - Lambda invocations`, 227 | period: Duration.minutes(5), 228 | statistic: "sum", 229 | }) 230 | 231 | const lambdaDurationMetric = 232 | astroAwsConstruct.cdk.lambdaFunction.metricDuration({ 233 | label: `${mode.toUpperCase()} - Lambda duration`, 234 | period: Duration.minutes(5), 235 | statistic: "avg", 236 | }) 237 | 238 | const lambdaThrottlesMetric = 239 | astroAwsConstruct.cdk.lambdaFunction.metricThrottles({ 240 | label: `${mode.toUpperCase()} - Lambda throttles`, 241 | period: Duration.minutes(5), 242 | statistic: "sum", 243 | }) 244 | 245 | widgets.unshift(lambdaLogQueryWidget) 246 | 247 | widgets.push( 248 | new BasicGraphWidget({ metric: lambdaFailureRateMetric }), 249 | new BasicGraphWidget({ metric: lambdaInvocationsMetric }), 250 | new BasicGraphWidget({ metric: lambdaDurationMetric }), 251 | new BasicGraphWidget({ metric: lambdaThrottlesMetric }), 252 | ) 253 | } 254 | 255 | cloudwatchDashboard?.addWidgets(...widgets) 256 | 257 | new CfnOutput(this, "CloudFrontDistributionId", { 258 | value: astroAwsConstruct.cdk.cloudfrontDistribution.distributionId, 259 | }) 260 | 261 | new CfnOutput(this, "CloudFrontDomainName", { 262 | value: 263 | astroAwsConstruct.cdk.cloudfrontDistribution.distributionDomainName, 264 | }) 265 | } 266 | } 267 | 268 | export { type StaticWebsiteStackProps, type WebsiteStackProps, WebsiteStack } 269 | -------------------------------------------------------------------------------- /apps/infra/src/lib/types/astro-aws-stack-props.ts: -------------------------------------------------------------------------------- 1 | import type { StackProps } from "aws-cdk-lib/core" 2 | 3 | import type { Environment } from "../constants/environments.js" 4 | 5 | export type AstroAWSStackProps = Readonly< 6 | Omit & { 7 | env: { 8 | account: string 9 | region: string 10 | } 11 | environment: Environment 12 | } 13 | > 14 | -------------------------------------------------------------------------------- /apps/infra/support/cross-region-certificate/index.mjs: -------------------------------------------------------------------------------- 1 | import { randomUUID } from "node:crypto" 2 | 3 | import { 4 | ACMClient, 5 | DeleteCertificateCommand, 6 | DescribeCertificateCommand, 7 | RequestCertificateCommand, 8 | } from "@aws-sdk/client-acm" 9 | 10 | /** @type {import("aws-lambda").CdkCustomResourceHandler} */ 11 | const onEvent = async (event) => { 12 | const { region, domainName, alternateNames, idempotencyToken } = 13 | event.ResourceProperties 14 | const client = new ACMClient({ 15 | region, 16 | }) 17 | 18 | if (event.RequestType === "Create" || event.RequestType === "Update") { 19 | const result = await client.send( 20 | new RequestCertificateCommand({ 21 | DomainName: domainName, 22 | IdempotencyToken: idempotencyToken, 23 | SubjectAlternativeNames: alternateNames?.length 24 | ? alternateNames 25 | : undefined, 26 | ValidationMethod: "DNS", 27 | }), 28 | ) 29 | 30 | return { 31 | Data: { 32 | alternateNames, 33 | certificateArn: result.CertificateArn, 34 | domainName, 35 | region, 36 | }, 37 | PhysicalResourceId: result.CertificateArn, 38 | } 39 | } 40 | 41 | await client.send( 42 | new DeleteCertificateCommand({ 43 | CertificateArn: event.PhysicalResourceId, 44 | }), 45 | ) 46 | 47 | return { 48 | Data: {}, 49 | PhysicalResourceId: event.PhysicalResourceId, 50 | } 51 | } 52 | 53 | /** @type {import("aws-lambda").CdkCustomResourceIsCompleteHandler} */ 54 | const isComplete = async (event) => { 55 | const client = new ACMClient({ 56 | region: event.Data.region, 57 | }) 58 | 59 | if (event.RequestType === "Create" || event.RequestType === "Update") { 60 | const result = await client.send( 61 | new DescribeCertificateCommand({ 62 | CertificateArn: event.Data.certificateArn, 63 | }), 64 | ) 65 | 66 | if (result.Certificate.Status === "ISSUED") { 67 | return { 68 | Data: { 69 | status: result.Certificate.Status, 70 | }, 71 | IsComplete: true, 72 | } 73 | } else if (result.Certificate.Status === "PENDING_VALIDATION") { 74 | return { 75 | IsComplete: false, 76 | } 77 | } 78 | 79 | throw new Error( 80 | `Unexpected certificate status: ${result.Certificate.Status}`, 81 | ) 82 | } 83 | 84 | return { 85 | IsComplete: true, 86 | } 87 | } 88 | 89 | export { onEvent, isComplete } 90 | -------------------------------------------------------------------------------- /apps/infra/support/redirect/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable eslint-comments/disable-enable-pair, unicorn/no-abusive-eslint-disable */ 2 | /* eslint-disable */ 3 | 4 | /** @param event {import("aws-lambda").CloudFrontFunctionsEvent}*/ 5 | function handler(event) { 6 | var qs = getURLSearchParams(event.request.querystring) 7 | return { 8 | statusCode: 307, 9 | statusDescription: "Temporary Redirect", 10 | headers: { 11 | location: { 12 | value: "https://www.astro-aws.org" + event.request.uri + qs, 13 | }, 14 | }, 15 | } 16 | } 17 | 18 | /** @param obj {import("aws-lambda").CloudFrontFunctionsEvent["request"]["querystring"]}*/ 19 | function getURLSearchParams(obj) { 20 | var str = [] 21 | 22 | for (var param in obj) { 23 | if (obj[param].multiValue) { 24 | str.push( 25 | obj[param].multiValue.map((item) => param + "=" + item.value).join("&"), 26 | ) 27 | } else if (obj[param].value === "") { 28 | str.push(param) 29 | } else { 30 | str.push(param + "=" + obj[param].value) 31 | } 32 | } 33 | 34 | if (str.length) { 35 | return "?" + str.join("&") 36 | } 37 | 38 | return "" 39 | } 40 | -------------------------------------------------------------------------------- /apps/infra/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "inlineSourceMap": true, 4 | "outDir": "dist" 5 | }, 6 | "extends": "../../tsconfig.base.json", 7 | "include": ["src/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/infra/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json" 3 | } 4 | -------------------------------------------------------------------------------- /examples/base/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # generated types 5 | .astro/ 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # logs 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /examples/base/README.md: -------------------------------------------------------------------------------- 1 | # Astro Starter Kit: Basics 2 | 3 | ```sh 4 | npm create astro@latest -- --template basics 5 | ``` 6 | 7 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) 8 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) 9 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) 10 | 11 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 12 | 13 | ![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) 14 | 15 | ## 🚀 Project Structure 16 | 17 | Inside of your Astro project, you'll see the following folders and files: 18 | 19 | ```text 20 | / 21 | ├── public/ 22 | │ └── favicon.svg 23 | ├── src/ 24 | │ ├── components/ 25 | │ │ └── Card.astro 26 | │ ├── layouts/ 27 | │ │ └── Layout.astro 28 | │ └── pages/ 29 | │ └── index.astro 30 | └── package.json 31 | ``` 32 | 33 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. 34 | 35 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. 36 | 37 | Any static assets, like images, can be placed in the `public/` directory. 38 | 39 | ## 🧞 Commands 40 | 41 | All commands are run from the root of the project, from a terminal: 42 | 43 | | Command | Action | 44 | | :------------------------ | :----------------------------------------------- | 45 | | `npm install` | Installs dependencies | 46 | | `npm run dev` | Starts local dev server at `localhost:4321` | 47 | | `npm run build` | Build your production site to `./dist/` | 48 | | `npm run preview` | Preview your build locally, before deploying | 49 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 50 | | `npm run astro -- --help` | Get help using the Astro CLI | 51 | 52 | ## 👀 Want to learn more? 53 | 54 | Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). 55 | -------------------------------------------------------------------------------- /examples/base/astro.config.ts: -------------------------------------------------------------------------------- 1 | import { env } from "node:process" 2 | 3 | import { defineConfig } from "astro/config" 4 | import aws from "@astro-aws/adapter" 5 | import tailwind from "@astrojs/tailwind" 6 | 7 | const mode = env.MODE as "edge" | "ssr-stream" | "ssr" 8 | 9 | // https://astro.build/config 10 | export default defineConfig({ 11 | adapter: aws({ 12 | mode, 13 | }), 14 | integrations: [tailwind()], 15 | outDir: `./dist/${mode}`, 16 | output: "server", 17 | }) 18 | -------------------------------------------------------------------------------- /examples/base/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@astro-aws/examples-base", 3 | "version": "0.0.0", 4 | "private": true, 5 | "homepage": "https://www.astro-aws.org/", 6 | "repository": { 7 | "type": "git", 8 | "url": "ssh://git@github.com/lukeshay/astro-aws.git", 9 | "directory": "examples/base" 10 | }, 11 | "license": "MIT", 12 | "type": "module", 13 | "files": [ 14 | "dist" 15 | ], 16 | "scripts": { 17 | "build:edge": "MODE=edge astro build", 18 | "build:ssr": "MODE=ssr astro build", 19 | "build:ssr-stream": "MODE=ssr-stream astro build", 20 | "astro": "astro", 21 | "build": "pnpm run clean && run-p build:*", 22 | "check": "astro check && tsc", 23 | "clean": "rimraf dist", 24 | "dev": "astro dev", 25 | "preview": "astro preview", 26 | "release": "pnpm run build && pnpm run package", 27 | "start": "astro dev" 28 | }, 29 | "dependencies": { 30 | "@astrojs/tailwind": "^5.1.0", 31 | "@faker-js/faker": "^8.4.1", 32 | "@middy/core": "^5.4.5", 33 | "astro": "^4.11.6", 34 | "daisyui": "^4.12.10", 35 | "http-status-codes": "^2.3.0", 36 | "tailwindcss": "^3.4.6" 37 | }, 38 | "devDependencies": { 39 | "@astro-aws/adapter": "workspace:^", 40 | "@astrojs/check": "^0.8.2", 41 | "@types/node": "^18.18.0", 42 | "eslint": "^9.7.0", 43 | "npm-run-all": "^4.1.5", 44 | "prettier": "^3.3.3", 45 | "rimraf": "^6.0.1", 46 | "typescript": "^5.5.3" 47 | }, 48 | "engines": { 49 | "node": "20.x || 22.x" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/base/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /examples/base/src/components/home/PersonRow.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { generateRandomPersonAsync } from "../../mocks/person"; 3 | 4 | export type Props = { 5 | row: number; 6 | } 7 | 8 | const { row } = Astro.props; 9 | 10 | const person = await generateRandomPersonAsync(); 11 | --- 12 | 13 | 14 | {row} 15 | {person.name} 16 | {person.email} 17 | {person.phone} 18 | -------------------------------------------------------------------------------- /examples/base/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | declare namespace App { 4 | // eslint-disable-next-line @typescript-eslint/consistent-type-definitions 5 | interface Locals { 6 | title: string 7 | rows: number[] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/base/src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | title: string; 4 | } 5 | 6 | const { title } = Astro.props; 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {title} 18 | 19 | 20 |
21 | 26 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /examples/base/src/middleware.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | import { defineMiddleware } from "astro:middleware" 3 | import { StatusCodes } from "http-status-codes" 4 | 5 | export const onRequest = defineMiddleware(async ({ locals, url }, next) => { 6 | console.log("Running middleware for base") 7 | 8 | // eslint-disable-next-line no-param-reassign 9 | locals.title = "Example" 10 | // eslint-disable-next-line no-param-reassign 11 | locals.rows = Array.from({ length: 100 }).map((_, i) => i + 1) 12 | 13 | if ( 14 | typeof import.meta.env.DOMAIN === "string" && 15 | url.host !== import.meta.env.DOMAIN 16 | ) { 17 | const redirectUrl = new URL( 18 | `${url.pathname}${url.search}`, 19 | import.meta.env.DOMAIN, 20 | ) 21 | 22 | console.log(`Redirecting to ${redirectUrl.toString()}`) 23 | 24 | return Response.redirect(redirectUrl, StatusCodes.PERMANENT_REDIRECT) 25 | } 26 | 27 | // return a Response or the result of calling `next()` 28 | return next() 29 | }) 30 | -------------------------------------------------------------------------------- /examples/base/src/mocks/person.ts: -------------------------------------------------------------------------------- 1 | import { faker } from "@faker-js/faker" 2 | 3 | const generateRandomPerson = () => ({ 4 | address: faker.location.streetAddress(), 5 | email: faker.internet.email(), 6 | name: faker.person.fullName(), 7 | phone: faker.phone.number(), 8 | }) 9 | 10 | // eslint-disable-next-line @typescript-eslint/require-await 11 | const generateRandomPersonAsync = async () => generateRandomPerson() 12 | 13 | export { generateRandomPerson, generateRandomPersonAsync } 14 | -------------------------------------------------------------------------------- /examples/base/src/pages/404.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../layouts/Layout.astro'; 3 | --- 4 | 5 | 6 |
7 |
8 |
9 |
10 |

404 Not Found

11 |

The page you requested was not found.

12 |
13 |
14 |
15 |
16 |
17 | 18 | -------------------------------------------------------------------------------- /examples/base/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../layouts/Layout.astro'; 3 | import PersonRow from "../components/home/PersonRow.astro" 4 | 5 | --- 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {Astro.locals.rows.map((n) => ( 22 | 23 | ))} 24 | 25 |
NameEmailPhone
26 |
27 |
28 |
29 | 30 | -------------------------------------------------------------------------------- /examples/base/src/pages/prerender.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../layouts/Layout.astro'; 3 | import PersonRow from "../components/home/PersonRow.astro" 4 | 5 | const rows = Array.from({ length: 100 }).map((_, i) => i + 1) 6 | 7 | export const prerender = true 8 | --- 9 | 10 | 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {rows.map((n) => ( 25 | 26 | ))} 27 | 28 |
NameEmailPhone
29 |
30 |
31 |
32 | 33 | -------------------------------------------------------------------------------- /examples/base/src/pages/rerender.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../layouts/Layout.astro'; 3 | import PersonRow from "../components/home/PersonRow.astro" 4 | 5 | const rows = Array.from({ length: 100 }).map((_, i) => i + 1) 6 | 7 | export const prerender = false 8 | --- 9 | 10 | 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {rows.map((n) => ( 25 | 26 | ))} 27 | 28 |
NameEmailPhone
29 |
30 |
31 |
32 | 33 | -------------------------------------------------------------------------------- /examples/base/tailwind.config.js: -------------------------------------------------------------------------------- 1 | import daisyui from "daisyui" 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | export default { 5 | content: [ 6 | "./src/**/*.astro", 7 | "./src/**/*.js", 8 | "./src/**/*.jsx", 9 | "./src/**/*.ts", 10 | "./src/**/*.tsx", 11 | "./src/**/*.md", 12 | "./src/**/*.mdx", 13 | ], 14 | plugins: [daisyui], 15 | theme: { 16 | extend: {}, 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /examples/base/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strictest" 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@astro-aws/root", 3 | "version": "0.0.0", 4 | "private": true, 5 | "homepage": "https://www.astro-aws.org/", 6 | "repository": { 7 | "type": "git", 8 | "url": "ssh://git@github.com/lukeshay/astro-aws.git", 9 | "directory": "." 10 | }, 11 | "license": "MIT", 12 | "workspaces": [ 13 | "apps/*", 14 | "examples/*", 15 | "packages/*", 16 | "scripts" 17 | ], 18 | "scripts": { 19 | "build": "turbo run build", 20 | "build:one": "turbo run build --filter", 21 | "clear-cache": "rimraf **/.turbo ./pompous", 22 | "deploy": "turbo run deploy", 23 | "deploy:one": "turbo run deploy --filter", 24 | "dev": "turbo run dev", 25 | "dev:one": "turbo run dev --filter", 26 | "format": "prettier --ignore-unknown --no-error-on-unmatched-pattern --write --check . '!**/*.astro'", 27 | "release:cut": "git add . && git commit -m 'new release' && bun run deploy:one @astro-aws/infra -- PROD && bun run changeset publish && git push && git push --follow-tags", 28 | "release:prepare": "bun run build && bun run synth && bun run changeset version && bun run format", 29 | "synth": "turbo run synth", 30 | "synth:one": "turbo run synth --filter", 31 | "test": "turbo run test", 32 | "test:one": "turbo run test --filter", 33 | "changeset": "changeset" 34 | }, 35 | "dependencies": { 36 | "@changesets/changelog-git": "^0.2.0", 37 | "@changesets/changelog-github": "^0.5.0", 38 | "@changesets/cli": "^2.27.7", 39 | "@lshay/prettier-config": "^0.7.0", 40 | "prettier": "^3.3.3", 41 | "turbo": "^2.0.7", 42 | "typescript": "^5.5.3" 43 | }, 44 | "engines": { 45 | "node": "20.x || 22.x" 46 | }, 47 | "trustedDependencies": [ 48 | "esbuild", 49 | "astro", 50 | "vite", 51 | "sharp" 52 | ], 53 | "packageManager": "bun@1.1.13" 54 | } 55 | -------------------------------------------------------------------------------- /packages/adapter/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @astro-aws/adapter 2 | 3 | ## 0.8.0 4 | 5 | ### Minor Changes 6 | 7 | - 3fc8a7f: Add support for node 22 8 | - 3fc8a7f: Remove support for node 18 9 | 10 | ## 0.7.0 11 | 12 | ### Minor Changes 13 | 14 | - b4bcad7: Upgrade dependencies 15 | 16 | ### Patch Changes 17 | 18 | - 48e4410: Updated dependencies 19 | - 0a93be7: Support x-forwarded-host in ssr setup 20 | 21 | ## 0.6.0 22 | 23 | ### Minor Changes 24 | 25 | - 839b188: Added support for response streaming 26 | - 3f1e6cb: Updated dependencies 27 | - 3f1e6cb: Support Astro v4 28 | - 839b188: Added support for 404 pages 29 | 30 | ### Patch Changes 31 | 32 | - 3f1e6cb: Use flatted to handle circular json 33 | 34 | ## 0.5.0 35 | 36 | ### Minor Changes 37 | 38 | - 942d062: Added support for AWS Lambda response streaming 39 | - 51c3296: Added support for response streaming 40 | - 48d293a: Added support for 404 pages 41 | 42 | ### Patch Changes 43 | 44 | - a96a6c4: Write a metadata file for constructs package 45 | - 4054f82: Set supported features 46 | - 374f1b9: Upgraded dependencies 47 | 48 | ## 0.4.0 49 | 50 | ### Minor Changes 51 | 52 | - e2adca1: Upgraded dependencies 53 | 54 | ### Patch Changes 55 | 56 | - e2adca1: Set Astro cookies in LambdaEdge function response 57 | - e2adca1: Allow extending the ESBuild JS banner 58 | 59 | ## 0.3.0 60 | 61 | ### Minor Changes 62 | 63 | - [#43](https://github.com/lukeshay/astro-aws/pull/43) [`81e10bc`](https://github.com/lukeshay/astro-aws/commit/81e10bc93d6febcdb1571150c29af5c63239b9a6) Thanks [@lukeshay](https://github.com/lukeshay)! - Added support for edge deployment 64 | 65 | ### Patch Changes 66 | 67 | - [`08476a0`](https://github.com/lukeshay/astro-aws/commit/08476a081c2c6bbac8b5beab1ca2afea6e7e2c60) Thanks [@lukeshay](https://github.com/lukeshay)! - Upgraded dependencies 68 | 69 | - [#43](https://github.com/lukeshay/astro-aws/pull/43) [`81e10bc`](https://github.com/lukeshay/astro-aws/commit/81e10bc93d6febcdb1571150c29af5c63239b9a6) Thanks [@lukeshay](https://github.com/lukeshay)! - fixed createRequire is getting defined twice bug 70 | 71 | ## 0.2.0 72 | 73 | ### Minor Changes 74 | 75 | - [#19](https://github.com/lukeshay/astro-aws/pull/19) [`303cf98`](https://github.com/lukeshay/astro-aws/commit/303cf98e055330e811744f18645d7936c80a0a5c) Thanks [@lukeshay](https://github.com/lukeshay)! - Added support for static deployments 76 | 77 | ## 0.1.0 78 | 79 | ### Minor Changes 80 | 81 | - [#7](https://github.com/lukeshay/astro-aws/pull/7) [`646915f`](https://github.com/lukeshay/astro-aws/commit/646915f227c27af02084e7fe7b1c1e69c9ad9e7d) Thanks [@lukeshay](https://github.com/lukeshay)! - Removed CJS support 82 | 83 | ### Patch Changes 84 | 85 | - [`2693b26`](https://github.com/lukeshay/astro-aws/commit/2693b26e90fb112ead1ca87712381830b9527e21) Thanks [@lukeshay](https://github.com/lukeshay)! - Require logging to be enabled 86 | 87 | - [`305552d`](https://github.com/lukeshay/astro-aws/commit/305552d954e60b59dc56cbae6b3e9843d282795f) Thanks [@lukeshay](https://github.com/lukeshay)! - Loop through query params 88 | 89 | - [#7](https://github.com/lukeshay/astro-aws/pull/7) [`646915f`](https://github.com/lukeshay/astro-aws/commit/646915f227c27af02084e7fe7b1c1e69c9ad9e7d) Thanks [@lukeshay](https://github.com/lukeshay)! - Upgraded dependencies 90 | 91 | - [`305552d`](https://github.com/lukeshay/astro-aws/commit/305552d954e60b59dc56cbae6b3e9843d282795f) Thanks [@lukeshay](https://github.com/lukeshay)! - More simple set cookie header 92 | 93 | - [`305552d`](https://github.com/lukeshay/astro-aws/commit/305552d954e60b59dc56cbae6b3e9843d282795f) Thanks [@lukeshay](https://github.com/lukeshay)! - Fix the multiValueHeaders 94 | 95 | - [`e67da15`](https://github.com/lukeshay/astro-aws/commit/e67da154a9584fd1c52e1b71197598d1133d4181) Thanks [@lukeshay](https://github.com/lukeshay)! - Use regular set cookie header 96 | 97 | - [`fa8459b`](https://github.com/lukeshay/astro-aws/commit/fa8459b7d832af1d3b618acd4bd5aa32861ab8b5) Thanks [@lukeshay](https://github.com/lukeshay)! - Use array buffer 98 | 99 | - [`305552d`](https://github.com/lukeshay/astro-aws/commit/305552d954e60b59dc56cbae6b3e9843d282795f) Thanks [@lukeshay](https://github.com/lukeshay)! - Log response 100 | 101 | - [`7dc5da2`](https://github.com/lukeshay/astro-aws/commit/7dc5da287af714b83e39b13a59eb2839d65c16d1) Thanks [@lukeshay](https://github.com/lukeshay)! - Upgraded dependencies 102 | 103 | - [`f2736e0`](https://github.com/lukeshay/astro-aws/commit/f2736e02afbc100a26b1fa29907e4960549bad31) Thanks [@lukeshay](https://github.com/lukeshay)! - Use correct event and response 104 | 105 | ## 0.1.0-next.8 106 | 107 | ### Minor Changes 108 | 109 | - [#7](https://github.com/lukeshay/astro-aws/pull/7) [`646915f`](https://github.com/lukeshay/astro-aws/commit/646915f227c27af02084e7fe7b1c1e69c9ad9e7d) Thanks [@lukeshay](https://github.com/lukeshay)! - Removed CJS support 110 | 111 | ### Patch Changes 112 | 113 | - [#7](https://github.com/lukeshay/astro-aws/pull/7) [`646915f`](https://github.com/lukeshay/astro-aws/commit/646915f227c27af02084e7fe7b1c1e69c9ad9e7d) Thanks [@lukeshay](https://github.com/lukeshay)! - Upgraded dependencies 114 | 115 | - [`7dc5da2`](https://github.com/lukeshay/astro-aws/commit/7dc5da287af714b83e39b13a59eb2839d65c16d1) Thanks [@lukeshay](https://github.com/lukeshay)! - Upgraded dependencies 116 | 117 | ## 0.0.6-next.7 118 | 119 | ### Patch Changes 120 | 121 | - Use array buffer 122 | 123 | ## 0.0.6-next.6 124 | 125 | ### Patch Changes 126 | 127 | - Require logging to be enabled 128 | 129 | ## 0.0.6-next.5 130 | 131 | ### Patch Changes 132 | 133 | - Use correct event and response 134 | 135 | ## 0.0.6-next.4 136 | 137 | ### Patch Changes 138 | 139 | - Use regular set cookie header 140 | 141 | ## 0.0.6-next.3 142 | 143 | ### Patch Changes 144 | 145 | - Log response 146 | 147 | ## 0.0.6-next.2 148 | 149 | ### Patch Changes 150 | 151 | - Fix the multiValueHeaders 152 | 153 | ## 0.0.6-next.1 154 | 155 | ### Patch Changes 156 | 157 | - More simple set cookie header 158 | 159 | ## 0.0.6-next.0 160 | 161 | ### Patch Changes 162 | 163 | - Loop through query params 164 | 165 | ## 0.0.5 166 | 167 | ### Patch Changes 168 | 169 | - [`b323e36`](https://github.com/lukeshay/astro-aws/commit/b323e366601f101a45f84e1a3a41179b4e393655) Thanks [@lukeshay](https://github.com/lukeshay)! - Pass through query strings 170 | 171 | ## 0.0.4 172 | 173 | ### Patch Changes 174 | 175 | - [`36ef4ce`](https://github.com/lukeshay/astro-aws/commit/36ef4ce54a834509c53f2ba5f768c66e974d21a4) Thanks [@lukeshay](https://github.com/lukeshay)! - User outDir instead of root 176 | 177 | ## 0.0.3 178 | 179 | ### Patch Changes 180 | 181 | - [`bc7b57a`](https://github.com/lukeshay/astro-aws/commit/bc7b57a3539e638ecb43ebbfdeee877092db6b81) Thanks [@lukeshay](https://github.com/lukeshay)! - Allow override of certain ESBuild options 182 | 183 | - [`d4e13a0`](https://github.com/lukeshay/astro-aws/commit/d4e13a060f30702d50e3cd2d3d076549b6aa4da9) Thanks [@lukeshay](https://github.com/lukeshay)! - Added support for ESM lambda 184 | 185 | ## 0.0.2 186 | 187 | ### Patch Changes 188 | 189 | - Fixed import in log.js 190 | 191 | ## 0.0.1 192 | 193 | ### Patch Changes 194 | 195 | - [#3](https://github.com/lukeshay/astro-aws/pull/3) [`68377c6`](https://github.com/lukeshay/astro-aws/commit/68377c6e2d5b3cf6fe53f706421d95161aba91f7) Thanks [@lukeshay](https://github.com/lukeshay)! - Initial release 196 | -------------------------------------------------------------------------------- /packages/adapter/README.md: -------------------------------------------------------------------------------- 1 | # @astro-aws/adapter 2 | 3 | An [Astro](https://astro.build) adapter for building an SSR application and deploying it to AWS Lambda. 4 | 5 | ## Install 6 | 7 | ```sh 8 | # Using NPM 9 | npx astro add @astro-aws/adapter 10 | 11 | # Using Yarn 12 | yarn astro add @astro-aws/adapter 13 | 14 | # Using PNPM 15 | pnpm astro add @astro-aws/adapter 16 | 17 | # Using Bun 18 | bun x astro add @astro-aws/adapter 19 | ``` 20 | 21 | ### Manually 22 | 23 | 1. Install the package. 24 | 25 | ``` 26 | # Using NPM 27 | npm install -D @astro-aws/adapter 28 | 29 | # Using Yarn 30 | yarn add -D @astro-aws/adapter 31 | 32 | # Using PNPM 33 | pnpm add -D @astro-aws/adapter 34 | 35 | # Using Bun 36 | bun add -D @astro-aws/adapter 37 | ``` 38 | 39 | 2. Add the following to your `astro.config.mjs` file. 40 | 41 | ```js 42 | import { defineConfig } from "astro/config" 43 | import astroAws from "@astro-aws/adapter" 44 | 45 | export default defineConfig({ 46 | output: "server", 47 | adapter: astroAws(), 48 | }) 49 | ``` 50 | 51 | ### SSR Usage 52 | 53 | 1. Install the package. 54 | 55 | ``` 56 | # Using NPM 57 | npm install -D @astro-aws/adapter 58 | 59 | # Using Yarn 60 | yarn add -D @astro-aws/adapter 61 | 62 | # Using PNPM 63 | pnpm add -D @astro-aws/adapter 64 | 65 | # Using Bun 66 | bun add -D @astro-aws/adapter 67 | ``` 68 | 69 | 2. Add the following to your `astro.config.mjs` file. 70 | 71 | ```js 72 | import { defineConfig } from "astro/config" 73 | import astroAws from "@astro-aws/adapter" 74 | 75 | export default defineConfig({ 76 | output: "server", 77 | adapter: astroAws({ 78 | mode: "ssr", 79 | }), 80 | }) 81 | ``` 82 | 83 | ### SSR Stream Usage 84 | 85 | 1. Install the package. 86 | 87 | ``` 88 | # Using NPM 89 | npm install -D @astro-aws/adapter 90 | 91 | # Using Yarn 92 | yarn add -D @astro-aws/adapter 93 | 94 | # Using PNPM 95 | pnpm add -D @astro-aws/adapter 96 | 97 | # Using Bun 98 | bun add -D @astro-aws/adapter 99 | ``` 100 | 101 | 2. Add the following to your `astro.config.mjs` file. 102 | 103 | ```js 104 | import { defineConfig } from "astro/config" 105 | import astroAws from "@astro-aws/adapter" 106 | 107 | export default defineConfig({ 108 | output: "server", 109 | adapter: astroAws({ 110 | mode: "ssr-stream", 111 | }), 112 | }) 113 | ``` 114 | 115 | ### Edge Usage 116 | 117 | > **NOTE:** Environment variables are not supported in edge mode. Due to the limitations of AWS Lambda@Edge. 118 | 119 | 1. Install the package. 120 | 121 | ``` 122 | # Using NPM 123 | npm install -D @astro-aws/adapter 124 | 125 | # Using Yarn 126 | yarn add -D @astro-aws/adapter 127 | 128 | # Using PNPM 129 | pnpm add -D @astro-aws/adapter 130 | 131 | # Using Bun 132 | bun add -D @astro-aws/adapter 133 | ``` 134 | 135 | 2. Add the following to your `astro.config.mjs` file. 136 | 137 | ```js 138 | import { defineConfig } from "astro/config" 139 | import astroAws from "@astro-aws/adapter" 140 | 141 | export default defineConfig({ 142 | output: "server", 143 | adapter: astroAws({ 144 | mode: "edge", 145 | }), 146 | }) 147 | ``` 148 | 149 | ## Example 150 | 151 | See [the source code of this site](https://github.com/lukeshay/astro-aws/blob/main/apps/www/astro.config.ts) 152 | -------------------------------------------------------------------------------- /packages/adapter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@astro-aws/adapter", 3 | "version": "0.8.0", 4 | "description": "An adapter for deploying an Astro application to AWS Lambda", 5 | "keywords": [ 6 | "withastro", 7 | "renderer", 8 | "performance", 9 | "perf", 10 | "astro-adapter", 11 | "aws", 12 | "aws-lambda" 13 | ], 14 | "homepage": "https://www.astro-aws.org/", 15 | "repository": { 16 | "type": "git", 17 | "url": "ssh://git@github.com/lukeshay/astro-aws.git", 18 | "directory": "packages/adapter" 19 | }, 20 | "license": "MIT", 21 | "type": "module", 22 | "exports": { 23 | ".": { 24 | "import": "./dist/index.js", 25 | "types": "./dist/index.d.ts" 26 | }, 27 | "./powertools": { 28 | "import": "./dist/powertools.js", 29 | "types": "./dist/powertools.d.ts" 30 | }, 31 | "./powertools.js": { 32 | "import": "./dist/powertools.js", 33 | "types": "./dist/powertools.d.ts" 34 | }, 35 | "./lambda/handlers/ssr": { 36 | "import": "./dist/lambda/handlers/ssr.js", 37 | "types": "./dist/lambda/handlers/ssr.d.ts" 38 | }, 39 | "./lambda/handlers/ssr.js": { 40 | "import": "./dist/lambda/handlers/ssr.js", 41 | "types": "./dist/lambda/handlers/ssr.d.ts" 42 | }, 43 | "./lambda/handlers/ssr-stream": { 44 | "import": "./dist/lambda/handlers/ssr-stream.js", 45 | "types": "./dist/lambda/handlers/ssr-stream.d.ts" 46 | }, 47 | "./lambda/handlers/ssr-stream.js": { 48 | "import": "./dist/lambda/handlers/ssr-stream.js", 49 | "types": "./dist/lambda/handlers/ssr-stream.d.ts" 50 | }, 51 | "./lambda/handlers/edge": { 52 | "import": "./dist/lambda/handlers/edge.js", 53 | "types": "./dist/lambda/handlers/edge.d.ts" 54 | }, 55 | "./lambda/handlers/edge.js": { 56 | "import": "./dist/lambda/handlers/edge.js", 57 | "types": "./dist/lambda/handlers/edge.d.ts" 58 | } 59 | }, 60 | "files": [ 61 | "dist" 62 | ], 63 | "scripts": { 64 | "build": "scripts build", 65 | "test": "vitest run" 66 | }, 67 | "dependencies": { 68 | "@astrojs/webapi": "^2.2.0", 69 | "@middy/core": "^5.4.5", 70 | "esbuild": "^0.23.0", 71 | "flatted": "^3.3.1", 72 | "http-status-codes": "^2.3.0", 73 | "merge-anything": "^6.0.2", 74 | "pino": "^9.3.1" 75 | }, 76 | "devDependencies": { 77 | "@astro-aws/scripts": "workspace:^", 78 | "@faker-js/faker": "^8.4.1", 79 | "@types/aws-lambda": "^8.10.141", 80 | "@types/node": "^18.18.4", 81 | "astro": "^4.11.6", 82 | "aws-lambda": "^1.0.7", 83 | "prettier": "^3.3.3", 84 | "typescript": "^5.5.3", 85 | "vitest": "^2.0.3" 86 | }, 87 | "peerDependencies": { 88 | "astro": ">=4" 89 | }, 90 | "engines": { 91 | "node": "20.x || 22.x" 92 | }, 93 | "publishConfig": { 94 | "access": "public" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /packages/adapter/src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { URL, fileURLToPath } from "node:url" 2 | import { writeFile } from "node:fs/promises" 3 | 4 | import { afterEach, beforeEach, describe, expect, test, vi } from "vitest" 5 | import { faker } from "@faker-js/faker" 6 | import type { AstroConfig, AstroIntegration, RouteData } from "astro" 7 | 8 | import type { Args } from "../args.js" 9 | import { ADAPTER_NAME } from "../constants.js" 10 | import { astroAWSFunctions, getAdapter } from "../index.js" 11 | import * as shared from "../shared.js" 12 | 13 | vi.mock("node:fs/promises", () => ({ 14 | writeFile: vi.fn(), 15 | })) 16 | 17 | describe("index.ts", () => { 18 | afterEach(() => { 19 | vi.clearAllMocks() 20 | }) 21 | 22 | describe("getAdapter", () => { 23 | const args: Args = { 24 | binaryMediaTypes: [faker.string.sample()], 25 | esBuildOptions: {}, 26 | locals: {}, 27 | mode: "ssr", 28 | } 29 | 30 | describe("when there are arguments", () => { 31 | test("should return the adapter info", () => { 32 | const result = getAdapter(args) 33 | 34 | expect(result).toStrictEqual({ 35 | adapterFeatures: { 36 | edgeMiddleware: false, 37 | functionPerRoute: false, 38 | }, 39 | args: { 40 | ...args, 41 | esBuildOptions: {}, 42 | locals: {}, 43 | mode: "ssr", 44 | }, 45 | exports: ["handler"], 46 | name: ADAPTER_NAME, 47 | serverEntrypoint: `${ADAPTER_NAME}/lambda/handlers/ssr.js`, 48 | supportedAstroFeatures: { 49 | assets: { 50 | isSharpCompatible: false, 51 | isSquooshCompatible: false, 52 | supportKind: "stable", 53 | }, 54 | hybridOutput: "stable", 55 | serverOutput: "stable", 56 | staticOutput: "unsupported", 57 | }, 58 | }) 59 | }) 60 | }) 61 | 62 | describe("when there are **not** arguments", () => { 63 | test("should return the adapter info", () => { 64 | const result = getAdapter() 65 | 66 | expect(result).toStrictEqual({ 67 | adapterFeatures: { 68 | edgeMiddleware: false, 69 | functionPerRoute: false, 70 | }, 71 | args: { 72 | binaryMediaTypes: [], 73 | esBuildOptions: {}, 74 | locals: {}, 75 | mode: "ssr", 76 | }, 77 | exports: ["handler"], 78 | name: ADAPTER_NAME, 79 | serverEntrypoint: `${ADAPTER_NAME}/lambda/handlers/ssr.js`, 80 | supportedAstroFeatures: { 81 | assets: { 82 | isSharpCompatible: false, 83 | isSquooshCompatible: false, 84 | supportKind: "stable", 85 | }, 86 | hybridOutput: "stable", 87 | serverOutput: "stable", 88 | staticOutput: "unsupported", 89 | }, 90 | }) 91 | }) 92 | }) 93 | }) 94 | 95 | describe("astroAWSFunctions", () => { 96 | const args: Args = { 97 | binaryMediaTypes: [faker.string.sample()], 98 | } 99 | 100 | describe("always", () => { 101 | test("should return the name and hooks", () => { 102 | const result = astroAWSFunctions(args) 103 | 104 | expect(result).toStrictEqual({ 105 | hooks: expect.any(Object), 106 | name: ADAPTER_NAME, 107 | }) 108 | }) 109 | }) 110 | 111 | describe("hooks", () => { 112 | let config: AstroConfig, 113 | result: AstroIntegration, 114 | routes: RouteData[], 115 | astroConfigSetup: NonNullable< 116 | AstroIntegration["hooks"]["astro:config:setup"] 117 | >, 118 | astroConfigDone: NonNullable< 119 | AstroIntegration["hooks"]["astro:config:done"] 120 | >, 121 | astroBuildDone: NonNullable< 122 | AstroIntegration["hooks"]["astro:build:done"] 123 | >, 124 | updateConfig: vi.MockedFunction, 125 | setAdapter: vi.MockedFunction 126 | 127 | beforeEach(() => { 128 | result = astroAWSFunctions(args) 129 | 130 | const outDir = new URL(`file:///dev/null/`) 131 | 132 | config = { 133 | build: { 134 | server: new URL("server/", outDir), 135 | serverEntry: "entry.mjs", 136 | }, 137 | outDir, 138 | } as unknown as AstroConfig 139 | routes = [ 140 | { 141 | route: faker.string.sample(), 142 | } as unknown as RouteData, 143 | { 144 | route: faker.string.sample(), 145 | } as unknown as RouteData, 146 | { 147 | route: faker.string.sample(), 148 | } as unknown as RouteData, 149 | ] 150 | 151 | updateConfig = vi.fn() 152 | setAdapter = vi.fn() 153 | 154 | /* eslint-disable @typescript-eslint/no-non-null-assertion*/ 155 | astroConfigSetup = result.hooks["astro:config:setup"]! 156 | astroConfigDone = result.hooks["astro:config:done"]! 157 | astroBuildDone = result.hooks["astro:build:done"]! 158 | /* eslint-enable @typescript-eslint/no-non-null-assertion*/ 159 | }) 160 | 161 | describe("astro:config:setup", () => { 162 | test("should call updateConfig", async () => { 163 | await astroConfigSetup({ 164 | config, 165 | updateConfig, 166 | } as unknown as Parameters[0]) 167 | 168 | expect(updateConfig).toHaveBeenCalledTimes(1) 169 | expect(updateConfig).toHaveBeenCalledWith({ 170 | build: { 171 | client: new URL("client/", config.outDir), 172 | server: new URL("server/", config.outDir), 173 | serverEntry: "entry.mjs", 174 | }, 175 | }) 176 | }) 177 | }) 178 | 179 | describe("astro:config:done", () => { 180 | test("should call setAdapter", async () => { 181 | await astroConfigDone({ 182 | config, 183 | setAdapter, 184 | } as unknown as Parameters[0]) 185 | 186 | expect(setAdapter).toHaveBeenCalledTimes(1) 187 | expect(setAdapter).toHaveBeenCalledWith(getAdapter(args)) 188 | }) 189 | }) 190 | 191 | describe("astro:build:done", () => { 192 | test("should bundle entry", async () => { 193 | const bundleEntry = vi 194 | .spyOn(shared, "bundleEntry") 195 | .mockResolvedValue() 196 | 197 | await astroConfigDone({ 198 | config, 199 | setAdapter, 200 | } as unknown as Parameters[0]) 201 | 202 | await astroBuildDone({ 203 | routes, 204 | } as unknown as Parameters[0]) 205 | 206 | expect(writeFile).toHaveBeenCalledTimes(1) 207 | expect(writeFile).toHaveBeenCalledWith( 208 | fileURLToPath(new URL("metadata.json", config.outDir)), 209 | expect.any(String), 210 | ) 211 | expect(bundleEntry).toHaveBeenCalledTimes(1) 212 | expect(bundleEntry).toHaveBeenCalledWith( 213 | fileURLToPath( 214 | new URL(config.build.serverEntry, config.build.server), 215 | ), 216 | fileURLToPath(new URL("lambda", config.outDir)), 217 | { 218 | ...args, 219 | esBuildOptions: {}, 220 | locals: {}, 221 | mode: "ssr", 222 | }, 223 | ) 224 | }) 225 | }) 226 | }) 227 | }) 228 | }) 229 | -------------------------------------------------------------------------------- /packages/adapter/src/args.ts: -------------------------------------------------------------------------------- 1 | import type { BuildOptions } from "esbuild" 2 | 3 | import { type WithLoggerOptions } from "./lambda/middleware.js" 4 | 5 | type EsBuildOptions = Omit< 6 | BuildOptions, 7 | "bundle" | "entryPoints" | "outdir" | "platform" 8 | > 9 | 10 | type Args = { 11 | /** Specifies what media types need to be base64 encoded. */ 12 | binaryMediaTypes: string[] 13 | /** Configures ESBuild options that are not configured automatically. */ 14 | esBuildOptions: EsBuildOptions 15 | /** Astro.locals that you want passed into the application. */ 16 | locals: object 17 | /** Specifies where you want your app deployed to. */ 18 | mode: "edge" | "ssr-stream" | "ssr" 19 | /** Settings for logging. */ 20 | logger?: WithLoggerOptions 21 | } 22 | 23 | export { type EsBuildOptions, type Args } 24 | -------------------------------------------------------------------------------- /packages/adapter/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const ADAPTER_NAME = "@astro-aws/adapter" 2 | -------------------------------------------------------------------------------- /packages/adapter/src/index.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url" 2 | import { writeFile } from "node:fs/promises" 3 | 4 | import { stringify } from "flatted" 5 | import type { AstroAdapter, AstroConfig, AstroIntegration } from "astro" 6 | 7 | import type { Args } from "./args.js" 8 | import { bundleEntry } from "./shared.js" 9 | import { ADAPTER_NAME } from "./constants.js" 10 | import { warn } from "./log.js" 11 | 12 | const DEFAULT_ARGS: Args = { 13 | binaryMediaTypes: [], 14 | esBuildOptions: {}, 15 | locals: {}, 16 | mode: "ssr", 17 | } 18 | 19 | const getAdapter = (args: Partial = {}): AstroAdapter => ({ 20 | adapterFeatures: { 21 | edgeMiddleware: false, 22 | functionPerRoute: false, 23 | }, 24 | args: { 25 | ...DEFAULT_ARGS, 26 | ...args, 27 | }, 28 | exports: ["handler"], 29 | name: ADAPTER_NAME, 30 | serverEntrypoint: `${ADAPTER_NAME}/lambda/handlers/${ 31 | args.mode ?? DEFAULT_ARGS.mode 32 | }.js`, 33 | supportedAstroFeatures: { 34 | assets: { 35 | isSharpCompatible: false, 36 | isSquooshCompatible: false, 37 | supportKind: "stable", 38 | }, 39 | hybridOutput: "stable", 40 | serverOutput: "stable", 41 | staticOutput: "unsupported", 42 | }, 43 | }) 44 | 45 | const astroAWSFunctions = (args: Partial = {}): AstroIntegration => { 46 | let astroConfig: AstroConfig 47 | 48 | const argsWithDefault: Args = { 49 | ...DEFAULT_ARGS, 50 | ...args, 51 | } 52 | 53 | /* eslint-disable sort-keys */ 54 | return { 55 | name: ADAPTER_NAME, 56 | hooks: { 57 | "astro:config:setup": ({ config, updateConfig }) => { 58 | updateConfig({ 59 | build: { 60 | client: new URL("client/", config.outDir), 61 | server: new URL("server/", config.outDir), 62 | serverEntry: "entry.mjs", 63 | }, 64 | }) 65 | }, 66 | "astro:config:done": ({ config, setAdapter }) => { 67 | setAdapter(getAdapter(argsWithDefault)) 68 | 69 | astroConfig = config 70 | 71 | if (config.output === "static") { 72 | warn('`output: "server"` is required to use this adapter.') 73 | warn( 74 | "Otherwise, this adapter is not required to deploy a static site to AWS.", 75 | ) 76 | } 77 | }, 78 | "astro:build:done": async (options) => { 79 | await writeFile( 80 | fileURLToPath(new URL("metadata.json", astroConfig.outDir)), 81 | stringify({ 82 | args: argsWithDefault, 83 | options, 84 | config: astroConfig, 85 | }), 86 | ) 87 | 88 | await bundleEntry( 89 | fileURLToPath( 90 | new URL(astroConfig.build.serverEntry, astroConfig.build.server), 91 | ), 92 | fileURLToPath(new URL("lambda", astroConfig.outDir)), 93 | argsWithDefault, 94 | ) 95 | }, 96 | }, 97 | } 98 | /* eslint-enable sort-keys */ 99 | } 100 | 101 | export default astroAWSFunctions 102 | export { getAdapter, astroAWSFunctions } 103 | -------------------------------------------------------------------------------- /packages/adapter/src/lambda/constants.ts: -------------------------------------------------------------------------------- 1 | const DISALLOWED_EDGE_HEADERS = [ 2 | "Connection", 3 | "Expect", 4 | "Keep-Alive", 5 | "Proxy-Authenticate", 6 | "Proxy-Authorization", 7 | "Proxy-Connection", 8 | "Trailer", 9 | "Upgrade", 10 | "X-Accel-Buffering", 11 | "X-Accel-Charset", 12 | "X-Accel-Limit-Rate", 13 | "X-Accel-Redirect", 14 | "X-Amzn-Auth", 15 | "X-Amzn-Cf-Billing", 16 | "X-Amzn-Cf-Id", 17 | "X-Amzn-Cf-Xff", 18 | "X-Amzn-Errortype", 19 | "X-Amzn-Fle-Profile", 20 | "X-Amzn-Header-Count", 21 | "X-Amzn-Header-Order", 22 | "X-Amzn-Lambda-Integration-Tag", 23 | "X-Amzn-RequestId", 24 | "X-Cache", 25 | "X-Forwarded-Proto", 26 | "X-Real-IP", 27 | "Accept-Encoding", 28 | "Content-Length", 29 | "If-Modified-Since", 30 | "If-None-Match", 31 | "If-Range", 32 | "If-Unmodified-Since", 33 | "Transfer-Encoding", 34 | "Via", 35 | "X-Amz-Cf-.*", 36 | "X-Edge-.*", 37 | ].map((str) => new RegExp(`^${str.toLowerCase()}$`, "u")) 38 | 39 | const KNOWN_BINARY_MEDIA_TYPES = [ 40 | "application/epub+zip", 41 | "application/java-archive", 42 | "application/msword", 43 | "application/octet-stream", 44 | "application/pdf", 45 | "application/rtf", 46 | "application/vnd.amazon.ebook", 47 | "application/vnd.apple.installer+xml", 48 | "application/vnd.ms-excel", 49 | "application/vnd.ms-powerpoint", 50 | "application/vnd.openxmlformats-officedocument.presentationml.presentation", 51 | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 52 | "application/vnd.openxmlformats-officedocument.wordprocessingml.document", 53 | "application/x-7z-compressed", 54 | "application/x-apple-diskimage", 55 | "application/x-bzip", 56 | "application/x-bzip2", 57 | "application/x-gzip", 58 | "application/x-java-archive", 59 | "application/x-rar-compressed", 60 | "application/x-tar", 61 | "application/x-zip", 62 | "application/zip", 63 | "audio/3gpp", 64 | "audio/aac", 65 | "audio/basic", 66 | "audio/mpeg", 67 | "audio/ogg", 68 | "audio/wavaudio/webm", 69 | "audio/x-aiff", 70 | "audio/x-midi", 71 | "audio/x-wav", 72 | "font/otf", 73 | "font/woff", 74 | "font/woff2", 75 | "image/bmp", 76 | "image/gif", 77 | "image/jpeg", 78 | "image/png", 79 | "image/tiff", 80 | "image/vnd.microsoft.icon", 81 | "image/webp", 82 | "video/3gpp", 83 | "video/mp2t", 84 | "video/mpeg", 85 | "video/ogg", 86 | "video/quicktime", 87 | "video/webm", 88 | "video/x-msvideo", 89 | ] 90 | 91 | export { DISALLOWED_EDGE_HEADERS, KNOWN_BINARY_MEDIA_TYPES } 92 | -------------------------------------------------------------------------------- /packages/adapter/src/lambda/handlers/edge.ts: -------------------------------------------------------------------------------- 1 | import { NodeApp } from "astro/app/node" 2 | import type { 3 | CloudFrontHeaders, 4 | CloudFrontRequest, 5 | CloudFrontRequestEvent, 6 | CloudFrontRequestHandler, 7 | CloudFrontRequestResult, 8 | CloudFrontResponseEvent, 9 | CloudFrontResponseResult, 10 | } from "aws-lambda" 11 | import { type SSRManifest } from "astro" 12 | import { polyfill } from "@astrojs/webapi" 13 | 14 | import type { Args } from "../../args.js" 15 | import { createRequestBody, parseContentType } from "../helpers.js" 16 | import { 17 | DISALLOWED_EDGE_HEADERS, 18 | KNOWN_BINARY_MEDIA_TYPES, 19 | } from "../constants.js" 20 | import { withLogger } from "../middleware.js" 21 | 22 | polyfill(globalThis, { 23 | exclude: "window document", 24 | }) 25 | 26 | const createLambdaEdgeFunctionResponse = async ( 27 | app: NodeApp, 28 | response: Response, 29 | knownBinaryMediaTypes: Set, 30 | ): Promise => { 31 | const cookies = [...app.setCookieHeaders(response)] 32 | 33 | const responseHeadersObj = Object.fromEntries(response.headers.entries()) 34 | 35 | const headers: CloudFrontHeaders = { 36 | ...Object.fromEntries( 37 | Object.entries(responseHeadersObj) 38 | .filter( 39 | ([key]) => 40 | !DISALLOWED_EDGE_HEADERS.some((reg) => reg.test(key.toLowerCase())), 41 | ) 42 | .map(([key, value]) => [ 43 | key.toLowerCase(), 44 | [ 45 | { 46 | key, 47 | value, 48 | }, 49 | ], 50 | ]), 51 | ), 52 | ...(cookies.length > 0 && { 53 | "set-cookie": cookies.map((cookie) => ({ 54 | key: "set-cookie", 55 | value: cookie, 56 | })), 57 | }), 58 | } 59 | const responseContentType = parseContentType( 60 | response.headers.get("content-type"), 61 | ) 62 | const bodyEncoding = knownBinaryMediaTypes.has(responseContentType) 63 | ? "base64" 64 | : "text" 65 | 66 | return { 67 | body: (await response.text()) as string, 68 | bodyEncoding, 69 | headers, 70 | status: String(response.status), 71 | statusDescription: response.statusText, 72 | } 73 | } 74 | 75 | const createExports = ( 76 | manifest: SSRManifest, 77 | args: Args, 78 | ): { handler: CloudFrontRequestHandler } => { 79 | const app = new NodeApp(manifest, false) 80 | 81 | const knownBinaryMediaTypes = new Set([ 82 | ...KNOWN_BINARY_MEDIA_TYPES, 83 | ...args.binaryMediaTypes, 84 | ]) 85 | 86 | const handleRequest = async ( 87 | request: Request, 88 | def: CloudFrontRequestResult | CloudFrontResponseResult, 89 | ): Promise => { 90 | const routeData = app.match(request) 91 | 92 | if (!routeData) { 93 | return def 94 | } 95 | 96 | const response = await app.render(request, { 97 | locals: args.locals, 98 | routeData, 99 | }) 100 | const fnResponse = await createLambdaEdgeFunctionResponse( 101 | app, 102 | response, 103 | knownBinaryMediaTypes, 104 | ) 105 | 106 | return fnResponse 107 | } 108 | 109 | const handler = async ( 110 | event: CloudFrontRequestEvent | CloudFrontResponseEvent, 111 | ): Promise => { 112 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 113 | const record = event.Records[0]!.cf 114 | 115 | const headers = new Headers( 116 | Object.fromEntries( 117 | Object.entries(record.request.headers).map(([key, value]) => [ 118 | value[0]?.key ?? key, 119 | value[0]?.value, 120 | ]), 121 | ) as HeadersInit, 122 | ) 123 | 124 | const scheme = headers.get("x-forwarded-protocol") ?? "https" 125 | const host = headers.get("x-forwarded-host") ?? headers.get("host") ?? "" 126 | const qs = record.request.querystring.length 127 | ? `?${record.request.querystring}` 128 | : "" 129 | 130 | const requestInit: RequestInit = { 131 | headers, 132 | method: record.request.method, 133 | } 134 | 135 | if ("response" in record) { 136 | if (record.response.status === "404") { 137 | const url = new URL(`404${qs}`, `https://${host}`) 138 | 139 | const request = new Request(url, requestInit) 140 | 141 | return handleRequest(request, record.response) 142 | } 143 | 144 | return record.response 145 | } 146 | 147 | const cloudFrontRequest = record.request as unknown as CloudFrontRequest 148 | 149 | const url = new URL( 150 | `${cloudFrontRequest.uri.replace(/\/?index\.html$/u, "")}${qs}`, 151 | `${scheme}://${host}`, 152 | ) 153 | 154 | requestInit.body = cloudFrontRequest.body?.data 155 | ? createRequestBody( 156 | cloudFrontRequest.method, 157 | cloudFrontRequest.body.data, 158 | cloudFrontRequest.body.encoding, 159 | ) 160 | : undefined 161 | 162 | const request = new Request(url, requestInit) 163 | 164 | return handleRequest(request, cloudFrontRequest) 165 | } 166 | 167 | return { 168 | handler: withLogger(args.logger, handler), 169 | } 170 | } 171 | 172 | export { createExports } 173 | -------------------------------------------------------------------------------- /packages/adapter/src/lambda/handlers/ssr-stream.ts: -------------------------------------------------------------------------------- 1 | export * from "./ssr.js" 2 | -------------------------------------------------------------------------------- /packages/adapter/src/lambda/handlers/ssr.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from "node:buffer" 2 | 3 | import { NodeApp } from "astro/app/node" 4 | import type { 5 | APIGatewayProxyEventV2, 6 | APIGatewayProxyHandlerV2, 7 | } from "aws-lambda" 8 | import middy, { MiddyfiedHandler } from "@middy/core" 9 | import { type SSRManifest } from "astro" 10 | import { polyfill } from "@astrojs/webapi" 11 | 12 | import type { Args } from "../../args.js" 13 | import { 14 | createReadableStream, 15 | createRequestBody, 16 | parseContentType, 17 | } from "../helpers.js" 18 | import { withLogger } from "../middleware.js" 19 | import { KNOWN_BINARY_MEDIA_TYPES } from "../constants.js" 20 | import { type CloudfrontResult } from "../types.js" 21 | 22 | polyfill(globalThis, { 23 | exclude: "window document", 24 | }) 25 | 26 | const createLambdaFunctionHeaders = ( 27 | app: NodeApp, 28 | response: Response, 29 | knownBinaryMediaTypes: Set, 30 | ) => { 31 | const cookies = [...app.setCookieHeaders(response)] 32 | const intermediateHeaders = new Headers(response.headers) 33 | 34 | intermediateHeaders.delete("set-cookie") 35 | 36 | const headers = Object.fromEntries(intermediateHeaders.entries()) 37 | const responseContentType = parseContentType(headers["content-type"]) 38 | const isBase64Encoded = knownBinaryMediaTypes.has(responseContentType) 39 | 40 | return { 41 | cookies, 42 | headers, 43 | isBase64Encoded, 44 | responseContentType, 45 | } 46 | } 47 | 48 | const createAPIGatewayProxyEventV2ResponseBody = async ( 49 | response: Response, 50 | shouldStream: boolean, 51 | isBase64Encoded: boolean, 52 | ) => { 53 | if (shouldStream) { 54 | return createReadableStream(await response.arrayBuffer()) 55 | } 56 | 57 | return isBase64Encoded 58 | ? Buffer.from(await response.arrayBuffer()).toString("base64") 59 | : ((await response.text()) as string) 60 | } 61 | 62 | const createLambdaFunctionResponse = async ( 63 | app: NodeApp, 64 | response: Response, 65 | knownBinaryMediaTypes: Set, 66 | shouldStream: boolean, 67 | ): Promise => { 68 | const { cookies, headers, isBase64Encoded } = createLambdaFunctionHeaders( 69 | app, 70 | response, 71 | knownBinaryMediaTypes, 72 | ) 73 | 74 | const body = await createAPIGatewayProxyEventV2ResponseBody( 75 | response, 76 | shouldStream, 77 | isBase64Encoded, 78 | ) 79 | 80 | return { 81 | body, 82 | cookies, 83 | headers, 84 | isBase64Encoded, 85 | statusCode: response.status, 86 | } 87 | } 88 | 89 | const createExports = ( 90 | manifest: SSRManifest, 91 | args: Args, 92 | ): { handler: APIGatewayProxyHandlerV2 } => { 93 | const shouldStream = args.mode === "ssr-stream" 94 | 95 | const app = new NodeApp(manifest, shouldStream) 96 | 97 | const knownBinaryMediaTypes = new Set([ 98 | ...KNOWN_BINARY_MEDIA_TYPES, 99 | ...args.binaryMediaTypes, 100 | ]) 101 | 102 | const handler: APIGatewayProxyHandlerV2 = async ( 103 | event: APIGatewayProxyEventV2, 104 | ) => { 105 | const headers = new Headers() 106 | 107 | for (const [k, v] of Object.entries(event.headers)) { 108 | if (v) headers.set(k, v) 109 | } 110 | 111 | if (event.cookies) { 112 | headers.set("cookie", event.cookies.join("; ")) 113 | } 114 | 115 | const domainName = 116 | headers.get("x-forwarded-host") ?? event.requestContext.domainName 117 | const qs = event.rawQueryString.length ? `?${event.rawQueryString}` : "" 118 | const url = new URL( 119 | `${event.rawPath.replace(/\/?index\.html$/u, "")}${qs}`, 120 | `https://${domainName}`, 121 | ) 122 | 123 | const request = new Request(url, { 124 | body: createRequestBody( 125 | event.requestContext.http.method, 126 | event.body, 127 | event.isBase64Encoded, 128 | ), 129 | headers, 130 | method: event.requestContext.http.method, 131 | }) 132 | 133 | let routeData = app.match(request) 134 | 135 | if (!routeData) { 136 | const request404 = new Request( 137 | new URL(`404${qs}`, `https://${domainName}`), 138 | { 139 | body: createRequestBody( 140 | event.requestContext.http.method, 141 | event.body, 142 | event.isBase64Encoded, 143 | ), 144 | headers, 145 | method: event.requestContext.http.method, 146 | }, 147 | ) 148 | 149 | routeData = app.match(request404) 150 | 151 | if (!routeData) { 152 | return { 153 | body: shouldStream ? createReadableStream("Not found") : "Not found", 154 | headers: { 155 | "content-type": "text/plain", 156 | }, 157 | statusCode: 404, 158 | } 159 | } 160 | } 161 | 162 | const response = await app.render(request, { 163 | locals: args.locals, 164 | routeData, 165 | }) 166 | 167 | return createLambdaFunctionResponse( 168 | app, 169 | response, 170 | knownBinaryMediaTypes, 171 | shouldStream, 172 | ) 173 | } 174 | 175 | return { 176 | handler: middy({ streamifyResponse: shouldStream }).handler( 177 | withLogger(args.logger, handler) as MiddyfiedHandler, 178 | ), 179 | } 180 | } 181 | 182 | export { createExports } 183 | -------------------------------------------------------------------------------- /packages/adapter/src/lambda/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from "node:buffer" 2 | import { Readable } from "node:stream" 3 | 4 | const createReadableStream = (val: ArrayBuffer | string): Readable => 5 | new Readable({ 6 | // eslint-disable-next-line get-off-my-lawn/prefer-arrow-functions 7 | read() { 8 | // @ts-expect-error - TS doesn't like this but it is valid 9 | this.push(Buffer.from(val)) 10 | // eslint-disable-next-line unicorn/no-null 11 | this.push(null) 12 | }, 13 | }) 14 | 15 | const parseContentType = (header?: string | null) => header?.split(";")[0] ?? "" 16 | 17 | const createRequestBody = ( 18 | method: string, 19 | body: string | undefined, 20 | isBase64Encoded: boolean | string, 21 | ) => { 22 | if (method !== "GET" && method !== "HEAD" && body) { 23 | return body && isBase64Encoded ? Buffer.from(body, "base64") : body 24 | } 25 | 26 | return undefined 27 | } 28 | 29 | export { parseContentType, createRequestBody, createReadableStream } 30 | -------------------------------------------------------------------------------- /packages/adapter/src/lambda/middleware.ts: -------------------------------------------------------------------------------- 1 | import { type Handler } from "aws-lambda" 2 | 3 | type WithLoggerOptions = { 4 | logErrors?: boolean 5 | logEvent?: boolean 6 | logResult?: boolean 7 | } 8 | 9 | const withLogger = 10 | ( 11 | options: WithLoggerOptions | undefined, 12 | handler: Handler, 13 | ): Handler => 14 | async (event, context, callback) => { 15 | if (options?.logEvent) { 16 | console.log("Lambda invocation event", { event }) 17 | } 18 | 19 | let result: TResult 20 | 21 | try { 22 | // @ts-expect-error - need void 23 | result = await handler(event, context, callback) 24 | } catch (error) { 25 | if (options?.logErrors ?? true) { 26 | console.log("Lambda invocation error", { error }) 27 | } 28 | 29 | throw error as Error 30 | } 31 | 32 | if (options?.logResult) { 33 | console.log("Lambda invocation result", { result }) 34 | } 35 | 36 | return result 37 | } 38 | 39 | export { type WithLoggerOptions, withLogger } 40 | -------------------------------------------------------------------------------- /packages/adapter/src/lambda/types.ts: -------------------------------------------------------------------------------- 1 | import { type Readable } from "node:stream" 2 | 3 | import { type APIGatewayProxyStructuredResultV2 } from "aws-lambda" 4 | 5 | export type CloudfrontResult = Omit< 6 | APIGatewayProxyStructuredResultV2, 7 | "body" 8 | > & { 9 | body: Readable | string 10 | } 11 | -------------------------------------------------------------------------------- /packages/adapter/src/log.ts: -------------------------------------------------------------------------------- 1 | import { env } from "node:process" 2 | 3 | import { ADAPTER_NAME } from "./constants.js" 4 | 5 | const LogLevels: Record = { 6 | DEBUG: 1, 7 | INFO: 2, 8 | WARN: 3, 9 | // eslint-disable-next-line sort-keys 10 | ERROR: 4, 11 | } 12 | 13 | const logLevel = LogLevels[env.ASTRO_AWS_LOG_LEVEL ?? "INFO"] ?? 2 14 | 15 | const debug = (message?: unknown, ...optionalParams: unknown[]) => { 16 | if (logLevel <= 1) { 17 | console.debug(`[${ADAPTER_NAME}]`, message, ...optionalParams) 18 | } 19 | } 20 | 21 | const warn = (message?: unknown, ...optionalParams: unknown[]) => { 22 | if (logLevel <= 3) { 23 | console.warn(`[${ADAPTER_NAME}]`, message, ...optionalParams) 24 | } 25 | } 26 | 27 | const log = (message?: unknown, ...optionalParams: unknown[]) => { 28 | if (logLevel <= 2) { 29 | console.log(`[${ADAPTER_NAME}]`, message, ...optionalParams) 30 | } 31 | } 32 | 33 | export { warn, log, debug } 34 | -------------------------------------------------------------------------------- /packages/adapter/src/shared.ts: -------------------------------------------------------------------------------- 1 | import type { BuildOptions } from "esbuild" 2 | import { build } from "esbuild" 3 | import { mergeAndConcat } from "merge-anything" 4 | 5 | import type { Args } from "./args.js" 6 | 7 | const DEFAULT_CONFIG: BuildOptions = { 8 | allowOverwrite: true, 9 | bundle: true, 10 | external: ["aws-sdk"], 11 | metafile: true, 12 | platform: "node", 13 | target: "node16", 14 | } 15 | 16 | const bundleEntry = async (entryFile: string, outDir: string, args: Args) => { 17 | const config = mergeAndConcat(DEFAULT_CONFIG, args.esBuildOptions, { 18 | banner: { 19 | js: [ 20 | "import { createRequire as topLevelCreateRequire } from 'module';", 21 | "const require = topLevelCreateRequire(import.meta.url);", 22 | args.esBuildOptions.banner?.js ?? "", 23 | ].join(""), 24 | }, 25 | entryPoints: [entryFile], 26 | format: "esm", 27 | outdir: outDir, 28 | outExtension: { 29 | ".js": ".mjs", 30 | }, 31 | } satisfies BuildOptions) 32 | 33 | await build(config) 34 | } 35 | 36 | export { bundleEntry } 37 | -------------------------------------------------------------------------------- /packages/adapter/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["**/__tests__/**/*"], 3 | "extends": "../../tsconfig.base.json" 4 | } 5 | -------------------------------------------------------------------------------- /packages/adapter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/constructs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @astro-aws/constructs 2 | 3 | ## 0.8.0 4 | 5 | ### Minor Changes 6 | 7 | - 3fc8a7f: Add support for node 22 8 | - 3fc8a7f: Remove support for node 18 9 | 10 | ## 0.7.0 11 | 12 | ### Minor Changes 13 | 14 | - b4bcad7: Upgrade dependencies 15 | 16 | ### Patch Changes 17 | 18 | - 48af5d1: Fixed circular reference in S3 bucket construct which prevented configuration of the S3 part of the infrastructure. 19 | - 48e4410: Updated dependencies 20 | 21 | ## 0.6.0 22 | 23 | ### Minor Changes 24 | 25 | - 839b188: Added support for response streaming 26 | - 3f1e6cb: Updated dependencies 27 | - 3f1e6cb: Support Astro v4 28 | - 839b188: Added support for 404 pages 29 | 30 | ### Patch Changes 31 | 32 | - 3f1e6cb: Use flatted to handle circular json 33 | 34 | ## 0.5.0 35 | 36 | ### Minor Changes 37 | 38 | - 942d062: Added support for AWS Lambda response streaming 39 | - 51c3296: Added support for response streaming 40 | - 48d293a: Added support for 404 pages 41 | 42 | ### Patch Changes 43 | 44 | - a96a6c4: Use metadata file from adapter to configure constructs 45 | - 374f1b9: Upgraded dependencies 46 | 47 | ## 0.4.0 48 | 49 | ### Minor Changes 50 | 51 | - e2adca1: Upgraded dependencies 52 | - e2adca1: Support hybrid output 53 | 54 | ## 0.3.0 55 | 56 | ### Minor Changes 57 | 58 | - [#43](https://github.com/lukeshay/astro-aws/pull/43) [`81e10bc`](https://github.com/lukeshay/astro-aws/commit/81e10bc93d6febcdb1571150c29af5c63239b9a6) Thanks [@lukeshay](https://github.com/lukeshay)! - Restructured props and resource access 59 | 60 | - [#43](https://github.com/lukeshay/astro-aws/pull/43) [`81e10bc`](https://github.com/lukeshay/astro-aws/commit/81e10bc93d6febcdb1571150c29af5c63239b9a6) Thanks [@lukeshay](https://github.com/lukeshay)! - Added support for edge deployment 61 | 62 | - [#29](https://github.com/lukeshay/astro-aws/pull/29) [`d17b2a7`](https://github.com/lukeshay/astro-aws/commit/d17b2a7cf7c1c8ee0ca4acc1d610c8ee040969c4) Thanks [@lukeshay](https://github.com/lukeshay)! - Renamed resources and removed the bare construct 63 | 64 | ### Patch Changes 65 | 66 | - [`08476a0`](https://github.com/lukeshay/astro-aws/commit/08476a081c2c6bbac8b5beab1ca2afea6e7e2c60) Thanks [@lukeshay](https://github.com/lukeshay)! - Upgraded dependencies 67 | 68 | - [#26](https://github.com/lukeshay/astro-aws/pull/26) [`cf09511`](https://github.com/lukeshay/astro-aws/commit/cf09511345e0ac932d518f9cc3561abe106ae4ec) Thanks [@lukeshay](https://github.com/lukeshay)! - Raised default lambda memory to 512mb 69 | 70 | ## 0.2.0 71 | 72 | ### Minor Changes 73 | 74 | - [#19](https://github.com/lukeshay/astro-aws/pull/19) [`303cf98`](https://github.com/lukeshay/astro-aws/commit/303cf98e055330e811744f18645d7936c80a0a5c) Thanks [@lukeshay](https://github.com/lukeshay)! - Added support for static deployments 75 | 76 | ## 0.0.4 77 | 78 | ### Patch Changes 79 | 80 | - [#7](https://github.com/lukeshay/astro-aws/pull/7) [`646915f`](https://github.com/lukeshay/astro-aws/commit/646915f227c27af02084e7fe7b1c1e69c9ad9e7d) Thanks [@lukeshay](https://github.com/lukeshay)! - Upgraded dependencies 81 | 82 | - [#14](https://github.com/lukeshay/astro-aws/pull/14) [`d7e5a17`](https://github.com/lukeshay/astro-aws/commit/d7e5a17337537343a4302920f8fbde1ba60c8e2c) Thanks [@lukeshay](https://github.com/lukeshay)! - Default to Node.js 18 runtime 83 | 84 | - [`12f4c9f`](https://github.com/lukeshay/astro-aws/commit/12f4c9ff03e368307895fd06c9ee35ad5958bfb0) Thanks [@lukeshay](https://github.com/lukeshay)! - Allow configuration of /api behavior 85 | 86 | - [`7dc5da2`](https://github.com/lukeshay/astro-aws/commit/7dc5da287af714b83e39b13a59eb2839d65c16d1) Thanks [@lukeshay](https://github.com/lukeshay)! - Upgraded dependencies 87 | 88 | ## 0.0.4-next.1 89 | 90 | ### Patch Changes 91 | 92 | - [#7](https://github.com/lukeshay/astro-aws/pull/7) [`646915f`](https://github.com/lukeshay/astro-aws/commit/646915f227c27af02084e7fe7b1c1e69c9ad9e7d) Thanks [@lukeshay](https://github.com/lukeshay)! - Upgraded dependencies 93 | 94 | - [`7dc5da2`](https://github.com/lukeshay/astro-aws/commit/7dc5da287af714b83e39b13a59eb2839d65c16d1) Thanks [@lukeshay](https://github.com/lukeshay)! - Upgraded dependencies 95 | 96 | ## 0.0.4-next.0 97 | 98 | ### Patch Changes 99 | 100 | - Allow configuration of /api behavior 101 | 102 | ## 0.0.3 103 | 104 | ### Patch Changes 105 | 106 | - [`dee988a`](https://github.com/lukeshay/astro-aws/commit/dee988a8c32edc15a62a17e3a053b9a333bf2f80) Thanks [@lukeshay](https://github.com/lukeshay)! - Allow more configuration of resources 107 | 108 | ## 0.0.2 109 | 110 | ### Patch Changes 111 | 112 | - [`f76a300`](https://github.com/lukeshay/astro-aws/commit/f76a30043aad8cd8a43973f4f9b93d45427dc406) Thanks [@lukeshay](https://github.com/lukeshay)! - Made createBucketDeployment public 113 | 114 | - [`f76a300`](https://github.com/lukeshay/astro-aws/commit/f76a30043aad8cd8a43973f4f9b93d45427dc406) Thanks [@lukeshay](https://github.com/lukeshay)! - Allow deployment to be skipped when using AstroAWSConstruct 115 | 116 | - [`d4e13a0`](https://github.com/lukeshay/astro-aws/commit/d4e13a060f30702d50e3cd2d3d076549b6aa4da9) Thanks [@lukeshay](https://github.com/lukeshay)! - Allow POST requests to /api/\* 117 | 118 | ## 0.0.1 119 | 120 | ### Patch Changes 121 | 122 | - [#3](https://github.com/lukeshay/astro-aws/pull/3) [`68377c6`](https://github.com/lukeshay/astro-aws/commit/68377c6e2d5b3cf6fe53f706421d95161aba91f7) Thanks [@lukeshay](https://github.com/lukeshay)! - Initial release 123 | -------------------------------------------------------------------------------- /packages/constructs/README.md: -------------------------------------------------------------------------------- 1 | # @astro-aws/constructs 2 | 3 | Constructs for deploying your [Astro](https://astro.build/) project that is built using [@astro-aws/adapter](https://www.npmjs.com/package/@astro-aws/adapter). 4 | 5 | ## Usage 6 | 7 | 1. Install this package and it's peer dependencies in your AWS CDK project. 8 | 9 | ```sh 10 | # Using NPM 11 | npm install @astro-aws/constructs constructs aws-cdk-lib 12 | 13 | # Using Yarn 14 | yarn add @astro-aws/constructs constructs aws-cdk-lib 15 | 16 | # Using PNPM 17 | pnpm add @astro-aws/constructs constructs aws-cdk-lib 18 | 19 | # Using Bun 20 | bun add @astro-aws/constructs constructs aws-cdk-lib 21 | ``` 22 | 23 | 2. Add the construct to your CDK stack. 24 | 25 | ```ts 26 | import { Stack } from "aws-cdk-lib/core" 27 | import type { StackProps } from "aws-cdk-lib/core" 28 | import { AstroAWS } from "@astro-aws/constructs" 29 | 30 | export interface MyAstroStackProps extends StackProps {} 31 | 32 | export class MyAstroStack extends Stack { 33 | public constructor(scope: Construct, id: string, props: MyAstroStackProps) { 34 | super(scope, id, props) 35 | 36 | new AstroAWS(this, "AstroAWS", { 37 | websitePath: "..", // Replace with the path to your website code. 38 | }) 39 | } 40 | } 41 | ``` 42 | 43 | ## Customization 44 | 45 | All the resources created by the `AstroAWS` construct can be customized. We expose every prop of the resources that is customizable. The props can be set by passing them in to the `cdk` field on the `AstroAWS` construct props. Depending on the deployment method, not all of the props will be used. The constructed can be access through the `cdk` field on the `AstroAWS` construct object. 46 | 47 | ```ts 48 | import { Stack, CfnOutput } from "aws-cdk-lib/core" 49 | import type { StackProps } from "aws-cdk-lib/core" 50 | import { AstroAWS } from "@astro-aws/constructs" 51 | 52 | export interface MyAstroStackProps extends StackProps {} 53 | 54 | export class MyAstroStack extends Stack { 55 | public constructor(scope: Construct, id: string, props: MyAstroStackProps) { 56 | super(scope, id, props) 57 | 58 | const astroAWS = new AstroAWS(this, "AstroAWS", { 59 | cdk: { 60 | lambdaFunction: { 61 | memorySize: 1024, 62 | }, 63 | }, 64 | websitePath: "..", // Replace with the path to your website code. 65 | }) 66 | 67 | new CfnOutput(this, "DistributionDomainName", { 68 | value: astroAWS.cdk.cloudfrontDistribution.distributionDomainName, 69 | }) 70 | } 71 | } 72 | ``` 73 | 74 | ## Example 75 | 76 | See [the source code of this site](https://github.com/lukeshay/astro-aws/blob/main/apps/infra/src/lib/stacks/website-stack.ts) 77 | -------------------------------------------------------------------------------- /packages/constructs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@astro-aws/constructs", 3 | "version": "0.8.0", 4 | "description": "Constructs for deploying an Astro application to AWS Lambda", 5 | "keywords": [ 6 | "withastro", 7 | "renderer", 8 | "performance", 9 | "perf", 10 | "astro-adapter", 11 | "aws", 12 | "aws-lambda", 13 | "aws-cdk" 14 | ], 15 | "homepage": "https://www.astro-aws.org/", 16 | "repository": { 17 | "type": "git", 18 | "url": "ssh://git@github.com/lukeshay/astro-aws.git", 19 | "directory": "packages/constructs" 20 | }, 21 | "license": "MIT", 22 | "type": "module", 23 | "exports": "./dist/index.js", 24 | "types": "./dist/index.d.ts", 25 | "files": [ 26 | "dist" 27 | ], 28 | "scripts": { 29 | "build": "scripts build" 30 | }, 31 | "devDependencies": { 32 | "@astro-aws/scripts": "workspace:^", 33 | "@types/node": "^18.18.4", 34 | "aws-cdk-lib": "^2.149.0", 35 | "constructs": "^10.3.0", 36 | "prettier": "^3.3.3", 37 | "typescript": "^5.5.3" 38 | }, 39 | "peerDependencies": { 40 | "aws-cdk-lib": "^2.94.0", 41 | "constructs": "^10.1.0" 42 | }, 43 | "engines": { 44 | "node": "20.x || 22.x" 45 | }, 46 | "publishConfig": { 47 | "access": "public" 48 | }, 49 | "dependencies": { 50 | "flatted": "^3.3.1" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/constructs/src/constructs/astro-aws-cloudfront-distribution.ts: -------------------------------------------------------------------------------- 1 | import { type Construct } from "constructs" 2 | import { type Function } from "aws-cdk-lib/aws-lambda" 3 | import { 4 | AllowedMethods, 5 | Distribution, 6 | Function as CfFunction, 7 | FunctionCode, 8 | FunctionEventType, 9 | LambdaEdgeEventType, 10 | OriginRequestPolicy, 11 | ResponseHeadersPolicy, 12 | ViewerProtocolPolicy, 13 | type BehaviorOptions, 14 | type DistributionProps, 15 | type EdgeLambda, 16 | type FunctionAssociation, 17 | type IOrigin, 18 | } from "aws-cdk-lib/aws-cloudfront" 19 | 20 | import { 21 | AstroAWSBaseConstruct, 22 | type AstroAWSBaseConstructProps, 23 | } from "../types/astro-aws-construct.js" 24 | 25 | type AstroAWSCloudfrontDistributionCdkProps = { 26 | cloudfrontDistribution?: Omit< 27 | DistributionProps, 28 | "apiBehavior" | "defaultBehavior" 29 | > & { 30 | apiBehavior?: Omit 31 | defaultBehavior?: Omit 32 | } 33 | } 34 | 35 | type AstroAWSCloudfrontDistributionProps = AstroAWSBaseConstructProps & { 36 | lambdaFunction?: Function 37 | origin: IOrigin 38 | lambdaFunctionOrigin?: IOrigin 39 | cdk?: AstroAWSCloudfrontDistributionCdkProps 40 | } 41 | 42 | type AstroAWSCloudfrontDistributionCdk = { 43 | cloudfrontDistribution: Distribution 44 | redirectToIndexCloudfrontFunction?: CfFunction 45 | } 46 | 47 | class AstroAWSCloudfrontDistribution extends AstroAWSBaseConstruct< 48 | AstroAWSCloudfrontDistributionProps, 49 | AstroAWSCloudfrontDistributionCdk 50 | > { 51 | #redirectToIndexCloudfrontFunction: CfFunction 52 | #cloudfrontDistribution: Distribution 53 | 54 | public constructor( 55 | scope: Construct, 56 | id: string, 57 | props: AstroAWSCloudfrontDistributionProps, 58 | ) { 59 | super(scope, id, props) 60 | 61 | const functionAssociations: FunctionAssociation[] = 62 | this.props.cdk?.cloudfrontDistribution?.defaultBehavior 63 | ?.functionAssociations ?? [] 64 | 65 | this.#redirectToIndexCloudfrontFunction = new CfFunction( 66 | this, 67 | "RedirectToIndexFunction", 68 | { 69 | code: FunctionCode.fromInline(` 70 | function handler(event) { 71 | var request = event.request; 72 | var uri = request.uri; 73 | 74 | if (uri.endsWith("/")) { 75 | request.uri += "index.html"; 76 | } else if (!uri.includes(".")) { 77 | request.uri += "/index.html"; 78 | } 79 | 80 | return request; 81 | } 82 | `), 83 | }, 84 | ) 85 | 86 | functionAssociations.push({ 87 | eventType: FunctionEventType.VIEWER_REQUEST, 88 | function: this.#redirectToIndexCloudfrontFunction, 89 | }) 90 | 91 | const edgeLambdas: EdgeLambda[] = 92 | this.props.cdk?.cloudfrontDistribution?.defaultBehavior?.edgeLambdas ?? [] 93 | 94 | if (this.metadata?.args.mode === "edge" && this.props.lambdaFunction) { 95 | edgeLambdas.push( 96 | { 97 | eventType: LambdaEdgeEventType.ORIGIN_REQUEST, 98 | functionVersion: this.props.lambdaFunction.currentVersion, 99 | includeBody: true, 100 | }, 101 | { 102 | eventType: LambdaEdgeEventType.ORIGIN_RESPONSE, 103 | functionVersion: this.props.lambdaFunction.currentVersion, 104 | }, 105 | ) 106 | } 107 | 108 | this.#cloudfrontDistribution = new Distribution(this, "Distribution", { 109 | ...this.props.cdk?.cloudfrontDistribution, 110 | additionalBehaviors: this.props.lambdaFunctionOrigin 111 | ? { 112 | "/api/*": { 113 | allowedMethods: AllowedMethods.ALLOW_ALL, 114 | origin: this.props.lambdaFunctionOrigin, 115 | originRequestPolicy: 116 | OriginRequestPolicy.USER_AGENT_REFERER_HEADERS, 117 | responseHeadersPolicy: 118 | ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT_AND_SECURITY_HEADERS, 119 | viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 120 | ...this.props.cdk?.cloudfrontDistribution?.apiBehavior, 121 | }, 122 | ...this.props.cdk?.cloudfrontDistribution?.additionalBehaviors, 123 | } 124 | : this.props.cdk?.cloudfrontDistribution?.additionalBehaviors, 125 | defaultBehavior: { 126 | origin: this.props.origin, 127 | originRequestPolicy: OriginRequestPolicy.USER_AGENT_REFERER_HEADERS, 128 | responseHeadersPolicy: 129 | ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT_AND_SECURITY_HEADERS, 130 | viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 131 | ...this.props.cdk?.cloudfrontDistribution?.defaultBehavior, 132 | edgeLambdas, 133 | functionAssociations, 134 | }, 135 | errorResponses: [ 136 | { 137 | httpStatus: 404, 138 | responsePagePath: "/404", 139 | }, 140 | ], 141 | }) 142 | } 143 | 144 | public get cdk() { 145 | return { 146 | cloudfrontDistribution: this.#cloudfrontDistribution, 147 | redirectToIndexCloudfrontFunction: 148 | this.#redirectToIndexCloudfrontFunction, 149 | } 150 | } 151 | } 152 | 153 | export { 154 | type AstroAWSCloudfrontDistributionCdkProps, 155 | type AstroAWSCloudfrontDistributionProps, 156 | type AstroAWSCloudfrontDistributionCdk, 157 | AstroAWSCloudfrontDistribution, 158 | } 159 | -------------------------------------------------------------------------------- /packages/constructs/src/constructs/astro-aws-origin.ts: -------------------------------------------------------------------------------- 1 | import { type Construct } from "constructs" 2 | import { 3 | type FunctionUrl, 4 | FunctionUrlAuthType, 5 | InvokeMode, 6 | type FunctionUrlOptions, 7 | type Function, 8 | } from "aws-cdk-lib/aws-lambda" 9 | import { type IOrigin } from "aws-cdk-lib/aws-cloudfront" 10 | import { type Bucket } from "aws-cdk-lib/aws-s3" 11 | import { 12 | HttpOrigin, 13 | OriginGroup, 14 | S3Origin, 15 | type HttpOriginProps, 16 | type OriginGroupProps, 17 | type S3OriginProps, 18 | } from "aws-cdk-lib/aws-cloudfront-origins" 19 | import { Fn } from "aws-cdk-lib/core" 20 | 21 | import { 22 | AstroAWSBaseConstruct, 23 | type AstroAWSBaseConstructProps, 24 | } from "../types/astro-aws-construct.js" 25 | 26 | type AstroAWSOriginCdkProps = { 27 | lambdaFunctionOrigin?: HttpOriginProps 28 | lambdaFunctionUrl?: FunctionUrlOptions 29 | originGroup?: OriginGroupProps 30 | s3Origin?: S3OriginProps 31 | } 32 | 33 | type AstroAWSOriginProps = AstroAWSBaseConstructProps & { 34 | lambdaFunction?: Function 35 | s3Bucket: Bucket 36 | cdk?: AstroAWSOriginCdkProps 37 | } 38 | 39 | type AstroAWSOriginCdk = { 40 | lambdaFunctionOrigin?: HttpOrigin 41 | lambdaFunctionUrl?: FunctionUrl 42 | origin: IOrigin 43 | originGroup?: OriginGroup 44 | s3Origin: S3Origin 45 | } 46 | 47 | class AstroAWSOrigin extends AstroAWSBaseConstruct< 48 | AstroAWSOriginProps, 49 | AstroAWSOriginCdk 50 | > { 51 | #origin: IOrigin 52 | #lambdaFunctionUrl?: FunctionUrl 53 | #s3Origin: S3Origin 54 | #lambdaFunctionOrigin?: HttpOrigin 55 | #originGroup?: OriginGroup 56 | 57 | public constructor(scope: Construct, id: string, props: AstroAWSOriginProps) { 58 | super(scope, id, props) 59 | 60 | this.#s3Origin = new S3Origin(this.props.s3Bucket, this.props.cdk?.s3Origin) 61 | 62 | if (this.props.lambdaFunction && this.isSSR) { 63 | this.#lambdaFunctionUrl = this.props.lambdaFunction.addFunctionUrl({ 64 | authType: FunctionUrlAuthType.NONE, 65 | invokeMode: 66 | this.metadata?.args.mode === "ssr-stream" 67 | ? InvokeMode.RESPONSE_STREAM 68 | : InvokeMode.BUFFERED, 69 | ...this.props.cdk?.lambdaFunctionUrl, 70 | }) 71 | 72 | this.#lambdaFunctionOrigin = new HttpOrigin( 73 | Fn.parseDomainName(this.#lambdaFunctionUrl.url), 74 | this.props.cdk?.lambdaFunctionOrigin, 75 | ) 76 | 77 | if (this.metadata?.args.mode.includes("ssr")) { 78 | this.#originGroup = new OriginGroup({ 79 | ...this.props.cdk?.originGroup, 80 | fallbackOrigin: this.#lambdaFunctionOrigin, 81 | fallbackStatusCodes: [ 82 | 400, 83 | 403, 84 | 404, 85 | 416, 86 | 500, 87 | 502, 88 | 503, 89 | 504, 90 | ...(this.props.cdk?.originGroup?.fallbackStatusCodes ?? []), 91 | ], 92 | primaryOrigin: this.#s3Origin, 93 | }) 94 | 95 | this.#origin = this.#originGroup 96 | } else { 97 | this.#origin = this.#s3Origin 98 | } 99 | } else { 100 | this.#origin = this.#s3Origin 101 | } 102 | } 103 | 104 | public get cdk() { 105 | return { 106 | lambdaFunctionOrigin: this.#lambdaFunctionOrigin, 107 | lambdaFunctionUrl: this.#lambdaFunctionUrl, 108 | origin: this.#origin, 109 | originGroup: this.#originGroup, 110 | s3Origin: this.#s3Origin, 111 | } 112 | } 113 | } 114 | 115 | export { 116 | type AstroAWSOriginCdkProps, 117 | type AstroAWSOriginProps, 118 | type AstroAWSOriginCdk, 119 | AstroAWSOrigin, 120 | } 121 | -------------------------------------------------------------------------------- /packages/constructs/src/constructs/astro-aws-s3-bucket-deployment.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "node:path" 2 | 3 | import { 4 | BucketDeployment, 5 | Source, 6 | type BucketDeploymentProps, 7 | } from "aws-cdk-lib/aws-s3-deployment" 8 | import { type Construct } from "constructs" 9 | import { type Distribution } from "aws-cdk-lib/aws-cloudfront" 10 | 11 | import { 12 | AstroAWSBaseConstruct, 13 | type AstroAWSBaseConstructProps, 14 | } from "../types/astro-aws-construct.js" 15 | 16 | type AstroAWSS3BucketDeploymentCdkProps = { 17 | s3BucketDeployment?: Partial< 18 | Omit 19 | > 20 | } 21 | 22 | type AstroAWSS3BucketDeploymentProps = AstroAWSBaseConstructProps & { 23 | bucket: BucketDeploymentProps["destinationBucket"] 24 | cdk?: AstroAWSS3BucketDeploymentCdkProps 25 | distribution: Distribution 26 | } 27 | 28 | type AstroAWSS3BucketDeploymentCdk = { 29 | s3BucketDeployment: BucketDeployment 30 | } 31 | 32 | class AstroAWSS3BucketDeployment extends AstroAWSBaseConstruct< 33 | AstroAWSS3BucketDeploymentProps, 34 | AstroAWSS3BucketDeploymentCdk 35 | > { 36 | #s3BucketDeployment: BucketDeployment 37 | 38 | public constructor( 39 | scope: Construct, 40 | id: string, 41 | props: AstroAWSS3BucketDeploymentProps, 42 | ) { 43 | super(scope, id, props) 44 | 45 | const { bucket, cdk = {}, distribution } = this.props 46 | 47 | const source = this.metadata 48 | ? resolve(this.distDir, "client") 49 | : resolve(this.distDir) 50 | 51 | this.#s3BucketDeployment = new BucketDeployment(this, "BucketDeployment", { 52 | ...cdk.s3BucketDeployment, 53 | destinationBucket: bucket, 54 | distribution, 55 | sources: [ 56 | Source.asset(source), 57 | ...(cdk.s3BucketDeployment?.sources ?? []), 58 | ], 59 | }) 60 | } 61 | 62 | public get cdk() { 63 | return { 64 | s3BucketDeployment: this.#s3BucketDeployment, 65 | } 66 | } 67 | } 68 | 69 | export { 70 | type AstroAWSS3BucketDeploymentCdkProps, 71 | type AstroAWSS3BucketDeploymentProps, 72 | type AstroAWSS3BucketDeploymentCdk, 73 | AstroAWSS3BucketDeployment, 74 | } 75 | -------------------------------------------------------------------------------- /packages/constructs/src/constructs/astro-aws-s3-bucket.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BlockPublicAccess, 3 | Bucket, 4 | BucketEncryption, 5 | type BucketProps, 6 | } from "aws-cdk-lib/aws-s3" 7 | import { type Construct } from "constructs" 8 | import { OriginAccessIdentity } from "aws-cdk-lib/aws-cloudfront" 9 | import { CanonicalUserPrincipal, PolicyStatement } from "aws-cdk-lib/aws-iam" 10 | 11 | import { 12 | AstroAWSBaseConstruct, 13 | type AstroAWSBaseConstructProps, 14 | } from "../types/astro-aws-construct.js" 15 | 16 | type AstroAWSS3BucketCdkProps = { 17 | s3Bucket?: Partial 18 | } 19 | 20 | type AstroAWSS3BucketProps = AstroAWSBaseConstructProps & { 21 | cdk?: AstroAWSS3BucketCdkProps 22 | } 23 | 24 | type AstroAWSS3BucketCdk = { 25 | originAccessIdentity: OriginAccessIdentity 26 | s3Bucket: Bucket 27 | } 28 | 29 | class AstroAWSS3Bucket extends AstroAWSBaseConstruct< 30 | AstroAWSS3BucketProps, 31 | AstroAWSS3BucketCdk 32 | > { 33 | #s3Bucket: Bucket 34 | #originAccessIdentity: OriginAccessIdentity 35 | 36 | public constructor( 37 | scope: Construct, 38 | id: string, 39 | props: AstroAWSS3BucketProps, 40 | ) { 41 | super(scope, id, props) 42 | 43 | this.#s3Bucket = new Bucket(this, "S3Bucket", { 44 | blockPublicAccess: BlockPublicAccess.BLOCK_ALL, 45 | encryption: BucketEncryption.S3_MANAGED, 46 | enforceSSL: true, 47 | ...props.cdk?.s3Bucket, 48 | }) 49 | 50 | this.#originAccessIdentity = new OriginAccessIdentity(this, "S3BucketOAI", { 51 | comment: `OAI for ${id}`, 52 | }) 53 | 54 | this.#s3Bucket.addToResourcePolicy( 55 | new PolicyStatement({ 56 | actions: ["s3:GetObject"], 57 | principals: [ 58 | new CanonicalUserPrincipal( 59 | this.#originAccessIdentity.cloudFrontOriginAccessIdentityS3CanonicalUserId, 60 | ).grantPrincipal, 61 | ], 62 | resources: [this.#s3Bucket.arnForObjects("*")], 63 | }), 64 | ) 65 | } 66 | 67 | public get cdk() { 68 | return { 69 | originAccessIdentity: this.#originAccessIdentity, 70 | s3Bucket: this.#s3Bucket, 71 | } 72 | } 73 | } 74 | 75 | export { 76 | type AstroAWSS3BucketCdkProps, 77 | type AstroAWSS3BucketProps, 78 | type AstroAWSS3BucketCdk, 79 | AstroAWSS3Bucket, 80 | } 81 | -------------------------------------------------------------------------------- /packages/constructs/src/constructs/astro-aws.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "node:path" 2 | 3 | import { 4 | type FunctionProps, 5 | Code, 6 | Function, 7 | Runtime, 8 | Architecture, 9 | } from "aws-cdk-lib/aws-lambda" 10 | import { type Construct } from "constructs" 11 | 12 | import { AstroAWSBaseConstruct } from "../types/astro-aws-construct.js" 13 | 14 | import { 15 | AstroAWSS3BucketDeployment, 16 | type AstroAWSS3BucketDeploymentCdk, 17 | type AstroAWSS3BucketDeploymentCdkProps, 18 | } from "./astro-aws-s3-bucket-deployment.js" 19 | import { 20 | AstroAWSS3Bucket, 21 | type AstroAWSS3BucketCdk, 22 | type AstroAWSS3BucketCdkProps, 23 | } from "./astro-aws-s3-bucket.js" 24 | import { 25 | type AstroAWSOriginCdkProps, 26 | AstroAWSOrigin, 27 | type AstroAWSOriginCdk, 28 | } from "./astro-aws-origin.js" 29 | import type { 30 | AstroAWSCloudfrontDistributionCdk, 31 | AstroAWSCloudfrontDistributionCdkProps, 32 | } from "./astro-aws-cloudfront-distribution.js" 33 | import { AstroAWSCloudfrontDistribution } from "./astro-aws-cloudfront-distribution.js" 34 | 35 | type AstroAWSCdkProps = { 36 | lambdaFunction?: Omit 37 | } 38 | 39 | type AstroAWSProps = { 40 | /** Passed through to the Bucket Origin. */ 41 | websiteDir?: string 42 | outDir?: string 43 | cdk?: AstroAWSCdkProps & 44 | AstroAWSCloudfrontDistributionCdkProps & 45 | AstroAWSOriginCdkProps & 46 | AstroAWSS3BucketCdkProps & 47 | AstroAWSS3BucketDeploymentCdkProps 48 | } 49 | 50 | type AstroAWSCdk = AstroAWSCloudfrontDistributionCdk & 51 | AstroAWSOriginCdk & 52 | AstroAWSS3BucketCdk & 53 | AstroAWSS3BucketDeploymentCdk & { 54 | lambdaFunction?: Function 55 | } 56 | 57 | /** 58 | * Constructs the required AWS resources to deploy an Astro website. The resources are: 59 | * 60 | * - S3 bucket to host the website assets 61 | * - Lambda function to handle the requests (if "server" output) 62 | * - CloudFront distribution to serve the website 63 | * - Origin access identity to restrict access to the S3 bucket 64 | * 65 | * If "server" output is selected, this works by routing requests to the Lambda function. If the Lambda function returns 66 | * a 404 response, the Cloudfront distribution falls back to the S3 bucket. 67 | */ 68 | class AstroAWS extends AstroAWSBaseConstruct { 69 | #lambdaFunction?: Function 70 | #astroAWSS3BucketDeployment: AstroAWSS3BucketDeployment 71 | #astroAWSS3Bucket: AstroAWSS3Bucket 72 | #astroAWSOrigin: AstroAWSOrigin 73 | #astroAWSCloudfrontDistribution: AstroAWSCloudfrontDistribution 74 | 75 | public constructor(scope: Construct, id: string, props: AstroAWSProps) { 76 | super(scope, id, props) 77 | 78 | this.#astroAWSS3Bucket = new AstroAWSS3Bucket( 79 | this, 80 | "AstroAWSS3Bucket", 81 | this.props, 82 | ) 83 | 84 | if (this.isSSR) { 85 | this.createSSROnlyResources() 86 | } 87 | 88 | this.#astroAWSOrigin = new AstroAWSOrigin(this, "AstroAWSOrigin", { 89 | ...this.props, 90 | lambdaFunction: this.#lambdaFunction, 91 | s3Bucket: this.#astroAWSS3Bucket.cdk.s3Bucket, 92 | }) 93 | 94 | this.#astroAWSCloudfrontDistribution = new AstroAWSCloudfrontDistribution( 95 | this, 96 | "AstroAWSCloudfrontDistribution", 97 | { 98 | ...this.props, 99 | lambdaFunction: this.#lambdaFunction, 100 | lambdaFunctionOrigin: this.#astroAWSOrigin.cdk.lambdaFunctionOrigin, 101 | origin: this.#astroAWSOrigin.cdk.origin, 102 | }, 103 | ) 104 | 105 | this.#astroAWSS3BucketDeployment = new AstroAWSS3BucketDeployment( 106 | this, 107 | "AstroAWSS3BucketDeployment", 108 | { 109 | ...this.props, 110 | bucket: this.#astroAWSS3Bucket.cdk.s3Bucket, 111 | distribution: 112 | this.#astroAWSCloudfrontDistribution.cdk.cloudfrontDistribution, 113 | }, 114 | ) 115 | } 116 | 117 | public get cdk(): AstroAWSCdk { 118 | return { 119 | ...this.#astroAWSS3BucketDeployment.cdk, 120 | ...this.#astroAWSS3Bucket.cdk, 121 | ...this.#astroAWSOrigin.cdk, 122 | ...this.#astroAWSCloudfrontDistribution.cdk, 123 | lambdaFunction: this.#lambdaFunction, 124 | } 125 | } 126 | 127 | private createSSROnlyResources() { 128 | const { 129 | environment = {}, 130 | architecture, 131 | runtime = Runtime.NODEJS_18_X, 132 | memorySize = 512, 133 | description = "SSR Lambda Function", 134 | ...givenProps 135 | } = this.props.cdk?.lambdaFunction ?? {} 136 | 137 | this.#lambdaFunction = new Function(this, "Function", { 138 | ...givenProps, 139 | architecture: 140 | this.metadata?.args.mode === "edge" 141 | ? Architecture.X86_64 142 | : architecture, 143 | code: Code.fromAsset(resolve(this.distDir, "lambda")), 144 | description, 145 | handler: "entry.handler", 146 | memorySize, 147 | runtime, 148 | }) 149 | 150 | Object.entries(environment).forEach(([key, value]) => { 151 | this.#lambdaFunction?.addEnvironment(key, value, { 152 | removeInEdge: true, 153 | }) 154 | }) 155 | } 156 | } 157 | 158 | export { type AstroAWSCdkProps, type AstroAWSProps, type AstroAWSCdk, AstroAWS } 159 | -------------------------------------------------------------------------------- /packages/constructs/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./constructs/astro-aws.js" 2 | -------------------------------------------------------------------------------- /packages/constructs/src/types/astro-aws-construct.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync } from "node:fs" 2 | import { resolve } from "node:path" 3 | import { URL, fileURLToPath } from "node:url" 4 | 5 | import { parse } from "flatted" 6 | import { Construct } from "constructs" 7 | 8 | const __dirname = fileURLToPath(new URL(".", import.meta.url)) 9 | 10 | type Config = { 11 | output: "hybrid" | "server" | "static" 12 | } 13 | 14 | type Args = { 15 | /** Specifies what media types need to be base64 encoded. */ 16 | binaryMediaTypes: string[] 17 | /** Configures ESBuild options that are not configured automatically. */ 18 | esBuildOptions: Record 19 | /** Specifies where you want your app deployed to. */ 20 | mode: "edge" | "ssr-stream" | "ssr" 21 | } 22 | 23 | type Metadata = { 24 | args: Args 25 | config: Config 26 | } 27 | 28 | type AstroAWSBaseConstructProps = { 29 | outDir?: string 30 | websiteDir?: string 31 | } 32 | 33 | abstract class AstroAWSBaseConstruct< 34 | Props extends AstroAWSBaseConstructProps, 35 | Cdk, 36 | > extends Construct { 37 | public readonly distDir: string 38 | public readonly props: Props 39 | public readonly metadata?: Metadata 40 | 41 | public constructor(scope: Construct, id: string, props: Props) { 42 | super(scope, id) 43 | 44 | const { outDir, websiteDir = __dirname } = props 45 | 46 | this.props = props 47 | 48 | this.distDir = outDir ? resolve(outDir) : resolve(websiteDir, "dist") 49 | 50 | this.metadata = existsSync(resolve(this.distDir, "metadata.json")) 51 | ? (parse( 52 | readFileSync(resolve(this.distDir, "metadata.json")).toString("utf8"), 53 | ) as Metadata) 54 | : undefined 55 | } 56 | 57 | public get isStatic(): boolean { 58 | return !this.metadata 59 | } 60 | 61 | public get isSSR(): boolean { 62 | return Boolean(this.metadata) 63 | } 64 | 65 | public abstract get cdk(): Cdk 66 | } 67 | 68 | export { 69 | type Args, 70 | type Metadata, 71 | type AstroAWSBaseConstructProps, 72 | AstroAWSBaseConstruct, 73 | } 74 | -------------------------------------------------------------------------------- /packages/constructs/src/types/partial-by.ts: -------------------------------------------------------------------------------- 1 | export type PartialBy = Omit & Partial> 2 | -------------------------------------------------------------------------------- /packages/constructs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json" 3 | } 4 | -------------------------------------------------------------------------------- /scripts/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @astro-aws/scripts 2 | 3 | ## 0.2.0 4 | 5 | ### Minor Changes 6 | 7 | - 3fc8a7f: Add support for node 22 8 | - 3fc8a7f: Remove support for node 18 9 | 10 | ## 0.1.1 11 | 12 | ### Patch Changes 13 | 14 | - 48e4410: Updated dependencies 15 | 16 | ## 0.1.0 17 | 18 | ### Minor Changes 19 | 20 | - e2adca1: Upgraded dependencies 21 | 22 | ## 0.0.1 23 | 24 | ### Patch Changes 25 | 26 | - [#7](https://github.com/lukeshay/astro-aws/pull/7) [`646915f`](https://github.com/lukeshay/astro-aws/commit/646915f227c27af02084e7fe7b1c1e69c9ad9e7d) Thanks [@lukeshay](https://github.com/lukeshay)! - Upgraded dependencies 27 | 28 | ## 0.0.1-next.0 29 | 30 | ### Patch Changes 31 | 32 | - [#7](https://github.com/lukeshay/astro-aws/pull/7) [`646915f`](https://github.com/lukeshay/astro-aws/commit/646915f227c27af02084e7fe7b1c1e69c9ad9e7d) Thanks [@lukeshay](https://github.com/lukeshay)! - Upgraded dependencies 33 | -------------------------------------------------------------------------------- /scripts/bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import "./dist/index.js" 4 | -------------------------------------------------------------------------------- /scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@astro-aws/scripts", 3 | "version": "0.2.0", 4 | "private": true, 5 | "homepage": "https://www.astro-aws.org/", 6 | "repository": { 7 | "type": "git", 8 | "url": "ssh://git@github.com/lukeshay/astro-aws.git", 9 | "directory": "scripts" 10 | }, 11 | "license": "MIT", 12 | "type": "module", 13 | "bin": { 14 | "aac": "./bin.js", 15 | "scripts": "./bin.js" 16 | }, 17 | "files": [ 18 | "dist", 19 | "bin.js" 20 | ], 21 | "scripts": { 22 | "build": "tsx ./src/index.ts build" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "../.eslintrc.cjs" 27 | ], 28 | "root": false 29 | }, 30 | "dependencies": { 31 | "commander": "^12.1.0", 32 | "cosmiconfig": "^9.0.0", 33 | "esbuild": "^0.23.0", 34 | "globby": "^14.0.2" 35 | }, 36 | "devDependencies": { 37 | "@types/node": "^18.18.4", 38 | "eslint": "^9.7.0", 39 | "prettier": "^3.3.3", 40 | "tsx": "^4.16.2", 41 | "typescript": "^5.5.3" 42 | }, 43 | "peerDependencies": { 44 | "typescript": "^5.2.2" 45 | }, 46 | "engines": { 47 | "node": "20.x || 22.x" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /scripts/src/cmds/build.ts: -------------------------------------------------------------------------------- 1 | import { rm } from "node:fs/promises" 2 | 3 | import { Command } from "commander" 4 | 5 | import { runTsc } from "../utils/ts-util.js" 6 | import { runEsBuild } from "../utils/esbuild-util.js" 7 | import { readPackageJson } from "../utils/pkg-util.js" 8 | import { readConfig } from "../utils/config-util.js" 9 | import { none } from "../utils/arg-util.js" 10 | 11 | type Options = { 12 | bundle: boolean 13 | skipClean: boolean 14 | skipTsc: boolean 15 | } 16 | 17 | const buildCommand = new Command("build") 18 | .description( 19 | "Builds the TypeScript files in the src directory and outputs them to the dist directory.", 20 | ) 21 | .argument("[fileGlob]", "Glob pattern for files to build.", "src/**/*.ts") 22 | .option("-b, --bundle", "Bundles the entrypoint.", false) 23 | .option("-c --skip-clean", "Does not delete dist folder.", false) 24 | .option("-t --skip-tsc", "Skips building typedefs.", false) 25 | .action(async (fileGlob: string) => { 26 | const { bundle, skipClean, skipTsc } = buildCommand.opts() 27 | const pkgJson = await readPackageJson() 28 | const config = await readConfig() 29 | 30 | if (none(skipClean, config.build?.skipClean)) { 31 | const cleanGlobs = [...(config.clean ?? []), "dist"] 32 | 33 | await Promise.all( 34 | cleanGlobs.map(async (glob) => 35 | rm(glob, { 36 | force: true, 37 | recursive: true, 38 | }), 39 | ), 40 | ) 41 | } 42 | 43 | await runEsBuild(pkgJson, { 44 | bundle, 45 | fileGlob, 46 | }) 47 | 48 | if (none(skipTsc, config.build?.skipTsc)) { 49 | runTsc() 50 | } 51 | }) 52 | 53 | export { buildCommand } 54 | -------------------------------------------------------------------------------- /scripts/src/index.ts: -------------------------------------------------------------------------------- 1 | import { program } from "commander" 2 | 3 | import { buildCommand } from "./cmds/build.js" 4 | 5 | program.addCommand(buildCommand) 6 | program.parse() 7 | -------------------------------------------------------------------------------- /scripts/src/utils/arg-util.ts: -------------------------------------------------------------------------------- 1 | const some = (...args: unknown[]) => args.some(Boolean) 2 | const none = (...args: unknown[]) => !some(...args) 3 | 4 | export { some, none } 5 | -------------------------------------------------------------------------------- /scripts/src/utils/config-util.ts: -------------------------------------------------------------------------------- 1 | import { cwd } from "node:process" 2 | 3 | import { cosmiconfig } from "cosmiconfig" 4 | 5 | export type Config = { 6 | clean?: string[] 7 | build?: { 8 | skipTsc?: boolean 9 | skipClean?: boolean 10 | bundle?: boolean 11 | } 12 | } 13 | 14 | export const readConfig = async (): Promise => { 15 | const result = await cosmiconfig("cli").search(cwd()) 16 | 17 | return (result?.config as Config | undefined) ?? {} 18 | } 19 | -------------------------------------------------------------------------------- /scripts/src/utils/esbuild-util.ts: -------------------------------------------------------------------------------- 1 | import type { BuildOptions } from "esbuild" 2 | import { build } from "esbuild" 3 | import { globby } from "globby" 4 | 5 | import type { PackageJson } from "./pkg-util.js" 6 | import { findTsConfig } from "./ts-util.js" 7 | 8 | const DEFAULT_CONFIG: BuildOptions = { 9 | minify: false, 10 | outdir: "dist", 11 | platform: "node", 12 | sourcemap: false, 13 | sourcesContent: false, 14 | target: "node14", 15 | } 16 | 17 | type CreateConfigOptions = { 18 | bundle: boolean 19 | fileGlob: string 20 | } 21 | 22 | const createEsBuildConfig = async ( 23 | pkgJson: PackageJson, 24 | options: CreateConfigOptions, 25 | ): Promise => { 26 | const { type = "module", version } = pkgJson 27 | const { bundle, fileGlob } = options 28 | const format = type === "module" ? "esm" : "cjs" 29 | const entryPoints = await globby( 30 | [fileGlob, "!**/__tests__/**/*", "!**/*.spec.*", "!**/*.test.*"], 31 | { 32 | absolute: true, 33 | expandDirectories: true, 34 | onlyFiles: true, 35 | unique: true, 36 | }, 37 | ) 38 | 39 | return { 40 | ...DEFAULT_CONFIG, 41 | bundle, 42 | define: { 43 | "process.env.PACKAGE_VERSION": JSON.stringify(version), 44 | }, 45 | entryPoints, 46 | format, 47 | tsconfig: findTsConfig(), 48 | } 49 | } 50 | 51 | const runEsBuild = async (pkgJson: PackageJson, options: CreateConfigOptions) => 52 | build(await createEsBuildConfig(pkgJson, options)) 53 | 54 | export { type CreateConfigOptions, createEsBuildConfig, runEsBuild } 55 | -------------------------------------------------------------------------------- /scripts/src/utils/pkg-util.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from "node:fs/promises" 2 | 3 | export type PackageJson = { 4 | dependencies?: Record 5 | devDependencies?: Record 6 | name: string 7 | type?: "commonjs" | "module" 8 | version?: string 9 | } 10 | 11 | export const readPackageJson = async () => 12 | JSON.parse(await readFile("./package.json", "utf-8")) as PackageJson 13 | -------------------------------------------------------------------------------- /scripts/src/utils/shell-util.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "node:child_process" 2 | 3 | export const runCommand = (...command: string[]) => 4 | execSync(command.join(" "), { stdio: "inherit" }) 5 | -------------------------------------------------------------------------------- /scripts/src/utils/ts-util.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "node:path" 2 | import { existsSync } from "node:fs" 3 | 4 | import { runCommand } from "./shell-util.js" 5 | 6 | const findTsConfigPath = () => 7 | existsSync("./tsconfig.build.json") 8 | ? "./tsconfig.build.json" 9 | : existsSync("./tsconfig.json") 10 | ? "./tsconfig.json" 11 | : existsSync("../tsconfig.base.json") 12 | ? "../tsconfig.base.json" 13 | : "../../tsconfig.base.json" 14 | 15 | const findTsConfig = () => resolve(findTsConfigPath()) 16 | 17 | const runTsc = () => { 18 | runCommand( 19 | "tsc", 20 | "--project", 21 | findTsConfigPath(), 22 | "--declaration", 23 | "--emitDeclarationOnly", 24 | "--outDir", 25 | "dist", 26 | ) 27 | } 28 | 29 | export { findTsConfig, runTsc } 30 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json" 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "allowUnreachableCode": false, 6 | "allowUnusedLabels": false, 7 | "esModuleInterop": true, 8 | "experimentalDecorators": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 11 | "module": "NodeNext", 12 | "moduleDetection": "force", 13 | "moduleResolution": "NodeNext", 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitOverride": true, 16 | "noImplicitReturns": true, 17 | "noUncheckedIndexedAccess": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "skipLibCheck": true, 21 | "strict": true, 22 | "target": "ES2022", 23 | "types": ["node"] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "paths": { 6 | "@astro-aws/adapter": ["./packages/adapter/src/index.ts"], 7 | "@astro-aws/constructs": ["./packages/constructs/src/index.ts"] 8 | } 9 | }, 10 | "extends": "./tsconfig.base.json" 11 | } 12 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalEnv": ["AWS_ACCOUNT", "AWS_REGION", "AWS_PROFILE", "USER"], 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": ["dist/**", "cdk.out/**"] 8 | }, 9 | "deploy": { 10 | "cache": false, 11 | "dependsOn": ["synth"] 12 | }, 13 | "dev": { 14 | "cache": false, 15 | "dependsOn": ["^build"] 16 | }, 17 | "lint": { 18 | "dependsOn": ["^build"] 19 | }, 20 | "lint:fix": { 21 | "dependsOn": ["^build"] 22 | }, 23 | "synth": { 24 | "dependsOn": ["build"] 25 | }, 26 | "test": { 27 | "dependsOn": ["^build"] 28 | } 29 | } 30 | } 31 | --------------------------------------------------------------------------------