diff --git a/.agents/skills/document-component/SKILL.md b/.agents/skills/document-component/SKILL.md new file mode 100644 index 0000000000..bae55d7774 --- /dev/null +++ b/.agents/skills/document-component/SKILL.md @@ -0,0 +1,22 @@ +# Document Component + +Steps to add a new component to the SST docs. + +1. Register in docs generator + - Add the component `.ts` path to `entryPoints` in `www/generate.ts` + +2. Add matching public getters + - Ensure every property returned by `getSSTLink()` has a corresponding public getter on the class + - The generator crashes otherwise because `renderLinks()` looks for a getter with the same name + +3. Analyze docs for relevant places to link the component + - Search existing guides (e.g. `cloudflare.mdx`) and component overviews for lists of related components + - Add the new component link where appropriate + +4. Add to Astro sidebar + - Add the generated doc slug to the appropriate section in `www/astro.config.mjs` + - Follow the existing visual ordering: components are sorted from shortest to longest name (least to most characters) + +5. Generate docs + - Run `bun run docs:generate` + diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index b631feb41b..0000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "image": "mcr.microsoft.com/devcontainers/universal:2", - "features": { - "ghcr.io/audacioustux/devcontainers/bun:1": {}, - "ghcr.io/devcontainers/features/aws-cli:1": {} - }, - "postCreateCommand": "bun install && cd platform && bun run build", - "customizations": { - "vscode": { - "settings": { - "terminal.integrated.splitCwd": "workspaceRoot" - }, - "extensions": [ - "esbuild.bun-vscode" - ], - "tasks": { - "version": "2.0.0", - "tasks": [ - { - "label": "Run Bun Dev", - "type": "shell", - "command": "bun dev", - "options": { - "cwd": "${workspaceFolder}/packages/platform" - }, - "problemMatcher": [], - "group": { - "kind": "build", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "new" - } - } - ] - } - } - } -} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 7595f2c809..0000000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: build - -on: - workflow_dispatch: - push: - branches: - - dev - -concurrency: ${{ github.workflow }}-${{ github.ref }} - -permissions: - contents: write - packages: write - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Setup Bun - uses: oven-sh/setup-bun@v1 - with: - bun-version: latest - - - name: Fetch tags - run: git fetch --force --tags - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: ">=1.23.2" - - - name: Download Go modules - run: go mod download - - - name: Install dependencies - run: bun i --frozen-lockfile - - - name: TypeScript check - run: cd platform && bun tsc --noEmit - - - name: Build platform - run: ./platform/scripts/build - env: - DOCKER_PUSH: false - - - name: Build CLI - uses: goreleaser/goreleaser-action@v6 - with: - distribution: goreleaser - version: latest - args: build --snapshot --clean diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index d671948112..128a071211 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -33,6 +33,9 @@ jobs: go-version: ">=1.23.2" cache: true + - name: Setup QEMU + uses: docker/setup-qemu-action@v3 + - name: Setup Docker Buildx uses: docker/setup-buildx-action@v3 @@ -50,14 +53,18 @@ jobs: env: DOCKER_PUSH: false - - name: Go vet - run: go vet ./... + - name: Setup uv + uses: astral-sh/setup-uv@v4 + with: + version: "latest" - name: Run Go tests - run: go test ./... + run: go test -vet=all ./... - name: Build CLI run: go build -o sst ./cmd/sst - name: Build docs run: cd www && bun run build + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b55fcbe3c7..553257d0dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,6 +47,13 @@ jobs: uses: actions/setup-go@v5 with: go-version: ">=1.23.2" + cache: true + + - name: Setup QEMU + uses: docker/setup-qemu-action@v3 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Download Go modules run: go mod download @@ -62,7 +69,15 @@ jobs: env: DOCKER_PUSH: true - - name: Release + - name: Setup uv + uses: astral-sh/setup-uv@v4 + with: + version: "latest" + + - name: Run Go tests + run: go test -vet=all ./... + + - name: Release CLI uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser @@ -71,12 +86,50 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }} AUR_KEY: ${{ secrets.AUR_KEY }} - NPM_CONFIG_PROVENANCE: false - name: Release JS SDK run: | cd sdk/js bun run release + + - name: Authenticate crates.io + id: crates-auth + uses: rust-lang/crates-io-auth-action@v1 + + - name: Release Rust SDK + run: | + VERSION=$(jq -r .version dist/metadata.json) + cd sdk/rust + sed -i "s/^version = \".*\"/version = \"$VERSION\"/" Cargo.toml + cargo publish --allow-dirty env: - NPM_CONFIG_PROVENANCE: false - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ steps.crates-auth.outputs.token }} + + - name: Build Python SDK + run: | + VERSION=$(jq -r .version dist/metadata.json) + cd sdk/python + sed -i "s/^version = \".*\"/version = \"$VERSION\"/" pyproject.toml + uv build + + - name: Publish Python SDK + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: sdk/python/dist/ + + - name: Announce on Discord + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + TAG: ${{ github.ref_name }} + run: | + RELEASE_URL="https://github.com/${{ github.repository }}/releases/tag/$TAG" + NOTES=$(gh release view "$TAG" --json body --jq .body \ + | sed -E "s|\(#([0-9]+)\)|([#\1](https://github.com/${{ github.repository }}/pull/\1))|g" \ + | sed -E 's|^\* \[[a-f0-9]+\]\([^)]+\): ||' \ + | sed '/^## Changelog$/d' \ + | sed -E 's|^(.+)$|- \1|') + CONTENT=$(printf '🏷️ [**Release - %s**](%s)\n%s' "$TAG" "$RELEASE_URL" "$NOTES") + PAYLOAD=$(jq -n --arg c "$CONTENT" '{content:$c, flags:4}') + curl -fsS -X POST -H "Content-Type: application/json" \ + -d "$PAYLOAD" "$DISCORD_WEBHOOK_URL" diff --git a/.gitignore b/.gitignore index dc81209e4c..bdfb61622e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,40 @@ .sst +sst dist +/sst +s node_modules + +# pulumi Pulumi.*.yaml Pulumi.yaml # osx .DS_Store -# Local Netlify folder .netlify .env tmp + +# coding agents .termai .opencode +.idea +.kiro + +# python +__pycache__ +uv.lock + +examples/**/sst-env.d.ts +examples/**/sst.pyi +www/src/content/docs/docs/examples.mdx +www/src/data/changelog.json +examples/**/bun.lock +examples/**/bun.lockb +examples/**/pnpm-lock.yaml +examples/**/package-lock.json +examples/**/uv.lock +examples/**/deno.lock +examples/**/Cargo.lock +examples/**/.env.* diff --git a/.goreleaser.yml b/.goreleaser.yml index 33c4a69206..67a7c25fc9 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,9 +1,5 @@ version: 2 project_name: sst -before: - hooks: - - go mod tidy - - go test ./cmd/... ./pkg/... builds: - env: - CGO_ENABLED=0 @@ -14,8 +10,8 @@ builds: main: ./cmd/sst archives: - - format: tar.gz - # this name template makes the OS and Arch compatible with the results of uname. + - formats: + - tar.gz name_template: >- sst- {{- if eq .Os "darwin" }}mac- @@ -25,14 +21,14 @@ archives: {{- else if eq .Arch "#86" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }} - # use zip for windows archives format_overrides: - goos: windows - format: zip + formats: + - zip checksum: name_template: "checksums.txt" snapshot: - name_template: "0.0.0-{{ .Timestamp }}" + version_template: "0.0.0-{{ .Timestamp }}" aurs: - homepage: "https://github.com/sst/sst" description: "Deploy anything" @@ -56,15 +52,9 @@ nfpms: {{- if eq .Os "darwin" }}mac {{- else }}{{ .Os }}{{ end }}-{{ .Arch }} -# scoop: -# bucket: -# owner: sst -# name: scoop-bucket -# homepage: "https://github.com/sst/ion" -# description: "sst cli" -# license: Apache 2.0 - changelog: + use: git + format: "[{{ .SHA }}](https://github.com/anomalyco/sst/commit/{{ .SHA }}): {{ .Message }}" sort: asc filters: exclude: diff --git a/CLAUDE.md b/CLAUDE.md index fccf9be5a5..f6de27996c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,31 +1,30 @@ -SST is a framework for building full-stack apps on your own infrastructure. It uses Pulumi under the hood to deploy to AWS, Cloudflare, and other providers with a high-level component model, resource linking, and a live dev mode that runs Lambda functions locally. +## Layout +- `platform/` β€” TS Pulumi components embedded via `//go:embed` (`platform/platform.go:16`) +- `examples/` β€” sample apps +- `cmd/sst/` β€” Go CLI entry, orchestrates everything +- `cmd/sst/mosaic/` β€” live dev TUI +- `pkg/server/` β€” JSON-RPC bridge for custom Pulumi dynamic providers (`rpc/rpc.ts` ↔ `pkg/server`) +- `pkg/bus/` β€” pub/sub event bus +- `sdk/js/` β€” runtime SDK for reading linked resources +- `www/` β€” docs site (auto-generated from JSDoc comments in platform and extracted from the Go CLI) ## Commands -- **Setup**: `bun install && go mod tidy && cd platform && bun run build` +- **Setup**: `bun run setup` +- **Build platform**: `bun run build:platform` +- **Build CLI**: `bun run build:cli` - **Run CLI locally**: `cd examples/ && go run ../../cmd/sst ` -- **Build CLI binary**: `go build ./cmd/sst` -- **Go tests**: `go test ./pkg/...` (all), `go test ./pkg/project/...` (single package) -- **Type check**: `cd platform && bun run dev` (tsc --watch --noEmit) -- **Build platform**: `cd platform && bun run build` (runs `scripts/build` β€” bundles workers, runtime, bridge binary, types) -- **Generate docs**: `cd www && bun run generate` -- **Run docs locally**: `cd www && bun run dev` - -## Codebase - -- `cmd/sst/` β€” Go CLI entry, orchestrates everything. Commands as tree in `main.go` -- `cmd/sst/mosaic/` β€” live dev TUI, Lambda stubs forward invocations to local runtimes -- `pkg/server/` β€” JSON-RPC bridge, Go side (`rpc/rpc.ts` ↔ `pkg/server`) -- `pkg/runtime/` β€” runtimes each implement `Match()`, `Build()`, `Run()`, `ShouldRebuild()` -- `pkg/project/provider/` β€” pluggable state backend + encrypted secrets -- `pkg/bus/` β€” pub/sub connecting watcher, deployer, runtimes, UI -- `platform/` β€” TS Pulumi components by provider (`aws/`, `cloudflare/`, `vercel/`), embedded into Go binary via `//go:embed` -- `sdk/js/` β€” runtime SDK for reading linked resources -- `internal/` β€” shared Go utilities -- `examples/` β€” sample apps (useful for testing CLI locally) -- `www/` β€” docs site +- **Go tests**: `bun run test:cli` +- **Typecheck**: `bun run typecheck` +- **Generate docs**: `bun run docs:generate` +- **Run docs locally**: `bun run docs:dev` -## Notes +## Verification -- This repo was renamed from `sst/sst` to `anomalyco/sst` +1. Build the platform +2. `cd examples/ && bun install` +3. `go run ../../cmd/sst deploy` +4. Verify with `curl` or AWS CLI +5. Don't clean up unless told to +6. `sst dev --mode=basic` for dev mode diff --git a/README.md b/README.md index 2e959e640a..cbc6c8b796 100644 --- a/README.md +++ b/README.md @@ -7,24 +7,25 @@

Discord npm - Build status + Build status

--- Build full-stack apps on your own infrastructure. -SST v3 uses a new engine for deploying SST apps. It uses Pulumi and Terraform, as opposed to CDK and CloudFormation. [Read the full announcement here](https://sst.dev/blog/sst-v3). - ## Installation -If you are using SST as a part of your Node project, we recommend installing it locally. +For JavaScript projects, install SST locally so the CLI version is tracked with your app. You can then run the CLI with the same package manager. ```bash npm install sst +# pnpm add sst +# bun add sst +# yarn add sst ``` -If you are not using Node, you can install the CLI globally. +If you are not using JavaScript, you can install the CLI globally. ```bash curl -fsSL https://sst.dev/install | bash @@ -70,10 +71,7 @@ Here's how you can contribute: ## Running Locally -1. Clone the repo -2. `bun install` -3. `go mod tidy` -4. `cd platform && bun run build` +Run `bun run setup`. You need [Go](https://go.dev/) and [Bun](https://bun.sh/) installed. Now you can run the CLI locally on any of the `examples/` apps. @@ -82,10 +80,9 @@ cd examples/aws-api go run ../../cmd/sst ``` -If you want to build the CLI, you can run `go build ./cmd/sst` from the root. This will create a -`sst` binary that you can use. +If you want to build the CLI binary, run `bun run build:cli`. This will create a `sst` binary that you can use. -For building the docs, you need to run `bun generate` and `bun dev` inside the `www` directory. +For building the docs, run `bun run docs:generate` and `bun run docs:dev`. --- diff --git a/bun.lockb b/bun.lockb index 35f35af945..5470431b11 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/cmd/colortest/main.go b/cmd/colortest/main.go deleted file mode 100644 index 585964ddbe..0000000000 --- a/cmd/colortest/main.go +++ /dev/null @@ -1,45 +0,0 @@ -package main - -import ( - "fmt" - "os" - "os/signal" -) - -func main() { - // Print system colors (0-15) - fmt.Println("System colors (0-15):") - for i := 0; i < 16; i++ { - if i > 0 && i%8 == 0 { - fmt.Println() - } - fmt.Printf("\033[38;5;%dm%4d\033[0m ", i, i) - } - - // Print color cube (16-231) - fmt.Println("Color cube (16-231):") - for i := 16; i < 232; i++ { - if (i-16)%6 == 0 && i != 16 { - fmt.Println() - } - if (i-16)%36 == 0 && i != 16 { - fmt.Println() - } - fmt.Printf("\033[38;5;%dm%4d\033[0m ", i, i) - } - // fmt.Println("\n") - // - // // Print grayscale (232-255) - // fmt.Println("Grayscale (232-255):") - // for i := 232; i < 256; i++ { - // if (i-232)%8 == 0 && i != 232 { - // fmt.Println() - // } - // fmt.Printf("\033[38;5;%dm%4d\033[0m ", i, i) - // } - // fmt.Println("\n") - - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt) - <-c -} diff --git a/cmd/darktile/main.go b/cmd/darktile/main.go deleted file mode 100644 index 92b9f3fe11..0000000000 --- a/cmd/darktile/main.go +++ /dev/null @@ -1,164 +0,0 @@ -package main - -import ( - "fmt" - "image/color" - "log/slog" - "os" - "strings" - - tea "github.com/charmbracelet/bubbletea/v2" - "github.com/charmbracelet/lipgloss" - "github.com/sst/sst/v3/cmd/darktile/termutil" -) - -type model struct { - term *termutil.Terminal - scroll int -} - -type InitMsg struct{} -type UpdateMsg struct{} - -func (m model) Init() (tea.Model, tea.Cmd) { - updates := make(chan struct{}) - os.Setenv("SST_SERVER", "http://localhost:13557") - m.term = termutil.New( - termutil.WithShell(`nvim`), - ) - go m.term.Run(updates, 100, 100) - return m, func() tea.Msg { return UpdateMsg{} } -} -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case UpdateMsg: - return m, func() tea.Msg { - return UpdateMsg{} - } - case tea.WindowSizeMsg: - if m.term == nil { - return m, nil - } - m.term.SetSize(uint16(msg.Height), uint16(msg.Width)) - return m, nil - case tea.KeyMsg: - switch msg.String() { - case "ctrl+c": - return m, tea.Quit - case "ctrl+u": - if m.scroll == 0 { - m.scroll = m.term.GetActiveBuffer().Height() - 1 - return m, nil - } - m.scroll-- - return m, nil - case "ctrl+d": - if m.scroll == 0 { - m.scroll = m.term.GetActiveBuffer().Height() + 1 - return m, nil - } - m.scroll++ - return m, nil - - default: - m.term.WriteToPty(KeyMsgToANSI(msg)) - } - } - return m, nil -} -func (m model) View() string { - if m.term == nil { - return "" - } - buf := m.term.GetActiveBuffer() - if m.scroll > 0 { - buf.SetScrollOffset(uint(buf.Height()) - uint(m.scroll)) - } - out := []string{} - for y := uint16(0); y < uint16(buf.Height()); y++ { - row := "" - for x := uint16(0); x < buf.Width(); x++ { - cell := buf.GetCell(x, y) - if cell != nil { - style := lipgloss.NewStyle().Foreground(cell.Fg()).Background(cell.Bg()) - row += style.Render(string(cell.Rune().Rune)) - - } else { - row += " " - } - } - out = append(out, row) - } - out = append(out, fmt.Sprintf("width: %d, scroll: %d height: %d", buf.Width(), m.scroll, buf.Height())) - return strings.Join(out, "\n") -} - -func convert(input color.Color) lipgloss.TerminalColor { - if input == nil { - return lipgloss.Color("#FF0000") - } - r, g, b, _ := input.RGBA() - hex := fmt.Sprintf("#%02x%02x%02x", r>>8, g>>8, b>>8) - return lipgloss.Color(hex) -} - -func main() { - logFile, err := os.Create("darktile.log") - if err != nil { - panic(err) - } - slog.SetDefault(slog.New(slog.NewTextHandler(logFile, nil))) - if _, err := tea.NewProgram(model{}, tea.WithAltScreen(), tea.WithFerociousRenderer()).Run(); err != nil { - fmt.Println("Error running program:", err) - } -} - -func KeyMsgToANSI(msg tea.KeyMsg) []byte { - str := msg.String() - - // Handle ctrl keys - if strings.HasPrefix(str, "ctrl+") { - ch := strings.ToUpper(strings.TrimPrefix(str, "ctrl+"))[0] - return []byte{byte(ch) - 64} // Convert to control character (1-26) - } - - // Handle special keys - switch str { - case "up": - return []byte("\x1b[A") - case "down": - return []byte("\x1b[B") - case "right": - return []byte("\x1b[C") - case "left": - return []byte("\x1b[D") - case "enter": - return []byte("\r") - case "tab": - return []byte("\t") - case "space": - return []byte(" ") - case "esc": - return []byte("\x1b") - case "backspace": - return []byte("\x7f") - case "delete": - return []byte("\x1b[3~") - } - - // Handle alt+key combinations - if strings.HasPrefix(str, "alt+") { - key := strings.TrimPrefix(str, "alt+") - if len(key) == 1 { - return []byte{0x1b, key[0]} - } - } - - // Handle single characters - if len(str) == 1 { - return []byte(str) - } - - // Fallback for unknown keys - return []byte{} -} diff --git a/cmd/darktile/sixel/colourmap.go b/cmd/darktile/sixel/colourmap.go deleted file mode 100644 index e00663b5df..0000000000 --- a/cmd/darktile/sixel/colourmap.go +++ /dev/null @@ -1,28 +0,0 @@ -package sixel - -import "image/color" - -type ColourMap struct { - data [0x100]color.Color -} - -func NewColourMap() *ColourMap { - return &ColourMap{} -} - -func (m *ColourMap) GetColour(id uint8) color.Color { - return m.data[id] -} - -func (m *ColourMap) SetColour(id uint8, c color.Color) { - m.data[id] = c -} - -func (m *ColourMap) FindColour(colour color.Color) (uint8, bool) { - for id, c := range m.data { - if c == colour { - return uint8(id), true - } - } - return 0, false -} diff --git a/cmd/darktile/sixel/decoder.go b/cmd/darktile/sixel/decoder.go deleted file mode 100644 index 0932734cd5..0000000000 --- a/cmd/darktile/sixel/decoder.go +++ /dev/null @@ -1,432 +0,0 @@ -package sixel - -import ( - "fmt" - "image" - "image/color" - "io" - "strconv" - "strings" -) - -// See https://vt100.net/docs/vt3xx-gp/chapter14.html for more info. - -type decoder struct { - r io.Reader - cursor image.Point - aspectRatio float64 // this is the ratio for vertical:horizontal pixels - bg color.Color - colourMap *ColourMap - currentColour color.Color - size image.Point // does not limit image size, just where bg is drawn! - scratchpad map[int]map[int]color.Color -} - -func Decode(reader io.Reader, bg color.Color) (image.Image, error) { - return NewDecoder(reader, bg).Decode() -} - -func NewDecoder(reader io.Reader, bg color.Color) *decoder { - return &decoder{ - r: reader, - aspectRatio: 2, - bg: bg, - colourMap: NewColourMap(), - scratchpad: make(map[int]map[int]color.Color), - } -} - -func (d *decoder) Decode() (image.Image, error) { - - if err := d.processHeader(); err != nil { - return nil, fmt.Errorf("error reading sixel header: %s", err) - } - - if err := d.processBody(); err != nil { - return nil, fmt.Errorf("error reading sixel header: %s", err) - } - - return d.draw(), nil -} - -func (d *decoder) readByte() (byte, error) { - buf := make([]byte, 1) - if _, err := d.r.Read(buf); err != nil { - return 0, err - } - return buf[0], nil -} - -func (d *decoder) readHeader() ([]byte, error) { - var header []byte - for { - chr, err := d.readByte() - if err != nil { - return nil, err - } - if chr == 'q' { - break - } - header = append(header, chr) - } - - return header, nil -} - -func (d *decoder) processHeader() error { - - data, err := d.readHeader() - if err != nil { - return err - } - - header := string(data) - - if len(header) == 0 { - return nil - } - - params := strings.Split(header, ";") - - switch params[1] { - case "0", "1", "5", "6", "": - d.aspectRatio = 2 - case "2": - d.aspectRatio = 5 - case "3", "4": - d.aspectRatio = 3 - case "7", "8", "9": - d.aspectRatio = 1 - default: - return fmt.Errorf("invalid P1 in sixel header") - } - - if len(params) == 1 { - return nil - } - - switch params[1] { - case "0", "2", "": - // use the configured terminal background colour - case "1": - d.bg = color.RGBA{A: 0} // transparent bg - } - - // NOTE: we currently ignore P3 if it is specified - - if len(params) > 3 { - return fmt.Errorf("unexpected extra parameters in sixel header") - } - - return nil -} - -func (d *decoder) processBody() error { - - for { - - byt, err := d.readByte() - if err != nil { - if err == io.EOF { - return nil - } - return err - } - - if err := d.processChar(byt); err != nil { - return err - } - } -} - -func (d *decoder) handleRepeat() error { - - var countStr string - - for { - byt, err := d.readByte() - if err != nil { - return err - } - - switch true { - case byt >= '0' && byt <= '9': - countStr += string(byt) - default: - count, err := strconv.Atoi(countStr) - if err != nil { - return fmt.Errorf("invalid count in sixel repeat sequence: %s: %s", countStr, err) - } - for i := 0; i < count; i++ { - if err := d.processDataChar(byt); err != nil { - return err - } - } - return nil - } - } -} - -func (d *decoder) handleRasterAttributes() error { - - var arg string - var args []string - - for { - b, err := d.readByte() - if err != nil { - return err - } - - switch true { - case b >= '0' && b <= '9': - arg += string(b) - case b == ';': - args = append(args, arg) - arg = "" - default: - args = append(args, arg) - if err := d.setRaster(args); err != nil { - return err - } - return d.processChar(b) - } - } - -} - -func (d *decoder) setRaster(args []string) error { - - if len(args) != 4 { - return fmt.Errorf("invalid raster command: %s", strings.Join(args, ";")) - } - - pan, err := strconv.Atoi(args[0]) - if err != nil { - return err - } - - pad, err := strconv.Atoi(args[1]) - if err != nil { - return err - } - - d.aspectRatio = float64(pan) / float64(pad) - - ph, err := strconv.Atoi(args[2]) - if err != nil { - return err - } - - pv, err := strconv.Atoi(args[3]) - if err != nil { - return err - } - - d.size = image.Point{X: ph, Y: pv} - - return nil -} - -func (d *decoder) handleColour() error { - - var arg string - var args []string - - for { - b, err := d.readByte() - if err != nil { - return err - } - - switch true { - case b >= '0' && b <= '9': - arg += string(b) - case b == ';': - args = append(args, arg) - arg = "" - default: - args = append(args, arg) - if err := d.setColour(args); err != nil { - return err - } - return d.processChar(b) - } - } -} - -func (d *decoder) setColour(args []string) error { - - if len(args) == 0 { - return fmt.Errorf("invalid colour string - missing identifier") - } - - colourID, err := strconv.Atoi(args[0]) - if err != nil { - return fmt.Errorf("invalid colour id: %s", args[0]) - } - - if len(args) == 1 { - d.currentColour = d.colourMap.GetColour(uint8(colourID)) - return nil - } - - if len(args) != 5 { - return fmt.Errorf("invalid colour introduction command - wrong number of args (%d): %s", len(args), strings.Join(args, ";")) - } - - x, err := strconv.Atoi(args[2]) - if err != nil { - return fmt.Errorf("invalid colour value") - } - - y, err := strconv.Atoi(args[3]) - if err != nil { - return fmt.Errorf("invalid colour value") - } - - z, err := strconv.Atoi(args[4]) - if err != nil { - return fmt.Errorf("invalid colour value") - } - - var colour color.Color - - switch args[1] { - case "1": - colour = colourFromHSL(x, z, y) - case "2": - colour = color.RGBA{ - R: uint8((x * 255) / 100), - G: uint8((y * 255) / 100), - B: uint8((z * 255) / 100), - A: 0xff, - } - default: - return fmt.Errorf("invalid colour co-ordinate system '%s'", args[1]) - } - - d.colourMap.SetColour(uint8(colourID), colour) - d.currentColour = colour - return nil -} - -func (d *decoder) processChar(b byte) error { - - switch b { - case '!': - return d.handleRepeat() - case '"': - return d.handleRasterAttributes() - case '#': - return d.handleColour() - case '$': - // graphics carriage return - d.cursor.X = 0 - return nil - case '-': - // graphics new line - d.cursor.Y += 6 - d.cursor.X = 0 - return nil - default: - return d.processDataChar(b) - } -} - -func (d *decoder) processDataChar(b byte) error { - if b < 0x3f || b > 0x7e { - return fmt.Errorf("invalid sixel data value 0x%02x: outside acceptable range", b) - } - - sixel := b - 0x3f - - for i := 0; i < 6; i++ { - if sixel&(1< 0 { - d.set(d.cursor.X, d.cursor.Y+i) - } - } - - d.cursor.X++ - return nil -} - -func hueToRGB(v1, v2, h float64) float64 { - if h < 0 { - h += 1 - } - if h > 1 { - h -= 1 - } - switch { - case 6*h < 1: - return (v1 + (v2-v1)*6*h) - case 2*h < 1: - return v2 - case 3*h < 2: - return v1 + (v2-v1)*((2.0/3.0)-h)*6 - } - return v1 -} - -func colourFromHSL(hi, si, li int) color.Color { - - h := float64(hi) / 360 - s := float64(si) / 100 - l := float64(li) / 100 - - if s == 0 { - // it's gray - return color.RGBA{uint8(l * 0xff), uint8(l * 0xff), uint8(l * 0xff), 0xff} - } - - var v1, v2 float64 - if l < 0.5 { - v2 = l * (1 + s) - } else { - v2 = (l + s) - (s * l) - } - - v1 = 2*l - v2 - - r := hueToRGB(v1, v2, h+(1.0/3.0)) - g := hueToRGB(v1, v2, h) - b := hueToRGB(v1, v2, h-(1.0/3.0)) - - return color.RGBA{R: uint8(r * 0xff), G: uint8(g * 0xff), B: uint8(b * 0xff), A: 0xff} -} - -func (d *decoder) set(x, y int) { - - if x > d.size.X { - d.size.X = x - } - - if y > d.size.Y { - d.size.Y = y - } - - if _, ok := d.scratchpad[x]; !ok { - d.scratchpad[x] = make(map[int]color.Color) - } - - d.scratchpad[x][y] = d.currentColour -} - -func (d *decoder) draw() image.Image { - img := image.NewRGBA(image.Rect(0, 0, d.size.X, d.size.Y)) - - for x := 0; x < d.size.X; x++ { - for y := 0; y < d.size.Y; y++ { - c := d.bg - if col, ok := d.scratchpad[x]; ok { - if row, ok := col[y]; ok { - c = row - } - } - img.Set(x, y, c) - } - } - - return img -} diff --git a/cmd/darktile/termutil/ansi.go b/cmd/darktile/termutil/ansi.go deleted file mode 100644 index 421e4ddb58..0000000000 --- a/cmd/darktile/termutil/ansi.go +++ /dev/null @@ -1,109 +0,0 @@ -package termutil - -func (t *Terminal) handleANSI(readChan chan MeasuredRune) (renderRequired bool) { - // if the byte is an escape character, read the next byte to determine which one - r := <-readChan - - t.log("ANSI SEQ %c 0x%X", r.Rune, r.Rune) - - t.mu.Lock() - defer t.mu.Unlock() - - switch r.Rune { - case '[': - return t.handleCSI(readChan) - case ']': - return t.handleOSC(readChan) - case '(': - return t.handleSCS0(readChan) // select character set into G0 - case ')': - return t.handleSCS1(readChan) // select character set into G1 - case '*': - return swallowHandler(1)(readChan) // character set bullshit - case '+': - return swallowHandler(1)(readChan) // character set bullshit - case '>': - return swallowHandler(0)(readChan) // numeric char selection - case '=': - return swallowHandler(0)(readChan) // alt char selection - case '7': - t.GetActiveBuffer().saveCursor() - case '8': - t.GetActiveBuffer().restoreCursor() - case 'D': - t.GetActiveBuffer().index() - case 'E': - t.GetActiveBuffer().newLineEx(true) - case 'H': - t.GetActiveBuffer().tabSetAtCursor() - case 'M': - t.GetActiveBuffer().reverseIndex() - case 'P': // sixel - t.handleSixel(readChan) - case 'c': - t.GetActiveBuffer().clear() - case '#': - return t.handleScreenState(readChan) - case '^': - return t.handlePrivacyMessage(readChan) - default: - t.log("UNKNOWN ESCAPE SEQUENCE: 0x%X", r.Rune) - return false - } - - return true -} - -func swallowHandler(size int) func(pty chan MeasuredRune) bool { - return func(pty chan MeasuredRune) bool { - for i := 0; i < size; i++ { - <-pty - } - return false - } -} - -func (t *Terminal) handleScreenState(readChan chan MeasuredRune) bool { - b := <-readChan - switch b.Rune { - case '8': // DECALN -- Screen Alignment Pattern - - // hide cursor? - buffer := t.GetActiveBuffer() - buffer.resetVerticalMargins(uint(buffer.viewHeight)) - buffer.SetScrollOffset(0) - - // Fill the whole screen with E's - count := buffer.ViewHeight() * buffer.ViewWidth() - for count > 0 { - buffer.write(MeasuredRune{Rune: 'E', Width: 1}) - count-- - if count > 0 && !buffer.modes.AutoWrap && count%buffer.ViewWidth() == 0 { - buffer.index() - buffer.carriageReturn() - } - } - // restore cursor - buffer.setPosition(0, 0) - default: - return false - } - return true -} - -func (t *Terminal) handlePrivacyMessage(readChan chan MeasuredRune) bool { - isEscaped := false - for { - b := <-readChan - if b.Rune == 0x18 /*CAN*/ || b.Rune == 0x1a /*SUB*/ || (b.Rune == 0x5c /*backslash*/ && isEscaped) { - break - } - if isEscaped { - isEscaped = false - } else if b.Rune == 0x1b { - isEscaped = true - continue - } - } - return false -} diff --git a/cmd/darktile/termutil/buffer.go b/cmd/darktile/termutil/buffer.go deleted file mode 100644 index 1a8f2ba6d3..0000000000 --- a/cmd/darktile/termutil/buffer.go +++ /dev/null @@ -1,892 +0,0 @@ -package termutil - -import ( - "image" - "sync" - - "github.com/charmbracelet/lipgloss" -) - -const TabSize = 8 - -type CursorShape uint8 - -const ( - CursorShapeBlinkingBlock CursorShape = iota - CursorShapeDefault - CursorShapeSteadyBlock - CursorShapeBlinkingUnderline - CursorShapeSteadyUnderline - CursorShapeBlinkingBar - CursorShapeSteadyBar -) - -type Buffer struct { - lines []Line - savedCursorPos Position - savedCursorAttr *CellAttributes - cursorShape CursorShape - savedCharsets []*map[rune]rune - savedCurrentCharset int - topMargin uint // see DECSTBM docs - this is for scrollable regions - bottomMargin uint // see DECSTBM docs - this is for scrollable regions - viewWidth uint16 - viewHeight uint16 - cursorPosition Position // raw - cursorAttr CellAttributes - scrollLinesFromBottom uint - maxLines uint64 - tabStops []uint16 - charsets []*map[rune]rune // array of 2 charsets, nil means ASCII (no conversion) - currentCharset int // active charset index in charsets array, valid values are 0 or 1 - modes Modes - selectionStart *Position - selectionEnd *Position - highlightStart *Position - highlightEnd *Position - highlightAnnotation *Annotation - sixels []Sixel - selectionMu sync.Mutex -} - -type Annotation struct { - Image image.Image - Text string - Width float64 // Width in cells - Height float64 // Height in cells -} - -type Selection struct { - Start Position - End Position -} - -type Position struct { - Line uint64 - Col uint16 -} - -// NewBuffer creates a new terminal buffer -func NewBuffer(width, height uint16, maxLines uint64, fg lipgloss.TerminalColor, bg lipgloss.TerminalColor) *Buffer { - b := &Buffer{ - lines: []Line{}, - viewHeight: height, - viewWidth: width, - maxLines: maxLines, - topMargin: 0, - bottomMargin: uint(height - 1), - cursorAttr: CellAttributes{ - fgColour: fg, - bgColour: bg, - }, - charsets: []*map[rune]rune{nil, nil}, - modes: Modes{ - LineFeedMode: true, - AutoWrap: true, - ShowCursor: true, - SixelScrolling: true, - }, - cursorShape: CursorShapeDefault, - } - return b -} - -func (buffer *Buffer) SetCursorShape(shape CursorShape) { - buffer.cursorShape = shape -} - -func (buffer *Buffer) GetCursorShape() CursorShape { - return buffer.cursorShape -} - -func (buffer *Buffer) IsCursorVisible() bool { - return buffer.modes.ShowCursor -} - -func (buffer *Buffer) IsApplicationCursorKeysModeEnabled() bool { - return buffer.modes.ApplicationCursorKeys -} - -func (buffer *Buffer) HasScrollableRegion() bool { - return buffer.topMargin > 0 || buffer.bottomMargin < uint(buffer.ViewHeight())-1 -} - -func (buffer *Buffer) InScrollableRegion() bool { - cursorVY := buffer.convertRawLineToViewLine(buffer.cursorPosition.Line) - return buffer.HasScrollableRegion() && uint(cursorVY) >= buffer.topMargin && uint(cursorVY) <= buffer.bottomMargin -} - -// NOTE: bottom is exclusive -func (buffer *Buffer) getAreaScrollRange() (top uint64, bottom uint64) { - top = buffer.convertViewLineToRawLine(uint16(buffer.topMargin)) - bottom = buffer.convertViewLineToRawLine(uint16(buffer.bottomMargin)) + 1 - if bottom > uint64(len(buffer.lines)) { - bottom = uint64(len(buffer.lines)) - } - return top, bottom -} - -func (buffer *Buffer) areaScrollDown(lines uint16) { - - // NOTE: bottom is exclusive - top, bottom := buffer.getAreaScrollRange() - - for i := bottom; i > top; { - i-- - if i >= top+uint64(lines) { - buffer.lines[i] = buffer.lines[i-uint64(lines)] - } else { - buffer.lines[i] = newLine() - } - } -} - -func (buffer *Buffer) areaScrollUp(lines uint16) { - - // NOTE: bottom is exclusive - top, bottom := buffer.getAreaScrollRange() - - for i := top; i < bottom; i++ { - from := i + uint64(lines) - if from < bottom { - buffer.lines[i] = buffer.lines[from] - } else { - buffer.lines[i] = newLine() - } - } -} - -func (buffer *Buffer) saveCursor() { - copiedAttr := buffer.cursorAttr - buffer.savedCursorAttr = &copiedAttr - buffer.savedCursorPos = buffer.cursorPosition - buffer.savedCharsets = make([]*map[rune]rune, len(buffer.charsets)) - copy(buffer.savedCharsets, buffer.charsets) - buffer.savedCurrentCharset = buffer.currentCharset -} - -func (buffer *Buffer) restoreCursor() { - // TODO: Do we need to restore attributes on cursor restore? conflicting sources but vim + htop work better without doing so - //if buffer.savedCursorAttr != nil { - // copiedAttr := *buffer.savedCursorAttr - // copiedAttr.bgColour = buffer.defaultCell(false).attr.bgColour - // copiedAttr.fgColour = buffer.defaultCell(false).attr.fgColour - // buffer.cursorAttr = copiedAttr - //} - buffer.cursorPosition = buffer.savedCursorPos - if buffer.savedCharsets != nil { - buffer.charsets = make([]*map[rune]rune, len(buffer.savedCharsets)) - copy(buffer.charsets, buffer.savedCharsets) - buffer.currentCharset = buffer.savedCurrentCharset - } -} - -func (buffer *Buffer) getCursorAttr() *CellAttributes { - return &buffer.cursorAttr -} - -func (buffer *Buffer) GetCell(viewCol uint16, viewRow uint16) *Cell { - rawLine := buffer.convertViewLineToRawLine(viewRow) - return buffer.getRawCell(viewCol, rawLine) -} - -func (buffer *Buffer) getRawCell(viewCol uint16, rawLine uint64) *Cell { - if rawLine >= uint64(len(buffer.lines)) { - return nil - } - line := &buffer.lines[rawLine] - if int(viewCol) >= len(line.cells) { - return nil - } - return &line.cells[viewCol] -} - -// Column returns cursor column -func (buffer *Buffer) CursorColumn() uint16 { - // @todo originMode and left margin - return buffer.cursorPosition.Col -} - -// CursorLineAbsolute returns absolute cursor line coordinate (ignoring Origin Mode) - view format -func (buffer *Buffer) CursorLineAbsolute() uint16 { - cursorVY := buffer.convertRawLineToViewLine(buffer.cursorPosition.Line) - return cursorVY -} - -// CursorLine returns cursor line (in Origin Mode it is relative to the top margin) -func (buffer *Buffer) CursorLine() uint16 { - if buffer.modes.OriginMode { - return buffer.CursorLineAbsolute() - uint16(buffer.topMargin) - } - return buffer.CursorLineAbsolute() -} - -func (buffer *Buffer) TopMargin() uint { - return buffer.topMargin -} - -func (buffer *Buffer) BottomMargin() uint { - return buffer.bottomMargin -} - -// cursor Y (raw) -func (buffer *Buffer) RawLine() uint64 { - return buffer.cursorPosition.Line -} - -func (buffer *Buffer) convertViewLineToRawLine(viewLine uint16) uint64 { - rawHeight := buffer.Height() - if int(buffer.viewHeight) > rawHeight { - return uint64(viewLine) - } - return uint64(int(viewLine) + (rawHeight - int(buffer.viewHeight+uint16(buffer.scrollLinesFromBottom)))) -} - -func (buffer *Buffer) convertRawLineToViewLine(rawLine uint64) uint16 { - rawHeight := buffer.Height() - if int(buffer.viewHeight) > rawHeight { - return uint16(rawLine) - } - return uint16(int(rawLine) - (rawHeight - int(buffer.viewHeight+uint16(buffer.scrollLinesFromBottom)))) -} - -func (buffer *Buffer) GetVPosition() int { - result := int(uint(buffer.Height()) - uint(buffer.ViewHeight()) - buffer.scrollLinesFromBottom) - if result < 0 { - result = 0 - } - - return result -} - -// Width returns the width of the buffer in columns -func (buffer *Buffer) Width() uint16 { - return buffer.viewWidth -} - -func (buffer *Buffer) ViewWidth() uint16 { - return buffer.viewWidth -} - -func (buffer *Buffer) Height() int { - return len(buffer.lines) -} - -func (buffer *Buffer) ViewHeight() uint16 { - return buffer.viewHeight -} - -func (buffer *Buffer) deleteLine() { - index := int(buffer.RawLine()) - buffer.lines = buffer.lines[:index+copy(buffer.lines[index:], buffer.lines[index+1:])] -} - -func (buffer *Buffer) insertLine() { - - if !buffer.InScrollableRegion() { - pos := buffer.RawLine() - maxLines := buffer.GetMaxLines() - newLineCount := uint64(len(buffer.lines) + 1) - if newLineCount > maxLines { - newLineCount = maxLines - } - - out := make([]Line, newLineCount) - copy( - out[:pos-(uint64(len(buffer.lines))+1-newLineCount)], - buffer.lines[uint64(len(buffer.lines))+1-newLineCount:pos]) - out[pos] = newLine() - copy(out[pos+1:], buffer.lines[pos:]) - buffer.lines = out - } else { - topIndex := buffer.convertViewLineToRawLine(uint16(buffer.topMargin)) - bottomIndex := buffer.convertViewLineToRawLine(uint16(buffer.bottomMargin)) - before := buffer.lines[:topIndex] - after := buffer.lines[bottomIndex+1:] - out := make([]Line, len(buffer.lines)) - copy(out[0:], before) - - pos := buffer.RawLine() - for i := topIndex; i < bottomIndex; i++ { - if i < pos { - out[i] = buffer.lines[i] - } else { - out[i+1] = buffer.lines[i] - } - } - - copy(out[bottomIndex+1:], after) - - out[pos] = newLine() - buffer.lines = out - } -} - -func (buffer *Buffer) insertBlankCharacters(count int) { - - index := int(buffer.RawLine()) - for i := 0; i < count; i++ { - cells := buffer.lines[index].cells - buffer.lines[index].cells = append(cells[:buffer.cursorPosition.Col], append([]Cell{buffer.defaultCell(true)}, cells[buffer.cursorPosition.Col:]...)...) - } -} - -func (buffer *Buffer) insertLines(count int) { - - if buffer.HasScrollableRegion() && !buffer.InScrollableRegion() { - // should have no effect outside of scrollable region - return - } - - buffer.cursorPosition.Col = 0 - - for i := 0; i < count; i++ { - buffer.insertLine() - } - -} - -func (buffer *Buffer) deleteLines(count int) { - - if buffer.HasScrollableRegion() && !buffer.InScrollableRegion() { - // should have no effect outside of scrollable region - return - } - - buffer.cursorPosition.Col = 0 - - for i := 0; i < count; i++ { - buffer.deleteLine() - } - -} - -func (buffer *Buffer) index() { - - // This sequence causes the active position to move downward one line without changing the column position. - // If the active position is at the bottom margin, a scroll up is performed." - - cursorVY := buffer.convertRawLineToViewLine(buffer.cursorPosition.Line) - - if buffer.InScrollableRegion() { - - if uint(cursorVY) < buffer.bottomMargin { - buffer.cursorPosition.Line++ - } else { - buffer.areaScrollUp(1) - } - - return - } - - if cursorVY >= buffer.ViewHeight()-1 { - buffer.lines = append(buffer.lines, newLine()) - maxLines := buffer.GetMaxLines() - if uint64(len(buffer.lines)) > maxLines { - copy(buffer.lines, buffer.lines[uint64(len(buffer.lines))-maxLines:]) - buffer.lines = buffer.lines[:maxLines] - } - } - buffer.cursorPosition.Line++ -} - -func (buffer *Buffer) reverseIndex() { - - cursorVY := buffer.convertRawLineToViewLine(buffer.cursorPosition.Line) - - if uint(cursorVY) == buffer.topMargin { - buffer.areaScrollDown(1) - } else if cursorVY > 0 { - buffer.cursorPosition.Line-- - } -} - -// write will write a rune to the terminal at the position of the cursor, and increment the cursor position -func (buffer *Buffer) write(runes ...MeasuredRune) { - - // scroll to bottom on input - buffer.scrollLinesFromBottom = 0 - - for _, r := range runes { - - line := buffer.getCurrentLine() - - if buffer.modes.ReplaceMode { - - if buffer.CursorColumn() >= buffer.Width() { - if buffer.modes.AutoWrap { - buffer.cursorPosition.Line++ - buffer.cursorPosition.Col = 0 - line = buffer.getCurrentLine() - - } else { - // no more room on line and wrapping is disabled - return - } - } - - for int(buffer.CursorColumn()) >= len(line.cells) { - line.append(buffer.defaultCell(int(buffer.CursorColumn()) == len(line.cells))) - } - line.cells[buffer.cursorPosition.Col].attr = buffer.cursorAttr - line.cells[buffer.cursorPosition.Col].setRune(r) - buffer.incrementCursorPosition() - continue - } - - if buffer.CursorColumn() >= buffer.Width() { // if we're after the line, move to next - - if buffer.modes.AutoWrap { - - buffer.newLineEx(true) - - newLine := buffer.getCurrentLine() - if len(newLine.cells) == 0 { - newLine.append(buffer.defaultCell(true)) - } - cell := &newLine.cells[0] - cell.setRune(r) - cell.attr = buffer.cursorAttr - - } else { - // no more room on line and wrapping is disabled - return - } - - } else { - - for int(buffer.CursorColumn()) >= len(line.cells) { - line.append(buffer.defaultCell(int(buffer.CursorColumn()) == len(line.cells))) - } - - cell := &line.cells[buffer.CursorColumn()] - cell.setRune(r) - cell.attr = buffer.cursorAttr - } - - buffer.incrementCursorPosition() - } -} - -func (buffer *Buffer) incrementCursorPosition() { - // we can increment one column past the end of the line. - // this is effectively the beginning of the next line, except when we \r etc. - if buffer.CursorColumn() < buffer.Width() { - buffer.cursorPosition.Col++ - } -} - -func (buffer *Buffer) inDoWrap() bool { - // xterm uses 'do_wrap' flag for this special terminal state - // we use the cursor position right after the boundary - // let's see how it works out - return buffer.cursorPosition.Col == buffer.viewWidth // @todo rightMargin -} - -func (buffer *Buffer) backspace() { - - if buffer.cursorPosition.Col == 0 { - line := buffer.getCurrentLine() - if line.wrapped { - buffer.movePosition(int16(buffer.Width()-1), -1) - } - } else if buffer.inDoWrap() { - // the "do_wrap" implementation - buffer.movePosition(-2, 0) - } else { - buffer.movePosition(-1, 0) - } -} - -func (buffer *Buffer) carriageReturn() { - - cursorVY := buffer.convertRawLineToViewLine(buffer.cursorPosition.Line) - - for { - line := buffer.getCurrentLine() - if line == nil { - break - } - if line.wrapped && cursorVY > 0 { - buffer.cursorPosition.Line-- - } else { - break - } - } - - buffer.cursorPosition.Col = 0 -} - -func (buffer *Buffer) tab() { - - tabStop := buffer.getNextTabStopAfter(buffer.cursorPosition.Col) - for buffer.cursorPosition.Col < tabStop && buffer.cursorPosition.Col < buffer.viewWidth-1 { // @todo rightMargin - buffer.write(MeasuredRune{Rune: ' ', Width: 1}) - } -} - -// return next tab stop x pos -func (buffer *Buffer) getNextTabStopAfter(col uint16) uint16 { - - defaultStop := col + (TabSize - (col % TabSize)) - if defaultStop == col { - defaultStop += TabSize - } - - var low uint16 - for _, stop := range buffer.tabStops { - if stop > col { - if stop < low || low == 0 { - low = stop - } - } - } - - if low == 0 { - return defaultStop - } - - return low -} - -func (buffer *Buffer) newLine() { - buffer.newLineEx(false) -} - -func (buffer *Buffer) verticalTab() { - buffer.index() - - for { - line := buffer.getCurrentLine() - if !line.wrapped { - break - } - buffer.index() - } -} - -func (buffer *Buffer) newLineEx(forceCursorToMargin bool) { - - if buffer.IsNewLineMode() || forceCursorToMargin { - buffer.cursorPosition.Col = 0 - } - buffer.index() - - for { - line := buffer.getCurrentLine() - if !line.wrapped { - break - } - buffer.index() - } -} - -func (buffer *Buffer) movePosition(x int16, y int16) { - - var toX uint16 - var toY uint16 - - if int16(buffer.CursorColumn())+x < 0 { - toX = 0 - } else { - toX = uint16(int16(buffer.CursorColumn()) + x) - } - - // should either use CursorLine() and setPosition() or use absolutes, mind Origin Mode (DECOM) - if int16(buffer.CursorLine())+y < 0 { - toY = 0 - } else { - toY = uint16(int16(buffer.CursorLine()) + y) - } - - buffer.setPosition(toX, toY) -} - -func (buffer *Buffer) setPosition(col uint16, line uint16) { - - useCol := col - useLine := line - maxLine := buffer.ViewHeight() - 1 - - if buffer.modes.OriginMode { - useLine += uint16(buffer.topMargin) - maxLine = uint16(buffer.bottomMargin) - // @todo left and right margins - } - if useLine > maxLine { - useLine = maxLine - } - - if useCol >= buffer.ViewWidth() { - useCol = buffer.ViewWidth() - 1 - } - - buffer.cursorPosition.Col = useCol - buffer.cursorPosition.Line = buffer.convertViewLineToRawLine(useLine) -} - -func (buffer *Buffer) GetVisibleLines() []Line { - lines := []Line{} - - for i := buffer.Height() - int(buffer.ViewHeight()); i < buffer.Height(); i++ { - y := i - int(buffer.scrollLinesFromBottom) - if y >= 0 && y < len(buffer.lines) { - lines = append(lines, buffer.lines[y]) - } - } - return lines -} - -// tested to here - -func (buffer *Buffer) clear() { - for i := 0; i < int(buffer.ViewHeight()); i++ { - buffer.lines = append(buffer.lines, newLine()) - } - buffer.setPosition(0, 0) -} - -// creates if necessary -func (buffer *Buffer) getCurrentLine() *Line { - cursorVY := buffer.convertRawLineToViewLine(buffer.cursorPosition.Line) - return buffer.getViewLine(cursorVY) -} - -func (buffer *Buffer) getViewLine(index uint16) *Line { - - if index >= buffer.ViewHeight() { - return &buffer.lines[len(buffer.lines)-1] - } - - if len(buffer.lines) < int(buffer.ViewHeight()) { - for int(index) >= len(buffer.lines) { - buffer.lines = append(buffer.lines, newLine()) - } - return &buffer.lines[int(index)] - } - - if raw := int(buffer.convertViewLineToRawLine(index)); raw < len(buffer.lines) { - return &buffer.lines[raw] - } - - return nil -} - -func (buffer *Buffer) eraseLine() { - - buffer.clearSixelsAtRawLine(buffer.cursorPosition.Line) - - line := buffer.getCurrentLine() - - for i := 0; i < int(buffer.viewWidth); i++ { - if i >= len(line.cells) { - line.cells = append(line.cells, buffer.defaultCell(false)) - } else { - line.cells[i] = buffer.defaultCell(false) - } - } -} - -func (buffer *Buffer) eraseLineToCursor() { - buffer.clearSixelsAtRawLine(buffer.cursorPosition.Line) - line := buffer.getCurrentLine() - for i := 0; i <= int(buffer.cursorPosition.Col); i++ { - if i < len(line.cells) { - line.cells[i].erase(buffer.cursorAttr.bgColour) - } - } -} - -func (buffer *Buffer) eraseLineFromCursor() { - buffer.clearSixelsAtRawLine(buffer.cursorPosition.Line) - line := buffer.getCurrentLine() - - for i := buffer.cursorPosition.Col; i < buffer.viewWidth; i++ { - if int(i) >= len(line.cells) { - line.cells = append(line.cells, buffer.defaultCell(false)) - } else { - line.cells[i] = buffer.defaultCell(false) - } - } -} - -func (buffer *Buffer) eraseDisplay() { - for i := uint16(0); i < (buffer.ViewHeight()); i++ { - rawLine := buffer.convertViewLineToRawLine(i) - buffer.clearSixelsAtRawLine(rawLine) - if int(rawLine) < len(buffer.lines) { - buffer.lines[int(rawLine)].cells = []Cell{} - } - } -} - -func (buffer *Buffer) deleteChars(n int) { - - line := buffer.getCurrentLine() - if int(buffer.cursorPosition.Col) >= len(line.cells) { - return - } - before := line.cells[:buffer.cursorPosition.Col] - if int(buffer.cursorPosition.Col)+n >= len(line.cells) { - n = len(line.cells) - int(buffer.cursorPosition.Col) - } - after := line.cells[int(buffer.cursorPosition.Col)+n:] - line.cells = append(before, after...) -} - -func (buffer *Buffer) eraseCharacters(n int) { - - line := buffer.getCurrentLine() - - max := int(buffer.cursorPosition.Col) + n - if max > len(line.cells) { - max = len(line.cells) - } - - for i := int(buffer.cursorPosition.Col); i < max; i++ { - line.cells[i].erase(buffer.cursorAttr.bgColour) - } -} - -func (buffer *Buffer) eraseDisplayFromCursor() { - line := buffer.getCurrentLine() - - max := int(buffer.cursorPosition.Col) - if max > len(line.cells) { - max = len(line.cells) - } - - line.cells = line.cells[:max] - - for rawLine := buffer.cursorPosition.Line + 1; int(rawLine) < len(buffer.lines); rawLine++ { - buffer.clearSixelsAtRawLine(rawLine) - buffer.lines[int(rawLine)].cells = []Cell{} - } -} - -func (buffer *Buffer) eraseDisplayToCursor() { - line := buffer.getCurrentLine() - - for i := 0; i <= int(buffer.cursorPosition.Col); i++ { - if i >= len(line.cells) { - break - } - line.cells[i].erase(buffer.cursorAttr.bgColour) - } - - cursorVY := buffer.convertRawLineToViewLine(buffer.cursorPosition.Line) - - for i := uint16(0); i < cursorVY; i++ { - rawLine := buffer.convertViewLineToRawLine(i) - buffer.clearSixelsAtRawLine(rawLine) - if int(rawLine) < len(buffer.lines) { - buffer.lines[int(rawLine)].cells = []Cell{} - } - } -} - -func (buffer *Buffer) GetMaxLines() uint64 { - result := buffer.maxLines - if result < uint64(buffer.viewHeight) { - result = uint64(buffer.viewHeight) - } - - return result -} - -func (buffer *Buffer) setVerticalMargins(top uint, bottom uint) { - buffer.topMargin = top - buffer.bottomMargin = bottom -} - -// resetVerticalMargins resets margins to extreme positions -func (buffer *Buffer) resetVerticalMargins(height uint) { - buffer.setVerticalMargins(0, height-1) -} - -func (buffer *Buffer) defaultCell(applyEffects bool) Cell { - attr := buffer.cursorAttr - if !applyEffects { - attr.blink = false - attr.bold = false - attr.dim = false - attr.inverse = false - attr.underline = false - attr.dim = false - } - return Cell{attr: attr} -} - -func (buffer *Buffer) IsNewLineMode() bool { - return !buffer.modes.LineFeedMode -} - -func (buffer *Buffer) tabReset() { - buffer.tabStops = nil -} - -func (buffer *Buffer) tabSet(index uint16) { - buffer.tabStops = append(buffer.tabStops, index) -} - -func (buffer *Buffer) tabClear(index uint16) { - var filtered []uint16 - for _, stop := range buffer.tabStops { - if stop != buffer.cursorPosition.Col { - filtered = append(filtered, stop) - } - } - buffer.tabStops = filtered -} - -func (buffer *Buffer) IsTabSetAtCursor() bool { - if buffer.cursorPosition.Col%TabSize > 0 { - return false - } - for _, stop := range buffer.tabStops { - if stop == buffer.cursorPosition.Col { - return true - } - } - return false -} - -func (buffer *Buffer) tabClearAtCursor() { - buffer.tabClear(buffer.cursorPosition.Col) -} - -func (buffer *Buffer) tabSetAtCursor() { - buffer.tabSet(buffer.cursorPosition.Col) -} - -func (buffer *Buffer) GetScrollOffset() uint { - return buffer.scrollLinesFromBottom -} - -func (buffer *Buffer) SetScrollOffset(offset uint) { - buffer.scrollLinesFromBottom = offset -} - -func (buffer *Buffer) ScrollToEnd() { - buffer.scrollLinesFromBottom = 0 -} - -func (buffer *Buffer) ScrollUp(lines uint) { - if int(buffer.scrollLinesFromBottom)+int(lines) < len(buffer.lines)-int(buffer.viewHeight) { - buffer.scrollLinesFromBottom += lines - } else { - lines := len(buffer.lines) - int(buffer.viewHeight) - if lines < 0 { - lines = 0 - } - buffer.scrollLinesFromBottom = uint(lines) - } -} - -func (buffer *Buffer) ScrollDown(lines uint) { - if int(buffer.scrollLinesFromBottom)-int(lines) >= 0 { - buffer.scrollLinesFromBottom -= lines - } else { - buffer.scrollLinesFromBottom = 0 - } -} diff --git a/cmd/darktile/termutil/buffer_test.go b/cmd/darktile/termutil/buffer_test.go deleted file mode 100644 index b37eb48f0f..0000000000 --- a/cmd/darktile/termutil/buffer_test.go +++ /dev/null @@ -1,688 +0,0 @@ -package termutil - -import ( - "strings" - "testing" - - "github.com/charmbracelet/lipgloss" - "github.com/stretchr/testify/require" - - "github.com/stretchr/testify/assert" -) - -func writeRaw(buf *Buffer, runes ...rune) { - for _, r := range runes { - buf.write(MeasuredRune{Rune: r, Width: 1}) - } -} - -func TestBufferCreation(t *testing.T) { - b := makeBufferForTesting(10, 20) - assert.Equal(t, uint16(10), b.Width()) - assert.Equal(t, uint16(20), b.ViewHeight()) - assert.Equal(t, uint16(0), b.CursorColumn()) - assert.Equal(t, uint16(0), b.CursorLine()) - assert.NotNil(t, b.lines) -} - -func TestNewLine(t *testing.T) { - b := makeBufferForTesting(30, 3) - writeRaw(b, []rune("hello")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("goodbye")...) - b.carriageReturn() - b.newLine() - expected := ` -hello -goodbye -` - - lines := b.GetVisibleLines() - strs := []string{} - for _, l := range lines { - strs = append(strs, l.String()) - } - require.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(strings.Join(strs, "\n"))) -} - -func TestTabbing(t *testing.T) { - b := makeBufferForTesting(30, 3) - writeRaw(b, []rune("hello")...) - b.tab() - writeRaw(b, []rune("x")...) - b.tab() - writeRaw(b, []rune("goodbye")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hell")...) - b.tab() - writeRaw(b, []rune("xxx")...) - b.tab() - writeRaw(b, []rune("good")...) - b.carriageReturn() - b.newLine() - expected := ` -hello x goodbye -hell xxx good -` - - lines := b.GetVisibleLines() - strs := []string{} - for _, l := range lines { - strs = append(strs, l.String()) - } - require.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(strings.Join(strs, "\n"))) -} - -func TestOffsets(t *testing.T) { - b := makeBufferForTesting(10, 3) - writeRaw(b, []rune("hello")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello")...) - assert.Equal(t, uint16(10), b.ViewWidth()) - assert.Equal(t, uint16(10), b.Width()) - assert.Equal(t, uint16(3), b.ViewHeight()) - assert.Equal(t, 5, b.Height()) -} - -func TestBufferWriteIncrementsCursorCorrectly(t *testing.T) { - b := makeBufferForTesting(5, 4) - - /*01234 - |----- - 0|xxxxx - 1| - 2| - 3| - |----- - */ - - writeRaw(b, 'x') - require.Equal(t, uint16(1), b.CursorColumn()) - require.Equal(t, uint16(0), b.CursorLine()) - - writeRaw(b, 'x') - require.Equal(t, uint16(2), b.CursorColumn()) - require.Equal(t, uint16(0), b.CursorLine()) - - writeRaw(b, 'x') - require.Equal(t, uint16(3), b.CursorColumn()) - require.Equal(t, uint16(0), b.CursorLine()) - - writeRaw(b, 'x') - require.Equal(t, uint16(4), b.CursorColumn()) - require.Equal(t, uint16(0), b.CursorLine()) - - writeRaw(b, 'x') - require.Equal(t, uint16(5), b.CursorColumn()) - require.Equal(t, uint16(0), b.CursorLine()) - - writeRaw(b, 'x') - require.Equal(t, uint16(1), b.CursorColumn()) - require.Equal(t, uint16(1), b.CursorLine()) - - writeRaw(b, 'x') - require.Equal(t, uint16(2), b.CursorColumn()) - require.Equal(t, uint16(1), b.CursorLine()) - - lines := b.GetVisibleLines() - require.Equal(t, 2, len(lines)) - assert.Equal(t, "xxxxx", lines[0].String()) - assert.Equal(t, "xx", lines[1].String()) - -} - -func TestWritingNewLineAsFirstRuneOnWrappedLine(t *testing.T) { - b := makeBufferForTesting(3, 20) - b.modes.LineFeedMode = false - - writeRaw(b, 'a', 'b', 'c') - assert.Equal(t, uint16(3), b.cursorPosition.Col) - assert.Equal(t, uint64(0), b.cursorPosition.Line) - b.newLine() - assert.Equal(t, uint16(0), b.cursorPosition.Col) - assert.Equal(t, uint64(1), b.cursorPosition.Line) - - writeRaw(b, 'd', 'e', 'f') - assert.Equal(t, uint16(3), b.cursorPosition.Col) - assert.Equal(t, uint64(1), b.cursorPosition.Line) - b.newLine() - - assert.Equal(t, uint16(0), b.cursorPosition.Col) - assert.Equal(t, uint64(2), b.cursorPosition.Line) - - require.Equal(t, 3, len(b.lines)) - assert.Equal(t, "abc", b.lines[0].String()) - assert.Equal(t, "def", b.lines[1].String()) - -} - -func TestWritingNewLineAsSecondRuneOnWrappedLine(t *testing.T) { - b := makeBufferForTesting(3, 20) - b.modes.LineFeedMode = false - /* - |abc - |d - |ef - | - | - |z - */ - - writeRaw(b, 'a', 'b', 'c', 'd') - b.newLine() - writeRaw(b, 'e', 'f') - b.newLine() - b.newLine() - b.newLine() - writeRaw(b, 'z') - - assert.Equal(t, "abc", b.lines[0].String()) - assert.Equal(t, "d", b.lines[1].String()) - assert.Equal(t, "ef", b.lines[2].String()) - assert.Equal(t, "", b.lines[3].String()) - assert.Equal(t, "", b.lines[4].String()) - assert.Equal(t, "z", b.lines[5].String()) -} - -func TestSetPosition(t *testing.T) { - b := makeBufferForTesting(120, 80) - assert.Equal(t, 0, int(b.CursorColumn())) - assert.Equal(t, 0, int(b.CursorLine())) - - b.setPosition(60, 10) - assert.Equal(t, 60, int(b.CursorColumn())) - assert.Equal(t, 10, int(b.CursorLine())) - - b.setPosition(0, 0) - assert.Equal(t, 0, int(b.CursorColumn())) - assert.Equal(t, 0, int(b.CursorLine())) - - b.setPosition(120, 90) - assert.Equal(t, 119, int(b.CursorColumn())) - assert.Equal(t, 79, int(b.CursorLine())) - -} - -func TestMovePosition(t *testing.T) { - b := makeBufferForTesting(120, 80) - assert.Equal(t, 0, int(b.CursorColumn())) - assert.Equal(t, 0, int(b.CursorLine())) - - b.movePosition(-1, -1) - assert.Equal(t, 0, int(b.CursorColumn())) - assert.Equal(t, 0, int(b.CursorLine())) - - b.movePosition(30, 20) - assert.Equal(t, 30, int(b.CursorColumn())) - assert.Equal(t, 20, int(b.CursorLine())) - - b.movePosition(30, 20) - assert.Equal(t, 60, int(b.CursorColumn())) - assert.Equal(t, 40, int(b.CursorLine())) - - b.movePosition(-1, -1) - assert.Equal(t, 59, int(b.CursorColumn())) - assert.Equal(t, 39, int(b.CursorLine())) - - b.movePosition(100, 100) - assert.Equal(t, 119, int(b.CursorColumn())) - assert.Equal(t, 79, int(b.CursorLine())) - -} - -func TestVisibleLines(t *testing.T) { - b := makeBufferForTesting(80, 10) - writeRaw(b, []rune("hello 1")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 2")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 3")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 4")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 5")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 6")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 7")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 8")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 9")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 10")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 11")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 12")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 13")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 14")...) - - lines := b.GetVisibleLines() - require.Equal(t, 10, len(lines)) - assert.Equal(t, "hello 5", lines[0].String()) - assert.Equal(t, "hello 14", lines[9].String()) - -} - -func TestClearWithoutFullView(t *testing.T) { - b := makeBufferForTesting(80, 10) - writeRaw(b, []rune("hello 1")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 1")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 1")...) - b.clear() - lines := b.GetVisibleLines() - for _, line := range lines { - assert.Equal(t, "", line.String()) - } -} - -func TestClearWithFullView(t *testing.T) { - b := makeBufferForTesting(80, 5) - writeRaw(b, []rune("hello 1")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 1")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 1")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 1")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 1")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 1")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 1")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("hello 1")...) - b.clear() - lines := b.GetVisibleLines() - for _, line := range lines { - assert.Equal(t, "", line.String()) - } -} - -func TestCarriageReturn(t *testing.T) { - b := makeBufferForTesting(80, 20) - writeRaw(b, []rune("hello!")...) - b.carriageReturn() - writeRaw(b, []rune("secret")...) - lines := b.GetVisibleLines() - assert.Equal(t, "secret", lines[0].String()) -} - -func TestCarriageReturnOnFullLine(t *testing.T) { - b := makeBufferForTesting(20, 20) - writeRaw(b, []rune("abcdeabcdeabcdeabcde")...) - b.carriageReturn() - writeRaw(b, []rune("xxxxxxxxxxxxxxxxxxxx")...) - lines := b.GetVisibleLines() - assert.Equal(t, "xxxxxxxxxxxxxxxxxxxx", lines[0].String()) -} - -func TestCarriageReturnOnFullLastLine(t *testing.T) { - b := makeBufferForTesting(20, 2) - b.newLine() - writeRaw(b, []rune("abcdeabcdeabcdeabcde")...) - b.carriageReturn() - writeRaw(b, []rune("xxxxxxxxxxxxxxxxxxxx")...) - lines := b.GetVisibleLines() - assert.Equal(t, "", lines[0].String()) - assert.Equal(t, "xxxxxxxxxxxxxxxxxxxx", lines[1].String()) -} - -func TestCarriageReturnOnWrappedLine(t *testing.T) { - b := makeBufferForTesting(80, 6) - writeRaw(b, []rune("hello!")...) - b.carriageReturn() - writeRaw(b, []rune("secret")...) - - lines := b.GetVisibleLines() - assert.Equal(t, "secret", lines[0].String()) -} - -func TestCarriageReturnOnLineThatDoesntExist(t *testing.T) { - b := makeBufferForTesting(6, 10) - b.cursorPosition.Line = 3 - b.carriageReturn() - assert.Equal(t, uint16(0), b.cursorPosition.Col) - assert.Equal(t, uint64(3), b.cursorPosition.Line) -} - -func TestGetCell(t *testing.T) { - b := makeBufferForTesting(80, 20) - writeRaw(b, []rune("Hello")...) - b.carriageReturn() - b.newLine() - - writeRaw(b, []rune("there")...) - b.carriageReturn() - b.newLine() - - writeRaw(b, []rune("something...")...) - cell := b.GetCell(8, 2) - require.NotNil(t, cell) - assert.Equal(t, 'g', cell.Rune().Rune) -} - -func TestGetCellWithHistory(t *testing.T) { - b := makeBufferForTesting(80, 2) - - writeRaw(b, []rune("Hello")...) - b.carriageReturn() - b.newLine() - - writeRaw(b, []rune("there")...) - b.carriageReturn() - b.newLine() - - writeRaw(b, []rune("something...")...) - - cell := b.GetCell(8, 1) - require.NotNil(t, cell) - assert.Equal(t, 'g', cell.Rune().Rune) -} - -func TestGetCellWithBadCursor(t *testing.T) { - b := makeBufferForTesting(80, 2) - writeRaw(b, []rune("Hello\r\nthere\r\nsomething...")...) - require.Nil(t, b.GetCell(8, 3)) - require.Nil(t, b.GetCell(90, 0)) - -} - -func TestCursorPositionQuerying(t *testing.T) { - b := makeBufferForTesting(80, 20) - b.cursorPosition.Col = 17 - b.cursorPosition.Line = 9 - assert.Equal(t, b.cursorPosition.Col, b.CursorColumn()) - assert.Equal(t, b.convertRawLineToViewLine(b.cursorPosition.Line), b.CursorLine()) -} - -// CSI 2 K -func TestEraseLine(t *testing.T) { - b := makeBufferForTesting(80, 5) - writeRaw(b, []rune("hello, this is a test")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("this line should be deleted")...) - b.eraseLine() - assert.Equal(t, "hello, this is a test", b.lines[0].String()) - assert.Equal(t, "", b.lines[1].String()) -} - -// CSI 1 K -func TestEraseLineToCursor(t *testing.T) { - b := makeBufferForTesting(80, 5) - writeRaw(b, []rune("hello, this is a test")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("deleted")...) - - b.movePosition(-3, 0) - b.eraseLineToCursor() - assert.Equal(t, "hello, this is a test", b.lines[0].String()) - assert.Equal(t, "\x00\x00\x00\x00\x00ed", b.lines[1].String()) -} - -// CSI 0 K -func TestEraseLineAfterCursor(t *testing.T) { - b := makeBufferForTesting(80, 5) - writeRaw(b, []rune("hello, this is a test")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("deleted")...) - b.movePosition(-3, 0) - b.eraseLineFromCursor() - assert.Equal(t, "hello, this is a test", b.lines[0].String()) - assert.Equal(t, "dele", b.lines[1].String()) -} -func TestEraseDisplay(t *testing.T) { - b := makeBufferForTesting(80, 5) - writeRaw(b, []rune("hello")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("asdasd")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("thing")...) - b.movePosition(2, 1) - b.eraseDisplay() - lines := b.GetVisibleLines() - for _, line := range lines { - assert.Equal(t, "", line.String()) - } -} -func TestEraseDisplayToCursor(t *testing.T) { - b := makeBufferForTesting(80, 5) - writeRaw(b, []rune("hello")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("asdasd")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("thing")...) - b.movePosition(-2, 0) - b.eraseDisplayToCursor() - lines := b.GetVisibleLines() - assert.Equal(t, "", lines[0].String()) - assert.Equal(t, "", lines[1].String()) - assert.Equal(t, "\x00\x00\x00\x00g", lines[2].String()) - -} - -func TestEraseDisplayFromCursor(t *testing.T) { - b := makeBufferForTesting(80, 5) - writeRaw(b, []rune("hello")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("asdasd")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("things")...) - b.movePosition(-3, -1) - b.eraseDisplayFromCursor() - lines := b.GetVisibleLines() - assert.Equal(t, "hello", lines[0].String()) - assert.Equal(t, "asd", lines[1].String()) - assert.Equal(t, "", lines[2].String()) -} -func TestBackspace(t *testing.T) { - b := makeBufferForTesting(80, 5) - writeRaw(b, []rune("hello")...) - b.backspace() - b.backspace() - writeRaw(b, []rune("p")...) - lines := b.GetVisibleLines() - assert.Equal(t, "helpo", lines[0].String()) -} - -func TestHorizontalResizeView(t *testing.T) { - b := makeBufferForTesting(80, 10) - - // 60 characters - writeRaw(b, []rune(`hellohellohellohellohellohellohellohellohellohellohellohello`)...) - - b.carriageReturn() - b.newLine() - - writeRaw(b, []rune(`goodbyegoodbye`)...) - - require.Equal(t, uint16(14), b.cursorPosition.Col) - require.Equal(t, uint64(1), b.cursorPosition.Line) - - b.resizeView(40, 10) - - expected := `hellohellohellohellohellohellohellohello -hellohellohellohello -goodbyegoodbye` - - require.Equal(t, uint16(14), b.cursorPosition.Col) - require.Equal(t, uint64(2), b.cursorPosition.Line) - - lines := b.GetVisibleLines() - strs := []string{} - for _, l := range lines { - strs = append(strs, l.String()) - } - require.Equal(t, expected, strings.Join(strs, "\n")) - - b.resizeView(20, 10) - - expected = `hellohellohellohello -hellohellohellohello -hellohellohellohello -goodbyegoodbye` - - lines = b.GetVisibleLines() - strs = []string{} - for _, l := range lines { - strs = append(strs, l.String()) - } - require.Equal(t, expected, strings.Join(strs, "\n")) - - b.resizeView(10, 10) - - expected = `hellohello -hellohello -hellohello -hellohello -hellohello -hellohello -goodbyegoo -dbye` - - lines = b.GetVisibleLines() - strs = []string{} - for _, l := range lines { - strs = append(strs, l.String()) - } - require.Equal(t, expected, strings.Join(strs, "\n")) - - b.resizeView(80, 20) - - expected = `hellohellohellohellohellohellohellohellohellohellohellohello -goodbyegoodbye` - - lines = b.GetVisibleLines() - strs = []string{} - for _, l := range lines { - strs = append(strs, l.String()) - } - require.Equal(t, expected, strings.Join(strs, "\n")) - - require.Equal(t, uint16(4), b.cursorPosition.Col) - require.Equal(t, uint64(1), b.cursorPosition.Line) - -} - -func TestBufferMaxLines(t *testing.T) { - b := NewBuffer(80, 2, 2, lipgloss.Color("#FFFFFF"), lipgloss.Color("#000000")) - b.modes.LineFeedMode = false - - writeRaw(b, []rune("hello")...) - b.newLine() - writeRaw(b, []rune("funny")...) - b.newLine() - writeRaw(b, []rune("world")...) - - assert.Equal(t, 2, len(b.lines)) - assert.Equal(t, "funny", b.lines[0].String()) - assert.Equal(t, "world", b.lines[1].String()) -} - -func TestShrinkingThenGrowing(t *testing.T) { - b := makeBufferForTesting(30, 100) - writeRaw(b, []rune("hellohellohellohellohello")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("01234567890123456789")...) - b.carriageReturn() - b.newLine() - - b.resizeView(25, 100) - b.resizeView(24, 100) - - b.resizeView(30, 100) - - expected := `hellohellohellohellohello -01234567890123456789 -` - lines := b.GetVisibleLines() - var strs []string - for _, l := range lines { - strs = append(strs, l.String()) - } - require.Equal(t, expected, strings.Join(strs, "\n")) -} - -func TestShrinkingThenRestoring(t *testing.T) { - b := makeBufferForTesting(30, 100) - writeRaw(b, []rune("hellohellohellohellohello")...) - b.carriageReturn() - b.newLine() - writeRaw(b, []rune("01234567890123456789")...) - b.carriageReturn() - b.newLine() - - b.cursorPosition.Line = 2 - - for i := uint16(29); i > 5; i-- { - b.resizeView(i, 100) - } - - for i := uint16(15); i < 30; i++ { - b.resizeView(i, 100) - } - - expected := `hellohellohellohellohello -01234567890123456789 -` - lines := b.GetVisibleLines() - var strs []string - for _, l := range lines { - strs = append(strs, l.String()) - } - require.Equal(t, expected, strings.Join(strs, "\n")) -} - -func makeBufferForTesting(cols, rows uint16) *Buffer { - return NewBuffer(cols, rows, 100, lipgloss.Color("#FFFFFF"), lipgloss.Color("#000000")) -} diff --git a/cmd/darktile/termutil/cell.go b/cmd/darktile/termutil/cell.go deleted file mode 100644 index 731d1a298c..0000000000 --- a/cmd/darktile/termutil/cell.go +++ /dev/null @@ -1,61 +0,0 @@ -package termutil - -import ( - "github.com/charmbracelet/lipgloss" -) - -type Cell struct { - r MeasuredRune - attr CellAttributes -} - -func (cell *Cell) Attr() CellAttributes { - return cell.attr -} - -func (cell *Cell) Rune() MeasuredRune { - return cell.r -} - -func (cell *Cell) Fg() lipgloss.TerminalColor { - if cell.Attr().inverse { - return cell.attr.bgColour - } - return cell.attr.fgColour -} - -func (cell *Cell) Bold() bool { - return cell.attr.bold -} - -func (cell *Cell) Dim() bool { - return cell.attr.dim -} - -func (cell *Cell) Italic() bool { - return cell.attr.italic -} - -func (cell *Cell) Underline() bool { - return cell.attr.underline -} - -func (cell *Cell) Strikethrough() bool { - return cell.attr.strikethrough -} - -func (cell *Cell) Bg() lipgloss.TerminalColor { - if cell.Attr().inverse { - return cell.attr.fgColour - } - return cell.attr.bgColour -} - -func (cell *Cell) erase(bgColour lipgloss.TerminalColor) { - cell.setRune(MeasuredRune{Rune: 0}) - cell.attr.bgColour = bgColour -} - -func (cell *Cell) setRune(r MeasuredRune) { - cell.r = r -} diff --git a/cmd/darktile/termutil/cell_attributes.go b/cmd/darktile/termutil/cell_attributes.go deleted file mode 100644 index eab49c8d96..0000000000 --- a/cmd/darktile/termutil/cell_attributes.go +++ /dev/null @@ -1,18 +0,0 @@ -package termutil - -import ( - "github.com/charmbracelet/lipgloss" -) - -type CellAttributes struct { - fgColour lipgloss.TerminalColor - bgColour lipgloss.TerminalColor - bold bool - italic bool - dim bool - underline bool - strikethrough bool - blink bool - inverse bool - hidden bool -} diff --git a/cmd/darktile/termutil/charsets.go b/cmd/darktile/termutil/charsets.go deleted file mode 100644 index 7e2d17a727..0000000000 --- a/cmd/darktile/termutil/charsets.go +++ /dev/null @@ -1,64 +0,0 @@ -package termutil - -var charSets = map[rune]*map[rune]rune{ - '0': &decSpecGraphics, - 'B': nil, // ASCII - // @todo 1,2,A -} - -var decSpecGraphics = map[rune]rune{ - 0x5f: 0x00A0, // NO-BREAK SPACE - 0x60: 0x25C6, // BLACK DIAMOND - 0x61: 0x2592, // MEDIUM SHADE - 0x62: 0x2409, // SYMBOL FOR HORIZONTAL TABULATION - 0x63: 0x240C, // SYMBOL FOR FORM FEED - 0x64: 0x240D, // SYMBOL FOR CARRIAGE RETURN - 0x65: 0x240A, // SYMBOL FOR LINE FEED - 0x66: 0x00B0, // DEGREE SIGN - 0x67: 0x00B1, // PLUS-MINUS SIGN - 0x68: 0x2424, // SYMBOL FOR NEWLINE - 0x69: 0x240B, // SYMBOL FOR VERTICAL TABULATION - 0x6a: 0x2518, // BOX DRAWINGS LIGHT UP AND LEFT - 0x6b: 0x2510, // BOX DRAWINGS LIGHT DOWN AND LEFT - 0x6c: 0x250C, // BOX DRAWINGS LIGHT DOWN AND RIGHT - 0x6d: 0x2514, // BOX DRAWINGS LIGHT UP AND RIGHT - 0x6e: 0x253C, // BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL - 0x6f: 0x23BA, // HORIZONTAL SCAN LINE-1 - 0x70: 0x23BB, // HORIZONTAL SCAN LINE-3 - 0x71: 0x2500, // BOX DRAWINGS LIGHT HORIZONTAL - 0x72: 0x23BC, // HORIZONTAL SCAN LINE-7 - 0x73: 0x23BD, // HORIZONTAL SCAN LINE-9 - 0x74: 0x251C, // BOX DRAWINGS LIGHT VERTICAL AND RIGHT - 0x75: 0x2524, // BOX DRAWINGS LIGHT VERTICAL AND LEFT - 0x76: 0x2534, // BOX DRAWINGS LIGHT UP AND HORIZONTAL - 0x77: 0x252C, // BOX DRAWINGS LIGHT DOWN AND HORIZONTAL - 0x78: 0x2502, // BOX DRAWINGS LIGHT VERTICAL - 0x79: 0x2264, // LESS-THAN OR EQUAL TO - 0x7a: 0x2265, // GREATER-THAN OR EQUAL TO - 0x7b: 0x03C0, // GREEK SMALL LETTER PI - 0x7c: 0x2260, // NOT EQUAL TO - 0x7d: 0x00A3, // POUND SIGN - 0x7e: 0x00B7, // MIDDLE DOT -} - -func (t *Terminal) handleSCS0(pty chan MeasuredRune) bool { - return t.scsHandler(pty, 0) -} - -func (t *Terminal) handleSCS1(pty chan MeasuredRune) bool { - return t.scsHandler(pty, 1) -} - -func (t *Terminal) scsHandler(pty chan MeasuredRune, which int) bool { - b := <-pty - - cs, ok := charSets[b.Rune] - if ok { - //terminal.logger.Debugf("Selected charset %v into G%v", string(b), which) - t.activeBuffer.charsets[which] = cs - return false - } - - t.activeBuffer.charsets[which] = nil - return false -} diff --git a/cmd/darktile/termutil/consts_unix.go b/cmd/darktile/termutil/consts_unix.go deleted file mode 100644 index 58537ee482..0000000000 --- a/cmd/darktile/termutil/consts_unix.go +++ /dev/null @@ -1,5 +0,0 @@ -//+build !windows - -package termutil - -var oscTerminators = []rune{0x07, 0x5c} diff --git a/cmd/darktile/termutil/consts_windows.go b/cmd/darktile/termutil/consts_windows.go deleted file mode 100644 index f965dd64c5..0000000000 --- a/cmd/darktile/termutil/consts_windows.go +++ /dev/null @@ -1,5 +0,0 @@ -//+build windows - -package termutil - -var oscTerminators = []rune{0x07, 0x00} diff --git a/cmd/darktile/termutil/csi.go b/cmd/darktile/termutil/csi.go deleted file mode 100644 index 4c066ab0fa..0000000000 --- a/cmd/darktile/termutil/csi.go +++ /dev/null @@ -1,997 +0,0 @@ -package termutil - -import ( - "fmt" - "strconv" - "strings" -) - -func parseCSI(readChan chan MeasuredRune) (final rune, params []string, intermediate []rune, raw []rune) { - var b MeasuredRune - - param := "" - intermediate = []rune{} -CSI: - for { - b = <-readChan - raw = append(raw, b.Rune) - switch true { - case b.Rune >= 0x30 && b.Rune <= 0x3F: - param = param + string(b.Rune) - case b.Rune > 0 && b.Rune <= 0x2F: - intermediate = append(intermediate, b.Rune) - case b.Rune >= 0x40 && b.Rune <= 0x7e: - final = b.Rune - break CSI - } - } - - unprocessed := strings.Split(param, ";") - for _, par := range unprocessed { - if par != "" { - par = strings.TrimLeft(par, "0") - if par == "" { - par = "0" - } - params = append(params, par) - } - } - - return final, params, intermediate, raw -} - -func (t *Terminal) handleCSI(readChan chan MeasuredRune) (renderRequired bool) { - final, params, intermediate, raw := parseCSI(readChan) - - t.log("CSI P(%q) I(%q) %c", strings.Join(params, ";"), string(intermediate), final) - - switch final { - case 'c': - return t.csiSendDeviceAttributesHandler(params) - case 'd': - return t.csiLinePositionAbsoluteHandler(params) - case 'f': - return t.csiCursorPositionHandler(params) - case 'g': - return t.csiTabClearHandler(params) - case 'h': - return t.csiSetModeHandler(params) - case 'l': - return t.csiResetModeHandler(params) - case 'm': - return t.sgrSequenceHandler(params) - case 'n': - return t.csiDeviceStatusReportHandler(params) - case 'r': - return t.csiSetMarginsHandler(params) - case 't': - return t.csiWindowManipulation(params) - case 'q': - if string(intermediate) == " " { - return t.csiCursorSelection(params) - } - case 'A': - return t.csiCursorUpHandler(params) - case 'B': - return t.csiCursorDownHandler(params) - case 'C': - return t.csiCursorForwardHandler(params) - case 'D': - return t.csiCursorBackwardHandler(params) - case 'E': - return t.csiCursorNextLineHandler(params) - case 'F': - return t.csiCursorPrecedingLineHandler(params) - case 'G': - return t.csiCursorCharacterAbsoluteHandler(params) - case 'H': - return t.csiCursorPositionHandler(params) - case 'J': - return t.csiEraseInDisplayHandler(params) - case 'K': - return t.csiEraseInLineHandler(params) - case 'L': - return t.csiInsertLinesHandler(params) - case 'M': - return t.csiDeleteLinesHandler(params) - case 'P': - return t.csiDeleteHandler(params) - case 'S': - return t.csiScrollUpHandler(params) - case 'T': - return t.csiScrollDownHandler(params) - case 'X': - return t.csiEraseCharactersHandler(params) - case '@': - return t.csiInsertBlankCharactersHandler(params) - case 'p': // reset handler - if string(intermediate) == "!" { - return t.csiSoftResetHandler(params) - } - return false - } - - for _, b := range intermediate { - t.processRunes(MeasuredRune{ - Rune: b, - Width: 1, - }) - } - - // TODO review this: - // if this is an unknown CSI sequence, write it to stdout as we can't handle it? - //_ = t.writeToRealStdOut(append([]rune{0x1b, '['}, raw...)...) - _ = raw - t.log("UNKNOWN CSI P(%s) I(%s) %c", strings.Join(params, ";"), string(intermediate), final) - return false - -} - -type WindowState uint8 - -const ( - StateUnknown WindowState = iota - StateMinimised - StateNormal - StateMaximised -) - -type WindowManipulator interface { - State() WindowState - Minimise() - Maximise() - Restore() - SetTitle(title string) - Position() (int, int) - SizeInPixels() (int, int) - CellSizeInPixels() (int, int) - SizeInChars() (int, int) - ResizeInPixels(int, int) - ResizeInChars(int, int) - ScreenSizeInPixels() (int, int) - ScreenSizeInChars() (int, int) - Move(x, y int) - IsFullscreen() bool - SetFullscreen(enabled bool) - GetTitle() string - SaveTitleToStack() - RestoreTitleFromStack() - ReportError(err error) -} - -func (t *Terminal) csiWindowManipulation(params []string) (renderRequired bool) { - - if t.windowManipulator == nil { - return false - } - - for i := 0; i < len(params); i++ { - switch params[i] { - case "1": - t.windowManipulator.Restore() - case "2": - t.windowManipulator.Minimise() - case "3": //move window - if i+2 >= len(params) { - return false - } - x, _ := strconv.Atoi(params[i+1]) - y, _ := strconv.Atoi(params[i+2]) - i += 2 - t.windowManipulator.Move(x, y) - case "4": //resize h,w - w, h := t.windowManipulator.SizeInPixels() - if i+1 < len(params) { - h, _ = strconv.Atoi(params[i+1]) - i++ - } - if i+2 < len(params) { - w, _ = strconv.Atoi(params[i+2]) - i++ - } - sw, sh := t.windowManipulator.ScreenSizeInPixels() - if w == 0 { - w = sw - } - if h == 0 { - h = sh - } - t.windowManipulator.ResizeInPixels(w, h) - case "8": - // resize in rows, cols - w, h := t.windowManipulator.SizeInChars() - if i+1 < len(params) { - h, _ = strconv.Atoi(params[i+1]) - i++ - } - if i+2 < len(params) { - w, _ = strconv.Atoi(params[i+2]) - i++ - } - sw, sh := t.windowManipulator.ScreenSizeInChars() - if w == 0 { - w = sw - } - if h == 0 { - h = sh - } - t.windowManipulator.ResizeInChars(w, h) - case "9": - if i+1 >= len(params) { - return false - } - switch params[i+1] { - case "0": - t.windowManipulator.Restore() - case "1": - t.windowManipulator.Maximise() - case "2": - w, _ := t.windowManipulator.SizeInPixels() - _, sh := t.windowManipulator.ScreenSizeInPixels() - t.windowManipulator.ResizeInPixels(w, sh) - case "3": - _, h := t.windowManipulator.SizeInPixels() - sw, _ := t.windowManipulator.ScreenSizeInPixels() - t.windowManipulator.ResizeInPixels(sw, h) - } - i++ - case "10": - if i+1 >= len(params) { - return false - } - switch params[i+1] { - case "0": - t.windowManipulator.SetFullscreen(false) - case "1": - t.windowManipulator.SetFullscreen(true) - case "2": - // toggle - t.windowManipulator.SetFullscreen(!t.windowManipulator.IsFullscreen()) - } - i++ - - case "11": - if t.windowManipulator.State() != StateMinimised { - t.WriteToPty([]byte("\x1b[1t")) - } else { - t.WriteToPty([]byte("\x1b[2t")) - } - case "13": - if i < len(params)-1 { - i++ - } - x, y := t.windowManipulator.Position() - t.WriteToPty([]byte(fmt.Sprintf("\x1b[3;%d;%dt", x, y))) - case "14": - if i < len(params)-1 { - i++ - } - w, h := t.windowManipulator.SizeInPixels() - t.WriteToPty([]byte(fmt.Sprintf("\x1b[4;%d;%dt", h, w))) - case "15": - w, h := t.windowManipulator.ScreenSizeInPixels() - t.WriteToPty([]byte(fmt.Sprintf("\x1b[5;%d;%dt", h, w))) - case "16": - w, h := t.windowManipulator.CellSizeInPixels() - t.WriteToPty([]byte(fmt.Sprintf("\x1b[6;%d;%dt", h, w))) - case "18": - w, h := t.windowManipulator.SizeInChars() - t.WriteToPty([]byte(fmt.Sprintf("\x1b[8;%d;%dt", h, w))) - case "19": - w, h := t.windowManipulator.ScreenSizeInChars() - t.WriteToPty([]byte(fmt.Sprintf("\x1b[9;%d;%dt", h, w))) - case "20": - t.WriteToPty([]byte(fmt.Sprintf("\x1b]L%s\x1b\\", t.windowManipulator.GetTitle()))) - case "21": - t.WriteToPty([]byte(fmt.Sprintf("\x1b]l%s\x1b\\", t.windowManipulator.GetTitle()))) - case "22": - if i < len(params)-1 { - i++ - } - t.windowManipulator.SaveTitleToStack() - case "23": - if i < len(params)-1 { - i++ - } - t.windowManipulator.RestoreTitleFromStack() - } - } - - return true -} - -// CSI c -// Send Device Attributes (Primary/Secondary/Tertiary DA) -func (t *Terminal) csiSendDeviceAttributesHandler(params []string) (renderRequired bool) { - - // we are VT100 - // for DA1 we'll respond ?1;2 - // for DA2 we'll respond >0;0;0 - response := "?1;2" - if len(params) > 0 && len(params[0]) > 0 && params[0][0] == '>' { - response = ">0;0;0" - } - - // write response to source pty - t.WriteToPty([]byte("\x1b[" + response + "c")) - return false -} - -// CSI n -// Device Status Report (DSR) -func (t *Terminal) csiDeviceStatusReportHandler(params []string) (renderRequired bool) { - - if len(params) == 0 { - return false - } - - switch params[0] { - case "5": - t.WriteToPty([]byte("\x1b[0n")) // everything is cool - case "6": // report cursor position - t.WriteToPty([]byte(fmt.Sprintf( - "\x1b[%d;%dR", - t.GetActiveBuffer().CursorLine()+1, - t.GetActiveBuffer().CursorColumn()+1, - ))) - } - - return false -} - -// CSI A -// Cursor Up Ps Times (default = 1) (CUU) -func (t *Terminal) csiCursorUpHandler(params []string) (renderRequired bool) { - distance := 1 - if len(params) > 0 { - var err error - distance, err = strconv.Atoi(params[0]) - if err != nil || distance < 1 { - distance = 1 - } - } - t.GetActiveBuffer().movePosition(0, -int16(distance)) - return true -} - -// CSI B -// Cursor Down Ps Times (default = 1) (CUD) -func (t *Terminal) csiCursorDownHandler(params []string) (renderRequired bool) { - distance := 1 - if len(params) > 0 { - var err error - distance, err = strconv.Atoi(params[0]) - if err != nil || distance < 1 { - distance = 1 - } - } - - t.GetActiveBuffer().movePosition(0, int16(distance)) - return true -} - -// CSI C -// Cursor Forward Ps Times (default = 1) (CUF) -func (t *Terminal) csiCursorForwardHandler(params []string) (renderRequired bool) { - distance := 1 - if len(params) > 0 { - var err error - distance, err = strconv.Atoi(params[0]) - if err != nil || distance < 1 { - distance = 1 - } - } - - t.GetActiveBuffer().movePosition(int16(distance), 0) - return true -} - -// CSI D -// Cursor Backward Ps Times (default = 1) (CUB) -func (t *Terminal) csiCursorBackwardHandler(params []string) (renderRequired bool) { - distance := 1 - if len(params) > 0 { - var err error - distance, err = strconv.Atoi(params[0]) - if err != nil || distance < 1 { - distance = 1 - } - } - - t.GetActiveBuffer().movePosition(-int16(distance), 0) - return true -} - -// CSI E -// Cursor Next Line Ps Times (default = 1) (CNL) -func (t *Terminal) csiCursorNextLineHandler(params []string) (renderRequired bool) { - - distance := 1 - if len(params) > 0 { - var err error - distance, err = strconv.Atoi(params[0]) - if err != nil || distance < 1 { - distance = 1 - } - } - - t.GetActiveBuffer().movePosition(0, int16(distance)) - t.GetActiveBuffer().setPosition(0, t.GetActiveBuffer().CursorLine()) - return true -} - -// CSI F -// Cursor Preceding Line Ps Times (default = 1) (CPL) -func (t *Terminal) csiCursorPrecedingLineHandler(params []string) (renderRequired bool) { - - distance := 1 - if len(params) > 0 { - var err error - distance, err = strconv.Atoi(params[0]) - if err != nil || distance < 1 { - distance = 1 - } - } - t.GetActiveBuffer().movePosition(0, -int16(distance)) - t.GetActiveBuffer().setPosition(0, t.GetActiveBuffer().CursorLine()) - return true -} - -// CSI G -// Cursor Horizontal Absolute [column] (default = [row,1]) (CHA) -func (t *Terminal) csiCursorCharacterAbsoluteHandler(params []string) (renderRequired bool) { - distance := 1 - if len(params) > 0 { - var err error - distance, err = strconv.Atoi(params[0]) - if err != nil || params[0] == "" { - distance = 1 - } - } - - t.GetActiveBuffer().setPosition(uint16(distance-1), t.GetActiveBuffer().CursorLine()) - return true -} - -func parseCursorPosition(params []string) (x, y int) { - x, y = 1, 1 - if len(params) >= 1 { - var err error - if params[0] != "" { - y, err = strconv.Atoi(string(params[0])) - if err != nil || y < 1 { - y = 1 - } - } - } - if len(params) >= 2 { - if params[1] != "" { - var err error - x, err = strconv.Atoi(string(params[1])) - if err != nil || x < 1 { - x = 1 - } - } - } - return x, y -} - -// CSI f -// Horizontal and Vertical Position [row;column] (default = [1,1]) (HVP) -// AND -// CSI H -// Cursor Position [row;column] (default = [1,1]) (CUP) -func (t *Terminal) csiCursorPositionHandler(params []string) (renderRequired bool) { - x, y := parseCursorPosition(params) - t.GetActiveBuffer().setPosition(uint16(x-1), uint16(y-1)) - return true -} - -// CSI S -// Scroll up Ps lines (default = 1) (SU), VT420, ECMA-48 -func (t *Terminal) csiScrollUpHandler(params []string) (renderRequired bool) { - distance := 1 - if len(params) > 1 { - return false - } - if len(params) == 1 { - var err error - distance, err = strconv.Atoi(params[0]) - if err != nil || distance < 1 { - distance = 1 - } - } - t.GetActiveBuffer().areaScrollUp(uint16(distance)) - return true -} - -// CSI @ -// Insert Ps (Blank) Character(s) (default = 1) (ICH) -func (t *Terminal) csiInsertBlankCharactersHandler(params []string) (renderRequired bool) { - count := 1 - if len(params) > 1 { - return false - } - if len(params) == 1 { - var err error - count, err = strconv.Atoi(params[0]) - if err != nil || count < 1 { - count = 1 - } - } - - t.GetActiveBuffer().insertBlankCharacters(count) - return true -} - -// CSI L -// Insert Ps Line(s) (default = 1) (IL) -func (t *Terminal) csiInsertLinesHandler(params []string) (renderRequired bool) { - count := 1 - if len(params) > 1 { - return false - } - if len(params) == 1 { - var err error - count, err = strconv.Atoi(params[0]) - if err != nil || count < 1 { - count = 1 - } - } - - t.GetActiveBuffer().insertLines(count) - return true -} - -// CSI M -// Delete Ps Line(s) (default = 1) (DL) -func (t *Terminal) csiDeleteLinesHandler(params []string) (renderRequired bool) { - count := 1 - if len(params) > 1 { - return false - } - if len(params) == 1 { - var err error - count, err = strconv.Atoi(params[0]) - if err != nil || count < 1 { - count = 1 - } - } - - t.GetActiveBuffer().deleteLines(count) - return true -} - -// CSI T -// Scroll down Ps lines (default = 1) (SD), VT420 -func (t *Terminal) csiScrollDownHandler(params []string) (renderRequired bool) { - distance := 1 - if len(params) > 1 { - return false - } - if len(params) == 1 { - var err error - distance, err = strconv.Atoi(params[0]) - if err != nil || distance < 1 { - distance = 1 - } - } - t.GetActiveBuffer().areaScrollDown(uint16(distance)) - return true -} - -// CSI r -// Set Scrolling Region [top;bottom] (default = full size of window) (DECSTBM), VT100 -func (t *Terminal) csiSetMarginsHandler(params []string) (renderRequired bool) { - top := 1 - bottom := int(t.GetActiveBuffer().ViewHeight()) - - if len(params) > 2 { - return false - } - - if len(params) > 0 { - var err error - top, err = strconv.Atoi(params[0]) - if err != nil || top < 1 { - top = 1 - } - - if len(params) > 1 { - var err error - bottom, err = strconv.Atoi(params[1]) - if err != nil || bottom > int(t.GetActiveBuffer().ViewHeight()) || bottom < 1 { - bottom = int(t.GetActiveBuffer().ViewHeight()) - } - } - } - top-- - bottom-- - - t.activeBuffer.setVerticalMargins(uint(top), uint(bottom)) - t.GetActiveBuffer().setPosition(0, 0) - return true -} - -// CSI X -// Erase Ps Character(s) (default = 1) (ECH) -func (t *Terminal) csiEraseCharactersHandler(params []string) (renderRequired bool) { - count := 1 - if len(params) > 0 { - var err error - count, err = strconv.Atoi(params[0]) - if err != nil || count < 1 { - count = 1 - } - } - - t.GetActiveBuffer().eraseCharacters(count) - return true -} - -// CSI l -// Reset Mode (RM) -func (t *Terminal) csiResetModeHandler(params []string) (renderRequired bool) { - return t.csiSetModes(params, false) -} - -// CSI h -// Set Mode (SM) -func (t *Terminal) csiSetModeHandler(params []string) (renderRequired bool) { - return t.csiSetModes(params, true) -} - -func (t *Terminal) csiSetModes(modes []string, enabled bool) bool { - if len(modes) == 0 { - return false - } - if len(modes) == 1 { - return t.csiSetMode(modes[0], enabled) - } - // should we propagate DEC prefix? - const decPrefix = '?' - isDec := len(modes[0]) > 0 && modes[0][0] == decPrefix - - var render bool - - // iterate through params, propagating DEC prefix to subsequent elements - for i, v := range modes { - updatedMode := v - if i > 0 && isDec { - updatedMode = string(decPrefix) + v - } - render = t.csiSetMode(updatedMode, enabled) || render - } - - return render -} - -func parseModes(mode string) []string { - - var output []string - - if mode == "" { - return nil - } - var prefix string - if mode[0] == '?' { - prefix = "?" - mode = mode[1:] - } - - for len(mode) > 4 { - output = append(output, prefix+mode[:4]) - mode = mode[4:] - } - - output = append(output, prefix+mode) - return output -} - -func (t *Terminal) csiSetMode(modes string, enabled bool) bool { - - for _, modeStr := range parseModes(modes) { - - switch modeStr { - case "4": - t.activeBuffer.modes.ReplaceMode = !enabled - case "20": - t.activeBuffer.modes.LineFeedMode = false - case "?1": - t.activeBuffer.modes.ApplicationCursorKeys = enabled - case "?3": - if t.windowManipulator != nil { - if enabled { - // DECCOLM - COLumn mode, 132 characters per line - t.windowManipulator.ResizeInChars(132, int(t.activeBuffer.viewHeight)) - } else { - // DECCOLM - 80 characters per line (erases screen) - t.windowManipulator.ResizeInChars(80, int(t.activeBuffer.viewHeight)) - } - t.activeBuffer.clear() - } - case "?5": // DECSCNM - t.activeBuffer.modes.ScreenMode = enabled - case "?6": - // DECOM - t.activeBuffer.modes.OriginMode = enabled - case "?7": - // auto-wrap mode - //DECAWM - t.activeBuffer.modes.AutoWrap = enabled - case "?9": - if enabled { - t.mouseMode = (MouseModeX10) - } else { - t.mouseMode = (MouseModeNone) - } - case "?12", "?13": - t.activeBuffer.modes.BlinkingCursor = enabled - case "?25": - t.activeBuffer.modes.ShowCursor = enabled - case "?47", "?1047": - if enabled { - t.useAltBuffer() - } else { - t.useMainBuffer() - } - case "?1000": // ?10061000 seen from htop - // enable mouse tracking - // 1000 refers to ext mode for extended mouse click area - otherwise only x <= 255-31 - if enabled { - t.mouseMode = (MouseModeVT200) - } else { - t.mouseMode = (MouseModeNone) - } - case "?1002": - if enabled { - t.mouseMode = (MouseModeButtonEvent) - } else { - t.mouseMode = (MouseModeNone) - } - case "?1003": - if enabled { - t.mouseMode = MouseModeAnyEvent - } else { - t.mouseMode = MouseModeNone - } - case "?1005": - if enabled { - t.mouseExtMode = MouseExtUTF - } else { - t.mouseExtMode = MouseExtNone - } - - case "?1006": - if enabled { - t.mouseExtMode = MouseExtSGR - } else { - t.mouseExtMode = (MouseExtNone) - } - case "?1015": - if enabled { - t.mouseExtMode = (MouseExtURXVT) - } else { - t.mouseExtMode = (MouseExtNone) - } - case "?1048": - if enabled { - t.GetActiveBuffer().saveCursor() - } else { - t.GetActiveBuffer().restoreCursor() - } - case "?1049": - if enabled { - t.useAltBuffer() - } else { - t.useMainBuffer() - } - case "?2004": - t.activeBuffer.modes.BracketedPasteMode = enabled - case "?80": - t.activeBuffer.modes.SixelScrolling = enabled - default: - t.log("Unsupported CSI mode %s = %t", modeStr, enabled) - } - } - return false -} - -// CSI d -// Line Position Absolute [row] (default = [1,column]) (VPA) -func (t *Terminal) csiLinePositionAbsoluteHandler(params []string) (renderRequired bool) { - row := 1 - if len(params) > 0 { - var err error - row, err = strconv.Atoi(params[0]) - if err != nil || row < 1 { - row = 1 - } - } - - t.GetActiveBuffer().setPosition(t.GetActiveBuffer().CursorColumn(), uint16(row-1)) - - return true -} - -// CSI P -// Delete Ps Character(s) (default = 1) (DCH) -func (t *Terminal) csiDeleteHandler(params []string) (renderRequired bool) { - n := 1 - if len(params) >= 1 { - var err error - n, err = strconv.Atoi(params[0]) - if err != nil || n < 1 { - n = 1 - } - } - - t.GetActiveBuffer().deleteChars(n) - return true -} - -// CSI g -// tab clear (TBC) -func (t *Terminal) csiTabClearHandler(params []string) (renderRequired bool) { - n := "0" - if len(params) > 0 { - n = params[0] - } - switch n { - case "0", "": - t.activeBuffer.tabClearAtCursor() - case "3": - t.activeBuffer.tabReset() - default: - return false - } - - return true -} - -// CSI J -// Erase in Display (ED), VT100 -func (t *Terminal) csiEraseInDisplayHandler(params []string) (renderRequired bool) { - n := "0" - if len(params) > 0 { - n = params[0] - } - - switch n { - case "0", "": - t.GetActiveBuffer().eraseDisplayFromCursor() - case "1": - t.GetActiveBuffer().eraseDisplayToCursor() - case "2", "3": - t.GetActiveBuffer().eraseDisplay() - default: - return false - } - - return true -} - -// CSI K -// Erase in Line (EL), VT100 -func (t *Terminal) csiEraseInLineHandler(params []string) (renderRequired bool) { - - n := "0" - if len(params) > 0 { - n = params[0] - } - - switch n { - case "0", "": //erase adter cursor - t.GetActiveBuffer().eraseLineFromCursor() - case "1": // erase to cursor inclusive - t.GetActiveBuffer().eraseLineToCursor() - case "2": // erase entire - t.GetActiveBuffer().eraseLine() - default: - return false - } - return true -} - -// CSI m -// Character Attributes (SGR) -func (t *Terminal) sgrSequenceHandler(params []string) bool { - - if len(params) == 0 { - params = []string{"0"} - } - - for i := range params { - - p := strings.Replace(strings.Replace(params[i], "[", "", -1), "]", "", -1) - - switch p { - case "00", "0", "": - attr := t.GetActiveBuffer().getCursorAttr() - *attr = CellAttributes{} - case "1", "01": - t.GetActiveBuffer().getCursorAttr().bold = true - t.GetActiveBuffer().getCursorAttr().dim = false - case "2", "02": - t.GetActiveBuffer().getCursorAttr().bold = false - t.GetActiveBuffer().getCursorAttr().dim = true - case "3", "03": - t.GetActiveBuffer().getCursorAttr().italic = true - case "4", "04": - t.GetActiveBuffer().getCursorAttr().underline = true - case "5", "05": - t.GetActiveBuffer().getCursorAttr().blink = true - case "7", "07": - t.GetActiveBuffer().getCursorAttr().inverse = true - case "8", "08": - t.GetActiveBuffer().getCursorAttr().hidden = true - case "9", "09": - t.GetActiveBuffer().getCursorAttr().strikethrough = true - case "21": - t.GetActiveBuffer().getCursorAttr().bold = false - case "22": - t.GetActiveBuffer().getCursorAttr().dim = false - t.GetActiveBuffer().getCursorAttr().bold = false - case "23": - t.GetActiveBuffer().getCursorAttr().italic = false - case "24": - t.GetActiveBuffer().getCursorAttr().underline = false - case "25": - t.GetActiveBuffer().getCursorAttr().blink = false - case "27": - t.GetActiveBuffer().getCursorAttr().inverse = false - case "28": - t.GetActiveBuffer().getCursorAttr().hidden = false - case "29": - t.GetActiveBuffer().getCursorAttr().strikethrough = false - case "38": // set foreground - t.GetActiveBuffer().getCursorAttr().fgColour, _ = ColourFromAnsi(params[i+1:], false) - return false - case "48": // set background - t.GetActiveBuffer().getCursorAttr().bgColour, _ = ColourFromAnsi(params[i+1:], true) - return false - case "39": - t.GetActiveBuffer().getCursorAttr().fgColour = DefaultForeground() - case "49": - t.GetActiveBuffer().getCursorAttr().bgColour = DefaultBackground() - default: - bi, err := strconv.Atoi(p) - if err != nil { - return false - } - i := byte(bi) - switch true { - case i >= 30 && i <= 37, i >= 90 && i <= 97: - t.GetActiveBuffer().getCursorAttr().fgColour = ColourFrom4Bit(i) - case i >= 40 && i <= 47, i >= 100 && i <= 107: - t.GetActiveBuffer().getCursorAttr().bgColour = ColourFrom4Bit(i) - } - - } - } - - x := t.GetActiveBuffer().CursorColumn() - y := t.GetActiveBuffer().CursorLine() - if cell := t.GetActiveBuffer().GetCell(x, y); cell != nil { - cell.attr = t.GetActiveBuffer().cursorAttr - } - - return false -} - -func (t *Terminal) csiSoftResetHandler(params []string) bool { - t.reset() - return true -} - -func (t *Terminal) csiCursorSelection(params []string) (renderRequired bool) { - if len(params) == 0 { - return false - } - i, err := strconv.Atoi(params[0]) - if err != nil { - return false - } - t.GetActiveBuffer().SetCursorShape(CursorShape(i)) - return true -} diff --git a/cmd/darktile/termutil/line.go b/cmd/darktile/termutil/line.go deleted file mode 100644 index 0b76251c4b..0000000000 --- a/cmd/darktile/termutil/line.go +++ /dev/null @@ -1,66 +0,0 @@ -package termutil - -import "strings" - -type Line struct { - wrapped bool // whether line was wrapped onto from the previous one - cells []Cell -} - -func newLine() Line { - return Line{ - wrapped: false, - cells: []Cell{}, - } -} - -func (line *Line) Len() uint16 { - return uint16(len(line.cells)) -} - -func (line *Line) String() string { - runes := []rune{} - for _, cell := range line.cells { - runes = append(runes, cell.r.Rune) - } - return strings.TrimRight(string(runes), "\x00") -} - -func (line *Line) append(cells ...Cell) { - line.cells = append(line.cells, cells...) -} - -func (line *Line) shrink(width uint16) { - if line.Len() <= width { - return - } - remove := line.Len() - width - var cells []Cell - for _, cell := range line.cells { - if cell.r.Rune == 0 && remove > 0 { - remove-- - } else { - cells = append(cells, cell) - } - } - line.cells = cells -} - -func (line *Line) wrap(width uint16) []Line { - - var output []Line - var current Line - - current.wrapped = line.wrapped - - for _, cell := range line.cells { - if len(current.cells) == int(width) { - output = append(output, current) - current = newLine() - current.wrapped = true - } - current.cells = append(current.cells, cell) - } - - return append(output, current) -} diff --git a/cmd/darktile/termutil/measured-rune.go b/cmd/darktile/termutil/measured-rune.go deleted file mode 100644 index 3d86f7ad01..0000000000 --- a/cmd/darktile/termutil/measured-rune.go +++ /dev/null @@ -1,6 +0,0 @@ -package termutil - -type MeasuredRune struct { - Rune rune - Width int -} diff --git a/cmd/darktile/termutil/modes.go b/cmd/darktile/termutil/modes.go deleted file mode 100644 index 5ded93bd2f..0000000000 --- a/cmd/darktile/termutil/modes.go +++ /dev/null @@ -1,30 +0,0 @@ -package termutil - -type Modes struct { - ShowCursor bool - ApplicationCursorKeys bool - BlinkingCursor bool - ReplaceMode bool // overwrite character at cursor or insert new - OriginMode bool // see DECOM docs - whether cursor is positioned within the margins or not - LineFeedMode bool - ScreenMode bool // DECSCNM (black on white background) - AutoWrap bool - SixelScrolling bool // DECSDM - BracketedPasteMode bool -} - -type MouseMode uint -type MouseExtMode uint - -const ( - MouseModeNone MouseMode = iota - MouseModeX10 - MouseModeVT200 - MouseModeVT200Highlight - MouseModeButtonEvent - MouseModeAnyEvent - MouseExtNone MouseExtMode = iota - MouseExtUTF - MouseExtSGR - MouseExtURXVT -) diff --git a/cmd/darktile/termutil/options.go b/cmd/darktile/termutil/options.go deleted file mode 100644 index bcd290815a..0000000000 --- a/cmd/darktile/termutil/options.go +++ /dev/null @@ -1,35 +0,0 @@ -package termutil - -import ( - "os" -) - -type Option func(t *Terminal) - -func WithLogFile(path string) Option { - return func(t *Terminal) { - if path == "-" { - t.logFile = os.Stdout - return - } - t.logFile, _ = os.Create(path) - } -} - -func WithShell(shell string) Option { - return func(t *Terminal) { - t.shell = shell - } -} - -func WithInitialCommand(cmd string) Option { - return func(t *Terminal) { - t.initialCommand = cmd + "\n" - } -} - -func WithWindowManipulator(m WindowManipulator) Option { - return func(t *Terminal) { - t.windowManipulator = m - } -} diff --git a/cmd/darktile/termutil/osc.go b/cmd/darktile/termutil/osc.go deleted file mode 100644 index af199d151b..0000000000 --- a/cmd/darktile/termutil/osc.go +++ /dev/null @@ -1,69 +0,0 @@ -package termutil - -import ( - "fmt" -) - -func (t *Terminal) handleOSC(readChan chan MeasuredRune) (renderRequired bool) { - - params := []string{} - param := "" - -READ: - for { - select { - case b := <-readChan: - if t.isOSCTerminator(b.Rune) { - params = append(params, param) - break READ - } - if b.Rune == ';' { - params = append(params, param) - param = "" - continue - } - param = fmt.Sprintf("%s%c", param, b.Rune) - default: - return false - } - } - - if len(params) == 0 { - return false - } - - pT := params[len(params)-1] - pS := params[:len(params)-1] - - if len(pS) == 0 { - pS = []string{pT} - pT = "" - } - - switch pS[0] { - case "0", "2", "l": - t.setTitle(pT) - case "10": // get/set foreground colour - if len(pS) > 1 { - if pS[1] == "?" { - t.WriteToPty([]byte("\x1b]10;15")) - } - } - case "11": // get/set background colour - if len(pS) > 1 { - if pS[1] == "?" { - t.WriteToPty([]byte("\x1b]10;0")) - } - } - } - return false -} - -func (t *Terminal) isOSCTerminator(r rune) bool { - for _, terminator := range oscTerminators { - if terminator == r { - return true - } - } - return false -} diff --git a/cmd/darktile/termutil/resize.go b/cmd/darktile/termutil/resize.go deleted file mode 100644 index 5be4bb25a3..0000000000 --- a/cmd/darktile/termutil/resize.go +++ /dev/null @@ -1,93 +0,0 @@ -package termutil - -func (buffer *Buffer) shrink(width uint16) { - - var replace []Line - - prevCursor := int(buffer.cursorPosition.Line) - - for i, line := range buffer.lines { - - line.shrink(width) - - // this line fits within the new width restriction, keep it as is and continue - if line.Len() <= width { - replace = append(replace, line) - continue - } - - wrappedLines := line.wrap(width) - - if prevCursor >= i { - buffer.cursorPosition.Line += uint64(len(wrappedLines) - 1) - - } - - replace = append(replace, wrappedLines...) - } - - buffer.cursorPosition.Col = buffer.cursorPosition.Col % width - - buffer.lines = replace -} - -func (buffer *Buffer) grow(width uint16) { - - var replace []Line - var current Line - - prevCursor := int(buffer.cursorPosition.Line) - - for i, line := range buffer.lines { - - if !line.wrapped { - if i > 0 { - replace = append(replace, current) - } - current = newLine() - } - - if i == prevCursor { - buffer.cursorPosition.Line -= uint64(i - len(replace)) - } - - for _, cell := range line.cells { - if len(current.cells) == int(width) { - replace = append(replace, current) - current = newLine() - current.wrapped = true - } - current.cells = append(current.cells, cell) - } - - } - - replace = append(replace, current) - - buffer.lines = replace -} - -// deprecated -func (buffer *Buffer) resizeView(width uint16, height uint16) { - - if buffer.viewHeight == 0 { - buffer.viewWidth = width - buffer.viewHeight = height - return - } - - // scroll to bottom - buffer.scrollLinesFromBottom = 0 - - if width < buffer.viewWidth { // wrap lines if we're shrinking - buffer.shrink(width) - buffer.grow(width) - } else if width > buffer.viewWidth { // unwrap lines if we're growing - buffer.grow(width) - } - - buffer.viewWidth = width - buffer.viewHeight = height - - buffer.resetVerticalMargins(uint(buffer.viewHeight)) -} diff --git a/cmd/darktile/termutil/selection.go b/cmd/darktile/termutil/selection.go deleted file mode 100644 index c4acbae9fa..0000000000 --- a/cmd/darktile/termutil/selection.go +++ /dev/null @@ -1,324 +0,0 @@ -package termutil - -func (buffer *Buffer) ClearSelection() { - buffer.selectionMu.Lock() - defer buffer.selectionMu.Unlock() - buffer.selectionStart = nil - buffer.selectionEnd = nil -} - -func (buffer *Buffer) GetBoundedTextAtPosition(pos Position) (start Position, end Position, text string, textIndex int, found bool) { - return buffer.FindWordAt(pos, func(r rune) bool { - return r > 0 && r < 256 - }) -} - -// if the selection is invalid - e.g. lines are selected that no longer exist in the buffer -func (buffer *Buffer) fixSelection() bool { - buffer.selectionMu.Lock() - defer buffer.selectionMu.Unlock() - - if buffer.selectionStart == nil || buffer.selectionEnd == nil { - return false - } - - if buffer.selectionStart.Line >= uint64(len(buffer.lines)) { - buffer.selectionStart.Line = uint64(len(buffer.lines)) - 1 - } - - if buffer.selectionEnd.Line >= uint64(len(buffer.lines)) { - buffer.selectionEnd.Line = uint64(len(buffer.lines)) - 1 - } - - if buffer.selectionStart.Col >= uint16(len(buffer.lines[buffer.selectionStart.Line].cells)) { - buffer.selectionStart.Col = 0 - if buffer.selectionStart.Line < uint64(len(buffer.lines))-1 { - buffer.selectionStart.Line++ - } - } - - if buffer.selectionEnd.Col >= uint16(len(buffer.lines[buffer.selectionEnd.Line].cells)) { - buffer.selectionEnd.Col = uint16(len(buffer.lines[buffer.selectionEnd.Line].cells)) - 1 - } - - return true -} - -func (buffer *Buffer) ExtendSelectionToEntireLines() { - if !buffer.fixSelection() { - return - } - - buffer.selectionMu.Lock() - defer buffer.selectionMu.Unlock() - - buffer.selectionStart.Col = 0 - buffer.selectionEnd.Col = uint16(len(buffer.lines[buffer.selectionEnd.Line].cells)) - 1 -} - -type RuneMatcher func(r rune) bool - -func (buffer *Buffer) SelectWordAt(pos Position, runeMatcher RuneMatcher) { - start, end, _, _, found := buffer.FindWordAt(pos, runeMatcher) - if !found { - return - } - buffer.setRawSelectionStart(start) - buffer.setRawSelectionEnd(end) -} - -// takes raw coords -func (buffer *Buffer) Highlight(start Position, end Position, annotation *Annotation) { - buffer.highlightStart = &start - buffer.highlightEnd = &end - buffer.highlightAnnotation = annotation -} - -func (buffer *Buffer) ClearHighlight() { - buffer.highlightStart = nil - buffer.highlightEnd = nil -} - -// returns raw lines -func (buffer *Buffer) FindWordAt(pos Position, runeMatcher RuneMatcher) (start Position, end Position, text string, textIndex int, found bool) { - line := buffer.convertViewLineToRawLine(uint16(pos.Line)) - col := pos.Col - - if line >= uint64(len(buffer.lines)) { - return - } - if col >= uint16(len(buffer.lines[line].cells)) { - return - } - - if !runeMatcher(buffer.lines[line].cells[col].r.Rune) { - return - } - - found = true - - start = Position{ - Line: line, - Col: col, - } - end = Position{ - Line: line, - Col: col, - } - - var startCol uint16 -BACK: - for y := int(line); y >= 0; y-- { - if y == int(line) { - startCol = col - } else { - if len(buffer.lines[y].cells) < int(buffer.viewWidth) { - break - } - startCol = uint16(len(buffer.lines[y].cells) - 1) - } - for x := int(startCol); x >= 0; x-- { - if runeMatcher(buffer.lines[y].cells[x].r.Rune) { - start = Position{ - Line: uint64(y), - Col: uint16(x), - } - text = string(buffer.lines[y].cells[x].r.Rune) + text - } else { - break BACK - } - } - - } - textIndex = len([]rune(text)) - 1 -FORWARD: - for y := uint64(line); y < uint64(len(buffer.lines)); y++ { - if y == line { - startCol = col + 1 - } else { - startCol = 0 - } - for x := int(startCol); x < len(buffer.lines[y].cells); x++ { - if runeMatcher(buffer.lines[y].cells[x].r.Rune) { - end = Position{ - Line: y, - Col: uint16(x), - } - text = text + string(buffer.lines[y].cells[x].r.Rune) - } else { - break FORWARD - } - } - if len(buffer.lines[y].cells) < int(buffer.viewWidth) { - break - } - } - - return -} - -func (buffer *Buffer) SetSelectionStart(pos Position) { - buffer.selectionMu.Lock() - defer buffer.selectionMu.Unlock() - buffer.selectionStart = &Position{ - Col: pos.Col, - Line: buffer.convertViewLineToRawLine(uint16(pos.Line)), - } -} - -func (buffer *Buffer) setRawSelectionStart(pos Position) { - buffer.selectionMu.Lock() - defer buffer.selectionMu.Unlock() - buffer.selectionStart = &pos -} - -func (buffer *Buffer) SetSelectionEnd(pos Position) { - buffer.selectionMu.Lock() - defer buffer.selectionMu.Unlock() - buffer.selectionEnd = &Position{ - Col: pos.Col, - Line: buffer.convertViewLineToRawLine(uint16(pos.Line)), - } -} - -func (buffer *Buffer) setRawSelectionEnd(pos Position) { - buffer.selectionMu.Lock() - defer buffer.selectionMu.Unlock() - buffer.selectionEnd = &pos -} - -func (buffer *Buffer) GetSelection() (string, *Selection) { - if !buffer.fixSelection() { - return "", nil - } - - buffer.selectionMu.Lock() - defer buffer.selectionMu.Unlock() - - start := *buffer.selectionStart - end := *buffer.selectionEnd - - if end.Line < start.Line || (end.Line == start.Line && end.Col < start.Col) { - swap := end - end = start - start = swap - } - - var text string - for y := start.Line; y <= end.Line; y++ { - if y >= uint64(len(buffer.lines)) { - break - } - line := buffer.lines[y] - startX := 0 - endX := len(line.cells) - 1 - if y == start.Line { - startX = int(start.Col) - } - if y == end.Line { - endX = int(end.Col) - } - if y > start.Line { - text += "\n" - } - for x := startX; x <= endX; x++ { - if x >= len(line.cells) { - break - } - mr := line.cells[x].Rune() - if mr.Width == 0 { - continue - } - x += mr.Width - 1 - text += string(mr.Rune) - } - } - - viewSelection := Selection{ - Start: start, - End: end, - } - - viewSelection.Start.Line = uint64(buffer.convertRawLineToViewLine(viewSelection.Start.Line)) - viewSelection.End.Line = uint64(buffer.convertRawLineToViewLine(viewSelection.End.Line)) - return text, &viewSelection -} - -func (buffer *Buffer) InSelection(pos Position) bool { - - if !buffer.fixSelection() { - return false - } - buffer.selectionMu.Lock() - defer buffer.selectionMu.Unlock() - - start := *buffer.selectionStart - end := *buffer.selectionEnd - - if end.Line < start.Line || (end.Line == start.Line && end.Col < start.Col) { - swap := end - end = start - start = swap - } - - rY := buffer.convertViewLineToRawLine(uint16(pos.Line)) - if rY < start.Line { - return false - } - if rY > end.Line { - return false - } - if rY == start.Line { - if pos.Col < start.Col { - return false - } - } - if rY == end.Line { - if pos.Col > end.Col { - return false - } - } - - return true -} - -func (buffer *Buffer) GetHighlightAnnotation() *Annotation { - return buffer.highlightAnnotation -} - -func (buffer *Buffer) GetViewHighlight() (start Position, end Position, exists bool) { - - if buffer.highlightStart == nil || buffer.highlightEnd == nil { - return - } - - if buffer.highlightStart.Line >= uint64(len(buffer.lines)) { - return - } - - if buffer.highlightEnd.Line >= uint64(len(buffer.lines)) { - return - } - - if buffer.highlightStart.Col >= uint16(len(buffer.lines[buffer.highlightStart.Line].cells)) { - return - } - - if buffer.highlightEnd.Col >= uint16(len(buffer.lines[buffer.highlightEnd.Line].cells)) { - return - } - - start = *buffer.highlightStart - end = *buffer.highlightEnd - - if end.Line < start.Line || (end.Line == start.Line && end.Col < start.Col) { - swap := end - end = start - start = swap - } - - start.Line = uint64(buffer.convertRawLineToViewLine(start.Line)) - end.Line = uint64(buffer.convertRawLineToViewLine(end.Line)) - - return start, end, true -} diff --git a/cmd/darktile/termutil/sixel.go b/cmd/darktile/termutil/sixel.go deleted file mode 100644 index cc4459eade..0000000000 --- a/cmd/darktile/termutil/sixel.go +++ /dev/null @@ -1,105 +0,0 @@ -package termutil - -import ( - "github.com/sst/sst/v3/cmd/darktile/sixel" - "image" - "math" - "strings" -) - -type Sixel struct { - X uint16 - Y uint64 // raw line - Width uint64 - Height uint64 - Image image.Image -} - -type VisibleSixel struct { - ViewLineOffset int - Sixel Sixel -} - -func (b *Buffer) addSixel(img image.Image, widthCells int, heightCells int) { - b.sixels = append(b.sixels, Sixel{ - X: b.CursorColumn(), - Y: b.cursorPosition.Line, - Width: uint64(widthCells), - Height: uint64(heightCells), - Image: img, - }) - if b.modes.SixelScrolling { - b.cursorPosition.Line += uint64(heightCells) - } -} - -func (b *Buffer) clearSixelsAtRawLine(rawLine uint64) { - var filtered []Sixel - - for _, sixelImage := range b.sixels { - if sixelImage.Y+sixelImage.Height-1 >= rawLine && sixelImage.Y <= rawLine { - continue - } - - filtered = append(filtered, sixelImage) - } - - b.sixels = filtered -} - -func (b *Buffer) GetVisibleSixels() []VisibleSixel { - - firstLine := b.convertViewLineToRawLine(0) - lastLine := b.convertViewLineToRawLine(b.viewHeight - 1) - - var visible []VisibleSixel - - for _, sixelImage := range b.sixels { - if sixelImage.Y+sixelImage.Height-1 < firstLine { - continue - } - if sixelImage.Y > lastLine { - continue - } - - visible = append(visible, VisibleSixel{ - ViewLineOffset: int(sixelImage.Y) - int(firstLine), - Sixel: sixelImage, - }) - } - - return visible -} - -func (t *Terminal) handleSixel(readChan chan MeasuredRune) (renderRequired bool) { - - var data []rune - - var inEscape bool - - for { - r := <-readChan - - switch r.Rune { - case 0x1b: - inEscape = true - continue - case 0x5c: - if inEscape { - img, err := sixel.Decode(strings.NewReader(string(data)), DefaultBackground()) - if err != nil { - return false - } - w, h := t.windowManipulator.CellSizeInPixels() - cw := int(math.Ceil(float64(img.Bounds().Dx()) / float64(w))) - ch := int(math.Ceil(float64(img.Bounds().Dy()) / float64(h))) - t.activeBuffer.addSixel(img, cw, ch) - return true - } - } - - inEscape = false - - data = append(data, r.Rune) - } -} diff --git a/cmd/darktile/termutil/terminal.go b/cmd/darktile/termutil/terminal.go deleted file mode 100644 index 3c4b1a5347..0000000000 --- a/cmd/darktile/termutil/terminal.go +++ /dev/null @@ -1,321 +0,0 @@ -package termutil - -import ( - "bufio" - "bytes" - "fmt" - "io" - "os" - "os/exec" - "strings" - "sync" - - "github.com/creack/pty" - "golang.org/x/term" -) - -const ( - MainBuffer uint8 = 0 - AltBuffer uint8 = 1 - InternalBuffer uint8 = 2 -) - -// Terminal communicates with the underlying terminal -type Terminal struct { - mu sync.Mutex - windowManipulator WindowManipulator - pty *os.File - updateChan chan struct{} - processChan chan MeasuredRune - closeChan chan struct{} - buffers []*Buffer - activeBuffer *Buffer - mouseMode MouseMode - mouseExtMode MouseExtMode - logFile *os.File - running bool - shell string - initialCommand string -} - -// NewTerminal creates a new terminal instance -func New(options ...Option) *Terminal { - term := &Terminal{ - processChan: make(chan MeasuredRune, 0xffff), - closeChan: make(chan struct{}), - } - for _, opt := range options { - opt(term) - } - fg := DefaultForeground() - bg := DefaultBackground() - term.buffers = []*Buffer{ - NewBuffer(1, 1, 0xffff, fg, bg), - NewBuffer(1, 1, 0xffff, fg, bg), - NewBuffer(1, 1, 0xffff, fg, bg), - } - term.activeBuffer = term.buffers[0] - return term -} - -func (t *Terminal) SetWindowManipulator(m WindowManipulator) { - t.windowManipulator = m -} - -func (t *Terminal) log(line string, params ...interface{}) { - if t.logFile != nil { - _, _ = fmt.Fprintf(t.logFile, line+"\n", params...) - } -} - -func (t *Terminal) reset() { - fg := DefaultForeground() - bg := DefaultBackground() - t.buffers = []*Buffer{ - NewBuffer(1, 1, 0xffff, fg, bg), - NewBuffer(1, 1, 0xffff, fg, bg), - NewBuffer(1, 1, 0xffff, fg, bg), - } - t.useMainBuffer() -} - -// Pty exposes the underlying terminal pty, if it exists -func (t *Terminal) Pty() *os.File { - return t.pty -} - -func (t *Terminal) WriteToPty(data []byte) error { - _, err := t.pty.Write(data) - return err -} - -func (t *Terminal) GetTitle() string { - return t.windowManipulator.GetTitle() -} - -// write takes data from StdOut of the child shell and processes it -func (t *Terminal) Write(data []byte) (n int, err error) { - reader := bufio.NewReader(bytes.NewBuffer(data)) - for { - r, size, err := reader.ReadRune() - if err == io.EOF { - break - } - t.processChan <- MeasuredRune{Rune: r, Width: size} - } - return len(data), nil -} - -func (t *Terminal) SetSize(rows, cols uint16) error { - if t.pty == nil { - return fmt.Errorf("terminal is not running") - } - - t.log("RESIZE %d, %d\n", cols, rows) - - t.activeBuffer.resizeView(cols, rows) - - if err := pty.Setsize(t.pty, &pty.Winsize{ - Rows: rows, - Cols: cols, - }); err != nil { - return err - } - - return nil -} - -// Run starts the terminal/shell proxying process -func (t *Terminal) Run(updateChan chan struct{}, rows uint16, cols uint16) error { - os.Setenv("TERM", "xterm-256color") - os.Setenv("COLORTERM", "truecolor") - t.updateChan = updateChan - - if t.shell == "" { - t.shell = os.Getenv("SHELL") - if t.shell == "" { - t.shell = "/bin/sh" - } - } - - // Create arbitrary command. - fields := strings.Fields(t.shell) - c := exec.Command(fields[0], fields[1:]...) - c.Env = os.Environ() - - // Start the command with a pty. - var err error - t.pty, err = pty.Start(c) - if err != nil { - return err - } - // Make sure to close the pty at the end. - defer func() { _ = t.pty.Close() }() // Best effort. - - if err := t.SetSize(rows, cols); err != nil { - return err - } - - // Set stdin in raw mode. - - if fd := int(os.Stdin.Fd()); term.IsTerminal(fd) { - oldState, err := term.MakeRaw(fd) - if err != nil { - t.windowManipulator.ReportError(err) - } - defer func() { _ = term.Restore(fd, oldState) }() // Best effort. - } - - go t.process() - - t.running = true - - if t.windowManipulator != nil { - t.windowManipulator.SetTitle("darktile") - } - - if t.initialCommand != "" { - if err := t.WriteToPty([]byte(t.initialCommand)); err != nil { - return err - } - } - - _, _ = io.Copy(t, t.pty) - close(t.closeChan) - return nil -} - -func (t *Terminal) IsRunning() bool { - return t.running -} - -func (t *Terminal) requestRender() { - select { - case t.updateChan <- struct{}{}: - default: - } -} - -func (t *Terminal) processSequence(mr MeasuredRune) (render bool) { - if mr.Rune == 0x1b { - return t.handleANSI(t.processChan) - } - return t.processRunes(mr) -} - -func (t *Terminal) process() { - for { - select { - case <-t.closeChan: - return - case mr := <-t.processChan: - if t.processSequence(mr) { - t.requestRender() - } - } - } -} - -func (t *Terminal) processRunes(runes ...MeasuredRune) (renderRequired bool) { - t.mu.Lock() - defer t.mu.Unlock() - - for _, r := range runes { - - t.log("%c 0x%X", r.Rune, r.Rune) - - switch r.Rune { - case 0x05: //enq - continue - case 0x07: //bell - //DING DING DING - continue - case 0x8: //backspace - t.activeBuffer.backspace() - renderRequired = true - case 0x9: //tab - t.activeBuffer.tab() - renderRequired = true - case 0xa, 0xc: //newLine/form feed - t.activeBuffer.newLine() - renderRequired = true - case 0xb: //vertical tab - t.activeBuffer.verticalTab() - renderRequired = true - case 0xd: //carriageReturn - t.activeBuffer.carriageReturn() - renderRequired = true - case 0xe: //shiftOut - t.activeBuffer.currentCharset = 1 - case 0xf: //shiftIn - t.activeBuffer.currentCharset = 0 - default: - if r.Rune < 0x20 { - // handle any other control chars here? - continue - } - - t.activeBuffer.write(t.translateRune(r)) - renderRequired = true - } - } - - return renderRequired -} - -func (t *Terminal) translateRune(b MeasuredRune) MeasuredRune { - table := t.activeBuffer.charsets[t.activeBuffer.currentCharset] - if table == nil { - return b - } - chr, ok := (*table)[b.Rune] - if ok { - return MeasuredRune{Rune: chr, Width: 1} - } - return b -} - -func (t *Terminal) setTitle(title string) { - t.windowManipulator.SetTitle(title) -} - -func (t *Terminal) switchBuffer(index uint8) { - var carrySize bool - var w, h uint16 - if t.activeBuffer != nil { - w, h = t.activeBuffer.viewWidth, t.activeBuffer.viewHeight - carrySize = true - } - t.activeBuffer = t.buffers[index] - if carrySize { - t.activeBuffer.resizeView(w, h) - } -} - -func (t *Terminal) GetMouseMode() MouseMode { - return t.mouseMode -} - -func (t *Terminal) GetMouseExtMode() MouseExtMode { - return t.mouseExtMode -} - -func (t *Terminal) GetActiveBuffer() *Buffer { - return t.activeBuffer -} - -func (t *Terminal) useMainBuffer() { - t.switchBuffer(MainBuffer) -} - -func (t *Terminal) useAltBuffer() { - t.switchBuffer(AltBuffer) -} - -func (t *Terminal) Lock() { - t.mu.Lock() -} - -func (t *Terminal) Unlock() { - t.mu.Unlock() -} diff --git a/cmd/darktile/termutil/theme.go b/cmd/darktile/termutil/theme.go deleted file mode 100644 index 20b03e4b68..0000000000 --- a/cmd/darktile/termutil/theme.go +++ /dev/null @@ -1,162 +0,0 @@ -package termutil - -import ( - "fmt" - "log/slog" - "strconv" - - "github.com/charmbracelet/lipgloss" -) - -type Colour uint8 - -// See https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit -const ( - ColourBlack Colour = iota - ColourRed - ColourGreen - ColourYellow - ColourBlue - ColourMagenta - ColourCyan - ColourWhite - ColourBrightBlack - ColourBrightRed - ColourBrightGreen - ColourBrightYellow - ColourBrightBlue - ColourBrightMagenta - ColourBrightCyan - ColourBrightWhite - ColourBackground - ColourForeground - ColourSelectionBackground - ColourSelectionForeground - ColourCursorForeground - ColourCursorBackground -) - -var ( - map4Bit = map[uint8]Colour{ - 30: ColourBlack, - 31: ColourRed, - 32: ColourGreen, - 33: ColourYellow, - 34: ColourBlue, - 35: ColourMagenta, - 36: ColourCyan, - 37: ColourWhite, - 90: ColourBrightBlack, - 91: ColourBrightRed, - 92: ColourBrightGreen, - 93: ColourBrightYellow, - 94: ColourBrightBlue, - 95: ColourBrightMagenta, - 96: ColourBrightCyan, - 97: ColourBrightWhite, - 40: ColourBlack, - 41: ColourRed, - 42: ColourGreen, - 43: ColourYellow, - 44: ColourBlue, - 45: ColourMagenta, - 46: ColourCyan, - 47: ColourWhite, - 100: ColourBrightBlack, - 101: ColourBrightRed, - 102: ColourBrightGreen, - 103: ColourBrightYellow, - 104: ColourBrightBlue, - 105: ColourBrightMagenta, - 106: ColourBrightCyan, - 107: ColourBrightWhite, - } -) - -func ColourFrom4Bit(code uint8) lipgloss.TerminalColor { - colour, ok := map4Bit[code] - if !ok { - return lipgloss.NoColor{} - } - return lipgloss.ANSIColor(colour) -} - -func DefaultBackground() lipgloss.TerminalColor { - // red - return lipgloss.ANSIColor(ColourBrightRed) -} - -func DefaultForeground() lipgloss.TerminalColor { - return lipgloss.NoColor{} -} - -func ColourFrom8Bit(n string) (lipgloss.TerminalColor, error) { - index, err := strconv.Atoi(n) - if err != nil { - return nil, err - } - slog.Info("converting", "index", index) - - if index < 16 { - return lipgloss.ANSIColor(index), nil - } - - if index >= 232 { - c := ((index - 232) * 0xff) / 0x18 - hex := fmt.Sprintf("#%02x%02x%02x", c, c, c) - return lipgloss.Color(hex), nil - } - - var r, g, b uint8 - indexR := ((index - 16) / 36) - if indexR > 0 { - r = uint8(55 + indexR*40) - } - indexG := (((index - 16) % 36) / 6) - if indexG > 0 { - g = uint8(55 + indexG*40) - } - indexB := ((index - 16) % 6) - if indexB > 0 { - b = uint8(55 + indexB*40) - } - hex := fmt.Sprintf("#%02x%02x%02x", r, g, b) - return lipgloss.Color(hex), nil -} - -func ColourFrom24Bit(r, g, b string) (lipgloss.TerminalColor, error) { - ri, err := strconv.Atoi(r) - if err != nil { - return nil, err - } - gi, err := strconv.Atoi(g) - if err != nil { - return nil, err - } - bi, err := strconv.Atoi(b) - if err != nil { - return nil, err - } - hex := fmt.Sprintf("#%02x%02x%02x", ri, gi, bi) - return lipgloss.Color(hex), nil -} - -func ColourFromAnsi(ansi []string, bg bool) (lipgloss.TerminalColor, error) { - if len(ansi) == 0 { - return nil, fmt.Errorf("invalid ansi colour code") - } - switch ansi[0] { - case "2": - if len(ansi) != 4 { - return nil, fmt.Errorf("invalid 24-bit ansi colour code") - } - return ColourFrom24Bit(ansi[1], ansi[2], ansi[3]) - case "5": - if len(ansi) != 2 { - return nil, fmt.Errorf("invalid 8-bit ansi colour code") - } - return ColourFrom8Bit(ansi[1]) - default: - return nil, fmt.Errorf("invalid ansi colour code") - } -} diff --git a/cmd/p2p/client/client.go b/cmd/p2p/client/client.go deleted file mode 100644 index cfc105b3d1..0000000000 --- a/cmd/p2p/client/client.go +++ /dev/null @@ -1,114 +0,0 @@ -package main - -import ( - "bufio" - "context" - "flag" - "fmt" - "os" - "os/signal" - "strings" - "time" - - "log/slog" - - libp2p "github.com/libp2p/go-libp2p" - "github.com/libp2p/go-libp2p/core/host" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/libp2p/go-libp2p/core/protocol" -) - -const RendezvousProtocol = protocol.ID("/rendezvous/1.0.0") - -// sendCommand opens a new stream to the given peer, sends a command, and prints the response. -func sendCommand(ctx context.Context, h host.Host, peerID peer.ID, cmd string) error { - s, err := h.NewStream(ctx, peerID, RendezvousProtocol) - if err != nil { - return fmt.Errorf("failed to create stream: %w", err) - } - defer s.Close() - - // Send the command (e.g. "REGISTER \n" or "DISCOVER \n") - _, err = s.Write([]byte(cmd)) - if err != nil { - return fmt.Errorf("failed to write command: %w", err) - } - - // Read and print each line of response. - scanner := bufio.NewScanner(s) - for scanner.Scan() { - line := scanner.Text() - if strings.TrimSpace(line) != "" { - fmt.Println("Response:", line) - } - } - if err := scanner.Err(); err != nil { - return fmt.Errorf("error reading response: %w", err) - } - return nil -} - -func main() { - ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt) - - // Command-line flags: - // -server: the multiaddress of the rendezvous server (including /p2p/). - // -rendezvous: the rendezvous point name. - var serverAddrStr string - var rendezvousPoint string - flag.StringVar(&serverAddrStr, "server", "", "Multiaddress of the rendezvous server (e.g., /ip4/127.0.0.1/tcp/12345/p2p/)") - flag.StringVar(&rendezvousPoint, "rendezvous", "example", "Rendezvous point name") - flag.Parse() - - if serverAddrStr == "" { - slog.Error("Please provide the server multiaddress using -server") - os.Exit(1) - } - - // Convert the string to a peer.AddrInfo. - serverAddr, err := peer.AddrInfoFromString(serverAddrStr) - if err != nil { - slog.Error("Error parsing server multiaddress", "error", err) - os.Exit(1) - } - - // Create a new libp2p host for the client. - h, err := libp2p.New() - if err != nil { - slog.Error("Failed to create client host", "error", err) - os.Exit(1) - } - defer h.Close() - - fmt.Println("Client running with Peer ID:", h.ID().String()) - for _, addr := range h.Addrs() { - fmt.Printf("Listening on: %s/p2p/%s\n", addr, h.ID().String()) - } - - // Connect to the rendezvous server. - if err := h.Connect(ctx, *serverAddr); err != nil { - slog.Error("Failed to connect to server", "error", err) - os.Exit(1) - } - slog.Info("Connected to rendezvous server") - - // Register at the rendezvous point. - slog.Info("Registering at rendezvous point", "rendezvous", rendezvousPoint) - if err := sendCommand(ctx, h, serverAddr.ID, fmt.Sprintf("REGISTER %s\n", rendezvousPoint)); err != nil { - slog.Error("Registration failed", "error", err) - os.Exit(1) - } - - // Wait a moment (so other peers might also register). - time.Sleep(2 * time.Second) - - // Discover peers registered under the same rendezvous point. - slog.Info("Discovering peers at rendezvous point", "rendezvous", rendezvousPoint) - if err := sendCommand(ctx, h, serverAddr.ID, fmt.Sprintf("DISCOVER %s\n", rendezvousPoint)); err != nil { - slog.Error("Discovery failed", "error", err) - os.Exit(1) - } - - <-ctx.Done() -} - diff --git a/cmd/p2p/server/server.go b/cmd/p2p/server/server.go deleted file mode 100644 index 82f1d6df5b..0000000000 --- a/cmd/p2p/server/server.go +++ /dev/null @@ -1,50 +0,0 @@ -package main - -import ( - "context" - "fmt" - "io" - "os" - "os/signal" - - libp2p "github.com/libp2p/go-libp2p" - "github.com/libp2p/go-libp2p/core/network" - "github.com/libp2p/go-libp2p/core/protocol" -) - -const ProtocolID = protocol.ID("/sst/1.0.0") - -func main() { - ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt) - - // Create a libp2p host with AutoRelay enabled. - host, err := libp2p.New(libp2p.EnableAutoNATv2(), libp2p.EnableAutoRelay()) - if err != nil { - fmt.Println("Error creating host:", err) - os.Exit(1) - } - defer host.Close() - - // Set a simple stream handler for our protocol. - host.SetStreamHandler(ProtocolID, func(s network.Stream) { - defer s.Close() - // Read all data sent on this stream. - data, err := io.ReadAll(s) - if err != nil { - fmt.Println("Error reading stream:", err) - return - } - fmt.Printf("Received: %s\n", string(data)) - }) - - // Print our peer ID and addresses. - fmt.Println("Server is running!") - fmt.Println("Peer ID:", host.ID().String()) - fmt.Println("Listening addresses:") - for _, addr := range host.Addrs() { - fmt.Printf(" %s/p2p/%s\n", addr, host.ID().String()) - } - - // Block forever. - <-ctx.Done() -} diff --git a/cmd/sst/add_test.go b/cmd/sst/add_test.go new file mode 100644 index 0000000000..dbf4df960a --- /dev/null +++ b/cmd/sst/add_test.go @@ -0,0 +1,60 @@ +package main + +import ( + "testing" + + "github.com/sst/sst/v3/pkg/project" +) + +func TestGetAliasName(t *testing.T) { + tests := []struct { + name string + entry *project.ProviderLockEntry + want string + }{ + { + name: "simple provider", + entry: &project.ProviderLockEntry{ + Name: "stripe", + Package: "pulumi-stripe", + Alias: "stripe", + }, + want: "stripe", + }, + { + name: "strip official suffix", + entry: &project.ProviderLockEntry{ + Name: "stripe-official", + Package: "@sst-provider/stripe-official", + Alias: "stripe", + }, + want: "stripe", + }, + { + name: "strip community suffix from alias", + entry: &project.ProviderLockEntry{ + Name: "@scope/pulumi-foo-community", + Package: "@scope/pulumi-foo-community", + Alias: "foocommunity", + }, + want: "foo", + }, + { + name: "package input still uses alias", + entry: &project.ProviderLockEntry{ + Name: "@paynearme/pulumi-jetstream", + Package: "@paynearme/pulumi-jetstream", + Alias: "jetstream", + }, + want: "jetstream", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getAliasName(tt.entry); got != tt.want { + t.Fatalf("got %q, want %q", got, tt.want) + } + }) + } +} diff --git a/cmd/sst/cli/project.go b/cmd/sst/cli/project.go index c5c1261074..e6dce082db 100644 --- a/cmd/sst/cli/project.go +++ b/cmd/sst/cli/project.go @@ -98,7 +98,8 @@ func (c *Cli) InitProject() (*project.Project, error) { } c.configureLog() - spin := spinner.New(spinner.CharSets[14], 100*time.Millisecond) + spin := spinner.New(spinner.CharSets[14], 100*time.Millisecond, spinner.WithWriterFile(os.Stderr)) + spin.Color("cyan") defer spin.Stop() if !p.CheckPlatform(c.version) { spin.Suffix = " Upgrading project..." diff --git a/cmd/sst/diff.go b/cmd/sst/diff.go index 6232edf709..2015cdd0db 100644 --- a/cmd/sst/diff.go +++ b/cmd/sst/diff.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "fmt" + "os" "sort" "strings" @@ -90,6 +91,14 @@ var CmdDiff = &cli.Command{ Long: "Run policy pack validation against the preview changes.", }, }, + { + Name: "json", + Type: "bool", + Description: cli.Description{ + Short: "Output as JSON", + Long: "Output the diff result as JSON to stdout. Useful for CI pipelines and scripting.", + }, + }, }, Examples: []cli.Example{ { @@ -104,8 +113,16 @@ var CmdDiff = &cli.Command{ Short: "See changes to production with policy validation", }, }, + { + Content: "sst diff --json", + Description: cli.Description{ + Short: "Output changes as JSON", + }, + }, }, Run: func(c *cli.Cli) error { + jsonOutput := c.Bool("json") + p, err := c.InitProject() if err != nil { return err @@ -123,9 +140,13 @@ var CmdDiff = &cli.Command{ } var wg errgroup.Group - defer wg.Wait() outputs := []*apitype.ResOutputsEvent{} - u := ui.New(c.Context) + uiOptions := []ui.Option{} + if jsonOutput { + // Keep stdout machine-readable when attached to a TTY. + uiOptions = append(uiOptions, ui.WithSilent) + } + u := ui.New(c.Context, uiOptions...) s, err := server.New() if err != nil { return err @@ -136,10 +157,11 @@ var CmdDiff = &cli.Command{ }) events := bus.SubscribeAll() - defer close(events) wg.Go(func() error { for evt := range events { - u.Event(evt) + if !jsonOutput { + u.Event(evt) + } switch evt := evt.(type) { case *apitype.ResOutputsEvent: outputs = append(outputs, evt) @@ -148,7 +170,6 @@ var CmdDiff = &cli.Command{ return nil }) defer u.Destroy() - defer c.Cancel() err = p.Run(c.Context, &project.StackInput{ Command: "diff", ServerPort: s.Port, @@ -158,93 +179,124 @@ var CmdDiff = &cli.Command{ Verbose: c.Bool("verbose"), PolicyPath: c.String("policy"), }) + bus.Unsubscribe(events) + close(events) + c.Cancel() + if waitErr := wg.Wait(); waitErr != nil && err == nil { + err = waitErr + } + + if jsonOutput { + if jsonErr := renderDiffJSON(outputs); jsonErr != nil { + return jsonErr + } + return err + } if err != nil { return err } - if len(outputs) == 0 { - fmt.Println( - ui.TEXT_HIGHLIGHT_BOLD.Render("➜"), - ui.TEXT_NORMAL_BOLD.Render(" No changes"), - ) - fmt.Println() - return nil + return renderDiffText(outputs, u) + }, +} + +func textDiffIcon(op apitype.OpType) string { + switch op { + case apitype.OpImport, apitype.OpReplace, apitype.OpCreate: + return ui.TEXT_SUCCESS_BOLD.Render("+") + case apitype.OpDelete: + return ui.TEXT_DANGER_BOLD.Render("-") + case apitype.OpUpdate: + return ui.TEXT_WARNING_BOLD.Render("*") + default: + return "" + } +} + +func renderDiffJSON(outputs []*apitype.ResOutputsEvent) error { + filtered := make([]apitype.StepEventMetadata, 0, len(outputs)) + for _, output := range outputs { + if output.Metadata.Op == apitype.OpSame { + continue + } + filtered = append(filtered, output.Metadata) + } + encoder := json.NewEncoder(os.Stdout) + encoder.SetIndent("", " ") + return encoder.Encode(filtered) +} + +func renderDiffText(outputs []*apitype.ResOutputsEvent, u *ui.UI) error { + rendered := make([]*apitype.ResOutputsEvent, 0, len(outputs)) + for _, output := range outputs { + if textDiffIcon(output.Metadata.Op) == "" { + continue } - for _, output := range outputs { - icon := "" - if output.Metadata.Op == apitype.OpImport { - icon = ui.TEXT_SUCCESS_BOLD.Render("+") + rendered = append(rendered, output) + } + if len(rendered) == 0 { + fmt.Println( + ui.TEXT_HIGHLIGHT_BOLD.Render("➜"), + ui.TEXT_NORMAL_BOLD.Render(" No changes"), + ) + fmt.Println() + return nil + } + for _, output := range rendered { + icon := textDiffIcon(output.Metadata.Op) + fmt.Println(icon, "", ui.TEXT_NORMAL_BOLD.Render(u.FormatURN(output.Metadata.URN))) + sorted := make([]string, 0, len(output.Metadata.DetailedDiff)) + for path := range output.Metadata.DetailedDiff { + sorted = append(sorted, path) + } + sort.Strings(sorted) + for _, path := range sorted { + diff := output.Metadata.DetailedDiff[path] + label := "" + if diff.Kind == apitype.DiffUpdate { + label = ui.TEXT_WARNING_BOLD.Render("*") } - if output.Metadata.Op == apitype.OpDelete { - icon = ui.TEXT_DANGER_BOLD.Render("-") + if diff.Kind == apitype.DiffDelete { + label = ui.TEXT_DANGER_BOLD.Render("-") } - if output.Metadata.Op == apitype.OpReplace { - icon = ui.TEXT_SUCCESS_BOLD.Render("+") + if diff.Kind == apitype.DiffAdd { + label = ui.TEXT_SUCCESS_BOLD.Render("+") } - if output.Metadata.Op == apitype.OpUpdate { - icon = ui.TEXT_WARNING_BOLD.Render("*") + if diff.Kind == apitype.DiffAddReplace { + label = ui.TEXT_SUCCESS_BOLD.Render("+") } - if output.Metadata.Op == apitype.OpCreate { - icon = ui.TEXT_SUCCESS_BOLD.Render("+") + if diff.Kind == apitype.DiffUpdateReplace { + label = ui.TEXT_WARNING_BOLD.Render("*") } - if icon == "" { - continue + if diff.Kind == apitype.DiffDeleteReplace { + label = ui.TEXT_DANGER_BOLD.Render("-") } - - fmt.Println(icon, "", ui.TEXT_NORMAL_BOLD.Render(u.FormatURN(output.Metadata.URN))) - sorted := make([]string, 0, len(output.Metadata.DetailedDiff)) - for path := range output.Metadata.DetailedDiff { - sorted = append(sorted, path) + fmt.Print(" ", label+" ", strings.TrimSpace(path)) + value, _ := jsonpath.Read(output.Metadata.New.Outputs, "$."+path) + if path == "__provider" { + value = "code changed" } - sort.Strings(sorted) - for _, path := range sorted { - diff := output.Metadata.DetailedDiff[path] - label := "" - if diff.Kind == apitype.DiffUpdate { - label = ui.TEXT_WARNING_BOLD.Render("*") + if value != nil { + formatted := "" + switch value.(type) { + case string: + formatted = value.(string) + default: + bytes, _ := json.MarshalIndent(value, "", " ") + formatted = string(bytes) } - if diff.Kind == apitype.DiffDelete { - label = ui.TEXT_DANGER_BOLD.Render("-") - } - if diff.Kind == apitype.DiffAdd { - label = ui.TEXT_SUCCESS_BOLD.Render("+") - } - if diff.Kind == apitype.DiffAddReplace { - label = ui.TEXT_SUCCESS_BOLD.Render("+") - } - if diff.Kind == apitype.DiffUpdateReplace { - label = ui.TEXT_WARNING_BOLD.Render("*") - } - if diff.Kind == apitype.DiffDeleteReplace { - label = ui.TEXT_DANGER_BOLD.Render("-") - } - fmt.Print(" ", label+" ", strings.TrimSpace(path)) - value, _ := jsonpath.Read(output.Metadata.New.Outputs, "$."+path) - if path == "__provider" { - value = "code changed" - } - if value != nil { - formatted := "" - switch value.(type) { - case string: - formatted = value.(string) - default: - bytes, _ := json.MarshalIndent(value, "", " ") - formatted = string(bytes) - } - lines := strings.Split(string(formatted), "\n") - fmt.Print(" = ") - for index, line := range lines { - if index > 0 { - fmt.Print(" ") - } - fmt.Print(ui.TEXT_DIM.Render(line) + "\n") + lines := strings.Split(string(formatted), "\n") + fmt.Print(" = ") + for index, line := range lines { + if index > 0 { + fmt.Print(" ") } - } else { - fmt.Println() + fmt.Print(ui.TEXT_DIM.Render(line) + "\n") } + } else { + fmt.Println() } - fmt.Println() } - return nil - }, + fmt.Println() + } + return nil } diff --git a/cmd/sst/diff_test.go b/cmd/sst/diff_test.go new file mode 100644 index 0000000000..a91df6cfca --- /dev/null +++ b/cmd/sst/diff_test.go @@ -0,0 +1,135 @@ +package main + +import ( + "bytes" + "encoding/json" + "io" + "os" + "testing" + + "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" +) + +func captureStdout(t *testing.T, fn func()) string { + t.Helper() + old := os.Stdout + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + os.Stdout = w + + fn() + + w.Close() + os.Stdout = old + + var buf bytes.Buffer + io.Copy(&buf, r) + return buf.String() +} + +func TestRenderDiffJSON_NoChanges(t *testing.T) { + out := captureStdout(t, func() { + if err := renderDiffJSON(nil); err != nil { + t.Fatal(err) + } + }) + + var result []apitype.StepEventMetadata + if err := json.Unmarshal([]byte(out), &result); err != nil { + t.Fatalf("invalid JSON: %v\noutput: %s", err, out) + } + if len(result) != 0 { + t.Fatalf("expected 0 changes, got %d", len(result)) + } +} + +func TestRenderDiffJSON_WithChanges(t *testing.T) { + outputs := []*apitype.ResOutputsEvent{ + { + Metadata: apitype.StepEventMetadata{ + URN: "urn:pulumi:dev::app::aws:ecs/service:Service::MyService", + Type: "aws:ecs/service:Service", + Op: apitype.OpUpdate, + DetailedDiff: map[string]apitype.PropertyDiff{ + "healthCheckGracePeriodSeconds": {Kind: apitype.DiffUpdate}, + "tags": {Kind: apitype.DiffAdd}, + }, + New: &apitype.StepEventStateMetadata{ + Outputs: map[string]interface{}{ + "healthCheckGracePeriodSeconds": 35, + "tags": map[string]interface{}{"env": "dev"}, + }, + }, + }, + }, + { + Metadata: apitype.StepEventMetadata{ + URN: "urn:pulumi:dev::app::aws:s3/bucket:Bucket::MyBucket", + Type: "aws:s3/bucket:Bucket", + Op: apitype.OpCreate, + }, + }, + { + Metadata: apitype.StepEventMetadata{ + URN: "urn:pulumi:dev::app::aws:ecs/service:Service::Replacement", + Type: "aws:ecs/service:Service", + Op: apitype.OpCreateReplacement, + }, + }, + { + Metadata: apitype.StepEventMetadata{ + URN: "urn:pulumi:dev::app::aws:lambda/function:Function::OldFn", + Type: "aws:lambda/function:Function", + Op: apitype.OpSame, + }, + }, + } + + out := captureStdout(t, func() { + if err := renderDiffJSON(outputs); err != nil { + t.Fatal(err) + } + }) + + var result []apitype.StepEventMetadata + if err := json.Unmarshal([]byte(out), &result); err != nil { + t.Fatalf("invalid JSON: %v\noutput: %s", err, out) + } + + // OpSame should be filtered out, but replacement steps should be kept. + if len(result) != 3 { + t.Fatalf("expected 3 changes, got %d", len(result)) + } + + update := result[0] + if update.Op != apitype.OpUpdate { + t.Errorf("expected op 'update', got %q", update.Op) + } + if update.URN != "urn:pulumi:dev::app::aws:ecs/service:Service::MyService" { + t.Errorf("unexpected URN: %s", update.URN) + } + if update.Type != "aws:ecs/service:Service" { + t.Errorf("unexpected type: %s", update.Type) + } + if len(update.DetailedDiff) != 2 { + t.Fatalf("expected 2 detailed diff entries, got %d", len(update.DetailedDiff)) + } + if update.DetailedDiff["healthCheckGracePeriodSeconds"].Kind != apitype.DiffUpdate { + t.Errorf("expected diff kind 'update', got %q", update.DetailedDiff["healthCheckGracePeriodSeconds"].Kind) + } + if update.DetailedDiff["tags"].Kind != apitype.DiffAdd { + t.Errorf("expected diff kind 'add', got %q", update.DetailedDiff["tags"].Kind) + } + + create := result[1] + if create.Op != apitype.OpCreate { + t.Errorf("expected op 'create', got %q", create.Op) + } + + replacement := result[2] + if replacement.Op != apitype.OpCreateReplacement { + t.Errorf("expected op 'create-replacement', got %q", replacement.Op) + } +} diff --git a/cmd/sst/init.go b/cmd/sst/init.go index b9756e4bf5..00c3abedd2 100644 --- a/cmd/sst/init.go +++ b/cmd/sst/init.go @@ -213,6 +213,7 @@ func CmdInit(cli *cli.Cli) error { var cmd *exec.Cmd spin := spinner.New(spinner.CharSets[14], 100*time.Millisecond) + spin.Color("cyan") spin.Suffix = " Installing providers..." spin.Start() diff --git a/cmd/sst/main.go b/cmd/sst/main.go index f52bfa7d97..82d5d2a6ae 100644 --- a/cmd/sst/main.go +++ b/cmd/sst/main.go @@ -28,13 +28,28 @@ import ( var version = "dev" +func getAliasName(entry *project.ProviderLockEntry) string { + name := entry.Name + if entry.Name == entry.Package { + name = entry.Alias + } + for _, suffix := range []string{"official", "community"} { + if !strings.HasSuffix(entry.Name, "-"+suffix) && !strings.HasSuffix(entry.Package, "-"+suffix) { + continue + } + name = strings.TrimSuffix(name, "-"+suffix) + name = strings.TrimSuffix(name, suffix) + } + return name +} + func main() { // check if node_modules/.bin/sst exists nodeModulesBinPath := filepath.Join("node_modules", ".bin", "sst") binary, _ := os.Executable() if _, err := os.Stat(nodeModulesBinPath); err == nil && !strings.Contains(binary, "node_modules") && os.Getenv("SST_SKIP_LOCAL") != "true" && version != "dev" { // forward command to node_modules/.bin/sst - fmt.Println(ui.TEXT_WARNING_BOLD.Render("Warning: ") + "You are using a global installation of SST but you also have a local installation specified in your package.json. The local installation will be used but you should typically run it through your package manager.") + fmt.Fprintln(os.Stderr, ui.TEXT_WARNING_BOLD.Render("Warning: ")+"You are using a global installation of SST but you also have a local installation specified in your package.json. The local installation will be used but you should typically run it through your package manager.") cmd := process.Command(nodeModulesBinPath, os.Args[1:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -69,7 +84,7 @@ func main() { if msg != "" { ui.Error(readableErr.Error()) if readableErr.IsHinted() { - fmt.Println(" " + ui.TEXT_DIM.Render(readableErr.Unwrap().Error())) + fmt.Fprintln(os.Stderr, " "+ui.TEXT_DIM.Render(readableErr.Unwrap().Error())) } } } else { @@ -107,6 +122,7 @@ func run() error { if !flag.SST_SKIP_DEPENDENCY_CHECK { spin := spinner.New(spinner.CharSets[14], 100*time.Millisecond) + spin.Color("cyan") spin.Suffix = " Download dependencies..." if global.NeedsPulumi() { spin.Suffix = " Installing pulumi..." @@ -519,7 +535,10 @@ var root = &cli.Command{ "```ts title=\"sst.config.ts\"", "{", " providers: {", - " aws: \"6.27.0\"", + " aws: {", + " package: \"@pulumi/aws\",", + " version: \"6.27.0\"", + " }", " }", "}", "```", @@ -536,6 +555,7 @@ var root = &cli.Command{ "{", " providers: {", " aws: {", + " package: \"@pulumi/aws\",", " version: \"6.26.0\"", " }", " }", @@ -549,6 +569,8 @@ var root = &cli.Command{ "```bash frame=\"none\"", "NPM_REGISTRY=https://my-registry.com sst add aws", "```", + "", + "You can also set the registry in your `.npmrc` file. If your registry requires authentication, SST supports `_authToken`, `_auth`, and `username`/`_password` from `.npmrc`.", }, "\n"), }, Args: []cli.Argument{ @@ -564,6 +586,7 @@ var root = &cli.Command{ Run: func(cli *cli.Cli) error { pkg := cli.Positional(0) spin := spinner.New(spinner.CharSets[14], 100*time.Millisecond) + spin.Color("cyan") spin.Suffix = " Adding provider..." spin.Start() defer spin.Stop() @@ -589,11 +612,18 @@ var root = &cli.Command{ return err } } - entry, err := project.FindProvider(pkg, "latest") + if p.NeedsInstall() { + err := p.Install() + if err != nil { + return err + } + } + entry, err := project.FindProvider(pkg, "latest", pkg) if err != nil { return util.NewReadableError(err, "Could not find provider "+pkg) } - err = p.Add(entry.Name, entry.Version) + providerName := getAliasName(entry) + err = p.Add(providerName, entry.Version, entry.Package) if err != nil { return util.NewReadableError(err, err.Error()) } @@ -655,6 +685,7 @@ var root = &cli.Command{ } spin := spinner.New(spinner.CharSets[14], 100*time.Millisecond) + spin.Color("cyan") defer spin.Stop() spin.Suffix = " Installing providers..." spin.Start() @@ -943,18 +974,6 @@ var root = &cli.Command{ return nil }, }, - { - Name: "common-errors", - Hidden: true, - Run: func(cli *cli.Cli) error { - data, err := json.MarshalIndent(project.CommonErrors, "", " ") - if err != nil { - return err - } - fmt.Println(string(data)) - return nil - }, - }, { Name: "refresh", Description: cli.Description{ @@ -970,6 +989,13 @@ var root = &cli.Command{ ":::note", "The `sst refresh` does not make changes to the resources in the cloud provider.", ":::", + "", + "By default, this refreshes the stage as it would be deployed using `sst deploy`. If the stage was deployed using `sst dev`, use the `--dev` flag.", + "", + "```bash frame=\"none\"", + "sst refresh --dev", + "```", + "", "You can also refresh a specific component by passing in the name of the component.", "", "```bash frame=\"none\"", @@ -1002,6 +1028,14 @@ var root = &cli.Command{ Long: "Exclude the specified component from the operation.", }, }, + { + Name: "dev", + Type: "bool", + Description: cli.Description{ + Short: "Refresh in dev mode", + Long: "Refresh the dev version of this stage.", + }, + }, }, Run: CmdRefresh, }, diff --git a/cmd/sst/mosaic.go b/cmd/sst/mosaic.go index 859cd49ee1..17b62fcdb7 100644 --- a/cmd/sst/mosaic.go +++ b/cmd/sst/mosaic.go @@ -40,10 +40,7 @@ func CmdMosaic(c *cli.Cli) error { child := os.Getenv("SST_CHILD") // spawning child process if len(c.Arguments()) > 0 || child != "" { - var args []string - for _, arg := range c.Arguments() { - args = append(args, strings.Fields(arg)...) - } + args := c.Arguments() slog.Info("dev mode with target", "args", c.Arguments()) cfgPath, err := c.Discover() stage, err := c.Stage(cfgPath) @@ -90,7 +87,7 @@ func CmdMosaic(c *cli.Cli) error { go func() { evts <- true }() - fmt.Println("\n"+ui.TEXT_DIM.Render("[timeout]")) + fmt.Println("\n" + ui.TEXT_DIM.Render("[timeout]")) timer.Reset(timeout) continue case _, ok := <-evts: @@ -127,7 +124,7 @@ func CmdMosaic(c *cli.Cli) error { for k, v := range nextEnv.Env { cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v)) } - process.Detach(cmd) + process.DetachSession(cmd) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -159,6 +156,9 @@ func CmdMosaic(c *cli.Cli) error { if err != nil { return err } + if p.App().Protect { + return project.ErrProtectedDevStage + } policyPath := c.String("policy") if policyPath != "" { if _, err := p.ResolvePolicyPackPath(policyPath); err != nil { @@ -259,8 +259,61 @@ func CmdMosaic(c *cli.Cli) error { fmt.Sprintf("SST_SERVER=http://localhost:%v", server.Port), "SST_STAGE="+p.App().Stage, ) - multi.AddProcess("deploy", []string{currentExecutable, "ui", "--filter=sst"}, "⑆", "SST", "", false, true, append(multiEnv, "SST_LOG="+p.PathLog("ui-deploy"))...) - multi.AddProcess("function", []string{currentExecutable, "ui", "--filter=function"}, "Ξ»", "Functions", "", false, true, append(multiEnv, "SST_LOG="+p.PathLog("ui-function"))...) + serverURL := fmt.Sprintf("http://localhost:%v", server.Port) + _, hasAWS := p.App().Providers["aws"] + showWorkers := p.App().Home == "cloudflare" && !hasAWS + fnTitle := "Functions" + fnFilter := "function" + if showWorkers { + fnTitle = "Workers" + fnFilter = "worker" + } + multi.AddProcess(multiplexer.PaneConfig{ + Key: "deploy", + Args: []string{currentExecutable, "ui", "--filter=sst"}, + Icon: "⑆", + Title: "SST", + Autostart: true, + Env: append(multiEnv, "SST_LOG="+p.PathLog("ui-deploy")), + }) + multi.AddProcess(multiplexer.PaneConfig{ + Key: "function", + Args: []string{currentExecutable, "ui", "--filter=" + fnFilter}, + Icon: "Ξ»", + Title: fnTitle, + Autostart: true, + Filterable: true, + FilterTitle: fnTitle, + FilterSubtitle: "Select to filter logs", + OnFilterChanged: func(value string) { + bus.Publish(&ui.PaneFilterEvent{PaneKey: "function", Value: value}) + }, + ListOptions: func() []multiplexer.FilterOption { + completed, err := dev.Completed(c.Context, serverURL) + if err != nil || completed == nil { + return nil + } + var options []multiplexer.FilterOption + for _, r := range completed.Resources { + if string(r.Type) == "sst:aws:Function" || string(r.Type) == "sst:cloudflare:Worker" { + name := r.URN.Name() + handler := name + if meta, ok := r.Outputs["_metadata"].(map[string]interface{}); ok { + if h, ok := meta["handler"].(string); ok { + handler = h + } + } + options = append(options, multiplexer.FilterOption{ + Label: name, + Description: handler, + Value: name, + }) + } + } + return options + }, + Env: append(multiEnv, "SST_LOG="+p.PathLog("ui-function")), + }) defer func() { multi.Exit() }() @@ -276,9 +329,6 @@ func CmdMosaic(c *cli.Cli) error { case unknown := <-evts: switch evt := unknown.(type) { case *project.CompleteEvent: - if evt.Old { - continue - } for _, d := range evt.Devs { if d.Command == "" { continue @@ -288,26 +338,75 @@ func CmdMosaic(c *cli.Cli) error { if title == "" { title = d.Name } - multi.AddProcess( - d.Name, - []string{currentExecutable, "dev"}, - "β†’", - title, - dir, - true, - d.Autostart, - append([]string{"SST_CHILD=" + d.Name}, multiEnv...)..., - ) + multi.AddProcess(multiplexer.PaneConfig{ + Key: d.Name, + Args: []string{currentExecutable, "dev"}, + Icon: "β†’", + Title: title, + Cwd: dir, + Killable: true, + Autostart: d.Autostart, + Env: append([]string{"SST_CHILD=" + d.Name}, multiEnv...), + }) } for name := range evt.Tunnels { - multi.AddProcess("tunnel", []string{currentExecutable, "tunnel", "--stage", p.App().Stage}, "β‡Œ", "Tunnel", "", true, true, append( - multiEnv, - "SST_LOG="+p.PathLog("tunnel_"+name), - )...) + multi.AddProcess(multiplexer.PaneConfig{ + Key: "tunnel", + Args: []string{currentExecutable, "tunnel", "--stage", p.App().Stage}, + Icon: "β‡Œ", + Title: "Tunnel", + Killable: true, + Autostart: true, + Env: append(multiEnv, "SST_LOG="+p.PathLog("tunnel_"+name)), + }) } if len(evt.Tasks) > 0 { - multi.AddProcess("task", []string{currentExecutable, "ui", "--filter=task"}, "⧉", "Tasks", "", false, true, append(multiEnv, "SST_LOG="+p.PathLog("ui-task"))...) + multi.AddProcess(multiplexer.PaneConfig{ + Key: "task", + Args: []string{currentExecutable, "ui", "--filter=task"}, + Icon: "⧉", + Title: "Tasks", + Autostart: true, + Filterable: true, + FilterTitle: "Tasks", + FilterSubtitle: "Select a task to filter logs", + OnFilterChanged: func(value string) { + bus.Publish(&ui.PaneFilterEvent{PaneKey: "task", Value: value}) + }, + ListOptions: func() []multiplexer.FilterOption { + completed, err := dev.Completed(c.Context, serverURL) + if err != nil || completed == nil { + return nil + } + var options []multiplexer.FilterOption + for name, t := range completed.Tasks { + desc := "" + if t.Command != nil { + desc = *t.Command + } + options = append(options, multiplexer.FilterOption{ + Label: name, + Description: desc, + Value: name, + }) + } + return options + }, + Env: append(multiEnv, "SST_LOG="+p.PathLog("ui-task")), + }) } + var fnNames []string + for _, r := range evt.Resources { + if string(r.Type) == "sst:aws:Function" || string(r.Type) == "sst:cloudflare:Worker" { + fnNames = append(fnNames, r.URN.Name()) + } + } + multi.CheckFilter("function", fnNames) + var taskNames []string + for name := range evt.Tasks { + taskNames = append(taskNames, name) + } + multi.CheckFilter("task", taskNames) break } } @@ -324,8 +423,15 @@ func CmdMosaic(c *cli.Cli) error { if mode == "mono" { mono := monoplexer.New() - mono.AddProcess("deploy", []string{currentExecutable, "ui", "--filter=sst"}, "", "SST") - mono.AddProcess("function", []string{currentExecutable, "ui", "--filter=function"}, "", "Function") + _, hasAWS := p.App().Providers["aws"] + fnTitle := "Function" + fnFilter := "function" + if p.App().Home == "cloudflare" && !hasAWS { + fnTitle = "Worker" + fnFilter = "worker" + } + mono.AddProcess("deploy", []string{currentExecutable, "ui", "--filter=sst"}, "", "SST", true) + mono.AddProcess("function", []string{currentExecutable, "ui", "--filter=" + fnFilter}, "", fnTitle, true) wg.Go(func() error { defer c.Cancel() @@ -341,9 +447,6 @@ func CmdMosaic(c *cli.Cli) error { case unknown := <-evts: switch evt := unknown.(type) { case *project.CompleteEvent: - if evt.Old { - continue - } for _, d := range evt.Devs { if d.Command == "" { continue @@ -359,10 +462,12 @@ func CmdMosaic(c *cli.Cli) error { append([]string{currentExecutable, "dev", "--"}, words...), dir, title, + d.Autostart, + "SST_CHILD="+d.Name, ) } for range evt.Tunnels { - mono.AddProcess("tunnel", []string{currentExecutable, "tunnel", "--stage", p.App().Stage}, "", "Tunnel") + mono.AddProcess("tunnel", []string{currentExecutable, "tunnel", "--stage", p.App().Stage}, "", "Tunnel", true) } break } diff --git a/cmd/sst/mosaic/aws/bridge/bridge.go b/cmd/sst/mosaic/aws/bridge/bridge.go index c1813a94e2..c6abb6edb8 100644 --- a/cmd/sst/mosaic/aws/bridge/bridge.go +++ b/cmd/sst/mosaic/aws/bridge/bridge.go @@ -44,15 +44,16 @@ type Message struct { } type Writer struct { - conn *appsync.Connection - message MessageType - source string - channel string - event string - buffer []byte - position int - index int - id string + conn *appsync.Connection + message MessageType + source string + channel string + event string + buffer []byte + position int + index int + id string + streaming bool } type InitBody struct { @@ -91,6 +92,10 @@ func (w *Writer) SetID(id string) { w.id = id } +func (w *Writer) SetStreaming(streaming bool) { + w.streaming = streaming +} + const BUFFER_SIZE = 1024 * 128 func (w *Writer) Write(p []byte) (int, error) { @@ -108,6 +113,11 @@ func (w *Writer) Write(p []byte) (int, error) { } } } + if w.streaming && w.position > 0 { + if err := w.Flush(false); err != nil { + return total, err + } + } return total, nil } diff --git a/cmd/sst/mosaic/aws/function.go b/cmd/sst/mosaic/aws/function.go index afd838607a..ae7406d761 100644 --- a/cmd/sst/mosaic/aws/function.go +++ b/cmd/sst/mosaic/aws/function.go @@ -76,6 +76,7 @@ func function(ctx context.Context, input input) { Worker runtime.Worker CurrentRequestID string Env []string + Streaming bool } workerShutdownChan := make(chan *WorkerInfo, 1000) nextChan := map[string]chan io.Reader{} @@ -143,19 +144,32 @@ func function(ctx context.Context, input input) { log.Info("got response", "workerID", workerID, "requestID", r.PathValue("requestID")) writer := input.client.NewWriter(bridge.MessageResponse, input.prefix+"/"+workerID+"/in") writer.SetID(requestID) - var buf bytes.Buffer - tee := io.TeeReader(r.Body, &buf) - io.Copy(writer, tee) - writer.Close() - w.WriteHeader(202) info, ok := workers[workerID] - if ok { + if ok && info.Streaming { + writer.SetStreaming(true) + io.Copy(writer, r.Body) + writer.Close() + w.WriteHeader(202) bus.Publish(&FunctionResponseEvent{ FunctionID: info.FunctionID, WorkerID: workerID, RequestID: requestID, - Output: buf.Bytes(), + Output: []byte("[streaming response]"), }) + } else { + var buf bytes.Buffer + tee := io.TeeReader(r.Body, &buf) + io.Copy(writer, tee) + writer.Close() + w.WriteHeader(202) + if ok { + bus.Publish(&FunctionResponseEvent{ + FunctionID: info.FunctionID, + WorkerID: workerID, + RequestID: requestID, + Output: buf.Bytes(), + }) + } } }) @@ -212,7 +226,7 @@ func function(ctx context.Context, input input) { return build } - run := func(functionID string, workerID string) bool { + startWorker := func(functionID string, workerID string) bool { build := getBuildOutput(functionID) if build == nil { return false @@ -234,10 +248,18 @@ func function(ctx context.Context, input input) { log.Error("failed to run worker", "error", err) return false } + streaming := false + for _, e := range workerEnv[workerID] { + if e == "SST_FUNCTION_STREAMING=true" { + streaming = true + break + } + } info := &WorkerInfo{ FunctionID: functionID, Worker: worker, WorkerID: workerID, + Streaming: streaming, } go func() { logs := worker.Logs() @@ -258,6 +280,19 @@ func function(ctx context.Context, input input) { return true } + restartOrDeferWorker := func(workerID string, info *WorkerInfo) { + target, ok := targets[info.FunctionID] + if !ok { + return + } + if input.project.Runtime.ShouldRunEagerly(target.Runtime) { + startWorker(info.FunctionID, workerID) + } else { + log.Info("lazy startup: deferring worker restart until invoked", "workerID", workerID, "functionID", info.FunctionID) + delete(workers, workerID) + } + } + for { select { case <-ctx.Done(): @@ -283,7 +318,7 @@ func function(ctx context.Context, input input) { } log.Info("worker init", "workerID", msg.Source, "functionID", init.FunctionID) workerEnv[workerID] = init.Environment - if ok := run(init.FunctionID, workerID); !ok { + if ok := startWorker(init.FunctionID, workerID); !ok { result, err := http.Post("http://"+server+workerID+"/runtime/init/error", "application/json", strings.NewReader(`{"errorMessage":"Function failed to build"}`)) if err != nil { continue @@ -363,7 +398,7 @@ func function(ctx context.Context, input input) { } builds = map[string]*runtime.BuildOutput{} for workerID, info := range workers { - run(info.FunctionID, workerID) + restartOrDeferWorker(workerID, info) } case *runtime.BuildInput: targets[evt.FunctionID] = evt @@ -397,7 +432,7 @@ func function(ctx context.Context, input input) { for workerID, info := range workers { if toBuild[info.FunctionID] { - run(info.FunctionID, workerID) + restartOrDeferWorker(workerID, info) } } break diff --git a/cmd/sst/mosaic/aws/iot_writer/writer.go b/cmd/sst/mosaic/aws/iot_writer/writer.go deleted file mode 100644 index b13a82f4e7..0000000000 --- a/cmd/sst/mosaic/aws/iot_writer/writer.go +++ /dev/null @@ -1,184 +0,0 @@ -package iot_writer - -import ( - "bytes" - "context" - "encoding/binary" - "io" - "log/slog" - "strings" - "time" - - "github.com/aws/aws-sdk-go-v2/service/s3" - "github.com/aws/aws-sdk-go/aws" - MQTT "github.com/eclipse/paho.mqtt.golang" - "github.com/sst/sst/v3/internal/util" -) - -const BUFFER_SIZE = 1024 * 120 -const MAX_COUNT = 3 - -type IoTWriter struct { - topic string - count int - s3 *s3.Client - bucket string - client MQTT.Client - buffer []byte // buffer to accumulate data - last time.Time -} - -func New(client MQTT.Client, s3 *s3.Client, bucket string, topic string) *IoTWriter { - return &IoTWriter{ - client: client, - buffer: make([]byte, 0, BUFFER_SIZE), - topic: topic, - last: time.Now(), - s3: s3, - bucket: bucket, - } -} - -func (iw *IoTWriter) Write(p []byte) (int, error) { - totalWritten := 0 - - for len(p) > 0 { - if iw.count == MAX_COUNT { - iw.buffer = append(iw.buffer, p...) - break - } - // Calculate the space left in the buffer - spaceLeft := BUFFER_SIZE - len(iw.buffer) - - // Determine how much data to copy to the buffer - toCopy := min(spaceLeft, len(p)) - - // Append data to the buffer - iw.buffer = append(iw.buffer, p[:toCopy]...) - - // Update the slice p and totalWritten - p = p[toCopy:] - totalWritten += toCopy - - // If the buffer is full, write the chunk - if len(iw.buffer) == BUFFER_SIZE { - iw.flush() - } - } - - return totalWritten, nil -} - -func (iw *IoTWriter) flush() error { - if len(iw.buffer) > 0 { - slog.Info("writing to topic", "topic", iw.topic, "data", len(iw.buffer)) - // encode int to 4 bytes at the beginning of the buffer - var buf [4]byte - binary.BigEndian.PutUint32(buf[:], uint32(iw.count)) - iw.buffer = append(buf[:], iw.buffer...) - iw.count++ - token := iw.client.Publish(iw.topic, 1, false, iw.buffer) - if token.Wait() && token.Error() != nil { - return token.Error() - } - iw.last = time.Now() - iw.buffer = iw.buffer[:0] - } - return nil -} - -func (iw *IoTWriter) Close() error { - if len(iw.buffer) <= BUFFER_SIZE { - iw.flush() - } else { - slog.Info("flushing to s3") - key := "temporary/" + util.RandomString(8) - iw.s3.PutObject(context.TODO(), &s3.PutObjectInput{ - Bucket: aws.String(iw.bucket), - Key: aws.String(key), - Body: bytes.NewReader(iw.buffer), - }) - bytes := make([]byte, 4) - binary.BigEndian.PutUint32(bytes, uint32(iw.count)) - iw.count++ - bytes = append(bytes, []byte("blk"+iw.bucket+"|"+key)...) - token := iw.client.Publish(iw.topic, 1, false, bytes) - if token.Wait() && token.Error() != nil { - return token.Error() - } - } - bytes := make([]byte, 4) - binary.BigEndian.PutUint32(bytes, uint32(iw.count)) - token := iw.client.Publish(iw.topic, 1, false, bytes) - if token.Wait() && token.Error() != nil { - return token.Error() - } - slog.Info("closed iot writer", "topic", iw.topic, "count", iw.count) - return nil -} - -type Reader struct { - buffer map[string]map[int]ReadMsg - next map[string]int - s3 *s3.Client -} - -type ReadMsg struct { - ID string - Data []byte -} - -func NewReader(s3 *s3.Client) *Reader { - return &Reader{ - buffer: map[string]map[int]ReadMsg{}, - next: map[string]int{}, - s3: s3, - } -} - -func (r *Reader) Read(m MQTT.Message) []ReadMsg { - payload := m.Payload() - topic := m.Topic() - requestID := strings.Split(topic, "/")[5] - requestBuffer, ok := r.buffer[requestID] - if !ok { - requestBuffer = map[int]ReadMsg{} - r.buffer[requestID] = requestBuffer - } - id := int(binary.BigEndian.Uint32(payload[:4])) - payload = payload[4:] - if bytes.HasPrefix(payload, []byte("blk")) { - data := string(payload) - bucket, key, _ := strings.Cut(data[3:], "|") - slog.Info("fetching from s3", "bucket", bucket, "key", key) - resp, _ := r.s3.GetObject(context.TODO(), &s3.GetObjectInput{ - Bucket: aws.String(bucket), - Key: aws.String(key), - }) - payload, _ = io.ReadAll(resp.Body) - } - requestBuffer[id] = ReadMsg{ - Data: payload, - ID: requestID, - } - result := []ReadMsg{} - for { - next := r.next[requestID] - match, ok := requestBuffer[next] - if !ok { - break - } - delete(requestBuffer, next) - r.next[requestID]++ - result = append(result, match) - } - return result -} - -// min returns the smaller of x or y. -func min(x, y int) int { - if x < y { - return x - } - return y -} diff --git a/cmd/sst/mosaic/aws/task.go b/cmd/sst/mosaic/aws/task.go index 573774eb13..53a21cbd3c 100644 --- a/cmd/sst/mosaic/aws/task.go +++ b/cmd/sst/mosaic/aws/task.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "log/slog" + "os" "time" "github.com/aws/aws-sdk-go-v2/aws" @@ -133,7 +134,7 @@ func task(ctx context.Context, input input) { } cmd := process.Command(fields[0], fields[1:]...) cmd.Dir = task.Directory - cmd.Env = body.Environment + cmd.Env = append(os.Environ(), body.Environment...) stdout, _ := cmd.StdoutPipe() stderr, _ := cmd.StderrPipe() go func() { diff --git a/cmd/sst/mosaic/dev/dev.go b/cmd/sst/mosaic/dev/dev.go index 94a15d43c7..5b2b419392 100644 --- a/cmd/sst/mosaic/dev/dev.go +++ b/cmd/sst/mosaic/dev/dev.go @@ -83,12 +83,21 @@ func Start(ctx context.Context, p *project.Project, server *server.Server) error server.Mux.HandleFunc("/api/env", func(w http.ResponseWriter, r *http.Request) { directory := r.URL.Query().Get("directory") name := r.URL.Query().Get("name") + resolved := complete + latest, err := p.GetCompleted(ctx) + if err == nil && (resolved == nil || resolved.Old) { + resolved = latest + } + if resolved == nil { + http.Error(w, "dev state not ready", http.StatusServiceUnavailable) + return + } cwd, _ := os.Getwd() - for _, d := range complete.Devs { + for _, d := range resolved.Devs { full := filepath.Join(cwd, d.Directory) log.Info("matching dev", "full", full, "directory", directory) if (directory != "" && full == directory) || (name != "" && d.Name == name) { - env, err := p.EnvFor(ctx, complete, d.Name) + env, err := p.EnvFor(ctx, resolved, d.Name) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/cmd/sst/mosaic/errors/errors.go b/cmd/sst/mosaic/errors/errors.go index fff12e53bf..635b1081f1 100644 --- a/cmd/sst/mosaic/errors/errors.go +++ b/cmd/sst/mosaic/errors/errors.go @@ -34,6 +34,7 @@ var transformers = []ErrorTransformer{ exact(server.ErrServerNotFound, "Could not find an `sst dev` session to connect to. Since you are running a command outside of the multiplexer be sure to start `sst dev` first."), exact(provider.ErrBucketMissing, "The state bucket is missing, it may have been accidentally deleted. Go to https://console.aws.amazon.com/systems-manager/parameters/%252Fsst%252Fbootstrap/description?tab=Table and check if the state bucket mentioned there exists. If it doesn't you can recreate it or delete the `/sst/bootstrap` key to force recreation."), exact(project.ErrProtectedStage, "Cannot remove protected stage. To remove a protected stage edit your sst.config.ts and remove the `protect` property."), + exact(project.ErrProtectedDevStage, "Cannot run `sst dev` on a protected stage."), exact(provider.ErrLockNotFound, "This app / stage is not locked"), exact(aws.ErrAppsyncNotReady, "SST creates an appsync event api to power live lambda. After 10 seconds of waiting this cli could not connect to it."), exact(js.ErrTopLevelImport, "Your sst.config.ts has top level imports - this is not allowed. Move imports inside the function they are used and do a dynamic import: `const mod = await import(\"./mod\")`"), diff --git a/cmd/sst/mosaic/monoplexer/monoplexer.go b/cmd/sst/mosaic/monoplexer/monoplexer.go index 0e00b80a51..702ba84f7a 100644 --- a/cmd/sst/mosaic/monoplexer/monoplexer.go +++ b/cmd/sst/mosaic/monoplexer/monoplexer.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "io" + "os" "os/exec" "github.com/sst/sst/v3/pkg/process" @@ -21,18 +22,21 @@ type Line struct { } type Process struct { - name string - title string - cmd *exec.Cmd - dir string + name string + title string + command []string + cmd *exec.Cmd + dir string + env []string + started bool } func (p *Process) IsDifferent(title string, command []string, directory string) bool { - if len(command) != len(p.cmd.Args) { + if len(command) != len(p.command) { return true } for i := range command { - if command[i] != p.cmd.Args[i] { + if command[i] != p.command[i] { return true } } @@ -48,50 +52,74 @@ func (p *Process) IsDifferent(title string, command []string, directory string) func New() *Monoplexer { return &Monoplexer{ processes: map[string]*Process{}, - lines: make(chan Line), + lines: make(chan Line, 32), } } -func (m *Monoplexer) AddProcess(name string, command []string, directory string, title string) { - exists, ok := m.processes[name] - if ok { - if !exists.IsDifferent(title, command, directory) { - return - } - m.lines <- Line{ - line: "dev config changed, restarting...", - process: name, - } - process.Kill(exists.cmd.Process) - delete(m.processes, name) - } - +func (p *Process) start(lines chan Line) { r, w := io.Pipe() - cmd := process.Command(command[0], command[1:]...) + cmd := process.Command(p.command[0], p.command[1:]...) cmd.SysProcAttr = getProcAttr() cmd.Stdout = w cmd.Stderr = w - if directory != "" { - cmd.Dir = directory + if p.dir != "" { + cmd.Dir = p.dir + } + if len(p.env) > 0 { + cmd.Env = append(os.Environ(), p.env...) } go func() { // read r line by line scanner := bufio.NewScanner(r) for scanner.Scan() { - m.lines <- Line{ + lines <- Line{ line: scanner.Text(), - process: name, + process: p.name, } } }() cmd.Start() - m.processes[name] = &Process{ - name: name, - title: title, - cmd: cmd, - dir: directory, + p.cmd = cmd + p.started = true +} + +func (m *Monoplexer) AddProcess(name string, command []string, directory string, title string, autostart bool, env ...string) { + exists, ok := m.processes[name] + if ok { + if !exists.IsDifferent(title, command, directory) { + if !exists.started && autostart { + exists.start(m.lines) + } + return + } + if exists.started { + m.lines <- Line{ + line: "dev config changed, restarting...", + process: name, + } + process.Kill(exists.cmd.Process) + } + delete(m.processes, name) + } + + proc := &Process{ + name: name, + title: title, + command: command, + dir: directory, + env: env, + } + m.processes[name] = proc + if autostart { + proc.start(m.lines) + return + } + m.lines <- Line{ + line: "auto-start disabled", + process: name, } } + func (m *Monoplexer) Start(ctx context.Context) error { for { select { diff --git a/cmd/sst/mosaic/monoplexer/monoplexer_test.go b/cmd/sst/mosaic/monoplexer/monoplexer_test.go new file mode 100644 index 0000000000..93f7e40b6e --- /dev/null +++ b/cmd/sst/mosaic/monoplexer/monoplexer_test.go @@ -0,0 +1,98 @@ +package monoplexer + +import ( + "strings" + "testing" + "time" + + "github.com/sst/sst/v3/pkg/process" +) + +func TestAddProcessAutostartFalse(t *testing.T) { + m := New() + m.AddProcess("web", []string{"sh", "-c", "sleep 5"}, "", "Web", false) + + proc := m.processes["web"] + if proc == nil { + t.Fatal("expected process to be registered") + } + if proc.started { + t.Fatal("expected process to remain stopped") + } + + select { + case line := <-m.lines: + if line.process != "web" { + t.Fatalf("expected message for web, got %q", line.process) + } + if !strings.Contains(line.line, "auto-start disabled") { + t.Fatalf("expected disabled message, got %q", line.line) + } + case <-time.After(time.Second): + t.Fatal("expected disabled message") + } +} + +func TestAddProcessAutostartTrue(t *testing.T) { + m := New() + m.AddProcess("web", []string{"sh", "-c", "sleep 5"}, "", "Web", true) + + proc := m.processes["web"] + if proc == nil { + t.Fatal("expected process to be registered") + } + if !proc.started { + t.Fatal("expected process to start") + } + if proc.cmd == nil || proc.cmd.Process == nil { + t.Fatal("expected child process to be running") + } + + defer process.Kill(proc.cmd.Process) +} + +func TestAddProcessStartsWhenAutostartEnabledLater(t *testing.T) { + m := New() + m.AddProcess("web", []string{"sh", "-c", "sleep 5"}, "", "Web", false) + <-m.lines + + m.AddProcess("web", []string{"sh", "-c", "sleep 5"}, "", "Web", true) + + proc := m.processes["web"] + if proc == nil { + t.Fatal("expected process to be registered") + } + if !proc.started { + t.Fatal("expected process to start when autostart becomes enabled") + } + if proc.cmd == nil || proc.cmd.Process == nil { + t.Fatal("expected child process to be running") + } + + defer process.Kill(proc.cmd.Process) +} + +func TestAddProcessConfigChangeKeepsProcessStopped(t *testing.T) { + m := New() + m.AddProcess("web", []string{"sh", "-c", "sleep 5"}, "", "Web", false) + <-m.lines + + m.AddProcess("web", []string{"sh", "-c", "sleep 10"}, "", "Web", false) + + proc := m.processes["web"] + if proc == nil { + t.Fatal("expected process to be registered") + } + if proc.started { + t.Fatal("expected process to remain stopped after config change") + } + + select { + case line := <-m.lines: + if !strings.Contains(line.line, "auto-start disabled") { + t.Fatalf("expected disabled message, got %q", line.line) + } + case <-time.After(time.Second): + t.Fatal("expected disabled message after config change") + } +} diff --git a/cmd/sst/mosaic/multiplexer/draw.go b/cmd/sst/mosaic/multiplexer/draw.go index fa92691e3a..0142598ac6 100644 --- a/cmd/sst/mosaic/multiplexer/draw.go +++ b/cmd/sst/mosaic/multiplexer/draw.go @@ -1,6 +1,7 @@ package multiplexer import ( + "fmt" "slices" "sort" "strings" @@ -12,6 +13,8 @@ import ( func (s *Multiplexer) draw() { defer s.screen.Show() + s.screen.Clear() + softGray := tcell.NewRGBColor(138, 138, 138) for _, w := range s.stack.Widgets() { s.stack.RemoveWidget(w) } @@ -30,41 +33,64 @@ func (s *Multiplexer) draw() { if index == s.selected { style = style.Bold(true) if !s.focused { - style = style.Foreground(tcell.ColorOrange) + if item.dead { + style = style.Foreground(tcell.ColorOrange).Dim(true) + } else { + style = style.Foreground(tcell.ColorOrange) + } } } + label := item.Title + textStyle := style + if item.filter != "" { + label = item.filter + textStyle = textStyle.Italic(true) + } title := views.NewTextBar() title.SetStyle(style) - title.SetLeft(" "+item.icon+" "+item.title, tcell.StyleDefault) + title.SetLeft(item.Icon+" "+label, textStyle) s.stack.AddWidget(title, 0) } s.stack.AddWidget(views.NewSpacer(), 1) hotkeys := map[string]string{} - if selected != nil && selected.killable && !s.focused { - if !selected.dead { - hotkeys["x"] = "kill" - hotkeys["enter"] = "focus" - } + if s.filtering && s.filterSearching { + hotkeys["esc"] = "cancel" + hotkeys["enter"] = "confirm" + } else if s.filtering { + hotkeys["j/k/↓/↑"] = "move" + hotkeys["enter"] = "select" + hotkeys["/"] = "search" + hotkeys["esc"] = "clear" + } else { + if selected != nil && selected.Killable && !s.focused { + if !selected.dead { + hotkeys["x"] = "kill" + hotkeys["enter"] = "focus" + } - if selected.dead { - hotkeys["enter"] = "start" + if selected.dead { + hotkeys["enter"] = "start" + } + } + if !s.focused { + hotkeys["j/k/↓/↑"] = "up/down" + } + if s.focused { + hotkeys["ctrl-z"] = "sidebar" + } + if selected != nil && selected.vt.HasSelection() { + hotkeys["enter"] = "copy" + } + hotkeys["ctrl-u/d"] = "scroll" + if selected != nil && selected.isScrolling() { + hotkeys["ctrl-g"] = "bottom" + } + hotkeys["ctrl-l"] = "clear" + if selected != nil && !s.focused && selected.Filterable && selected.filterAvailable { + hotkeys["f"] = "filter" } } - if !s.focused { - hotkeys["j/k/↓/↑"] = "up/down" - } - if s.focused { - hotkeys["ctrl-z"] = "sidebar" - } - if selected != nil && selected.isScrolling() && (s.focused || !selected.killable) { - hotkeys["enter"] = "reset" - } - if selected != nil && selected.vt.HasSelection() { - hotkeys["enter"] = "copy" - } - hotkeys["ctrl-u/d"] = "scroll" - hotkeys["ctrl-l"] = "clear" // sort hotkeys keys := make([]string, 0, len(hotkeys)) for key := range hotkeys { @@ -81,28 +107,122 @@ func (s *Multiplexer) draw() { for _, key := range keys { label := hotkeys[key] title := views.NewTextBar() - title.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray)) - title.SetLeft(" "+key, tcell.StyleDefault.Foreground(tcell.ColorGray).Bold(true)) - title.SetRight(label+" ", tcell.StyleDefault) + title.SetStyle(tcell.StyleDefault.Foreground(softGray)) + title.SetLeft(key, tcell.StyleDefault.Foreground(softGray).Bold(true)) + title.SetRight(label+" ", tcell.StyleDefault) s.stack.AddWidget(title, 0) } s.stack.Draw() - borderStyle := tcell.StyleDefault.Foreground(tcell.ColorGray) - for i := 0; i < s.height; i++ { - s.screen.SetContent(SIDEBAR_WIDTH-1, i, 'β”‚', nil, borderStyle) + + borderStyle := tcell.StyleDefault.Foreground(tcell.ColorGray).Dim(true) + for i := PAD_HEIGHT; i < s.height-PAD_HEIGHT; i++ { + s.screen.SetContent(SIDEBAR_WIDTH, i, 'β”‚', nil, borderStyle) } // render virtual terminal - if selected != nil { + if selected != nil && !s.filtering { selected.vt.Draw() if s.focused { y, x, _, _ := selected.vt.Cursor() - s.screen.ShowCursor(SIDEBAR_WIDTH+1+x, y+PAD_HEIGHT) + s.screen.ShowCursor(s.mainX()+x, y+s.contentY()) } if !s.focused { s.screen.HideCursor() } } + + if s.filtering { + if !s.filterSearching { + s.screen.HideCursor() + } + s.drawFilterSelect(selected) + } +} + +func (s *Multiplexer) drawFilterSelect(selected *pane) { + startX := s.mainX() + startY := s.contentY() + endY := s.height - s.contentY() + mainW := s.mainWidth() + softGray := tcell.NewRGBColor(138, 138, 138) + dimStyle := tcell.StyleDefault.Foreground(tcell.ColorGray) + grayStyle := tcell.StyleDefault.Foreground(softGray) + tealStyle := tcell.StyleDefault.Foreground(tcell.ColorTeal).Bold(true) + tealDimStyle := tcell.StyleDefault.Foreground(tcell.ColorTeal).Dim(true) + normalStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite) + + // clear main area + for y := startY; y < endY; y++ { + for x := startX; x < s.width; x++ { + s.screen.SetContent(x, y, ' ', nil, tcell.StyleDefault) + } + } + + y := startY + s.drawLine(startX, y, selected.FilterTitle, tealStyle, mainW) + y++ // blank + y++ + + if s.filterSearching { + x := s.drawLine(startX, y, "Search: ", grayStyle, mainW) + x = s.drawLine(x, y, s.filterQuery, normalStyle, mainW-(x-startX)) + s.screen.ShowCursor(x, y) + } else if s.filterQuery != "" { + x := s.drawLine(startX, y, "Search: ", grayStyle, mainW) + s.drawLine(x, y, s.filterQuery, normalStyle.Italic(true), mainW-(x-startX)) + } else { + s.drawLine(startX, y, selected.FilterSubtitle, grayStyle, mainW) + } + y++ // blank + y++ + + total := len(s.filterFiltered) + + if total > 0 && s.filterScroll > 0 { + s.drawLine(startX, y, fmt.Sprintf("↑ %d more", s.filterScroll), dimStyle, mainW) + } + y++ + + if total == 0 { + s.drawLine(startX, y, "No results found.", dimStyle, mainW) + return + } + + visible := s.filterVisibleRows() + end := min(s.filterScroll+visible, total) + + for fi := s.filterScroll; fi < end && y < endY-1; fi++ { + opt := s.filterOptions[s.filterFiltered[fi]] + style := normalStyle + descStyle := dimStyle + prefix := " " + if fi == s.filterSelected { + style = tealStyle + descStyle = tealDimStyle + prefix = "> " + } + x := s.drawLine(startX, y, prefix+opt.Label, style, mainW) + if opt.Description != "" { + s.drawLine(x, y, " "+opt.Description, descStyle, mainW-(x-startX)) + } + y++ + } + + if end < total && y < endY { + s.drawLine(startX, y, fmt.Sprintf("↓ %d more", total-end), dimStyle, mainW) + } +} + +func (s *Multiplexer) drawLine(startX, y int, text string, style tcell.Style, maxW int) int { + x := startX + for _, ch := range text { + if x-startX >= maxW { + break + } + s.screen.SetContent(x, y, ch, nil, style) + x++ + } + return x } func (s *Multiplexer) move(offset int) { @@ -136,12 +256,12 @@ func (s *Multiplexer) sort() { if len(s.processes) == 0 { return } - key := s.selectedProcess().key + key := s.selectedProcess().Key sort.Slice(s.processes, func(i, j int) bool { - if !s.processes[i].killable && s.processes[j].killable { + if !s.processes[i].Killable && s.processes[j].Killable { return true } - if s.processes[i].killable && !s.processes[j].killable { + if s.processes[i].Killable && !s.processes[j].Killable { return false } if !s.processes[i].dead && s.processes[j].dead { @@ -150,10 +270,10 @@ func (s *Multiplexer) sort() { if s.processes[i].dead && !s.processes[j].dead { return false } - return len(s.processes[i].title) < len(s.processes[j].title) + return len(s.processes[i].Title) < len(s.processes[j].Title) }) for i, p := range s.processes { - if p.key == key { + if p.Key == key { s.selected = i return } diff --git a/cmd/sst/mosaic/multiplexer/multiplexer.go b/cmd/sst/mosaic/multiplexer/multiplexer.go index d185e5b733..cba7a37a44 100644 --- a/cmd/sst/mosaic/multiplexer/multiplexer.go +++ b/cmd/sst/mosaic/multiplexer/multiplexer.go @@ -17,9 +17,11 @@ import ( "github.com/sst/sst/v3/pkg/process" ) -var PAD_HEIGHT = 0 -var PAD_WIDTH = 0 -var SIDEBAR_WIDTH = 20 +var PAD_HEIGHT = 1 +var PAD_WIDTH = 1 +var MAIN_PAD_WIDTH = 3 +var CONTENT_PAD_HEIGHT = 0 +var SIDEBAR_WIDTH = 24 type Multiplexer struct { focused bool @@ -32,12 +34,26 @@ type Multiplexer struct { main *views.ViewPort stack *views.BoxLayout - dragging bool - click *tcell.EventMouse - scrollTicker *time.Ticker - scrollDone chan struct{} - scrollDir int - lastDragX int + dragging bool + click *tcell.EventMouse + scrollTicker *time.Ticker + scrollDone chan struct{} + scrollDir int + lastDragX int + + filtering bool + filterOptions []FilterOption + filterFiltered []int + filterSelected int + filterScroll int + filterSearching bool + filterQuery string +} + +type FilterOption struct { + Label string + Description string + Value string } func New() (*Multiplexer, error) { @@ -65,14 +81,34 @@ func New() (*Multiplexer, error) { } func (s *Multiplexer) mainRect() (int, int) { - return s.width - SIDEBAR_WIDTH + 1, s.height + return s.mainWidth(), s.mainHeight() +} + +func (s *Multiplexer) mainX() int { + return SIDEBAR_WIDTH + 1 + MAIN_PAD_WIDTH +} + +func (s *Multiplexer) mainWidth() int { + return max(0, s.width-s.mainX()-MAIN_PAD_WIDTH) +} + +func (s *Multiplexer) contentY() int { + return PAD_HEIGHT + CONTENT_PAD_HEIGHT +} + +func (s *Multiplexer) mainHeight() int { + return max(0, s.height-s.contentY()*2) +} + +func (s *Multiplexer) sidebarWidth() int { + return SIDEBAR_WIDTH - PAD_WIDTH - 1 } func (s *Multiplexer) resize(width int, height int) { s.width = width s.height = height - s.root.Resize(PAD_WIDTH, PAD_HEIGHT, SIDEBAR_WIDTH, height-PAD_HEIGHT*2) - s.main.Resize(PAD_WIDTH+SIDEBAR_WIDTH+PAD_WIDTH+1, PAD_HEIGHT, width-PAD_WIDTH-SIDEBAR_WIDTH-PAD_WIDTH-PAD_WIDTH-1, height-PAD_HEIGHT*2) + s.root.Resize(PAD_WIDTH, s.contentY(), s.sidebarWidth(), s.mainHeight()) + s.main.Resize(s.mainX(), s.contentY(), s.mainWidth(), s.mainHeight()) mw, mh := s.main.Size() for _, p := range s.processes { p.vt.Resize(mw, mh) @@ -111,7 +147,7 @@ func (s *Multiplexer) Start() { selected.vt.SelectEnd(s.lastDragX, 0) } else { s.scrollDown(1) - selected.vt.SelectEnd(s.lastDragX, s.height-1) + selected.vt.SelectEnd(s.lastDragX, max(0, s.mainHeight()-1)) } } return @@ -120,9 +156,32 @@ func (s *Multiplexer) Start() { shouldBreak = true return + case *EventCheckFilter: + for _, p := range s.processes { + if p.Key != evt.PaneKey || !p.Filterable { + continue + } + p.filterAvailable = len(evt.Names) > 0 + if p.filter == "" { + continue + } + found := false + for _, name := range evt.Names { + if name == p.filter { + found = true + break + } + } + if !found { + s.clearPaneFilter(p) + } + } + s.draw() + return + case *EventProcess: for _, p := range s.processes { - if p.key == evt.Key { + if p.Key == evt.Key { if p.dead && evt.Autostart { p.start() s.sort() @@ -131,15 +190,7 @@ func (s *Multiplexer) Start() { return } } - proc := &pane{ - icon: evt.Icon, - key: evt.Key, - dir: evt.Cwd, - title: evt.Title, - args: evt.Args, - killable: evt.Killable, - env: evt.Env, - } + proc := &pane{PaneConfig: evt.PaneConfig} term := tcellterm.New() term.SetSurface(s.main) term.Attach(func(ev tcell.Event) { @@ -177,25 +228,31 @@ func (s *Multiplexer) Start() { } if evt.Buttons()&tcell.ButtonPrimary != 0 { x, y := evt.Position() - if x <= SIDEBAR_WIDTH && s.dragging && selected != nil { - if y <= 0 { + contentY := y - s.contentY() + maxContentY := max(0, s.mainHeight()-1) + if x < s.mainX() && s.dragging && selected != nil { + if y < s.contentY() { s.scrollUp(1) s.startAutoScroll(-1) selected.vt.SelectEnd(0, 0) s.draw() - } else if y >= s.height-1 { + } else if y >= s.height-s.contentY() { s.scrollDown(1) s.startAutoScroll(1) - selected.vt.SelectEnd(0, s.height-1) + selected.vt.SelectEnd(0, maxContentY) s.draw() - } else { + } else if contentY >= 0 && contentY <= maxContentY { s.stopAutoScroll() - selected.vt.SelectEnd(0, y) + selected.vt.SelectEnd(0, contentY) s.draw() } return } if x < SIDEBAR_WIDTH && !s.dragging { + if contentY < 0 || contentY > maxContentY { + return + } + y = contentY alive := 0 for _, p := range s.processes { if !p.dead { @@ -217,35 +274,41 @@ func (s *Multiplexer) Start() { s.blur() return } - if x > SIDEBAR_WIDTH { + if x >= s.mainX() { + if selected == nil { + return + } + if !s.dragging && (contentY < 0 || contentY > maxContentY) { + return + } if !s.dragging && s.click != nil && time.Since(s.click.When()) < time.Millisecond*500 { oldX, oldY := s.click.Position() if oldX == x && oldY == y { - selected.vt.SelectStart(0, y) - selected.vt.SelectEnd(s.width-1, y) + selected.vt.SelectStart(0, contentY) + selected.vt.SelectEnd(s.width-1, contentY) s.dragging = true s.draw() return } } s.click = evt - offsetX := x - SIDEBAR_WIDTH - 1 + offsetX := x - s.mainX() s.lastDragX = offsetX if s.dragging { - if y <= 0 { + if y < s.contentY() { s.scrollUp(1) s.startAutoScroll(-1) - } else if y >= s.height-1 { + } else if y >= s.height-s.contentY() { s.scrollDown(1) s.startAutoScroll(1) } else { s.stopAutoScroll() } - selected.vt.SelectEnd(offsetX, y) + selected.vt.SelectEnd(offsetX, min(max(contentY, 0), maxContentY)) } if !s.dragging { s.dragging = true - selected.vt.SelectStart(offsetX, y) + selected.vt.SelectStart(offsetX, contentY) } s.draw() return @@ -261,6 +324,9 @@ func (s *Multiplexer) Start() { return case *tcellterm.EventRedraw: + if s.filtering { + return + } if selected != nil && selected.vt == evt.VT() { selected.vt.Draw() s.screen.Show() @@ -284,6 +350,10 @@ func (s *Multiplexer) Start() { return case *tcell.EventKey: + if s.filtering && evt.Key() != tcell.KeyCtrlC { + s.handleFilterKey(evt) + return + } switch evt.Key() { case 256: switch evt.Rune() { @@ -298,9 +368,35 @@ func (s *Multiplexer) Start() { return } case 'x': - if selected.killable && !selected.dead && !s.focused { + if selected.Killable && !selected.dead && !s.focused { selected.Kill() } + case 'f': + if !s.focused && selected != nil && selected.Filterable && selected.filterAvailable && selected.ListOptions != nil { + options := selected.ListOptions() + if len(options) == 0 { + return + } + s.filterOptions = options + s.filterFiltered = make([]int, len(options)) + for i := range options { + s.filterFiltered[i] = i + } + s.filterSelected = 0 + for i, opt := range s.filterOptions { + if opt.Value == selected.filter { + s.filterSelected = i + break + } + } + s.filterScroll = 0 + s.filterSearching = false + s.filterQuery = "" + s.filtering = true + s.filterEnsureVisible() + s.draw() + return + } } case tcell.KeyUp: if !s.focused { @@ -314,12 +410,12 @@ func (s *Multiplexer) Start() { } case tcell.KeyCtrlU: if selected != nil { - s.scrollUp(s.height/2 + 1) + s.scrollUp(s.mainHeight()/2 + 1) return } case tcell.KeyCtrlD: if selected != nil { - s.scrollDown(s.height/2 + 1) + s.scrollDown(s.mainHeight()/2 + 1) return } case tcell.KeyEnter: @@ -329,14 +425,8 @@ func (s *Multiplexer) Start() { s.draw() return } - if selected != nil && selected.isScrolling() && (s.focused || !selected.killable) { - selected.scrollReset() - s.draw() - s.screen.Sync() - return - } if !s.focused { - if selected.killable { + if selected.Killable { if selected.dead { selected.start() s.sort() @@ -355,11 +445,23 @@ func (s *Multiplexer) Start() { process.Signal(syscall.SIGINT) return } + case tcell.KeyEscape: + if !s.focused && selected != nil && selected.filter != "" { + s.clearPaneFilter(selected) + return + } case tcell.KeyCtrlZ: if s.focused { s.blur() return } + case tcell.KeyCtrlG: + if selected != nil && selected.isScrolling() { + selected.scrollReset() + s.draw() + s.screen.Sync() + return + } case tcell.KeyCtrlL: if selected != nil { selected.Clear() @@ -380,6 +482,162 @@ func (s *Multiplexer) Start() { } } +func (s *Multiplexer) handleFilterKey(evt *tcell.EventKey) { + if s.filterSearching { + s.handleFilterSearchKey(evt) + return + } + switch evt.Key() { + case tcell.KeyEscape: + selected := s.selectedProcess() + if selected != nil && selected.filter != "" { + s.clearPaneFilter(selected) + } + s.filtering = false + s.draw() + case tcell.KeyEnter: + if len(s.filterFiltered) == 0 { + return + } + value := s.filterOptions[s.filterFiltered[s.filterSelected]].Value + selected := s.selectedProcess() + if selected != nil && selected.filter == value { + value = "" + } + s.applyFilter(value) + s.filtering = false + s.draw() + case tcell.KeyUp: + if s.filterSelected > 0 { + s.filterSelected-- + s.filterEnsureVisible() + s.draw() + } + case tcell.KeyDown: + if s.filterSelected < len(s.filterFiltered)-1 { + s.filterSelected++ + s.filterEnsureVisible() + s.draw() + } + case tcell.KeyRune: + switch evt.Rune() { + case 'j': + if s.filterSelected < len(s.filterFiltered)-1 { + s.filterSelected++ + s.filterEnsureVisible() + s.draw() + } + case 'k': + if s.filterSelected > 0 { + s.filterSelected-- + s.filterEnsureVisible() + s.draw() + } + case '/': + s.filterSearching = true + s.filterQuery = "" + s.draw() + } + } +} + +func (s *Multiplexer) handleFilterSearchKey(evt *tcell.EventKey) { + switch evt.Key() { + case tcell.KeyEscape: + s.filterSearching = false + s.filterQuery = "" + s.refilterOptions() + s.draw() + case tcell.KeyEnter: + if len(s.filterFiltered) == 1 { + value := s.filterOptions[s.filterFiltered[0]].Value + selected := s.selectedProcess() + if selected != nil && selected.filter == value { + value = "" + } + s.applyFilter(value) + s.filtering = false + s.filterSearching = false + s.draw() + return + } + s.filterSearching = false + s.draw() + case tcell.KeyBackspace, tcell.KeyBackspace2: + if len(s.filterQuery) > 0 { + s.filterQuery = s.filterQuery[:len(s.filterQuery)-1] + s.refilterOptions() + s.draw() + } + case tcell.KeyRune: + s.filterQuery += string(evt.Rune()) + s.refilterOptions() + s.draw() + } +} + +func (s *Multiplexer) refilterOptions() { + q := strings.ToLower(s.filterQuery) + s.filterFiltered = s.filterFiltered[:0] + for i, opt := range s.filterOptions { + if q == "" || strings.Contains(strings.ToLower(opt.Label), q) || strings.Contains(strings.ToLower(opt.Description), q) { + s.filterFiltered = append(s.filterFiltered, i) + } + } + if s.filterSelected >= len(s.filterFiltered) { + s.filterSelected = max(0, len(s.filterFiltered)-1) + } + s.filterEnsureVisible() +} + +func (s *Multiplexer) filterVisibleRows() int { + // header (y=0) + blank + subtitle (y=2) + blank + ↑/blank (y=4) = list starts at y=5 + // reserve 1 row at bottom for ↓ indicator + 6 rows padding + rows := s.mainHeight() - 5 - 1 - 6 + if rows < 1 { + rows = 1 + } + return rows +} + +func (s *Multiplexer) filterEnsureVisible() { + visible := s.filterVisibleRows() + if s.filterSelected < s.filterScroll { + s.filterScroll = s.filterSelected + } + if s.filterSelected >= s.filterScroll+visible { + s.filterScroll = s.filterSelected - visible + 1 + } + if s.filterScroll < 0 { + s.filterScroll = 0 + } +} + +func (s *Multiplexer) clearPaneFilter(p *pane) { + if p.filter == "" { + return + } + p.filter = "" + if p.OnFilterChanged != nil { + p.OnFilterChanged("") + } + s.draw() +} + +func (s *Multiplexer) applyFilter(value string) { + selected := s.selectedProcess() + if selected == nil { + return + } + if selected.filter == value { + return + } + selected.filter = value + if selected.OnFilterChanged != nil { + selected.OnFilterChanged(value) + } +} + type EventExit struct { when time.Time } @@ -392,6 +650,16 @@ func (s *Multiplexer) Exit() { s.screen.PostEvent(&EventExit{}) } +type EventCheckFilter struct { + tcell.EventTime + PaneKey string + Names []string +} + +func (s *Multiplexer) CheckFilter(paneKey string, names []string) { + s.screen.PostEvent(&EventCheckFilter{PaneKey: paneKey, Names: names}) +} + func (s *Multiplexer) stopAutoScroll() { if s.scrollTicker != nil { s.scrollTicker.Stop() diff --git a/cmd/sst/mosaic/multiplexer/process.go b/cmd/sst/mosaic/multiplexer/process.go index 51390f394b..c929bbfaa3 100644 --- a/cmd/sst/mosaic/multiplexer/process.go +++ b/cmd/sst/mosaic/multiplexer/process.go @@ -8,54 +8,47 @@ import ( "github.com/sst/sst/v3/pkg/process" ) -type vterm struct { - Resize func(int, int) - Start func(cmd *exec.Cmd) error +type PaneConfig struct { + Key string + Args []string + Icon string + Title string + Cwd string + Killable bool + Autostart bool + Env []string + Filterable bool + FilterTitle string + FilterSubtitle string + ListOptions func() []FilterOption + OnFilterChanged func(string) } type pane struct { - icon string - key string - args []string - title string - dir string - killable bool - env []string - vt *tcellterm.VT - dead bool - cmd *exec.Cmd + PaneConfig + filterAvailable bool + vt *tcellterm.VT + dead bool + cmd *exec.Cmd + filter string } type EventProcess struct { tcell.EventTime - Key string - Args []string - Icon string - Title string - Cwd string - Killable bool - Autostart bool - Env []string + PaneConfig } -func (s *Multiplexer) AddProcess(key string, args []string, icon string, title string, cwd string, killable bool, autostart bool, env ...string) { +func (s *Multiplexer) AddProcess(cfg PaneConfig) { s.screen.PostEventWait(&EventProcess{ - Key: key, - Args: args, - Icon: icon, - Title: title, - Cwd: cwd, - Killable: killable, - Autostart: autostart, - Env: env, + PaneConfig: cfg, }) } func (p *pane) start() error { - p.cmd = process.Command(p.args[0], p.args[1:]...) - p.cmd.Env = p.env - if p.dir != "" { - p.cmd.Dir = p.dir + p.cmd = process.Command(p.Args[0], p.Args[1:]...) + p.cmd.Env = p.Env + if p.Cwd != "" { + p.cmd.Dir = p.Cwd } p.vt.Clear() err := p.vt.Start(p.cmd) diff --git a/cmd/sst/mosaic/ui/footer.go b/cmd/sst/mosaic/ui/footer.go index d236b0ee31..490a33367f 100644 --- a/cmd/sst/mosaic/ui/footer.go +++ b/cmd/sst/mosaic/ui/footer.go @@ -90,6 +90,9 @@ func (m *footer) Start(ctx context.Context) { } func (m *footer) clear() { + if m.previous == "" { + return + } oldLines := strings.Split(m.previous, "\n") out := &bytes.Buffer{} if len(oldLines) > 0 { @@ -105,18 +108,25 @@ func (m *footer) clear() { } func (m *footer) Render(width int, next string) { - oldLines := strings.Split(m.previous, "\n") - nextLines := strings.Split(next, "\n") + if next == m.previous { + return + } - out := &bytes.Buffer{} + var oldLines []string + if m.previous != "" { + oldLines = strings.Split(m.previous, "\n") + } - // if next == m.previous { - // return - // } + var nextLines []string + if next != "" { + nextLines = strings.Split(next, "\n") + } + + out := &bytes.Buffer{} if len(oldLines) > 0 { for i := range oldLines { - if i < len(oldLines)-len(nextLines) || next == "" { + if i < len(oldLines)-len(nextLines) { out.WriteString(ansi.EraseEntireLine) } if i < len(oldLines)-1 { @@ -129,15 +139,16 @@ func (m *footer) Render(width int, next string) { if i == 0 { out.WriteByte('\r') } - truncated := ansi.Truncate(line, width, "…") - out.WriteString(truncated) + out.WriteString(ansi.Truncate(line, width, "…")) out.WriteString(ansi.EraseLine(0)) if i < len(nextLines)-1 { out.WriteString("\r\n") } } - out.WriteString(ansi.CursorLeft(10000)) - os.Stdout.Write(out.Bytes()) + if out.Len() > 0 { + out.WriteString(ansi.CursorLeft(10000)) + os.Stdout.Write(out.Bytes()) + } m.previous = next } @@ -225,14 +236,19 @@ func (m *footer) Destroy() { var TEXT_HIGHLIGHT = lipgloss.NewStyle().Foreground(lipgloss.Color("14")) var TEXT_HIGHLIGHT_BOLD = TEXT_HIGHLIGHT.Copy().Bold(true) +var SPINNER_STYLE = lipgloss.NewStyle().Foreground(lipgloss.Color("14")) + var TEXT_DIM = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) var TEXT_DIM_BOLD = TEXT_DIM.Copy().Bold(true) +var TEXT_GRAY = lipgloss.NewStyle().Foreground(lipgloss.Color("245")) +var TEXT_GRAY_BOLD = TEXT_GRAY.Copy().Bold(true) var TEXT_NORMAL = lipgloss.NewStyle() var TEXT_NORMAL_BOLD = TEXT_NORMAL.Copy().Bold(true) var TEXT_WARNING = lipgloss.NewStyle().Foreground(lipgloss.Color("11")) var TEXT_WARNING_BOLD = TEXT_WARNING.Copy().Bold(true) +var TEXT_WARNING_DIM = lipgloss.NewStyle().Foreground(lipgloss.Color("3")) var TEXT_DANGER = lipgloss.NewStyle().Foreground(lipgloss.Color("1")) var TEXT_DANGER_BOLD = TEXT_DANGER.Copy().Bold(true) @@ -247,7 +263,7 @@ func (m *footer) View(width int) string { if !m.started || m.complete != nil { return "" } - spinner := spinner.MiniDot.Frames[m.spinner%len(spinner.MiniDot.Frames)] + spinner := SPINNER_STYLE.Render(spinner.MiniDot.Frames[m.spinner%len(spinner.MiniDot.Frames)]) result := []string{} keys := make([]string, 0, len(m.downloading)) for k := range m.downloading { diff --git a/cmd/sst/mosaic/ui/logs.go b/cmd/sst/mosaic/ui/logs.go new file mode 100644 index 0000000000..8ab7167ded --- /dev/null +++ b/cmd/sst/mosaic/ui/logs.go @@ -0,0 +1,74 @@ +package ui + +import ( + "encoding/json" +) + +func compactWorkflowLogLine(line string) (string, bool) { + var parsed map[string]any + if err := json.Unmarshal([]byte(line), &parsed); err != nil { + return "", false + } + + if !isWorkflowLogEnvelope(parsed) { + return "", false + } + + message, ok := parsed["message"] + if !ok { + return "", false + } + + switch value := message.(type) { + case string: + return value, true + default: + data, err := json.Marshal(value) + if err != nil { + return "", false + } + return string(data), true + } +} + +func isWorkflowLogEnvelope(parsed map[string]any) bool { + if _, hasMessage := parsed["message"]; !hasMessage { + return false + } + + if !hasStringField(parsed, "timestamp") { + return false + } + + if !hasStringField(parsed, "level") { + return false + } + + if !hasStringField(parsed, "requestId") && !hasStringField(parsed, "AWSrequestId") { + return false + } + + if hasStringField(parsed, "executionArn") { + return true + } + + if hasStringField(parsed, "execution_arn") { + return true + } + + return false +} + +func hasStringField(parsed map[string]any, key string) bool { + value, ok := parsed[key].(string) + return ok && value != "" +} + +func (u *UI) formatFunctionLogLine(line string) string { + formatted, ok := compactWorkflowLogLine(line) + if !ok { + return line + } + + return formatted +} diff --git a/cmd/sst/mosaic/ui/logs_test.go b/cmd/sst/mosaic/ui/logs_test.go new file mode 100644 index 0000000000..33b7d0a07c --- /dev/null +++ b/cmd/sst/mosaic/ui/logs_test.go @@ -0,0 +1,90 @@ +package ui + +import ( + "testing" +) + +func TestCompactWorkflowLogLine(t *testing.T) { + tests := []struct { + name string + input string + expected string + ok bool + }{ + { + name: "plain text passthrough", + input: "Executing step 1: Hello from the invoker", + expected: "", + ok: false, + }, + { + name: "workflow string message", + input: `{"requestId":"a8220e1b-1f9b-4529-a40f-9e3419bc494f","timestamp":"2026-04-07T15:20:59.866Z","level":"INFO","executionArn":"arn:aws:lambda:us-east-1:123456789012:function:workflow","operationId":"c4ca4238a0b92382","attempt":1,"message":"Executing step 1: Hello from the invoker"}`, + expected: "Executing step 1: Hello from the invoker", + ok: true, + }, + { + name: "workflow object message", + input: `{"requestId":"a8220e1b-1f9b-4529-a40f-9e3419bc494f","timestamp":"2026-04-07T15:21:01.611Z","level":"INFO","executionArn":"arn:aws:lambda:us-east-1:123456789012:function:workflow","operationId":"eccbc87e4b5ce2fe","attempt":1,"message":{"callbackResult":"{\"message\":\"Hello from the invoker\"}","step1":"Hello from the invoker"}}`, + expected: `{"callbackResult":"{\"message\":\"Hello from the invoker\"}","step1":"Hello from the invoker"}`, + ok: true, + }, + { + name: "python workflow message", + input: `{"timestamp":"2026-04-07T15:21:01.611Z","level":"INFO","execution_arn":"arn:aws:lambda:us-east-1:123456789012:function:workflow","message":{"step":"complete","ok":true},"requestId":"req-123"}`, + expected: `{"ok":true,"step":"complete"}`, + ok: true, + }, + { + name: "raw user json passthrough", + input: `{"timestamp":"2026-04-07T15:21:01.611Z","level":"INFO","message":{"step":"complete","ok":true},"requestId":"req-123"}`, + expected: "", + ok: false, + }, + { + name: "custom json with execution arn passthrough", + input: `{"executionArn":"arn:aws:lambda:us-east-1:123456789012:function:workflow","message":{"step":"complete","ok":true}}`, + expected: "", + ok: false, + }, + { + name: "missing request id passthrough", + input: `{"timestamp":"2026-04-07T15:21:01.611Z","level":"INFO","executionArn":"arn:aws:lambda:us-east-1:123456789012:function:workflow","message":"hello"}`, + expected: "", + ok: false, + }, + { + name: "malformed json passthrough", + input: `{"timestamp":"2026-04-07T15:21:01.611Z","message":`, + expected: "", + ok: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual, ok := compactWorkflowLogLine(tt.input) + if ok != tt.ok { + t.Fatalf("expected ok=%v, got %v", tt.ok, ok) + } + if actual != tt.expected { + t.Fatalf("expected %q, got %q", tt.expected, actual) + } + }) + } +} + +func TestFormatFunctionLogLine(t *testing.T) { + line := `{"timestamp":"2026-04-07T15:20:59.866Z","level":"INFO","executionArn":"arn:aws:lambda:us-east-1:123456789012:function:workflow","message":"Executing step 1: Hello from the invoker","requestId":"req-123"}` + + u := &UI{} + + if actual := u.formatFunctionLogLine(line); actual != "Executing step 1: Hello from the invoker" { + t.Fatalf("expected compacted workflow log, got %q", actual) + } + + rawJSON := `{"timestamp":"2026-04-07T15:20:59.866Z","level":"INFO","message":{"user":true},"requestId":"req-123"}` + if actual := u.formatFunctionLogLine(rawJSON); actual != rawJSON { + t.Fatalf("expected raw user json to be unchanged, got %q", actual) + } +} diff --git a/cmd/sst/mosaic/ui/ui.go b/cmd/sst/mosaic/ui/ui.go index 74ce91cd0f..0a17f05d49 100644 --- a/cmd/sst/mosaic/ui/ui.go +++ b/cmd/sst/mosaic/ui/ui.go @@ -22,6 +22,7 @@ import ( "github.com/sst/sst/v3/cmd/sst/mosaic/ui/common" "github.com/sst/sst/v3/pkg/flag" "github.com/sst/sst/v3/pkg/project" + "github.com/sst/sst/v3/pkg/types/typescript" "golang.org/x/crypto/ssh/terminal" ) @@ -47,7 +48,6 @@ type UI struct { dedupe map[string]bool timing map[string]time.Time parents map[string]string - colors map[string]lipgloss.Style workerTime map[string]time.Time complete *project.CompleteEvent footer *footer @@ -61,7 +61,12 @@ type UI struct { type Options struct { Silent bool Log *os.File - Dev bool + Filter string +} + +type PaneFilterEvent struct { + PaneKey string `json:"paneKey"` + Value string `json:"value"` } type Option func(*Options) @@ -70,8 +75,17 @@ func WithSilent(u *Options) { u.Silent = true } -func WithDev(u *Options) { - u.Dev = true +func (u *UI) SetFilter(filter string, paneKey string) { + icons := map[string]string{"function": "Ξ»", "task": "⧉"} + icon := icons[paneKey] + u.options.Filter = filter + u.blank() + if filter != "" { + u.println(TEXT_HIGHLIGHT.Render(icon), " ", TEXT_NORMAL_BOLD.Render("Filter"), " ", TEXT_GRAY.Render(filter)) + } else { + u.println(TEXT_DANGER.Render(icon), " ", TEXT_NORMAL_BOLD.Render("Filter"), " ", TEXT_DIM.Render("Removed")) + } + u.blank() } func WithLog(file *os.File) Option { @@ -88,7 +102,6 @@ func New(ctx context.Context, options ...Option) *UI { isTTY := terminal.IsTerminal(int(os.Stdout.Fd())) slog.Info("initializing ui", "isTTY", isTTY) result := &UI{ - colors: map[string]lipgloss.Style{}, workerTime: map[string]time.Time{}, hasBlank: false, options: opts, @@ -155,57 +168,87 @@ func (u *UI) Event(unknown interface{}) { u.println(evt.Line) case *aws.TaskProvisionEvent: - u.printEvent(u.getColor(""), fmt.Sprintf("%-11s", "Provision"), evt.Name) + if !u.matchFilter(evt.Name) { + return + } + u.printEvent(GetColor(""), fmt.Sprintf("%-11s", "Provision"), evt.Name) case *aws.TaskStartEvent: + if !u.matchFilter(evt.TaskID) { + return + } u.workerTime[evt.WorkerID] = time.Now() - u.printEvent(u.getColor(evt.WorkerID), fmt.Sprintf("%-11s", "Start"), evt.Command) + u.printEvent(GetColor(evt.WorkerID), fmt.Sprintf("%-11s", "Start"), evt.Command) case *aws.TaskLogEvent: + if !u.matchFilter(evt.TaskID) { + return + } duration := time.Since(u.workerTime[evt.WorkerID]).Round(time.Millisecond) formattedDuration := fmt.Sprintf("%.9s", fmt.Sprintf("+%v", duration)) - u.printEvent(u.getColor(evt.WorkerID), formattedDuration, evt.Line) + u.printEvent(GetColor(evt.WorkerID), formattedDuration, evt.Line) case *aws.TaskCompleteEvent: + if !u.matchFilter(evt.TaskID) { + return + } duration := time.Since(u.workerTime[evt.WorkerID]).Round(time.Millisecond) formattedDuration := fmt.Sprintf("took %.9s", fmt.Sprintf("+%v", duration)) - u.printEvent(u.getColor(evt.WorkerID), "Done", formattedDuration) + u.printEvent(GetColor(evt.WorkerID), "Done", formattedDuration) case *aws.TaskMissingCommandEvent: + if !u.matchFilter(evt.Name) { + return + } u.printEvent(TEXT_DANGER, fmt.Sprintf("%-11s", "Missing"), fmt.Sprintf("Dev command not configured for the \"%s\" task. Set `dev.command` to configure how the task works in `sst dev`.", evt.Name)) case *aws.FunctionInvokedEvent: + if !u.matchFilter(evt.FunctionID) { + return + } u.workerTime[evt.WorkerID] = time.Now() - u.printEvent(u.getColor(evt.WorkerID), TEXT_NORMAL_BOLD.Render(fmt.Sprintf("%-11s", "Invoke")), u.functionName(evt.FunctionID)) + u.printEvent(GetColor(evt.WorkerID), TEXT_NORMAL_BOLD.Render(fmt.Sprintf("%-11s", "Invoke")), u.functionName(evt.FunctionID)) case *aws.FunctionResponseEvent: + if !u.matchFilter(evt.FunctionID) { + return + } duration := time.Since(u.workerTime[evt.WorkerID]).Round(time.Millisecond) formattedDuration := fmt.Sprintf("took %.9s", fmt.Sprintf("+%v", duration)) - u.printEvent(u.getColor(evt.WorkerID), "Done", formattedDuration) + u.printEvent(GetColor(evt.WorkerID), "Done", formattedDuration) case *aws.FunctionLogEvent: + if !u.matchFilter(evt.FunctionID) { + return + } duration := time.Since(u.workerTime[evt.WorkerID]).Round(time.Millisecond) formattedDuration := fmt.Sprintf("%.9s", fmt.Sprintf("+%v", duration)) - u.printEvent(u.getColor(evt.WorkerID), formattedDuration, evt.Line) + u.printEvent(GetColor(evt.WorkerID), formattedDuration, u.formatFunctionLogLine(evt.Line)) case *aws.FunctionBuildEvent: + if !u.matchFilter(evt.FunctionID) { + return + } if len(evt.Errors) > 0 { u.printEvent(TEXT_DANGER, "Build Error", u.functionName(evt.FunctionID)) for _, item := range evt.Errors { - u.printEvent(TEXT_DANGER, "", "↳ "+strings.TrimSpace(item)) + u.printEvent(TEXT_DANGER, "", " "+strings.TrimSpace(item)) } return } u.printEvent(TEXT_SUCCESS, "Build", u.functionName(evt.FunctionID)) case *aws.FunctionErrorEvent: - u.printEvent(u.getColor(evt.WorkerID), TEXT_DANGER.Render(fmt.Sprintf("%-11s", "Error")), u.functionName(evt.FunctionID)) - u.printEvent(u.getColor(evt.WorkerID), "", evt.ErrorMessage) + if !u.matchFilter(evt.FunctionID) { + return + } + u.printEvent(GetColor(evt.WorkerID), TEXT_DANGER.Render(fmt.Sprintf("%-11s", "Error")), u.functionName(evt.FunctionID)) + u.printEvent(GetColor(evt.WorkerID), "", evt.ErrorMessage) for _, item := range evt.Trace { if strings.Contains(item, "Error:") { continue } - u.printEvent(u.getColor(evt.WorkerID), "", "↳ "+strings.TrimSpace(item)) + u.printEvent(GetColor(evt.WorkerID), "", "↳ "+strings.TrimSpace(item)) } case *project.ConcurrentUpdateEvent: @@ -221,6 +264,9 @@ func (u *UI) Event(unknown interface{}) { case *project.PolicyAdvisoryEvent: u.printEvent(TEXT_WARNING, "Warning", u.FormatURN(evt.URN)+" "+evt.Policy+": "+evt.Message) + case *typescript.WarningEvent: + u.printEvent(TEXT_WARNING, "Warning", evt.Message) + case *project.StackCommandEvent: u.reset() u.header(evt.Version, evt.App, evt.Stage) @@ -383,16 +429,7 @@ func (u *UI) Event(unknown interface{}) { if evt.Old { break } - if evt.UpdateID != "" && len(evt.Errors) == 0 { - u.blank() - u.println( - TEXT_INFO.Render("β†—"), - " ", - TEXT_NORMAL_BOLD.Render("Permalink"), - " ", - TEXT_NORMAL.Render(`https://sst.dev/u/`+evt.UpdateID[len(evt.UpdateID)-8:]), - ) - } + u.blank() if len(evt.Errors) == 0 && evt.Finished { u.print(TEXT_SUCCESS_BOLD.Render(IconCheck)) @@ -424,20 +461,20 @@ func (u *UI) Event(unknown interface{}) { for k, v := range evt.Hints { splits := strings.Split(k, "::") u.println( - TEXT_DIM_BOLD.Render(" "), - TEXT_DIM_BOLD.Render(splits[len(splits)-1]+": "), + TEXT_GRAY_BOLD.Render(" "), + TEXT_GRAY_BOLD.Render(splits[len(splits)-1]+": "), TEXT_NORMAL.Render(v), ) } } if len(evt.Outputs) > 0 { if len(evt.Hints) > 0 { - u.println(TEXT_DIM_BOLD.Render(" ---")) + u.println(TEXT_GRAY_BOLD.Render(" ---")) } for k, v := range evt.Outputs { u.println( - TEXT_DIM_BOLD.Render(" "), - TEXT_DIM_BOLD.Render(k+": "), + TEXT_GRAY_BOLD.Render(" "), + TEXT_GRAY_BOLD.Render(k+": "), TEXT_NORMAL.Render(fmt.Sprint(v)), ) } @@ -498,28 +535,29 @@ func (u *UI) Event(unknown interface{}) { } } - if evt.UpdateID != "" { - u.blank() - u.println( - TEXT_NORMAL_BOLD.Render("View more in the console:"), - " ", - TEXT_INFO.Render(`https://sst.dev/u/`+evt.UpdateID[len(evt.UpdateID)-8:]), - ) - } } u.blank() case *cloudflare.WorkerBuildEvent: + if !u.matchFilter(evt.WorkerID) { + return + } if len(evt.Errors) > 0 { u.printEvent(TEXT_DANGER, "Build Error", u.functionName(evt.WorkerID)+" "+strings.Join(evt.Errors, "\n")) return } u.printEvent(TEXT_INFO, "Build", u.functionName(evt.WorkerID)) case *cloudflare.WorkerUpdatedEvent: + if !u.matchFilter(evt.WorkerID) { + return + } u.printEvent(TEXT_INFO, "Reload", u.functionName(evt.WorkerID)) case *cloudflare.WorkerInvokedEvent: + if !u.matchFilter(evt.WorkerID) { + return + } url, _ := url.Parse(evt.TailEvent.Event.Request.URL) u.printEvent( - u.getColor(evt.WorkerID), + GetColor(evt.WorkerID), TEXT_NORMAL_BOLD.Render(fmt.Sprintf("%-11s", "Invoke")), u.functionName(evt.WorkerID)+" "+evt.TailEvent.Event.Request.Method+" "+url.Path, ) @@ -539,28 +577,27 @@ func (u *UI) Event(unknown interface{}) { } for _, item := range strings.Split(strings.Join(line, " "), "\n") { - u.printEvent(u.getColor(evt.WorkerID), formattedDuration, item) + u.printEvent(GetColor(evt.WorkerID), formattedDuration, item) } } - u.printEvent(u.getColor(evt.WorkerID), "Done", evt.TailEvent.Outcome) + u.printEvent(GetColor(evt.WorkerID), "Done", evt.TailEvent.Outcome) } } -var COLORS = []lipgloss.Style{ +var Colors = []lipgloss.Style{ lipgloss.NewStyle().Foreground(lipgloss.Color("13")), lipgloss.NewStyle().Foreground(lipgloss.Color("14")), lipgloss.NewStyle().Foreground(lipgloss.Color("2")), lipgloss.NewStyle().Foreground(lipgloss.Color("12")), } -func (u *UI) getColor(input string) lipgloss.Style { - result, ok := u.colors[input] - if !ok { - result = COLORS[len(u.colors)%len(COLORS)] - u.colors[input] = result +func GetColor(input string) lipgloss.Style { + hash := 0 + for _, c := range input { + hash += int(c) } - return result + return Colors[hash%len(Colors)] } func (u *UI) functionName(functionID string) string { @@ -569,10 +606,10 @@ func (u *UI) functionName(functionID string) string { } for _, resource := range u.complete.Resources { if resource.Type == "sst:aws:Function" && resource.URN.Name() == functionID { - return resource.Outputs["_metadata"].(map[string]interface{})["handler"].(string) + return strings.TrimPrefix(resource.Outputs["_metadata"].(map[string]interface{})["handler"].(string), "./") } if resource.Type == "sst:cloudflare:Worker" && resource.URN.Name() == functionID { - return resource.Outputs["_metadata"].(map[string]interface{})["handler"].(string) + return strings.TrimPrefix(resource.Outputs["_metadata"].(map[string]interface{})["handler"].(string), "./") } } return functionID @@ -618,25 +655,19 @@ func (u *UI) header(version, app, stage string) { } u.println( TEXT_HIGHLIGHT_BOLD.Render("SST "+version), - TEXT_DIM.Render(" ready!"), + TEXT_GRAY.Render(" ready!"), ) u.blank() u.println( TEXT_HIGHLIGHT_BOLD.Render("➜ "), TEXT_NORMAL_BOLD.Render(fmt.Sprintf("%-12s", "App:")), - TEXT_DIM.Render(app), + TEXT_GRAY.Render(app), ) u.println( TEXT_NORMAL_BOLD.Render(fmt.Sprintf(" %-12s", "Stage:")), - TEXT_DIM.Render(stage), + TEXT_GRAY.Render(stage), ) - if u.options.Dev { - u.println( - TEXT_NORMAL_BOLD.Render(fmt.Sprintf(" %-12s", "Console:")), - TEXT_DIM.Render("https://console.sst.dev/local/"+app+"/"+stage), - ) - } u.blank() u.hasHeader = true } @@ -679,3 +710,18 @@ func Success(msg string) { func Error(msg string) { fmt.Fprintln(os.Stderr, strings.TrimSpace(TEXT_DANGER_BOLD.Render(IconX)+" "+TEXT_NORMAL.Render(msg))) } + +func (u *UI) matchFilter(id string) bool { + if u.options.Filter == "" { + return true + } + filter := strings.ToLower(u.options.Filter) + if strings.Contains(strings.ToLower(id), filter) { + return true + } + name := u.functionName(id) + if strings.Contains(strings.ToLower(name), filter) { + return true + } + return false +} diff --git a/cmd/sst/mosaic/watcher/watcher.go b/cmd/sst/mosaic/watcher/watcher.go index 7e7b2e4280..48176d8dbf 100644 --- a/cmd/sst/mosaic/watcher/watcher.go +++ b/cmd/sst/mosaic/watcher/watcher.go @@ -4,12 +4,14 @@ import ( "context" "log/slog" "os" + pathpkg "path" "path/filepath" "strings" "time" "github.com/fsnotify/fsnotify" "github.com/sst/sst/v3/pkg/bus" + "github.com/sst/sst/v3/pkg/project" ) type FileChangedEvent struct { @@ -18,61 +20,293 @@ type FileChangedEvent struct { type WatchConfig struct { Root string - Watch []string + Watch project.Watch +} + +type fileChange struct { + timestamp time.Time + discovered bool +} + +func resolveWatch(root string, watch project.Watch) ([]string, []string, error) { + root = filepath.Clean(root) + paths := watch.Paths + if watch.UsesLegacyArray() { + var err error + paths, err = expandLegacyPaths(root, paths) + if err != nil { + return nil, nil, err + } + } + + roots, err := resolveRoots(root, paths) + if err != nil { + return nil, nil, err + } + + ignore := make([]string, 0, len(watch.Ignore)) + for _, path := range watch.Ignore { + ignore = append(ignore, normalizePath(path)) + } + + return roots, ignore, nil +} + +func expandLegacyPaths(root string, paths []string) ([]string, error) { + expanded := make([]string, 0, len(paths)) + seen := map[string]bool{} + for _, path := range paths { + matches := []string{path} + if strings.ContainsAny(path, "*?[") { + resolved, err := filepath.Glob(filepath.Join(root, filepath.FromSlash(path))) + if err != nil { + return nil, err + } + matches = nil + for _, match := range resolved { + watchPath, err := legacyWatchPath(root, match) + if err != nil { + return nil, err + } + matches = append(matches, watchPath) + } + } + + for _, match := range matches { + match = filepath.ToSlash(filepath.Clean(match)) + if seen[match] { + continue + } + seen[match] = true + expanded = append(expanded, match) + } + } + + return expanded, nil +} + +func legacyWatchPath(root string, path string) (string, error) { + resolved := path + if !filepath.IsAbs(resolved) { + resolved = filepath.Join(root, filepath.FromSlash(path)) + } + + info, err := os.Stat(resolved) + if err == nil && !info.IsDir() { + resolved = filepath.Dir(resolved) + } + if err != nil && !os.IsNotExist(err) { + return "", err + } + + rel, err := filepath.Rel(root, resolved) + if err != nil { + return "", err + } + + return filepath.ToSlash(rel), nil +} + +func resolveRoots(root string, paths []string) ([]string, error) { + if len(paths) == 0 { + paths = []string{"."} + } + + seen := map[string]bool{} + var roots []string + + for _, path := range paths { + resolved := filepath.Clean(filepath.Join(root, filepath.FromSlash(path))) + if seen[resolved] { + continue + } + + info, err := os.Stat(resolved) + if err != nil { + if os.IsNotExist(err) { + continue + } + return nil, err + } + + if !info.IsDir() { + continue + } + + seen[resolved] = true + roots = append(roots, resolved) + } + + return roots, nil +} + +func shouldSkipDir(root string, ignore []string, path string, info os.FileInfo) bool { + if !info.IsDir() { + return false + } + + if strings.HasPrefix(info.Name(), ".") { + return true + } + + if strings.Contains(path, "node_modules") { + return true + } + + return isIgnored(root, ignore, path) +} + +func isIgnored(root string, ignore []string, path string) bool { + if len(ignore) == 0 { + return false + } + + rel, err := filepath.Rel(root, path) + if err != nil { + return false + } + + rel = normalizePath(rel) + for _, prefix := range ignore { + if matchesIgnore(prefix, rel) { + return true + } + } + + return false +} + +func matchesIgnore(pattern string, path string) bool { + if pattern == "." { + return true + } + + if strings.Contains(pattern, "/") { + return path == pattern || strings.HasPrefix(path, pattern+"/") + } + + for part := range strings.SplitSeq(path, "/") { + matched, err := pathpkg.Match(pattern, part) + if err == nil && matched { + return true + } + } + + return false +} + +func isWatchedPath(roots []string, path string) bool { + for _, root := range roots { + if isWithinPath(root, path) { + return true + } + } + + return false +} + +func shouldWatchDir(projectRoot string, roots []string, path string) bool { + if filepath.Clean(path) == filepath.Clean(projectRoot) { + return true + } + if isWatchedPath(roots, path) { + return true + } + if !isWithinPath(projectRoot, path) { + return false + } + + for _, root := range roots { + if isWithinPath(path, root) { + return true + } + } + + return false +} + +func normalizePath(path string) string { + path = filepath.ToSlash(filepath.Clean(path)) + if path == "./" { + return "." + } + return strings.TrimPrefix(path, "./") } func Start(ctx context.Context, config WatchConfig) error { log := slog.Default().With("service", "watcher") + log.Info("starting", "root", config.Root) defer log.Info("done") - log.Info("starting watcher", "root", config.Root) + + roots, ignore, err := resolveWatch(config.Root, config.Watch) + if err != nil { + return err + } watcher, err := fsnotify.NewWatcher() if err != nil { return err } + defer watcher.Close() err = watcher.AddWith(config.Root) if err != nil { return err } - matches := []string{} - if len(config.Watch) == 0 { - matches = append(matches, config.Root) - } else { - for _, watch := range config.Watch { - pattern := filepath.Join(config.Root, watch) - patternMatches, err := filepath.Glob(pattern) - if err != nil { - return err + limiter := map[string]fileChange{} + + publishChange := func(path string, discovered bool) { + last, ok := limiter[path] + if ok && time.Since(last.timestamp) <= 500*time.Millisecond { + if discovered || !last.discovered { + return } - matches = append(matches, patternMatches...) } + + limiter[path] = fileChange{timestamp: time.Now(), discovered: discovered} + bus.Publish(&FileChangedEvent{Path: path}) } - ignoreSubstrings := []string{"node_modules"} - for _, match := range matches { - err = filepath.Walk(match, func(path string, info os.FileInfo, err error) error { + watchTree := func(path string, publish bool) error { + return filepath.Walk(path, func(path string, info os.FileInfo, err error) error { if err != nil { + if os.IsNotExist(err) { + return nil + } return err } + if info.IsDir() { - if strings.HasPrefix(info.Name(), ".") { + if shouldSkipDir(config.Root, ignore, path, info) { return filepath.SkipDir } - for _, substring := range ignoreSubstrings { - if strings.Contains(path, substring) { - return filepath.SkipDir - } + if !shouldWatchDir(config.Root, roots, path) { + return filepath.SkipDir } log.Info("watching", "path", path) - err = watcher.Add(path) - if err != nil { - return err - } + return watcher.Add(path) } + + if publish && isWatchedPath(roots, path) && !isIgnored(config.Root, ignore, path) { + log.Info("discovered new file in directory", "path", path) + publishChange(path, true) + } + return nil }) + } + + err = watchTree(config.Root, false) + if err != nil { + return err + } + + for _, match := range roots { + if isWithinPath(config.Root, match) { + continue + } + err = watchTree(match, false) if err != nil { return err } @@ -80,27 +314,67 @@ func Start(ctx context.Context, config WatchConfig) error { headFile := filepath.Join(config.Root, ".git/HEAD") watcher.Add(headFile) - limiter := map[string]time.Time{} for { select { case event, ok := <-watcher.Events: if !ok { return nil } - // if event.Name == headFile { - // return nil - // } if event.Op&(fsnotify.Write|fsnotify.Create) == 0 { log.Info("ignoring file event", "path", event.Name, "op", event.Op) continue } - log.Info("file event", "path", event.Name, "op", event.Op) - if time.Since(limiter[event.Name]) > 500*time.Millisecond { - limiter[event.Name] = time.Now() - bus.Publish(&FileChangedEvent{Path: event.Name}) + if event.Op&fsnotify.Create != 0 { + info, err := os.Stat(event.Name) + if err != nil && !os.IsNotExist(err) { + return err + } + if err == nil && info.IsDir() { + if shouldSkipDir(config.Root, ignore, event.Name, info) || !shouldWatchDir(config.Root, roots, event.Name) { + log.Info("ignoring created directory", "path", event.Name) + continue + } + + err = watchTree(event.Name, true) + if err != nil { + return err + } + + // Catch nested dirs/files created while fsnotify attaches to the new dir. + select { + case <-time.After(50 * time.Millisecond): + case <-ctx.Done(): + return nil + } + err = watchTree(event.Name, true) + if err != nil { + return err + } + continue + } + } + if !isWatchedPath(roots, event.Name) { + log.Info("ignoring unwatched file event", "path", event.Name, "op", event.Op) + continue } + if isIgnored(config.Root, ignore, event.Name) { + log.Info("ignoring ignored file event", "path", event.Name, "op", event.Op) + continue + } + log.Info("file event", "path", event.Name, "op", event.Op) + publishChange(event.Name, false) case <-ctx.Done(): return nil } } } + +func isWithinPath(root string, path string) bool { + rel, err := filepath.Rel(root, path) + if err != nil { + return false + } + + rel = normalizePath(rel) + return rel == "." || (!strings.HasPrefix(rel, "../") && rel != "..") +} diff --git a/cmd/sst/mosaic/watcher/watcher_test.go b/cmd/sst/mosaic/watcher/watcher_test.go new file mode 100644 index 0000000000..3b645b1c66 --- /dev/null +++ b/cmd/sst/mosaic/watcher/watcher_test.go @@ -0,0 +1,284 @@ +package watcher + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "testing" + "time" + + "github.com/sst/sst/v3/pkg/bus" + "github.com/sst/sst/v3/pkg/project" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestResolveWatchDefaultsToProjectRoot(t *testing.T) { + root := t.TempDir() + roots, ignore, err := resolveWatch(root, project.Watch{}) + require.NoError(t, err) + assert.Equal(t, []string{root}, roots) + assert.False(t, isIgnored(root, ignore, filepath.Join(root, "sst.config.ts"))) +} + +func TestResolveWatchResolvesExternalIncludeRoots(t *testing.T) { + workspace := t.TempDir() + root := filepath.Join(workspace, "app") + external := filepath.Join(workspace, "external-package") + require.NoError(t, os.MkdirAll(filepath.Join(root, "packages", "api"), 0755)) + require.NoError(t, os.MkdirAll(external, 0755)) + + roots, _, err := resolveWatch(root, project.Watch{ + Paths: []string{"packages/api", "../external-package"}, + }) + require.NoError(t, err) + assert.ElementsMatch(t, []string{filepath.Join(root, "packages", "api"), external}, roots) +} + +func TestResolveWatchExpandsLegacyArrayGlobs(t *testing.T) { + root := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(root, "packages", "api"), 0755)) + require.NoError(t, os.MkdirAll(filepath.Join(root, "packages", "web"), 0755)) + + var watch project.Watch + require.NoError(t, json.Unmarshal([]byte(`["packages/*"]`), &watch)) + + roots, _, err := resolveWatch(root, watch) + require.NoError(t, err) + assert.ElementsMatch(t, []string{filepath.Join(root, "packages", "api"), filepath.Join(root, "packages", "web")}, roots) +} + +func TestResolveWatchExpandsLegacyArrayFileGlobsToParentDirs(t *testing.T) { + root := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(root, "src"), 0755)) + require.NoError(t, os.WriteFile(filepath.Join(root, "src", "index.ts"), []byte("export {}\n"), 0644)) + + var watch project.Watch + require.NoError(t, json.Unmarshal([]byte(`["src/*"]`), &watch)) + + roots, _, err := resolveWatch(root, watch) + require.NoError(t, err) + assert.Equal(t, []string{filepath.Join(root, "src")}, roots) +} + +func TestResolveWatchMatchesIgnorePaths(t *testing.T) { + workspace := t.TempDir() + root := filepath.Join(workspace, "app") + external := filepath.Join(workspace, "external-package") + require.NoError(t, os.MkdirAll(filepath.Join(root, "packages", "api", "generated"), 0755)) + require.NoError(t, os.MkdirAll(filepath.Join(external, "dist"), 0755)) + + _, ignore, err := resolveWatch(root, project.Watch{ + Ignore: []string{"packages/api/generated", "../external-package/dist"}, + }) + require.NoError(t, err) + + generated := mustInfo(t, filepath.Join(root, "packages", "api", "generated")) + dist := mustInfo(t, filepath.Join(external, "dist")) + + assert.True(t, shouldSkipDir(root, ignore, filepath.Join(root, "packages", "api", "generated"), generated)) + assert.True(t, shouldSkipDir(root, ignore, filepath.Join(external, "dist"), dist)) + assert.True(t, isIgnored(root, ignore, filepath.Join(root, "packages", "api", "generated", "index.ts"))) + assert.True(t, isIgnored(root, ignore, filepath.Join(external, "dist", "index.js"))) + assert.False(t, isIgnored(root, ignore, filepath.Join(root, "sst.config.ts"))) +} + +func TestResolveWatchMatchesIgnoreNamesAnywhere(t *testing.T) { + root := t.TempDir() + _, ignore, err := resolveWatch(root, project.Watch{ + Ignore: []string{".env", "*.egg-info"}, + }) + require.NoError(t, err) + + eggInfo := mustInfo(t, filepath.Join(root, "packages", "api", "foo.egg-info")) + + assert.True(t, isIgnored(root, ignore, filepath.Join(root, ".env"))) + assert.True(t, isIgnored(root, ignore, filepath.Join(root, "packages", "api", ".env"))) + assert.True(t, shouldSkipDir(root, ignore, filepath.Join(root, "packages", "api", "foo.egg-info"), eggInfo)) + assert.True(t, isIgnored(root, ignore, filepath.Join(root, "packages", "api", "foo.egg-info", "PKG-INFO"))) + assert.False(t, isIgnored(root, ignore, filepath.Join(root, "packages", "api", ".env.local"))) +} + +func TestResolveWatchSkipsBuiltInDirs(t *testing.T) { + root := t.TempDir() + _, ignore, err := resolveWatch(root, project.Watch{}) + require.NoError(t, err) + + hidden := mustInfo(t, filepath.Join(root, ".sst")) + nodeModules := mustInfo(t, filepath.Join(root, "node_modules")) + normal := mustInfo(t, filepath.Join(root, "src")) + + assert.True(t, shouldSkipDir(root, ignore, filepath.Join(root, ".sst"), hidden)) + assert.True(t, shouldSkipDir(root, ignore, filepath.Join(root, "node_modules"), nodeModules)) + assert.False(t, shouldSkipDir(root, ignore, filepath.Join(root, "src"), normal)) +} + +func TestStartDiscoversFilesInNewDirectories(t *testing.T) { + root := t.TempDir() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + events := bus.SubscribeAll() + defer bus.Unsubscribe(events) + + errCh := make(chan error, 1) + go func() { + errCh <- Start(ctx, WatchConfig{Root: root, Watch: project.Watch{}}) + }() + + waitForWatcherReady(t, filepath.Join(root, "watcher-ready.txt"), events) + + path := filepath.Join(root, "src", "newpkg", "handler.go") + require.NoError(t, os.MkdirAll(filepath.Dir(path), 0755)) + + deadline := time.Now().Add(5 * time.Second) + for time.Now().Before(deadline) { + require.NoError(t, os.WriteFile(path, []byte(time.Now().String()), 0644)) + if waitForFileChangedEvent(events, path, 200*time.Millisecond) { + cancel() + require.NoError(t, <-errCh) + return + } + time.Sleep(50 * time.Millisecond) + } + + t.Fatalf("expected file change for %s", path) +} + +func TestStartWatchesNewDirectories(t *testing.T) { + root := t.TempDir() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + events := bus.SubscribeAll() + defer bus.Unsubscribe(events) + + errCh := make(chan error, 1) + go func() { + errCh <- Start(ctx, WatchConfig{Root: root, Watch: project.Watch{}}) + }() + + waitForWatcherReady(t, filepath.Join(root, "watcher-ready.txt"), events) + + dir := filepath.Join(root, "src", "newpkg") + require.NoError(t, os.MkdirAll(dir, 0755)) + + path := filepath.Join(dir, "handler.go") + deadline := time.Now().Add(5 * time.Second) + for time.Now().Before(deadline) { + require.NoError(t, os.WriteFile(path, []byte(time.Now().String()), 0644)) + if waitForFileChangedEvent(events, path, 200*time.Millisecond) { + cancel() + require.NoError(t, <-errCh) + return + } + time.Sleep(50 * time.Millisecond) + } + + t.Fatalf("expected file change for %s", path) +} + +func TestStartPicksUpImmediateEditsAfterNewFileDiscovery(t *testing.T) { + root := t.TempDir() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + events := bus.SubscribeAll() + defer bus.Unsubscribe(events) + + errCh := make(chan error, 1) + go func() { + errCh <- Start(ctx, WatchConfig{Root: root, Watch: project.Watch{}}) + }() + + waitForWatcherReady(t, filepath.Join(root, "watcher-ready.txt"), events) + + dir := filepath.Join(root, "src", "newpkg") + require.NoError(t, os.MkdirAll(dir, 0755)) + + path := filepath.Join(dir, "handler.go") + require.NoError(t, os.WriteFile(path, []byte("package newpkg\n"), 0644)) + require.True(t, waitForFileChangedEvent(events, path, 5*time.Second), "expected initial file change for %s", path) + + require.NoError(t, os.WriteFile(path, []byte("package newpkg\n\nfunc Handler() {}\n"), 0644)) + require.True(t, waitForFileChangedEvent(events, path, 1*time.Second), "expected immediate follow-up file change for %s", path) + + cancel() + require.NoError(t, <-errCh) +} + +func TestStartOnlyWatchesConfiguredPaths(t *testing.T) { + root := t.TempDir() + watchedDir := filepath.Join(root, "packages", "api") + require.NoError(t, os.MkdirAll(watchedDir, 0755)) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + events := bus.SubscribeAll() + defer bus.Unsubscribe(events) + + errCh := make(chan error, 1) + go func() { + errCh <- Start(ctx, WatchConfig{ + Root: root, + Watch: project.Watch{ + Paths: []string{"packages/api"}, + }, + }) + }() + + waitForWatcherReady(t, filepath.Join(watchedDir, "watcher-ready.txt"), events) + + unwatched := filepath.Join(root, "docs", "guide.md") + require.NoError(t, os.MkdirAll(filepath.Dir(unwatched), 0755)) + require.NoError(t, os.WriteFile(unwatched, []byte("# docs\n"), 0644)) + assert.False(t, waitForFileChangedEvent(events, unwatched, 750*time.Millisecond), "did not expect file change for %s", unwatched) + + watched := filepath.Join(watchedDir, "handler.go") + require.NoError(t, os.WriteFile(watched, []byte("package api\n"), 0644)) + require.True(t, waitForFileChangedEvent(events, watched, 5*time.Second), "expected file change for %s", watched) + + cancel() + require.NoError(t, <-errCh) +} + +func mustInfo(t *testing.T, path string) os.FileInfo { + t.Helper() + require.NoError(t, os.MkdirAll(path, 0755)) + info, err := os.Stat(path) + require.NoError(t, err) + return info +} + +func waitForWatcherReady(t *testing.T, probe string, events <-chan interface{}) { + t.Helper() + + require.NoError(t, os.MkdirAll(filepath.Dir(probe), 0755)) + deadline := time.Now().Add(5 * time.Second) + for time.Now().Before(deadline) { + require.NoError(t, os.WriteFile(probe, []byte(time.Now().String()), 0644)) + if waitForFileChangedEvent(events, probe, 200*time.Millisecond) { + return + } + time.Sleep(50 * time.Millisecond) + } + + t.Fatalf("timed out waiting for watcher startup") +} + +func waitForFileChangedEvent(events <-chan interface{}, path string, timeout time.Duration) bool { + deadline := time.After(timeout) + for { + select { + case evt := <-events: + changed, ok := evt.(*FileChangedEvent) + if ok && changed.Path == path { + return true + } + case <-deadline: + return false + } + } +} diff --git a/cmd/sst/refresh.go b/cmd/sst/refresh.go index e154134181..35fddc9aee 100644 --- a/cmd/sst/refresh.go +++ b/cmd/sst/refresh.go @@ -54,6 +54,7 @@ func CmdRefresh(c *cli.Cli) error { Target: target, Exclude: exclude, ServerPort: s.Port, + Dev: c.Bool("dev"), Verbose: c.Bool("verbose"), }) if err != nil { diff --git a/cmd/sst/secret.go b/cmd/sst/secret.go index fecbe16cdb..794858dd34 100644 --- a/cmd/sst/secret.go +++ b/cmd/sst/secret.go @@ -8,7 +8,6 @@ import ( "regexp" "strings" - "github.com/fatih/color" "github.com/sst/sst/v3/cmd/sst/cli" "github.com/sst/sst/v3/cmd/sst/mosaic/dev" "github.com/sst/sst/v3/cmd/sst/mosaic/ui" @@ -79,13 +78,16 @@ var CmdSecretList = &cli.Command{ return util.NewReadableError(nil, "No secrets found") } if len(fallback) > 0 { - color.White("# fallback") + fmt.Println(ui.TEXT_DIM.Render("# fallback")) for key, value := range fallback { fmt.Println(key + "=" + value) } } if len(secrets) > 0 { - color.White("# %s/%s", p.App().Name, p.App().Stage) + if len(fallback) > 0 { + fmt.Println() + } + fmt.Println(ui.TEXT_DIM.Render(fmt.Sprintf("# %s/%s", p.App().Name, p.App().Stage))) for key, value := range secrets { fmt.Println(key + "=" + value) } @@ -132,7 +134,7 @@ var CmdSecretLoad = &cli.Command{ "sst secret load --stage production ./secrets.env", "```", "", - "This works becase `secret list` outputs the secrets in the right format.", + "This works because `secret list` outputs the secrets in the right format.", }, "\n"), }, Args: []cli.Argument{ @@ -347,7 +349,7 @@ var CmdSecretSet = &cli.Command{ if err != nil { if err == io.EOF { if input != "" { - value += input + value += input } break } diff --git a/cmd/sst/shell.go b/cmd/sst/shell.go index 0bd2718b48..a143e1dbb2 100644 --- a/cmd/sst/shell.go +++ b/cmd/sst/shell.go @@ -86,8 +86,10 @@ func CmdShell(c *cli.Cli) error { } } if target == "" { - // On Windows with many resources, use a consolidated environment variable to avoid 32KB limit - if runtime.GOOS == "windows" && len(complete.Links) > 50 { + // On Windows, always use a consolidated environment variable because + // Windows uppercases all environment variable names, breaking + // case-sensitive resource lookups. + if runtime.GOOS == "windows" { // Create a single JSON with all resources allResources := make(map[string]any) for resource, value := range complete.Links { diff --git a/cmd/sst/state.go b/cmd/sst/state.go index 3701e3036f..88140eb8a3 100644 --- a/cmd/sst/state.go +++ b/cmd/sst/state.go @@ -23,10 +23,21 @@ var CmdState = &cli.Command{ }, Children: []*cli.Command{ { - Name: "edit", - Hidden: true, + Name: "edit", Description: cli.Description{ Short: "Edit the state of your app", + Long: strings.Join([]string{ + "Edit the raw state of your app directly.", + "", + "This opens your state file in your local editor (`$EDITOR`, or `vim` by default).", + "When you save and exit, SST pushes those changes back to your backend.", + "", + ":::danger", + "This command is dangerous. If you make an invalid change, you can corrupt your state and break deploys.", + "Only use this if you understand the state format and know exactly what you are changing.", + "Consider using safer commands like `sst state remove` or `sst state repair` first.", + ":::", + }, "\n"), }, Run: func(c *cli.Cli) error { p, err := c.InitProject() @@ -124,7 +135,7 @@ var CmdState = &cli.Command{ return err } if c.Bool("decrypt") { - passphrase, err := provider.Passphrase(p.Backend(), p.App().Name, p.App().Stage) + passphrase, err := provider.GetPassphrase(p.Backend(), p.App().Name, p.App().Stage) if err != nil { return err } @@ -135,36 +146,23 @@ var CmdState = &cli.Command{ } encoder := json.NewEncoder(os.Stdout) encoder.SetIndent("", " ") - encoder.Encode(exported) - return err + return encoder.Encode(exported) }, }, { Name: "list", - // TODO: Fix https://github.com/sst/sst/issues/5566 before enabling - Hidden: true, Description: cli.Description{ Short: "List all deployed stages", Long: strings.Join([]string{ - "Lists all the deployed stages of your app for the current set of credentials.", + "Lists all the stages of your app for the current set of credentials.", "", ":::note", - "This does not list stages that are deployed in other accounts.", + "This does not list the stages that are deployed in other accounts.", ":::", "", - "This pull the state of your app from the cloud provider and then prints out all the stages that are listed in the state.", + "This pulls the state of your app from the cloud provider and then prints out all the stages that are listed in the state.", }, "\n"), }, - // Flags: []cli.Flag{ - // { - // Name: "simple", - // Type: "bool", - // Description: cli.Description{ - // Short: "Output a basic list of stages", - // Long: "Output a basic list of stages without additional information about the provider.", - // }, - // }, - // }, Run: func(c *cli.Cli) error { p, err := c.InitProject() if err != nil { @@ -172,23 +170,17 @@ var CmdState = &cli.Command{ } defer p.Cleanup() backend := p.Backend() + currentStage := p.App().Stage stages, err := provider.ListStages(backend, p.App().Name) if err != nil { return err } - // Not sure if we need to enable this yet - // if c.Bool("simple") { - // for _, stage := range stages { - // fmt.Println(stage) - // } - // return nil - // } - lines, err := provider.Info(backend) if err != nil { ui.Error("Failed to load provider information") + return err } renderKeyValue("App", p.App().Name) @@ -198,14 +190,31 @@ var CmdState = &cli.Command{ } if len(stages) == 0 { + fmt.Println( + ui.TEXT_NORMAL_BOLD.Render(indent("Stages:")) + + ui.TEXT_NORMAL.Render(currentStage) + " " + ui.TEXT_WARNING_DIM.Render("(not deployed)"), + ) return nil } - renderKeyValue("Stages", stages[0]) - if len(stages) > 1 { - for _, stage := range stages[1:] { - fmt.Println(indent("") + ui.TEXT_INFO.Render(stage)) + currentDeployed := false + for i, stage := range stages { + rendered := ui.TEXT_GRAY.Render(stage) + if stage == currentStage { + rendered = ui.TEXT_NORMAL.Render(stage) + currentDeployed = true + } + + if i == 0 { + fmt.Println(ui.TEXT_NORMAL_BOLD.Render(indent("Stages:")) + rendered) + continue } + + fmt.Println(indent("") + rendered) + } + + if !currentDeployed { + fmt.Println(indent("") + ui.TEXT_NORMAL.Render(currentStage) + " " + ui.TEXT_WARNING_DIM.Render("(not deployed)")) } return nil @@ -381,17 +390,6 @@ var CmdState = &cli.Command{ return err } - // prompt for confirmation to continue - fmt.Print("Do you want to commit these changes? (y/n): ") - var response string - _, err = fmt.Scanln(&response) - if err != nil { - return fmt.Errorf("failed to read user input: %w", err) - } - if strings.ToLower(response) != "y" { - return util.NewReadableError(nil, "Cancelled repair") - } - err = workdir.Import(checkpoint) if err != nil { return util.NewReadableError(err, "Could not import state") @@ -426,7 +424,7 @@ func confirmMutations(muts []state.Mutation) error { } // prompt for confirmation to continue - fmt.Print("Do you want to commit these changes? (y/n): ") + fmt.Print("Do you want to commit these changes? (Y/n): ") var response string _, err := fmt.Scanln(&response) if err != nil { @@ -443,5 +441,5 @@ func indent(key string) string { } func renderKeyValue(key string, value string) { - fmt.Println(ui.TEXT_NORMAL_BOLD.Render(indent(key+":")) + ui.TEXT_INFO.Render(value)) + fmt.Println(ui.TEXT_NORMAL_BOLD.Render(indent(key+":")) + ui.TEXT_GRAY.Render(value)) } diff --git a/cmd/sst/ui.go b/cmd/sst/ui.go index 06a3b44aea..fb913a7b9e 100644 --- a/cmd/sst/ui.go +++ b/cmd/sst/ui.go @@ -14,6 +14,7 @@ import ( "github.com/sst/sst/v3/cmd/sst/mosaic/ui/common" "github.com/sst/sst/v3/pkg/project" "github.com/sst/sst/v3/pkg/server" + "github.com/sst/sst/v3/pkg/types/typescript" ) func CmdUI(c *cli.Cli) error { @@ -23,15 +24,21 @@ func CmdUI(c *cli.Cli) error { } types := []interface{}{} filter := c.String("filter") - var u *ui.UI - opts := []ui.Option{ - ui.WithDev, + isWorker := filter == "worker" + if isWorker { + filter = "function" } + var u *ui.UI + opts := []ui.Option{} if filter == "function" || filter == "" { if filter != "" { - fmt.Println(ui.TEXT_HIGHLIGHT_BOLD.Render("Function Logs")) + title := "Function Logs" + if isWorker { + title = "Worker Logs" + } + fmt.Println(ui.TEXT_HIGHLIGHT_BOLD.Render(title)) fmt.Println() - fmt.Println(ui.TEXT_DIM.Render("Waiting for invocations...")) + fmt.Println(ui.TEXT_GRAY.Render("Waiting for invocations...")) fmt.Println() } types = append(types, @@ -50,7 +57,7 @@ func CmdUI(c *cli.Cli) error { if filter != "" { fmt.Println(ui.TEXT_HIGHLIGHT_BOLD.Render("Task Logs")) fmt.Println() - fmt.Println(ui.TEXT_DIM.Render("Waiting for tasks...")) + fmt.Println(ui.TEXT_GRAY.Render("Waiting for tasks...")) fmt.Println() } types = append(types, @@ -61,8 +68,11 @@ func CmdUI(c *cli.Cli) error { aws.TaskMissingCommandEvent{}, ) } + if filter == "function" || filter == "task" { + types = append(types, ui.PaneFilterEvent{}) + } if filter == "sst" || filter == "" { - u = ui.New(c.Context, ui.WithDev) + u = ui.New(c.Context) types = append(types, common.StdoutEvent{}, deployer.DeployFailedEvent{}, @@ -77,6 +87,7 @@ func CmdUI(c *cli.Cli) error { apitype.ResOutputsEvent{}, apitype.DiagnosticEvent{}, project.CompleteEvent{}, + typescript.WarningEvent{}, ) } evts, err := dev.Stream(c.Context, url, types...) @@ -101,7 +112,14 @@ func CmdUI(c *cli.Cli) error { c.Cancel() return nil } - u.Event(evt) + switch e := evt.(type) { + case *ui.PaneFilterEvent: + if e.PaneKey == filter { + u.SetFilter(e.Value, e.PaneKey) + } + default: + u.Event(evt) + } } } } diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 9a54f0dd4c..0000000000 --- a/examples/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Examples - -A collection of example SST apps. You can also [view these in our docs](https://sst.dev/docs/examples/). - -## Generated Docs - -The comments in the `sst.config.ts` of these apps are used to generate the doc. Here's an [example comment block](/examples/aws-info/sst.config.ts). - -``` -/** - * ## Current AWS account - * - * You can use the `aws.getXXXXOutput()` provider functions to get info about the current - * AWS account. - * Learn more about [provider functions](/docs/providers/#functions). - */ -``` - -The [generate script](/www/generate.ts) looks for examples that have a comment block like this in their `sst.config.ts`. It'll extract this add it as a section to the examples doc. - -## Contributing - -To contribute an example or to edit one, submit a PR. Make sure to document the `sst.config.ts` in your example if you want to add it to the docs. diff --git a/examples/aws-analog/README.md b/examples/aws-analog/README.md deleted file mode 100644 index 684df4a42f..0000000000 --- a/examples/aws-analog/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Analog App - -This project was generated with [Analog](https://analogjs.org), the fullstack meta-framework for Angular. - -## Setup - -Run `npm install` to install the application dependencies. - -## Development - -Run `npm start` for a dev server. Navigate to `http://localhost:5173/`. The application automatically reloads if you change any of the source files. - -## Build - -Run `npm run build` to build the client/server project. The client build artifacts are located in the `dist/analog/public` directory. The server for the API build artifacts are located in the `dist/analog/server` directory. - -## Test - -Run `npm run test` to run unit tests with [Vitest](https://vitest.dev). - -## Community - -- Visit and Star the [GitHub Repo](https://github.com/analogjs/analog) -- Join the [Discord](https://chat.analogjs.org) -- Follow us on [Twitter](https://twitter.com/analogjs) -- Become a [Sponsor](https://github.com/sponsors/brandonroberts) diff --git a/examples/aws-analog/package.json b/examples/aws-analog/package.json index 5d9dda8d9b..f56f53e377 100644 --- a/examples/aws-analog/package.json +++ b/examples/aws-analog/package.json @@ -36,7 +36,7 @@ "marked-mangle": "^1.1.7", "prismjs": "^1.29.0", "rxjs": "~7.8.0", - "sst": "^3", + "sst": "file:../../sdk/js", "tslib": "^2.3.0", "zone.js": "~0.14.3" }, diff --git a/examples/aws-analog/sst-env.d.ts b/examples/aws-analog/sst-env.d.ts deleted file mode 100644 index 5732622b1f..0000000000 --- a/examples/aws-analog/sst-env.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyWeb": { - "type": "sst.aws.Analog" - "url": string - } - } -} diff --git a/examples/aws-angular/README.md b/examples/aws-angular/README.md deleted file mode 100644 index 83f0b7491d..0000000000 --- a/examples/aws-angular/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# AwsAngular - -This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.2.2. - -## Development server - -Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. - -## Code scaffolding - -Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. - -## Build - -Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. - -## Running unit tests - -Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). - -## Running end-to-end tests - -Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. - -## Further help - -To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. diff --git a/examples/aws-angular/package.json b/examples/aws-angular/package.json index df4a689a08..26c545ea06 100644 --- a/examples/aws-angular/package.json +++ b/examples/aws-angular/package.json @@ -36,7 +36,7 @@ "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", - "sst": "^3", + "sst": "file:../../sdk/js", "typescript": "~5.5.2" } } diff --git a/examples/aws-angular/sst-env.d.ts b/examples/aws-angular/sst-env.d.ts deleted file mode 100644 index 4ed427fb9e..0000000000 --- a/examples/aws-angular/sst-env.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyFunction": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyWeb": { - "type": "sst.aws.StaticSite" - "url": string - } - } -} -export {} diff --git a/examples/aws-api/package.json b/examples/aws-api/package.json index c54e0276ea..544351cff4 100644 --- a/examples/aws-api/package.json +++ b/examples/aws-api/package.json @@ -12,7 +12,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.540.0", "@aws-sdk/s3-request-presigner": "^3.540.0", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "8.10.142" diff --git a/examples/aws-api/sst-env.d.ts b/examples/aws-api/sst-env.d.ts deleted file mode 100644 index 48922d51e9..0000000000 --- a/examples/aws-api/sst-env.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - MyApi: { - type: "sst.aws.ApiGatewayV2" - url: string - } - MyBucket: { - name: string - type: "sst.aws.Bucket" - } - } -} -export {} diff --git a/examples/aws-apig-auth/package.json b/examples/aws-apig-auth/package.json new file mode 100644 index 0000000000..7776c8ad27 --- /dev/null +++ b/examples/aws-apig-auth/package.json @@ -0,0 +1,7 @@ +{ + "name": "aws-apig-auth", + "version": "0.0.0", + "dependencies": { + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-apig-websocket/package.json b/examples/aws-apig-websocket/package.json index a62b1989eb..3b0dba2a21 100644 --- a/examples/aws-apig-websocket/package.json +++ b/examples/aws-apig-websocket/package.json @@ -11,6 +11,6 @@ "license": "ISC", "dependencies": { "@aws-sdk/client-apigatewaymanagementapi": "^3.699.0", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-apig-websocket/sst-env.d.ts b/examples/aws-apig-websocket/sst-env.d.ts deleted file mode 100644 index 872e6dddd4..0000000000 --- a/examples/aws-apig-websocket/sst-env.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyApi": { - "managementEndpoint": string - "type": "sst.aws.ApiGatewayWebSocket" - "url": string - } - } -} diff --git a/examples/aws-apigv1-stream/hono.ts b/examples/aws-apigv1-stream/hono.ts new file mode 100644 index 0000000000..a80f1f5cbc --- /dev/null +++ b/examples/aws-apigv1-stream/hono.ts @@ -0,0 +1,13 @@ +import { Hono } from "hono"; +import { streamText } from "hono/streaming"; +import { streamHandle } from "hono/aws-lambda"; + +const app = new Hono().get("/hono", (c) => { + return streamText(c, async (stream) => { + await stream.writeln("Hello"); + await stream.sleep(3000); + await stream.writeln("World"); + }); +}); + +export const handler = streamHandle(app); diff --git a/examples/aws-apigv1-stream/index.ts b/examples/aws-apigv1-stream/index.ts new file mode 100644 index 0000000000..fb8e0b89d2 --- /dev/null +++ b/examples/aws-apigv1-stream/index.ts @@ -0,0 +1,17 @@ +export const handler = awslambda.streamifyResponse( + async (event, stream) => { + stream = awslambda.HttpResponseStream.from(stream, { + statusCode: 200, + headers: { + "Content-Type": "text/plain; charset=UTF-8", + "X-Content-Type-Options": "nosniff", + }, + }); + + stream.write("Hello "); + await new Promise((resolve) => setTimeout(resolve, 3000)); + stream.write("World"); + + stream.end(); + }, +); diff --git a/examples/aws-apigv1-stream/package.json b/examples/aws-apigv1-stream/package.json new file mode 100644 index 0000000000..6669ffcf77 --- /dev/null +++ b/examples/aws-apigv1-stream/package.json @@ -0,0 +1,9 @@ +{ + "name": "aws-apigv1-stream", + "version": "1.0.0", + "dependencies": { + "@types/aws-lambda": "^8.10.161", + "hono": "^4", + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-apigv1-stream/sst.config.ts b/examples/aws-apigv1-stream/sst.config.ts new file mode 100644 index 0000000000..8e27d03985 --- /dev/null +++ b/examples/aws-apigv1-stream/sst.config.ts @@ -0,0 +1,63 @@ +/// + +/** + * ## AWS API Gateway V1 streaming + * + * An example on how to enable streaming for API Gateway REST API routes. + * + * ```ts title="sst.config.ts" + * api.route("GET /", { + * handler: "index.handler", + * streaming: true, + * }); + * ``` + * + * The handler uses the native `awslambda.streamifyResponse` and + * `awslambda.HttpResponseStream.from` to stream responses through API Gateway. + * + * ```ts title="index.ts" + * export const handler = awslambda.streamifyResponse( + * async (event, stream) => { + * stream = awslambda.HttpResponseStream.from(stream, { + * statusCode: 200, + * headers: { + * "Content-Type": "text/plain; charset=UTF-8", + * "X-Content-Type-Options": "nosniff", + * }, + * }); + * + * stream.write("Hello "); + * await new Promise((resolve) => setTimeout(resolve, 3000)); + * stream.write("World"); + * + * stream.end(); + * }, + * ); + * ``` + * + */ +export default $config({ + app(input) { + return { + name: "aws-apigv1-stream", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + }; + }, + async run() { + const api = new sst.aws.ApiGatewayV1("MyApi"); + api.route("GET /", { + handler: "index.handler", + streaming: true, + }); + api.route("GET /hono", { + handler: "hono.handler", + streaming: true, + }); + api.deploy(); + + return { + api: api.url, + }; + }, +}); diff --git a/examples/aws-apigv1-stream/tsconfig.json b/examples/aws-apigv1-stream/tsconfig.json new file mode 100644 index 0000000000..853c71f05c --- /dev/null +++ b/examples/aws-apigv1-stream/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + } +} diff --git a/examples/aws-apigv1/package.json b/examples/aws-apigv1/package.json index 2f68c4a6b9..132ea3562d 100644 --- a/examples/aws-apigv1/package.json +++ b/examples/aws-apigv1/package.json @@ -12,6 +12,6 @@ "dependencies": { "@aws-sdk/client-s3": "^3.540.0", "@aws-sdk/s3-request-presigner": "^3.540.0", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-apigv1/sst-env.d.ts b/examples/aws-apigv1/sst-env.d.ts deleted file mode 100644 index ac46edb58b..0000000000 --- a/examples/aws-apigv1/sst-env.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - MyApi: { - type: "sst.aws.ApiGatewayV1" - url: string - } - MyApiAuthorizerMyAuthorizerFunction: { - name: string - type: "sst.aws.Function" - } - } -} -export {} \ No newline at end of file diff --git a/examples/aws-app-sync/package.json b/examples/aws-app-sync/package.json new file mode 100644 index 0000000000..36cadba873 --- /dev/null +++ b/examples/aws-app-sync/package.json @@ -0,0 +1,7 @@ +{ + "name": "aws-app-sync", + "version": "0.0.0", + "dependencies": { + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-astro-container/README.md b/examples/aws-astro-container/README.md deleted file mode 100644 index ff19a3e7ec..0000000000 --- a/examples/aws-astro-container/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Astro Starter Kit: Basics - -```sh -npm create astro@latest -- --template basics -``` - -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) -[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) - -> πŸ§‘β€πŸš€ **Seasoned astronaut?** Delete this file. Have fun! - -![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) - -## πŸš€ Project Structure - -Inside of your Astro project, you'll see the following folders and files: - -```text -/ -β”œβ”€β”€ public/ -β”‚ └── favicon.svg -β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ layouts/ -β”‚ β”‚ └── Layout.astro -β”‚ └── pages/ -β”‚ └── index.astro -└── package.json -``` - -To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/). - -## 🧞 Commands - -All commands are run from the root of the project, from a terminal: - -| Command | Action | -| :------------------------ | :----------------------------------------------- | -| `npm install` | Installs dependencies | -| `npm run dev` | Starts local dev server at `localhost:4321` | -| `npm run build` | Build your production site to `./dist/` | -| `npm run preview` | Preview your build locally, before deploying | -| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | -| `npm run astro -- --help` | Get help using the Astro CLI | - -## πŸ‘€ Want to learn more? - -Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). diff --git a/examples/aws-astro-container/package.json b/examples/aws-astro-container/package.json index 45c0da476f..1bd6f7c6b7 100644 --- a/examples/aws-astro-container/package.json +++ b/examples/aws-astro-container/package.json @@ -15,7 +15,7 @@ "@aws-sdk/s3-request-presigner": "^3.701.0", "astro": "^4.16.16", "astro-sst": "2.43.5", - "sst": "^3", + "sst": "file:../../sdk/js", "typescript": "^5.7.2" } } diff --git a/examples/aws-astro-container/sst-env.d.ts b/examples/aws-astro-container/sst-env.d.ts deleted file mode 100644 index ff1d425c12..0000000000 --- a/examples/aws-astro-container/sst-env.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-astro-redis/README.md b/examples/aws-astro-redis/README.md deleted file mode 100644 index 1db3fb3991..0000000000 --- a/examples/aws-astro-redis/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# Astro Starter Kit: Basics - -```sh -npm create astro@latest -- --template basics -``` - -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) -[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) - -> πŸ§‘β€πŸš€ **Seasoned astronaut?** Delete this file. Have fun! - -![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) - -## πŸš€ Project Structure - -Inside of your Astro project, you'll see the following folders and files: - -```text -/ -β”œβ”€β”€ public/ -β”‚ └── favicon.svg -β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ components/ -β”‚ β”‚ └── Card.astro -β”‚ β”œβ”€β”€ layouts/ -β”‚ β”‚ └── Layout.astro -β”‚ └── pages/ -β”‚ └── index.astro -└── package.json -``` - -Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. - -There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. - -Any static assets, like images, can be placed in the `public/` directory. - -## 🧞 Commands - -All commands are run from the root of the project, from a terminal: - -| Command | Action | -| :------------------------ | :----------------------------------------------- | -| `npm install` | Installs dependencies | -| `npm run dev` | Starts local dev server at `localhost:4321` | -| `npm run build` | Build your production site to `./dist/` | -| `npm run preview` | Preview your build locally, before deploying | -| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | -| `npm run astro -- --help` | Get help using the Astro CLI | - -## πŸ‘€ Want to learn more? - -Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). diff --git a/examples/aws-astro-redis/package.json b/examples/aws-astro-redis/package.json index 70168e3506..24ddfc5ac6 100644 --- a/examples/aws-astro-redis/package.json +++ b/examples/aws-astro-redis/package.json @@ -15,7 +15,7 @@ "astro": "^4.16.3", "astro-sst": "2.43.5", "ioredis": "^5.4.1", - "sst": "^3", + "sst": "file:../../sdk/js", "typescript": "^5.6.3" } } diff --git a/examples/aws-astro-redis/sst-env.d.ts b/examples/aws-astro-redis/sst-env.d.ts deleted file mode 100644 index 261f14f5b2..0000000000 --- a/examples/aws-astro-redis/sst-env.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyRedis": { - "host": string - "password": string - "port": number - "type": "sst.aws.Redis" - "username": string - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "bastion": string - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-astro-stream/README.md b/examples/aws-astro-stream/README.md deleted file mode 100644 index 1db3fb3991..0000000000 --- a/examples/aws-astro-stream/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# Astro Starter Kit: Basics - -```sh -npm create astro@latest -- --template basics -``` - -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) -[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) - -> πŸ§‘β€πŸš€ **Seasoned astronaut?** Delete this file. Have fun! - -![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) - -## πŸš€ Project Structure - -Inside of your Astro project, you'll see the following folders and files: - -```text -/ -β”œβ”€β”€ public/ -β”‚ └── favicon.svg -β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ components/ -β”‚ β”‚ └── Card.astro -β”‚ β”œβ”€β”€ layouts/ -β”‚ β”‚ └── Layout.astro -β”‚ └── pages/ -β”‚ └── index.astro -└── package.json -``` - -Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. - -There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. - -Any static assets, like images, can be placed in the `public/` directory. - -## 🧞 Commands - -All commands are run from the root of the project, from a terminal: - -| Command | Action | -| :------------------------ | :----------------------------------------------- | -| `npm install` | Installs dependencies | -| `npm run dev` | Starts local dev server at `localhost:4321` | -| `npm run build` | Build your production site to `./dist/` | -| `npm run preview` | Preview your build locally, before deploying | -| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | -| `npm run astro -- --help` | Get help using the Astro CLI | - -## πŸ‘€ Want to learn more? - -Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). diff --git a/examples/aws-astro-stream/package.json b/examples/aws-astro-stream/package.json index c2f1390a9b..d2ee3a1fb7 100644 --- a/examples/aws-astro-stream/package.json +++ b/examples/aws-astro-stream/package.json @@ -13,7 +13,7 @@ "@astrojs/check": "^0.9.3", "astro": "^4.15.9", "astro-sst": "2.43.5", - "sst": "^3", + "sst": "file:../../sdk/js", "typescript": "^5.6.2" } } diff --git a/examples/aws-astro-stream/sst-env.d.ts b/examples/aws-astro-stream/sst-env.d.ts deleted file mode 100644 index 5213abcbfa..0000000000 --- a/examples/aws-astro-stream/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - } -} diff --git a/examples/aws-astro/README.md b/examples/aws-astro/README.md deleted file mode 100644 index 1db3fb3991..0000000000 --- a/examples/aws-astro/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# Astro Starter Kit: Basics - -```sh -npm create astro@latest -- --template basics -``` - -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) -[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) - -> πŸ§‘β€πŸš€ **Seasoned astronaut?** Delete this file. Have fun! - -![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) - -## πŸš€ Project Structure - -Inside of your Astro project, you'll see the following folders and files: - -```text -/ -β”œβ”€β”€ public/ -β”‚ └── favicon.svg -β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ components/ -β”‚ β”‚ └── Card.astro -β”‚ β”œβ”€β”€ layouts/ -β”‚ β”‚ └── Layout.astro -β”‚ └── pages/ -β”‚ └── index.astro -└── package.json -``` - -Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. - -There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. - -Any static assets, like images, can be placed in the `public/` directory. - -## 🧞 Commands - -All commands are run from the root of the project, from a terminal: - -| Command | Action | -| :------------------------ | :----------------------------------------------- | -| `npm install` | Installs dependencies | -| `npm run dev` | Starts local dev server at `localhost:4321` | -| `npm run build` | Build your production site to `./dist/` | -| `npm run preview` | Preview your build locally, before deploying | -| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | -| `npm run astro -- --help` | Get help using the Astro CLI | - -## πŸ‘€ Want to learn more? - -Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). diff --git a/examples/aws-astro/bun.lockb b/examples/aws-astro/bun.lockb deleted file mode 100755 index eb7d4e2f46..0000000000 Binary files a/examples/aws-astro/bun.lockb and /dev/null differ diff --git a/examples/aws-astro/package.json b/examples/aws-astro/package.json index e7ce42a653..6c677d9e32 100644 --- a/examples/aws-astro/package.json +++ b/examples/aws-astro/package.json @@ -15,7 +15,7 @@ "@aws-sdk/s3-request-presigner": "^3.540.0", "astro": "^4.5.9", "astro-sst": "^2.41.2", - "sst": "^3", + "sst": "file:../../sdk/js", "typescript": "^5.4.3" } } diff --git a/examples/aws-astro/sst-env.d.ts b/examples/aws-astro/sst-env.d.ts deleted file mode 100644 index 53610d327e..0000000000 --- a/examples/aws-astro/sst-env.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - MyBucket: { - name: string - type: "sst.aws.Bucket" - } - MyWeb: { - type: "sst.aws.Astro" - url: string - } - } -} -export {} diff --git a/examples/aws-aurora-local/package.json b/examples/aws-aurora-local/package.json index 31b94792d7..d1dee66d4c 100644 --- a/examples/aws-aurora-local/package.json +++ b/examples/aws-aurora-local/package.json @@ -11,6 +11,6 @@ "license": "ISC", "dependencies": { "postgres": "^3.4.5", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-aurora-local/sst-env.d.ts b/examples/aws-aurora-local/sst-env.d.ts deleted file mode 100644 index 31f0714de1..0000000000 --- a/examples/aws-aurora-local/sst-env.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyFunction": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyPostgres": { - "database": string - "host": string - "password": string - "port": number - "type": "sst.aws.Aurora" - "username": string - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-aurora-mysql/package.json b/examples/aws-aurora-mysql/package.json index 70b27ae216..e1996251eb 100644 --- a/examples/aws-aurora-mysql/package.json +++ b/examples/aws-aurora-mysql/package.json @@ -11,6 +11,6 @@ "description": "", "dependencies": { "mysql2": "^3.12.0", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-aurora-postgres/package.json b/examples/aws-aurora-postgres/package.json index 60003d4939..941512a6ae 100644 --- a/examples/aws-aurora-postgres/package.json +++ b/examples/aws-aurora-postgres/package.json @@ -11,6 +11,6 @@ "description": "", "dependencies": { "postgres": "^3.4.5", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-auth-nextjs/README.md b/examples/aws-auth-nextjs/README.md deleted file mode 100644 index e215bc4ccf..0000000000 --- a/examples/aws-auth-nextjs/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/examples/aws-auth-nextjs/package.json b/examples/aws-auth-nextjs/package.json index 335a78a37e..0c83c699ac 100644 --- a/examples/aws-auth-nextjs/package.json +++ b/examples/aws-auth-nextjs/package.json @@ -14,7 +14,7 @@ "next": "15.1.4", "react": "^19.0.0", "react-dom": "^19.0.0", - "sst": "^3", + "sst": "file:../../sdk/js", "valibot": "^1.0.0-beta.11" }, "devDependencies": { diff --git a/examples/aws-auth-nextjs/sst-env.d.ts b/examples/aws-auth-nextjs/sst-env.d.ts deleted file mode 100644 index c1b62bdd43..0000000000 --- a/examples/aws-auth-nextjs/sst-env.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -declare module "sst" { - export interface Resource { - } -} -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-auth-react/README.md b/examples/aws-auth-react/README.md deleted file mode 100644 index 98550acf6a..0000000000 --- a/examples/aws-auth-react/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# Monorepo Template - -A template to create a monorepo SST v3 project. [Learn more](https://sst.dev/docs/set-up-a-monorepo). - -## Get started - -1. Use this template to [create your own repo](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template). - -2. Clone the new repo. - - ```bash - git clone MY_APP - cd MY_APP - ``` - -3. Rename the files in the project to the name of your app. - - ```bash - npx replace-in-file '/aws-auth-react/g' 'MY_APP' '**/*.*' --verbose - ``` - -4. Deploy! - - ```bash - npm install - npx sst deploy - ``` - -5. Optionally, enable [_git push to deploy_](https://sst.dev/docs/console/#autodeploy). - -## Usage - -This template uses [npm Workspaces](https://docs.npmjs.com/cli/v8/using-npm/workspaces). It has 3 packages to start with and you can add more it. - -1. `core/` - - This is for any shared code. It's defined as modules. For example, there's the `Example` module. - - ```ts - export module Example { - export function hello() { - return "Hello, world!"; - } - } - ``` - - That you can use across other packages using. - - ```ts - import { Example } from "@aws-monorepo/core/example"; - - Example.hello(); - ``` - - We also have [Vitest](https://vitest.dev/) configured for testing this package with the `sst shell` CLI. - - ```bash - npm test - ``` - -2. `functions/` - - This is for your Lambda functions and it uses the `core` package as a local dependency. - -3. `scripts/` - - This is for any scripts that you can run on your SST app using the `sst shell` CLI and [`tsx`](https://www.npmjs.com/package/tsx). For example, you can run the example script using: - - ```bash - npm run shell src/example.ts - ``` - -### Infrastructure - -The `infra/` directory allows you to logically split the infrastructure of your app into separate files. This can be helpful as your app grows. - -In the template, we have an `api.ts`, and `storage.ts`. These export the created resources. And are imported in the `sst.config.ts`. - ---- - -**Join our community** [Discord](https://sst.dev/discord) | [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev) diff --git a/examples/aws-auth-react/package.json b/examples/aws-auth-react/package.json index a33c26861c..1f1027b316 100644 --- a/examples/aws-auth-react/package.json +++ b/examples/aws-auth-react/package.json @@ -10,6 +10,6 @@ "typescript": "^5" }, "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-auth-react/packages/core/package.json b/examples/aws-auth-react/packages/core/package.json index f7030a5e61..e7d239d85e 100644 --- a/examples/aws-auth-react/packages/core/package.json +++ b/examples/aws-auth-react/packages/core/package.json @@ -12,7 +12,7 @@ ] }, "dependencies": { - "sst": "*" + "sst": "file:../../../../sdk/js" }, "devDependencies": { "vitest": "^2" diff --git a/examples/aws-auth-react/packages/core/src/example/index.ts b/examples/aws-auth-react/packages/core/src/example/index.ts index 53b1d6dd50..9cb1c03477 100644 --- a/examples/aws-auth-react/packages/core/src/example/index.ts +++ b/examples/aws-auth-react/packages/core/src/example/index.ts @@ -1,4 +1,4 @@ -export module Example { +export namespace Example { export function hello() { return "Hello, world!"; } diff --git a/examples/aws-auth-react/packages/core/sst-env.d.ts b/examples/aws-auth-react/packages/core/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/aws-auth-react/packages/core/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-auth-react/packages/functions/package.json b/examples/aws-auth-react/packages/functions/package.json index 4ebf232b1e..c6348f56a0 100644 --- a/examples/aws-auth-react/packages/functions/package.json +++ b/examples/aws-auth-react/packages/functions/package.json @@ -6,7 +6,7 @@ "@aws-auth-react/core": "*", "@openauthjs/openauth": "^0.3.6", "hono": "^4.6.18", - "sst": "*", + "sst": "file:../../../../sdk/js", "valibot": "^1.0.0-beta.14" }, "devDependencies": { diff --git a/examples/aws-auth-react/packages/functions/sst-env.d.ts b/examples/aws-auth-react/packages/functions/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/aws-auth-react/packages/functions/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-auth-react/packages/scripts/package.json b/examples/aws-auth-react/packages/scripts/package.json index 90d97c6b5b..39ae89e8c4 100644 --- a/examples/aws-auth-react/packages/scripts/package.json +++ b/examples/aws-auth-react/packages/scripts/package.json @@ -4,7 +4,7 @@ "type": "module", "dependencies": { "@aws-auth-react/core": "*", - "sst": "*" + "sst": "file:../../../../sdk/js" }, "scripts": { "shell": "sst shell tsx" diff --git a/examples/aws-auth-react/packages/scripts/sst-env.d.ts b/examples/aws-auth-react/packages/scripts/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/aws-auth-react/packages/scripts/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-auth-react/packages/web/README.md b/examples/aws-auth-react/packages/web/README.md deleted file mode 100644 index 74872fd4af..0000000000 --- a/examples/aws-auth-react/packages/web/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - -- Configure the top-level `parserOptions` property like this: - -```js -export default tseslint.config({ - languageOptions: { - // other options... - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - }, -}) -``` - -- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` -- Optionally add `...tseslint.configs.stylisticTypeChecked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: - -```js -// eslint.config.js -import react from 'eslint-plugin-react' - -export default tseslint.config({ - // Set the react version - settings: { react: { version: '18.3' } }, - plugins: { - // Add the react plugin - react, - }, - rules: { - // other rules... - // Enable its recommended rules - ...react.configs.recommended.rules, - ...react.configs['jsx-runtime'].rules, - }, -}) -``` diff --git a/examples/aws-auth-react/packages/web/package.json b/examples/aws-auth-react/packages/web/package.json index 6a1b8d3d2c..4afc4bb251 100644 --- a/examples/aws-auth-react/packages/web/package.json +++ b/examples/aws-auth-react/packages/web/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@openauthjs/openauth": "^0.3.8", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/examples/aws-auth-react/packages/web/src/sst-env.d.ts b/examples/aws-auth-react/packages/web/src/sst-env.d.ts deleted file mode 100644 index 8c7ad2d348..0000000000 --- a/examples/aws-auth-react/packages/web/src/sst-env.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/// -interface ImportMetaEnv { - readonly VITE_API_URL: string - readonly VITE_AUTH_URL: string -} -interface ImportMeta { - readonly env: ImportMetaEnv -} \ No newline at end of file diff --git a/examples/aws-auth-react/packages/web/sst-env.d.ts b/examples/aws-auth-react/packages/web/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/aws-auth-react/packages/web/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-auth-react/sst-env.d.ts b/examples/aws-auth-react/sst-env.d.ts deleted file mode 100644 index 4116dbd238..0000000000 --- a/examples/aws-auth-react/sst-env.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -declare module "sst" { - export interface Resource { - "MyApi": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyAuth": { - "type": "sst.aws.Auth" - "url": string - } - "MyWeb": { - "type": "sst.aws.StaticSite" - "url": string - } - } -} -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-base/package.json b/examples/aws-base/package.json index fc89f80539..8721162115 100644 --- a/examples/aws-base/package.json +++ b/examples/aws-base/package.json @@ -2,6 +2,6 @@ "name": "aws-base", "version": "0.0.0", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-bucket-lifecycle-rules/package.json b/examples/aws-bucket-lifecycle-rules/package.json new file mode 100644 index 0000000000..5a92f09ea9 --- /dev/null +++ b/examples/aws-bucket-lifecycle-rules/package.json @@ -0,0 +1,7 @@ +{ + "name": "aws-bucket-lifecycle-rules", + "version": "0.0.0", + "dependencies": { + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-bucket-lifecycle-rules/sst-env.d.ts b/examples/aws-bucket-lifecycle-rules/sst-env.d.ts deleted file mode 100644 index bfe7264453..0000000000 --- a/examples/aws-bucket-lifecycle-rules/sst-env.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - MyBucket: { - name: string - type: "sst.aws.Bucket" - } - } -} -export {} diff --git a/examples/aws-bucket-policy/package.json b/examples/aws-bucket-policy/package.json new file mode 100644 index 0000000000..a336ca8ce1 --- /dev/null +++ b/examples/aws-bucket-policy/package.json @@ -0,0 +1,7 @@ +{ + "name": "aws-bucket-policy", + "version": "0.0.0", + "dependencies": { + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-bucket-policy/sst-env.d.ts b/examples/aws-bucket-policy/sst-env.d.ts deleted file mode 100644 index bfe7264453..0000000000 --- a/examples/aws-bucket-policy/sst-env.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - MyBucket: { - name: string - type: "sst.aws.Bucket" - } - } -} -export {} diff --git a/examples/aws-bucket-queue-subscriber/package.json b/examples/aws-bucket-queue-subscriber/package.json index c36e1cb075..f0361aa21e 100644 --- a/examples/aws-bucket-queue-subscriber/package.json +++ b/examples/aws-bucket-queue-subscriber/package.json @@ -14,6 +14,6 @@ "license": "ISC", "devDependencies": { "@types/aws-lambda": "^8.10.149", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-bucket-queue-subscriber/sst-env.d.ts b/examples/aws-bucket-queue-subscriber/sst-env.d.ts deleted file mode 100644 index 81384d3591..0000000000 --- a/examples/aws-bucket-queue-subscriber/sst-env.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyQueue": { - "type": "sst.aws.Queue" - "url": string - } - } -} -export {} diff --git a/examples/aws-bucket-subscriber/package.json b/examples/aws-bucket-subscriber/package.json new file mode 100644 index 0000000000..4cdabdf595 --- /dev/null +++ b/examples/aws-bucket-subscriber/package.json @@ -0,0 +1,7 @@ +{ + "name": "aws-bucket-subscriber", + "version": "0.0.0", + "dependencies": { + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-bucket-topic-subscriber/package.json b/examples/aws-bucket-topic-subscriber/package.json new file mode 100644 index 0000000000..c54f3182da --- /dev/null +++ b/examples/aws-bucket-topic-subscriber/package.json @@ -0,0 +1,7 @@ +{ + "name": "aws-bucket-topic-subscriber", + "version": "0.0.0", + "dependencies": { + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-bucket-topic-subscriber/sst-env.d.ts b/examples/aws-bucket-topic-subscriber/sst-env.d.ts deleted file mode 100644 index ff3c0fbd3a..0000000000 --- a/examples/aws-bucket-topic-subscriber/sst-env.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyTopic": { - "arn": string - "type": "sst.aws.SnsTopic" - } - } -} -export {} diff --git a/examples/aws-bun-elysia/README.md b/examples/aws-bun-elysia/README.md deleted file mode 100644 index 688c87e69a..0000000000 --- a/examples/aws-bun-elysia/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Elysia with Bun runtime - -## Getting Started -To get started with this template, simply paste this command into your terminal: -```bash -bun create elysia ./elysia-example -``` - -## Development -To start the development server run: -```bash -bun run dev -``` - -Open http://localhost:3000/ with your browser to see the result. \ No newline at end of file diff --git a/examples/aws-bun-elysia/bun.lockb b/examples/aws-bun-elysia/bun.lockb deleted file mode 100755 index 5119659a51..0000000000 Binary files a/examples/aws-bun-elysia/bun.lockb and /dev/null differ diff --git a/examples/aws-bun-elysia/package.json b/examples/aws-bun-elysia/package.json index 526d210659..931c1cff18 100644 --- a/examples/aws-bun-elysia/package.json +++ b/examples/aws-bun-elysia/package.json @@ -10,7 +10,7 @@ "@aws-sdk/lib-storage": "^3.658.1", "@aws-sdk/s3-request-presigner": "^3.658.1", "elysia": "latest", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "8.10.145", diff --git a/examples/aws-bun-elysia/sst-env.d.ts b/examples/aws-bun-elysia/sst-env.d.ts deleted file mode 100644 index 99616cc2b4..0000000000 --- a/examples/aws-bun-elysia/sst-env.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-bun-redis/README.md b/examples/aws-bun-redis/README.md deleted file mode 100644 index 5f3ca715fc..0000000000 --- a/examples/aws-bun-redis/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# aws-bun-redis - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run index.ts -``` - -This project was created using `bun init` in bun v1.1.29. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/examples/aws-bun-redis/package.json b/examples/aws-bun-redis/package.json index b8f525afd8..c4aa0fa55c 100644 --- a/examples/aws-bun-redis/package.json +++ b/examples/aws-bun-redis/package.json @@ -15,6 +15,6 @@ }, "dependencies": { "ioredis": "^5.4.1", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-bun-redis/sst-env.d.ts b/examples/aws-bun-redis/sst-env.d.ts deleted file mode 100644 index 261f14f5b2..0000000000 --- a/examples/aws-bun-redis/sst-env.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyRedis": { - "host": string - "password": string - "port": number - "type": "sst.aws.Redis" - "username": string - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "bastion": string - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-bun/README.md b/examples/aws-bun/README.md deleted file mode 100644 index 41958527f8..0000000000 --- a/examples/aws-bun/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# aws-bun - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run index.ts -``` - -This project was created using `bun init` in bun v1.1.29. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/examples/aws-bun/bun.lockb b/examples/aws-bun/bun.lockb deleted file mode 100755 index 36eb2efe1f..0000000000 Binary files a/examples/aws-bun/bun.lockb and /dev/null differ diff --git a/examples/aws-bun/package.json b/examples/aws-bun/package.json index 3a30995fc7..d2c2c3afb3 100644 --- a/examples/aws-bun/package.json +++ b/examples/aws-bun/package.json @@ -16,6 +16,6 @@ "@aws-sdk/client-s3": "^3.658.1", "@aws-sdk/lib-storage": "^3.658.1", "@aws-sdk/s3-request-presigner": "^3.658.1", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-bun/sst-env.d.ts b/examples/aws-bun/sst-env.d.ts deleted file mode 100644 index ff1d425c12..0000000000 --- a/examples/aws-bun/sst-env.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-bundle/bun.lock b/examples/aws-bundle/bun.lock deleted file mode 100644 index 8d712dc88e..0000000000 --- a/examples/aws-bundle/bun.lock +++ /dev/null @@ -1,45 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "base-ts", - "dependencies": { - "@types/node": "^22.13.2", - "sst": "3.8.8", - }, - }, - }, - "packages": { - "@types/node": ["@types/node@22.13.2", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-Z+r8y3XL9ZpI2EY52YYygAFmo2/oWfNSj4BCpAXE2McAexDk8VcnBMGC9Djn9gTKt4d2T/hhXqmPzo4hfIXtTg=="], - - "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], - - "jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="], - - "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - - "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], - - "oidc-token-hash": ["oidc-token-hash@5.0.3", "", {}, "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw=="], - - "openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="], - - "sst": ["sst@3.8.8", "", { "dependencies": { "aws4fetch": "^1.0.18", "jose": "5.2.3", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.8.8", "sst-darwin-x64": "3.8.8", "sst-linux-arm64": "3.8.8", "sst-linux-x64": "3.8.8", "sst-linux-x86": "3.8.8" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-1de39fi3m2TdY2v/C/58qRx1fXbM8Nmg2+7B7J2fXabYTxYRP/ky3K7Jk/9aS649U5iI2Gmnvs3ltW7wO1/8FA=="], - - "sst-darwin-arm64": ["sst-darwin-arm64@3.8.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FJyYk63zZ89pXf/PZnZrOHTU9reuP2IE6kdFlakxukS0SX2sGHv0BXkoiS1f+9p3VDf+74+VKDrZGn1ECIiNmA=="], - - "sst-darwin-x64": ["sst-darwin-x64@3.8.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-0jNpXXkSW1pGI5vVg2je8mlmCegjJJmWVSrUXb6wJqSVYySuoZIwqQB5jvCePts3YM+/NnW5INeUFurbC6j/dQ=="], - - "sst-linux-arm64": ["sst-linux-arm64@3.8.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-vl/H5EDkHeFW2nnOIEOUV/oNE4ns/I1F/KtK/faDY2VotxuPjT3Uvtqdg1A2mItvyx1FKwI8/KgP9CytVagRrw=="], - - "sst-linux-x64": ["sst-linux-x64@3.8.8", "", { "os": "linux", "cpu": "x64" }, "sha512-S+iCcFhtGqK5px0+oJxic5TSHLZdulWfy+qYzSMl+e3evQGIcJmwHjLuEwlmyuwwgs0+dZYt9NYhLTKyUThLDA=="], - - "sst-linux-x86": ["sst-linux-x86@3.8.8", "", { "os": "linux", "cpu": "none" }, "sha512-lfGHfbL7U196Y8MtqCvBNOz9qaNsWGldPj6HLR6KsW5LTNxFYYehGVebB+tj0uK3gjWPou1bGPASt9/Q69VwIQ=="], - - "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], - - "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - - "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], - } -} diff --git a/examples/aws-bundle/package.json b/examples/aws-bundle/package.json index 8d12cbb7e1..737d546186 100644 --- a/examples/aws-bundle/package.json +++ b/examples/aws-bundle/package.json @@ -4,6 +4,6 @@ "type": "module", "dependencies": { "@types/node": "^22.13.2", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-bundle/sst-env.d.ts b/examples/aws-bundle/sst-env.d.ts deleted file mode 100644 index 21b8814928..0000000000 --- a/examples/aws-bundle/sst-env.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -declare module "sst" { - export interface Resource { - "Function": { - "name": string - "type": "sst.aws.Function" - "url": string - } - } -} -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-bus/bun.lockb b/examples/aws-bus/bun.lockb deleted file mode 100755 index 3cb8fb062f..0000000000 Binary files a/examples/aws-bus/bun.lockb and /dev/null differ diff --git a/examples/aws-bus/package.json b/examples/aws-bus/package.json index 1420439638..17ef5c7687 100644 --- a/examples/aws-bus/package.json +++ b/examples/aws-bus/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "dependencies": { "@aws-sdk/client-eventbridge": "^3.582.0", - "sst": "^3", + "sst": "file:../../sdk/js", "zod": "^4.1.13" }, "devDependencies": { diff --git a/examples/aws-bus/sst-env.d.ts b/examples/aws-bus/sst-env.d.ts deleted file mode 100644 index da03daad2d..0000000000 --- a/examples/aws-bus/sst-env.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -declare module "sst" { - export interface Resource { - "Bus": { - "arn": string - "name": string - "type": "sst.aws.Bus" - } - "Publisher": { - "name": string - "type": "sst.aws.Function" - "url": string - } - } -} -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-cloudflare-combined/.gitignore b/examples/aws-cloudflare-combined/.gitignore new file mode 100644 index 0000000000..f4d5339fb2 --- /dev/null +++ b/examples/aws-cloudflare-combined/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.env +.sst diff --git a/examples/aws-cloudflare-combined/package.json b/examples/aws-cloudflare-combined/package.json new file mode 100644 index 0000000000..0b47f9bf35 --- /dev/null +++ b/examples/aws-cloudflare-combined/package.json @@ -0,0 +1,14 @@ +{ + "name": "aws-cloudflare-combined", + "type": "module", + "dependencies": { + "@aws-sdk/client-s3": "^3.701.0", + "@aws-sdk/s3-request-presigner": "^3.701.0", + "@cloudflare/workers-types": "^4.20240403.0", + "hono": "^4.6.12", + "sst": "../../sdk/js" + }, + "devDependencies": { + "@types/aws-lambda": "8.10.146" + } +} diff --git a/examples/scrap/src/index.ts b/examples/aws-cloudflare-combined/src/index.ts similarity index 100% rename from examples/scrap/src/index.ts rename to examples/aws-cloudflare-combined/src/index.ts diff --git a/examples/aws-cloudflare-combined/sst.config.ts b/examples/aws-cloudflare-combined/sst.config.ts new file mode 100644 index 0000000000..32c74ea0f1 --- /dev/null +++ b/examples/aws-cloudflare-combined/sst.config.ts @@ -0,0 +1,35 @@ +/// + +export default $config({ + app(input) { + return { + name: "aws-cloudflare-combined", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + providers: { + cloudflare: "6.13.0", + }, + }; + }, + async run() { + const bucket = new sst.aws.Bucket("MyBucket"); + const api = new sst.aws.Function("Hono", { + url: true, + link: [bucket], + handler: "src/index.handler", + }); + + // Add a Cloudflare Bucket and link it to the worker + const cfBucket = new sst.cloudflare.Bucket("CfBucket"); + const worker = new sst.cloudflare.Worker("MyWorker", { + handler: "./worker.ts", + link: [cfBucket], + url: true, + }); + + return { + api: api.url, + worker: worker.url, + }; + }, +}); diff --git a/examples/aws-cloudflare-combined/tsconfig.json b/examples/aws-cloudflare-combined/tsconfig.json new file mode 100644 index 0000000000..95a44f83fe --- /dev/null +++ b/examples/aws-cloudflare-combined/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "types": ["node"] + } +} diff --git a/examples/aws-cloudflare-combined/worker.ts b/examples/aws-cloudflare-combined/worker.ts new file mode 100644 index 0000000000..abea65a222 --- /dev/null +++ b/examples/aws-cloudflare-combined/worker.ts @@ -0,0 +1,35 @@ +import { Resource } from "sst"; + +export default { + async fetch(req: Request) { + if (req.method === "PUT") { + const key = crypto.randomUUID(); + await Resource.CfBucket.put(key, req.body, { + httpMetadata: { + contentType: req.headers.get("content-type"), + }, + }); + return new Response(`Object created with key: ${key}`); + } + + if (req.method === "GET") { + const first = await Resource.CfBucket.list().then( + (res) => + res.objects.toSorted( + (a, b) => a.uploaded.getTime() - b.uploaded.getTime(), + )[0], + ); + if (!first) { + return new Response("No objects found"); + } + const result = await Resource.CfBucket.get(first.key); + return new Response(result.body, { + headers: { + "content-type": result.httpMetadata.contentType, + }, + }); + } + + return new Response("Hello from Cloudflare Worker!"); + }, +}; diff --git a/examples/aws-cluster-autoscaling/package.json b/examples/aws-cluster-autoscaling/package.json index c1854234f3..6afabf1390 100644 --- a/examples/aws-cluster-autoscaling/package.json +++ b/examples/aws-cluster-autoscaling/package.json @@ -12,6 +12,6 @@ "dependencies": { "@aws-sdk/client-sqs": "^3.682.0", "express": "^4.21.1", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-cluster-autoscaling/sst-env.d.ts b/examples/aws-cluster-autoscaling/sst-env.d.ts deleted file mode 100644 index 4aeabf853d..0000000000 --- a/examples/aws-cluster-autoscaling/sst-env.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyQueue": { - "type": "sst.aws.Queue" - "url": string - } - "MyQueuePurger": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyQueueSeeder": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-cluster-internal/sst-env.d.ts b/examples/aws-cluster-internal/sst-env.d.ts deleted file mode 100644 index 504a9b4641..0000000000 --- a/examples/aws-cluster-internal/sst-env.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "bastion": string - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-cluster-spot/package.json b/examples/aws-cluster-spot/package.json index a8e7e9c2a7..4efa76a9ce 100644 --- a/examples/aws-cluster-spot/package.json +++ b/examples/aws-cluster-spot/package.json @@ -11,6 +11,6 @@ "description": "", "dependencies": { "express": "^4.21.1", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-cluster-spot/sst-env.d.ts b/examples/aws-cluster-spot/sst-env.d.ts deleted file mode 100644 index e6125d82cd..0000000000 --- a/examples/aws-cluster-spot/sst-env.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-cluster-vpclink/sst-env.d.ts b/examples/aws-cluster-vpclink/sst-env.d.ts deleted file mode 100644 index c4f62b8bbb..0000000000 --- a/examples/aws-cluster-vpclink/sst-env.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyApi": { - "type": "sst.aws.ApiGatewayV2" - "url": string - } - "MyService": { - "service": string - "type": "sst.aws.Service" - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-cognito/package.json b/examples/aws-cognito/package.json new file mode 100644 index 0000000000..0710943f9c --- /dev/null +++ b/examples/aws-cognito/package.json @@ -0,0 +1,7 @@ +{ + "name": "aws-cognito", + "version": "0.0.0", + "dependencies": { + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-cognito/sst.config.ts b/examples/aws-cognito/sst.config.ts index 427b126fa0..e1392fcf95 100644 --- a/examples/aws-cognito/sst.config.ts +++ b/examples/aws-cognito/sst.config.ts @@ -1,8 +1,9 @@ /// + /** * ## AWS Cognito User Pool * - * Create a Cognito user pool with triggers and identity pool. + * Create a Cognito User Pool with a hosted UI domain, client, and identity pool. * */ export default $config({ @@ -15,6 +16,9 @@ export default $config({ }, async run() { const userPool = new sst.aws.CognitoUserPool("MyUserPool", { + domain: { + prefix: `my-app-${$app.stage}`, + }, triggers: { preSignUp: { handler: "index.handler", @@ -39,6 +43,7 @@ export default $config({ UserPool: userPool.id, Client: client.id, IdentityPool: identityPool.id, + DomainUrl: userPool.domainUrl, }; }, }); diff --git a/examples/aws-copy-files/package.json b/examples/aws-copy-files/package.json index 09b4798d4c..e343eb1f30 100644 --- a/examples/aws-copy-files/package.json +++ b/examples/aws-copy-files/package.json @@ -2,6 +2,6 @@ "name": "aws-copy-files", "version": "0.0.0", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-dart-api/README.md b/examples/aws-dart-api/README.md deleted file mode 100644 index c2a23d3e35..0000000000 --- a/examples/aws-dart-api/README.md +++ /dev/null @@ -1,107 +0,0 @@ -# ❍ AWS Dart API Example - -Deploy a Dart API on AWS using SST ION. - -**IMPORTANT:** Docker must be installed to use this approach. - -This project was initiated as a standard Dart package, and then SST was initialized using the command: - -```bash -sst init -``` - -The **aws_lambda_dart_runtime** package was added to the project using the katallaxie GitHub fork: - -```yaml -dependencies: - aws_lambda_dart_runtime: - git: - url: https://github.com/katallaxie/aws-lambda-dart-runtime -``` - -In the `lib/src/main.dart` a simple hello-world function named `hello` has been registered to the `Runtime` singleton: - -```dart -import 'package:aws_lambda_dart_runtime/aws_lambda_dart_runtime.dart'; -import 'package:aws_lambda_dart_runtime/runtime/context.dart'; - -void main() async { - /// This demo's handling an API Gateway request. - hello(Context context, AwsApiGatewayEvent event) async { - final response = { - "message": "Hello from Dart!", - }; - return AwsApiGatewayResponse.fromJson(response); - } - - /// The Runtime is a singleton. You can define the handlers as you wish. - Runtime() - ..registerHandler( - 'hello', - hello, - ) - ..invoke(); -} -``` - -Inside root folder a `build.sh` file was created with the necessary commands to compile the Linux binary: - -```bash -#!/bin/sh - -# Install dependencies -dart pub get - -# build the binary -dart compile exe bin/main.dart -o dist/bootstrap - -# Exit -exit -``` - -Lastly, the `sst.config.ts` file was modified to create the API: - -```typescript -/// - -export default $config({ - app(input) { - return { - name: "aws-dart-api", - removal: input?.stage === "production" ? "retain" : "remove", - home: "aws", - }; - }, - async run() { - const api = new sst.aws.ApiGatewayV2("MyApi"); - api.route("GET /", { - runtime: "provided.al2023", - architecture: process.arch === "arm64" ? "arm64" : "x86_64", - bundle: build(), - handler: "hello", - }); - }, -}); - -function build() { - require("child_process").execSync(` -mkdir -p .build -docker run -v $PWD:/app -w /app --entrypoint ./build.sh dart:stable-sdk -`); - return `.build/`; -} -``` - -## Build - -Building your application for deployment requires installing Docker. - -When deploying with `sst deploy`, your application will be built for Amazon Linux, ensuring it's compatible with the AWS Lambda provided runtime. - -## Deploy - -Deploy just like any other sst project: - -```sh -sst deploy --stage production -``` diff --git a/examples/aws-dead-letter-queue/package.json b/examples/aws-dead-letter-queue/package.json index 41ee9b3f47..f2b2de79e4 100644 --- a/examples/aws-dead-letter-queue/package.json +++ b/examples/aws-dead-letter-queue/package.json @@ -14,7 +14,7 @@ "license": "ISC", "devDependencies": { "@types/aws-lambda": "^8.10.149", - "sst": "^3" + "sst": "file:../../sdk/js" }, "dependencies": { "@aws-sdk/client-sqs": "^3.515.0" diff --git a/examples/aws-deno-redis/deno.lock b/examples/aws-deno-redis/deno.lock deleted file mode 100644 index dda97e899e..0000000000 --- a/examples/aws-deno-redis/deno.lock +++ /dev/null @@ -1,140 +0,0 @@ -{ - "version": "4", - "specifiers": { - "jsr:@std/assert@1": "1.0.6", - "jsr:@std/internal@^1.0.4": "1.0.4", - "npm:ioredis@^5.4.1": "5.4.1", - "npm:sst@latest": "3.2.40" - }, - "jsr": { - "@std/assert@1.0.6": { - "integrity": "1904c05806a25d94fe791d6d883b685c9e2dcd60e4f9fc30f4fc5cf010c72207", - "dependencies": [ - "jsr:@std/internal" - ] - }, - "@std/internal@1.0.4": { - "integrity": "62e8e4911527e5e4f307741a795c0b0a9e6958d0b3790716ae71ce085f755422" - } - }, - "npm": { - "@ioredis/commands@1.2.0": { - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" - }, - "aws4fetch@1.0.20": { - "integrity": "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==" - }, - "cluster-key-slot@1.1.2": { - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==" - }, - "debug@4.3.7": { - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dependencies": [ - "ms" - ] - }, - "denque@2.1.0": { - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==" - }, - "ioredis@5.4.1": { - "integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==", - "dependencies": [ - "@ioredis/commands", - "cluster-key-slot", - "debug", - "denque", - "lodash.defaults", - "lodash.isarguments", - "redis-errors", - "redis-parser", - "standard-as-callback" - ] - }, - "jose@4.15.9": { - "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==" - }, - "jose@5.2.3": { - "integrity": "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA==" - }, - "lodash.defaults@4.2.0": { - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" - }, - "lodash.isarguments@3.1.0": { - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" - }, - "lru-cache@6.0.0": { - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": [ - "yallist" - ] - }, - "ms@2.1.3": { - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "object-hash@2.2.0": { - "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==" - }, - "oidc-token-hash@5.0.3": { - "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==" - }, - "openid-client@5.6.4": { - "integrity": "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA==", - "dependencies": [ - "jose@4.15.9", - "lru-cache", - "object-hash", - "oidc-token-hash" - ] - }, - "redis-errors@1.2.0": { - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==" - }, - "redis-parser@3.0.0": { - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "dependencies": [ - "redis-errors" - ] - }, - "sst-darwin-arm64@3.2.40": { - "integrity": "sha512-m7xrx5m4cryYQ83gL8G9ewJEv29MSy6jVRY0sQPENOjZQxVPUc+gz5wy1bUnOdr+3FjuHgKpK8RqM5xnA+tD4Q==" - }, - "sst-darwin-x64@3.2.40": { - "integrity": "sha512-Vfe78j9nLGMh4q3qwpx9cMhdjSHkGkYH9NEF7SKxNbexKlEJybeeAfMzWlHd/cYPakNnaHjJ3LaHZ1ZtJsycGw==" - }, - "sst-linux-arm64@3.2.40": { - "integrity": "sha512-ZH9rP6ys8SIyx7244PpNhr+T/QkYVXmfessIHZQcUQqvZ0gmv74NH3shMkaC6J3GwVSFvHzMplmrKVgox80LGw==" - }, - "sst-linux-x64@3.2.40": { - "integrity": "sha512-oWaKyrBsRtj3su+Xuqg8e7ABDAOvM0oU+D3GPijkcdCKCkk8BK+DrMrKgw+SyUy1J29QfWpVWGP9S/Heyfwt2g==" - }, - "sst-linux-x86@3.2.40": { - "integrity": "sha512-of1FJYSk01mi3OPedcX/9NPIETCBcXWzcvYZhMlxtYTtYIe7OdufKgsBz+mV8V37npJZ2UGg/0Xa6D34zUMgRQ==" - }, - "sst@3.2.40": { - "integrity": "sha512-+X91Xh/NhVVbFR8d/pcFK7Rancs+//CbYNVXkrYh4aSCHwXYdui3InqTW7NuiqfzsRyvbxYFYvN0ZSTzD8WtXA==", - "dependencies": [ - "aws4fetch", - "jose@5.2.3", - "openid-client", - "sst-darwin-arm64", - "sst-darwin-x64", - "sst-linux-arm64", - "sst-linux-x64", - "sst-linux-x86" - ] - }, - "standard-as-callback@2.1.0": { - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" - }, - "yallist@4.0.0": { - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - }, - "workspace": { - "dependencies": [ - "jsr:@std/assert@1", - "npm:ioredis@^5.4.1", - "npm:sst@latest" - ] - } -} diff --git a/examples/aws-deno/deno.lock b/examples/aws-deno/deno.lock deleted file mode 100644 index 11ba59e042..0000000000 --- a/examples/aws-deno/deno.lock +++ /dev/null @@ -1,1893 +0,0 @@ -{ - "version": "4", - "specifiers": { - "jsr:@std/assert@1": "1.0.8", - "jsr:@std/internal@^1.0.5": "1.0.5", - "npm:@aws-sdk/client-s3@^3.705.0": "3.705.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "npm:@aws-sdk/lib-storage@^3.705.0": "3.705.0_@aws-sdk+client-s3@3.705.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0______@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sso-oidc@3.699.0", - "npm:@aws-sdk/s3-request-presigner@^3.705.0": "3.705.0", - "npm:sst@^3.3.47": "3.3.47" - }, - "jsr": { - "@std/assert@1.0.8": { - "integrity": "ebe0bd7eb488ee39686f77003992f389a06c3da1bbd8022184804852b2fa641b", - "dependencies": [ - "jsr:@std/internal" - ] - }, - "@std/internal@1.0.5": { - "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" - } - }, - "npm": { - "@aws-crypto/crc32@5.2.0": { - "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", - "dependencies": [ - "@aws-crypto/util", - "@aws-sdk/types", - "tslib" - ] - }, - "@aws-crypto/crc32c@5.2.0": { - "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", - "dependencies": [ - "@aws-crypto/util", - "@aws-sdk/types", - "tslib" - ] - }, - "@aws-crypto/sha1-browser@5.2.0": { - "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", - "dependencies": [ - "@aws-crypto/supports-web-crypto", - "@aws-crypto/util", - "@aws-sdk/types", - "@aws-sdk/util-locate-window", - "@smithy/util-utf8@2.3.0", - "tslib" - ] - }, - "@aws-crypto/sha256-browser@5.2.0": { - "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", - "dependencies": [ - "@aws-crypto/sha256-js", - "@aws-crypto/supports-web-crypto", - "@aws-crypto/util", - "@aws-sdk/types", - "@aws-sdk/util-locate-window", - "@smithy/util-utf8@2.3.0", - "tslib" - ] - }, - "@aws-crypto/sha256-js@5.2.0": { - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "dependencies": [ - "@aws-crypto/util", - "@aws-sdk/types", - "tslib" - ] - }, - "@aws-crypto/supports-web-crypto@5.2.0": { - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", - "dependencies": [ - "tslib" - ] - }, - "@aws-crypto/util@5.2.0": { - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "dependencies": [ - "@aws-sdk/types", - "@smithy/util-utf8@2.3.0", - "tslib" - ] - }, - "@aws-sdk/client-s3@3.705.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-Fm0Cbc4zr0yG0DnNycz7ywlL5tQFdLSb7xCIPfzrxJb3YQiTXWxH5eu61SSsP/Z6RBNRolmRPvst/iNgX0fWvA==", - "dependencies": [ - "@aws-crypto/sha1-browser", - "@aws-crypto/sha256-browser", - "@aws-crypto/sha256-js", - "@aws-sdk/client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0", - "@aws-sdk/core", - "@aws-sdk/credential-provider-node@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/middleware-bucket-endpoint", - "@aws-sdk/middleware-expect-continue", - "@aws-sdk/middleware-flexible-checksums", - "@aws-sdk/middleware-host-header", - "@aws-sdk/middleware-location-constraint", - "@aws-sdk/middleware-logger", - "@aws-sdk/middleware-recursion-detection", - "@aws-sdk/middleware-sdk-s3", - "@aws-sdk/middleware-ssec", - "@aws-sdk/middleware-user-agent", - "@aws-sdk/region-config-resolver", - "@aws-sdk/signature-v4-multi-region", - "@aws-sdk/types", - "@aws-sdk/util-endpoints", - "@aws-sdk/util-user-agent-browser", - "@aws-sdk/util-user-agent-node", - "@aws-sdk/xml-builder", - "@smithy/config-resolver", - "@smithy/core", - "@smithy/eventstream-serde-browser", - "@smithy/eventstream-serde-config-resolver", - "@smithy/eventstream-serde-node", - "@smithy/fetch-http-handler", - "@smithy/hash-blob-browser", - "@smithy/hash-node", - "@smithy/hash-stream-node", - "@smithy/invalid-dependency", - "@smithy/md5-js", - "@smithy/middleware-content-length", - "@smithy/middleware-endpoint", - "@smithy/middleware-retry", - "@smithy/middleware-serde", - "@smithy/middleware-stack", - "@smithy/node-config-provider", - "@smithy/node-http-handler", - "@smithy/protocol-http", - "@smithy/smithy-client", - "@smithy/types", - "@smithy/url-parser", - "@smithy/util-base64", - "@smithy/util-body-length-browser", - "@smithy/util-body-length-node", - "@smithy/util-defaults-mode-browser", - "@smithy/util-defaults-mode-node", - "@smithy/util-endpoints", - "@smithy/util-middleware", - "@smithy/util-retry", - "@smithy/util-stream", - "@smithy/util-utf8@3.0.0", - "@smithy/util-waiter", - "tslib" - ] - }, - "@aws-sdk/client-s3@3.705.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-Fm0Cbc4zr0yG0DnNycz7ywlL5tQFdLSb7xCIPfzrxJb3YQiTXWxH5eu61SSsP/Z6RBNRolmRPvst/iNgX0fWvA==", - "dependencies": [ - "@aws-crypto/sha1-browser", - "@aws-crypto/sha256-browser", - "@aws-crypto/sha256-js", - "@aws-sdk/client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0", - "@aws-sdk/core", - "@aws-sdk/credential-provider-node@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/middleware-bucket-endpoint", - "@aws-sdk/middleware-expect-continue", - "@aws-sdk/middleware-flexible-checksums", - "@aws-sdk/middleware-host-header", - "@aws-sdk/middleware-location-constraint", - "@aws-sdk/middleware-logger", - "@aws-sdk/middleware-recursion-detection", - "@aws-sdk/middleware-sdk-s3", - "@aws-sdk/middleware-ssec", - "@aws-sdk/middleware-user-agent", - "@aws-sdk/region-config-resolver", - "@aws-sdk/signature-v4-multi-region", - "@aws-sdk/types", - "@aws-sdk/util-endpoints", - "@aws-sdk/util-user-agent-browser", - "@aws-sdk/util-user-agent-node", - "@aws-sdk/xml-builder", - "@smithy/config-resolver", - "@smithy/core", - "@smithy/eventstream-serde-browser", - "@smithy/eventstream-serde-config-resolver", - "@smithy/eventstream-serde-node", - "@smithy/fetch-http-handler", - "@smithy/hash-blob-browser", - "@smithy/hash-node", - "@smithy/hash-stream-node", - "@smithy/invalid-dependency", - "@smithy/md5-js", - "@smithy/middleware-content-length", - "@smithy/middleware-endpoint", - "@smithy/middleware-retry", - "@smithy/middleware-serde", - "@smithy/middleware-stack", - "@smithy/node-config-provider", - "@smithy/node-http-handler", - "@smithy/protocol-http", - "@smithy/smithy-client", - "@smithy/types", - "@smithy/url-parser", - "@smithy/util-base64", - "@smithy/util-body-length-browser", - "@smithy/util-body-length-node", - "@smithy/util-defaults-mode-browser", - "@smithy/util-defaults-mode-node", - "@smithy/util-endpoints", - "@smithy/util-middleware", - "@smithy/util-retry", - "@smithy/util-stream", - "@smithy/util-utf8@3.0.0", - "@smithy/util-waiter", - "tslib" - ] - }, - "@aws-sdk/client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-u8a1GorY5D1l+4FQAf4XBUC1T10/t7neuwT21r0ymrtMFSK2a9QqVHKMoLkvavAwyhJnARSBM9/UQC797PFOFw==", - "dependencies": [ - "@aws-crypto/sha256-browser", - "@aws-crypto/sha256-js", - "@aws-sdk/client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0", - "@aws-sdk/core", - "@aws-sdk/credential-provider-node@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/middleware-host-header", - "@aws-sdk/middleware-logger", - "@aws-sdk/middleware-recursion-detection", - "@aws-sdk/middleware-user-agent", - "@aws-sdk/region-config-resolver", - "@aws-sdk/types", - "@aws-sdk/util-endpoints", - "@aws-sdk/util-user-agent-browser", - "@aws-sdk/util-user-agent-node", - "@smithy/config-resolver", - "@smithy/core", - "@smithy/fetch-http-handler", - "@smithy/hash-node", - "@smithy/invalid-dependency", - "@smithy/middleware-content-length", - "@smithy/middleware-endpoint", - "@smithy/middleware-retry", - "@smithy/middleware-serde", - "@smithy/middleware-stack", - "@smithy/node-config-provider", - "@smithy/node-http-handler", - "@smithy/protocol-http", - "@smithy/smithy-client", - "@smithy/types", - "@smithy/url-parser", - "@smithy/util-base64", - "@smithy/util-body-length-browser", - "@smithy/util-body-length-node", - "@smithy/util-defaults-mode-browser", - "@smithy/util-defaults-mode-node", - "@smithy/util-endpoints", - "@smithy/util-middleware", - "@smithy/util-retry", - "@smithy/util-utf8@3.0.0", - "tslib" - ] - }, - "@aws-sdk/client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0": { - "integrity": "sha512-u8a1GorY5D1l+4FQAf4XBUC1T10/t7neuwT21r0ymrtMFSK2a9QqVHKMoLkvavAwyhJnARSBM9/UQC797PFOFw==", - "dependencies": [ - "@aws-crypto/sha256-browser", - "@aws-crypto/sha256-js", - "@aws-sdk/client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0", - "@aws-sdk/core", - "@aws-sdk/credential-provider-node@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/middleware-host-header", - "@aws-sdk/middleware-logger", - "@aws-sdk/middleware-recursion-detection", - "@aws-sdk/middleware-user-agent", - "@aws-sdk/region-config-resolver", - "@aws-sdk/types", - "@aws-sdk/util-endpoints", - "@aws-sdk/util-user-agent-browser", - "@aws-sdk/util-user-agent-node", - "@smithy/config-resolver", - "@smithy/core", - "@smithy/fetch-http-handler", - "@smithy/hash-node", - "@smithy/invalid-dependency", - "@smithy/middleware-content-length", - "@smithy/middleware-endpoint", - "@smithy/middleware-retry", - "@smithy/middleware-serde", - "@smithy/middleware-stack", - "@smithy/node-config-provider", - "@smithy/node-http-handler", - "@smithy/protocol-http", - "@smithy/smithy-client", - "@smithy/types", - "@smithy/url-parser", - "@smithy/util-base64", - "@smithy/util-body-length-browser", - "@smithy/util-body-length-node", - "@smithy/util-defaults-mode-browser", - "@smithy/util-defaults-mode-node", - "@smithy/util-endpoints", - "@smithy/util-middleware", - "@smithy/util-retry", - "@smithy/util-utf8@3.0.0", - "tslib" - ] - }, - "@aws-sdk/client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-u8a1GorY5D1l+4FQAf4XBUC1T10/t7neuwT21r0ymrtMFSK2a9QqVHKMoLkvavAwyhJnARSBM9/UQC797PFOFw==", - "dependencies": [ - "@aws-crypto/sha256-browser", - "@aws-crypto/sha256-js", - "@aws-sdk/client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0", - "@aws-sdk/core", - "@aws-sdk/credential-provider-node@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/middleware-host-header", - "@aws-sdk/middleware-logger", - "@aws-sdk/middleware-recursion-detection", - "@aws-sdk/middleware-user-agent", - "@aws-sdk/region-config-resolver", - "@aws-sdk/types", - "@aws-sdk/util-endpoints", - "@aws-sdk/util-user-agent-browser", - "@aws-sdk/util-user-agent-node", - "@smithy/config-resolver", - "@smithy/core", - "@smithy/fetch-http-handler", - "@smithy/hash-node", - "@smithy/invalid-dependency", - "@smithy/middleware-content-length", - "@smithy/middleware-endpoint", - "@smithy/middleware-retry", - "@smithy/middleware-serde", - "@smithy/middleware-stack", - "@smithy/node-config-provider", - "@smithy/node-http-handler", - "@smithy/protocol-http", - "@smithy/smithy-client", - "@smithy/types", - "@smithy/url-parser", - "@smithy/util-base64", - "@smithy/util-body-length-browser", - "@smithy/util-body-length-node", - "@smithy/util-defaults-mode-browser", - "@smithy/util-defaults-mode-node", - "@smithy/util-endpoints", - "@smithy/util-middleware", - "@smithy/util-retry", - "@smithy/util-utf8@3.0.0", - "tslib" - ] - }, - "@aws-sdk/client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0______@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-u8a1GorY5D1l+4FQAf4XBUC1T10/t7neuwT21r0ymrtMFSK2a9QqVHKMoLkvavAwyhJnARSBM9/UQC797PFOFw==", - "dependencies": [ - "@aws-crypto/sha256-browser", - "@aws-crypto/sha256-js", - "@aws-sdk/client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/core", - "@aws-sdk/credential-provider-node@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/middleware-host-header", - "@aws-sdk/middleware-logger", - "@aws-sdk/middleware-recursion-detection", - "@aws-sdk/middleware-user-agent", - "@aws-sdk/region-config-resolver", - "@aws-sdk/types", - "@aws-sdk/util-endpoints", - "@aws-sdk/util-user-agent-browser", - "@aws-sdk/util-user-agent-node", - "@smithy/config-resolver", - "@smithy/core", - "@smithy/fetch-http-handler", - "@smithy/hash-node", - "@smithy/invalid-dependency", - "@smithy/middleware-content-length", - "@smithy/middleware-endpoint", - "@smithy/middleware-retry", - "@smithy/middleware-serde", - "@smithy/middleware-stack", - "@smithy/node-config-provider", - "@smithy/node-http-handler", - "@smithy/protocol-http", - "@smithy/smithy-client", - "@smithy/types", - "@smithy/url-parser", - "@smithy/util-base64", - "@smithy/util-body-length-browser", - "@smithy/util-body-length-node", - "@smithy/util-defaults-mode-browser", - "@smithy/util-defaults-mode-node", - "@smithy/util-endpoints", - "@smithy/util-middleware", - "@smithy/util-retry", - "@smithy/util-utf8@3.0.0", - "tslib" - ] - }, - "@aws-sdk/client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0______@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-u8a1GorY5D1l+4FQAf4XBUC1T10/t7neuwT21r0ymrtMFSK2a9QqVHKMoLkvavAwyhJnARSBM9/UQC797PFOFw==", - "dependencies": [ - "@aws-crypto/sha256-browser", - "@aws-crypto/sha256-js", - "@aws-sdk/client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/core", - "@aws-sdk/credential-provider-node@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/middleware-host-header", - "@aws-sdk/middleware-logger", - "@aws-sdk/middleware-recursion-detection", - "@aws-sdk/middleware-user-agent", - "@aws-sdk/region-config-resolver", - "@aws-sdk/types", - "@aws-sdk/util-endpoints", - "@aws-sdk/util-user-agent-browser", - "@aws-sdk/util-user-agent-node", - "@smithy/config-resolver", - "@smithy/core", - "@smithy/fetch-http-handler", - "@smithy/hash-node", - "@smithy/invalid-dependency", - "@smithy/middleware-content-length", - "@smithy/middleware-endpoint", - "@smithy/middleware-retry", - "@smithy/middleware-serde", - "@smithy/middleware-stack", - "@smithy/node-config-provider", - "@smithy/node-http-handler", - "@smithy/protocol-http", - "@smithy/smithy-client", - "@smithy/types", - "@smithy/url-parser", - "@smithy/util-base64", - "@smithy/util-body-length-browser", - "@smithy/util-body-length-node", - "@smithy/util-defaults-mode-browser", - "@smithy/util-defaults-mode-node", - "@smithy/util-endpoints", - "@smithy/util-middleware", - "@smithy/util-retry", - "@smithy/util-utf8@3.0.0", - "tslib" - ] - }, - "@aws-sdk/client-sso@3.696.0": { - "integrity": "sha512-q5TTkd08JS0DOkHfUL853tuArf7NrPeqoS5UOvqJho8ibV9Ak/a/HO4kNvy9Nj3cib/toHYHsQIEtecUPSUUrQ==", - "dependencies": [ - "@aws-crypto/sha256-browser", - "@aws-crypto/sha256-js", - "@aws-sdk/core", - "@aws-sdk/middleware-host-header", - "@aws-sdk/middleware-logger", - "@aws-sdk/middleware-recursion-detection", - "@aws-sdk/middleware-user-agent", - "@aws-sdk/region-config-resolver", - "@aws-sdk/types", - "@aws-sdk/util-endpoints", - "@aws-sdk/util-user-agent-browser", - "@aws-sdk/util-user-agent-node", - "@smithy/config-resolver", - "@smithy/core", - "@smithy/fetch-http-handler", - "@smithy/hash-node", - "@smithy/invalid-dependency", - "@smithy/middleware-content-length", - "@smithy/middleware-endpoint", - "@smithy/middleware-retry", - "@smithy/middleware-serde", - "@smithy/middleware-stack", - "@smithy/node-config-provider", - "@smithy/node-http-handler", - "@smithy/protocol-http", - "@smithy/smithy-client", - "@smithy/types", - "@smithy/url-parser", - "@smithy/util-base64", - "@smithy/util-body-length-browser", - "@smithy/util-body-length-node", - "@smithy/util-defaults-mode-browser", - "@smithy/util-defaults-mode-node", - "@smithy/util-endpoints", - "@smithy/util-middleware", - "@smithy/util-retry", - "@smithy/util-utf8@3.0.0", - "tslib" - ] - }, - "@aws-sdk/client-sts@3.699.0": { - "integrity": "sha512-++lsn4x2YXsZPIzFVwv3fSUVM55ZT0WRFmPeNilYIhZClxHLmVAWKH4I55cY9ry60/aTKYjzOXkWwyBKGsGvQg==", - "dependencies": [ - "@aws-crypto/sha256-browser", - "@aws-crypto/sha256-js", - "@aws-sdk/client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0", - "@aws-sdk/core", - "@aws-sdk/credential-provider-node@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0", - "@aws-sdk/middleware-host-header", - "@aws-sdk/middleware-logger", - "@aws-sdk/middleware-recursion-detection", - "@aws-sdk/middleware-user-agent", - "@aws-sdk/region-config-resolver", - "@aws-sdk/types", - "@aws-sdk/util-endpoints", - "@aws-sdk/util-user-agent-browser", - "@aws-sdk/util-user-agent-node", - "@smithy/config-resolver", - "@smithy/core", - "@smithy/fetch-http-handler", - "@smithy/hash-node", - "@smithy/invalid-dependency", - "@smithy/middleware-content-length", - "@smithy/middleware-endpoint", - "@smithy/middleware-retry", - "@smithy/middleware-serde", - "@smithy/middleware-stack", - "@smithy/node-config-provider", - "@smithy/node-http-handler", - "@smithy/protocol-http", - "@smithy/smithy-client", - "@smithy/types", - "@smithy/url-parser", - "@smithy/util-base64", - "@smithy/util-body-length-browser", - "@smithy/util-body-length-node", - "@smithy/util-defaults-mode-browser", - "@smithy/util-defaults-mode-node", - "@smithy/util-endpoints", - "@smithy/util-middleware", - "@smithy/util-retry", - "@smithy/util-utf8@3.0.0", - "tslib" - ] - }, - "@aws-sdk/client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0": { - "integrity": "sha512-++lsn4x2YXsZPIzFVwv3fSUVM55ZT0WRFmPeNilYIhZClxHLmVAWKH4I55cY9ry60/aTKYjzOXkWwyBKGsGvQg==", - "dependencies": [ - "@aws-crypto/sha256-browser", - "@aws-crypto/sha256-js", - "@aws-sdk/client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/core", - "@aws-sdk/credential-provider-node@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/middleware-host-header", - "@aws-sdk/middleware-logger", - "@aws-sdk/middleware-recursion-detection", - "@aws-sdk/middleware-user-agent", - "@aws-sdk/region-config-resolver", - "@aws-sdk/types", - "@aws-sdk/util-endpoints", - "@aws-sdk/util-user-agent-browser", - "@aws-sdk/util-user-agent-node", - "@smithy/config-resolver", - "@smithy/core", - "@smithy/fetch-http-handler", - "@smithy/hash-node", - "@smithy/invalid-dependency", - "@smithy/middleware-content-length", - "@smithy/middleware-endpoint", - "@smithy/middleware-retry", - "@smithy/middleware-serde", - "@smithy/middleware-stack", - "@smithy/node-config-provider", - "@smithy/node-http-handler", - "@smithy/protocol-http", - "@smithy/smithy-client", - "@smithy/types", - "@smithy/url-parser", - "@smithy/util-base64", - "@smithy/util-body-length-browser", - "@smithy/util-body-length-node", - "@smithy/util-defaults-mode-browser", - "@smithy/util-defaults-mode-node", - "@smithy/util-endpoints", - "@smithy/util-middleware", - "@smithy/util-retry", - "@smithy/util-utf8@3.0.0", - "tslib" - ] - }, - "@aws-sdk/client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0": { - "integrity": "sha512-++lsn4x2YXsZPIzFVwv3fSUVM55ZT0WRFmPeNilYIhZClxHLmVAWKH4I55cY9ry60/aTKYjzOXkWwyBKGsGvQg==", - "dependencies": [ - "@aws-crypto/sha256-browser", - "@aws-crypto/sha256-js", - "@aws-sdk/client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/core", - "@aws-sdk/credential-provider-node@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/middleware-host-header", - "@aws-sdk/middleware-logger", - "@aws-sdk/middleware-recursion-detection", - "@aws-sdk/middleware-user-agent", - "@aws-sdk/region-config-resolver", - "@aws-sdk/types", - "@aws-sdk/util-endpoints", - "@aws-sdk/util-user-agent-browser", - "@aws-sdk/util-user-agent-node", - "@smithy/config-resolver", - "@smithy/core", - "@smithy/fetch-http-handler", - "@smithy/hash-node", - "@smithy/invalid-dependency", - "@smithy/middleware-content-length", - "@smithy/middleware-endpoint", - "@smithy/middleware-retry", - "@smithy/middleware-serde", - "@smithy/middleware-stack", - "@smithy/node-config-provider", - "@smithy/node-http-handler", - "@smithy/protocol-http", - "@smithy/smithy-client", - "@smithy/types", - "@smithy/url-parser", - "@smithy/util-base64", - "@smithy/util-body-length-browser", - "@smithy/util-body-length-node", - "@smithy/util-defaults-mode-browser", - "@smithy/util-defaults-mode-node", - "@smithy/util-endpoints", - "@smithy/util-middleware", - "@smithy/util-retry", - "@smithy/util-utf8@3.0.0", - "tslib" - ] - }, - "@aws-sdk/client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-++lsn4x2YXsZPIzFVwv3fSUVM55ZT0WRFmPeNilYIhZClxHLmVAWKH4I55cY9ry60/aTKYjzOXkWwyBKGsGvQg==", - "dependencies": [ - "@aws-crypto/sha256-browser", - "@aws-crypto/sha256-js", - "@aws-sdk/client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0______@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/core", - "@aws-sdk/credential-provider-node@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0", - "@aws-sdk/middleware-host-header", - "@aws-sdk/middleware-logger", - "@aws-sdk/middleware-recursion-detection", - "@aws-sdk/middleware-user-agent", - "@aws-sdk/region-config-resolver", - "@aws-sdk/types", - "@aws-sdk/util-endpoints", - "@aws-sdk/util-user-agent-browser", - "@aws-sdk/util-user-agent-node", - "@smithy/config-resolver", - "@smithy/core", - "@smithy/fetch-http-handler", - "@smithy/hash-node", - "@smithy/invalid-dependency", - "@smithy/middleware-content-length", - "@smithy/middleware-endpoint", - "@smithy/middleware-retry", - "@smithy/middleware-serde", - "@smithy/middleware-stack", - "@smithy/node-config-provider", - "@smithy/node-http-handler", - "@smithy/protocol-http", - "@smithy/smithy-client", - "@smithy/types", - "@smithy/url-parser", - "@smithy/util-base64", - "@smithy/util-body-length-browser", - "@smithy/util-body-length-node", - "@smithy/util-defaults-mode-browser", - "@smithy/util-defaults-mode-node", - "@smithy/util-endpoints", - "@smithy/util-middleware", - "@smithy/util-retry", - "@smithy/util-utf8@3.0.0", - "tslib" - ] - }, - "@aws-sdk/core@3.696.0": { - "integrity": "sha512-3c9III1k03DgvRZWg8vhVmfIXPG6hAciN9MzQTzqGngzWAELZF/WONRTRQuDFixVtarQatmLHYVw/atGeA2Byw==", - "dependencies": [ - "@aws-sdk/types", - "@smithy/core", - "@smithy/node-config-provider", - "@smithy/property-provider", - "@smithy/protocol-http", - "@smithy/signature-v4", - "@smithy/smithy-client", - "@smithy/types", - "@smithy/util-middleware", - "fast-xml-parser", - "tslib" - ] - }, - "@aws-sdk/credential-provider-env@3.696.0": { - "integrity": "sha512-T9iMFnJL7YTlESLpVFT3fg1Lkb1lD+oiaIC8KMpepb01gDUBIpj9+Y+pA/cgRWW0yRxmkDXNazAE2qQTVFGJzA==", - "dependencies": [ - "@aws-sdk/core", - "@aws-sdk/types", - "@smithy/property-provider", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-http@3.696.0": { - "integrity": "sha512-GV6EbvPi2eq1+WgY/o2RFA3P7HGmnkIzCNmhwtALFlqMroLYWKE7PSeHw66Uh1dFQeVESn0/+hiUNhu1mB0emA==", - "dependencies": [ - "@aws-sdk/core", - "@aws-sdk/types", - "@smithy/fetch-http-handler", - "@smithy/node-http-handler", - "@smithy/property-provider", - "@smithy/protocol-http", - "@smithy/smithy-client", - "@smithy/types", - "@smithy/util-stream", - "tslib" - ] - }, - "@aws-sdk/credential-provider-ini@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0": { - "integrity": "sha512-dXmCqjJnKmG37Q+nLjPVu22mNkrGHY8hYoOt3Jo9R2zr5MYV7s/NHsCHr+7E+BZ+tfZYLRPeB1wkpTeHiEcdRw==", - "dependencies": [ - "@aws-sdk/client-sts@3.699.0", - "@aws-sdk/core", - "@aws-sdk/credential-provider-env", - "@aws-sdk/credential-provider-http", - "@aws-sdk/credential-provider-process", - "@aws-sdk/credential-provider-sso@3.699.0", - "@aws-sdk/credential-provider-web-identity@3.696.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0", - "@aws-sdk/types", - "@smithy/credential-provider-imds", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-ini@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-dXmCqjJnKmG37Q+nLjPVu22mNkrGHY8hYoOt3Jo9R2zr5MYV7s/NHsCHr+7E+BZ+tfZYLRPeB1wkpTeHiEcdRw==", - "dependencies": [ - "@aws-sdk/client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0", - "@aws-sdk/core", - "@aws-sdk/credential-provider-env", - "@aws-sdk/credential-provider-http", - "@aws-sdk/credential-provider-process", - "@aws-sdk/credential-provider-sso@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0", - "@aws-sdk/credential-provider-web-identity@3.696.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/types", - "@smithy/credential-provider-imds", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-ini@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0______@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-dXmCqjJnKmG37Q+nLjPVu22mNkrGHY8hYoOt3Jo9R2zr5MYV7s/NHsCHr+7E+BZ+tfZYLRPeB1wkpTeHiEcdRw==", - "dependencies": [ - "@aws-sdk/client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/core", - "@aws-sdk/credential-provider-env", - "@aws-sdk/credential-provider-http", - "@aws-sdk/credential-provider-process", - "@aws-sdk/credential-provider-sso@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0______@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0", - "@aws-sdk/credential-provider-web-identity@3.696.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0______@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/types", - "@smithy/credential-provider-imds", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-ini@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-dXmCqjJnKmG37Q+nLjPVu22mNkrGHY8hYoOt3Jo9R2zr5MYV7s/NHsCHr+7E+BZ+tfZYLRPeB1wkpTeHiEcdRw==", - "dependencies": [ - "@aws-sdk/client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0", - "@aws-sdk/core", - "@aws-sdk/credential-provider-env", - "@aws-sdk/credential-provider-http", - "@aws-sdk/credential-provider-process", - "@aws-sdk/credential-provider-sso@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/credential-provider-web-identity@3.696.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0", - "@aws-sdk/types", - "@smithy/credential-provider-imds", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-ini@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0______@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-dXmCqjJnKmG37Q+nLjPVu22mNkrGHY8hYoOt3Jo9R2zr5MYV7s/NHsCHr+7E+BZ+tfZYLRPeB1wkpTeHiEcdRw==", - "dependencies": [ - "@aws-sdk/client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/core", - "@aws-sdk/credential-provider-env", - "@aws-sdk/credential-provider-http", - "@aws-sdk/credential-provider-process", - "@aws-sdk/credential-provider-sso@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/credential-provider-web-identity@3.696.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0______@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/types", - "@smithy/credential-provider-imds", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-node@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0": { - "integrity": "sha512-MmEmNDo1bBtTgRmdNfdQksXu4uXe66s0p1hi1YPrn1h59Q605eq/xiWbGL6/3KdkViH6eGUuABeV2ODld86ylg==", - "dependencies": [ - "@aws-sdk/credential-provider-env", - "@aws-sdk/credential-provider-http", - "@aws-sdk/credential-provider-ini@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0", - "@aws-sdk/credential-provider-process", - "@aws-sdk/credential-provider-sso@3.699.0", - "@aws-sdk/credential-provider-web-identity@3.696.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0", - "@aws-sdk/types", - "@smithy/credential-provider-imds", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-node@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-MmEmNDo1bBtTgRmdNfdQksXu4uXe66s0p1hi1YPrn1h59Q605eq/xiWbGL6/3KdkViH6eGUuABeV2ODld86ylg==", - "dependencies": [ - "@aws-sdk/credential-provider-env", - "@aws-sdk/credential-provider-http", - "@aws-sdk/credential-provider-ini@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/credential-provider-process", - "@aws-sdk/credential-provider-sso@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0", - "@aws-sdk/credential-provider-web-identity@3.696.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/types", - "@smithy/credential-provider-imds", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-node@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-MmEmNDo1bBtTgRmdNfdQksXu4uXe66s0p1hi1YPrn1h59Q605eq/xiWbGL6/3KdkViH6eGUuABeV2ODld86ylg==", - "dependencies": [ - "@aws-sdk/credential-provider-env", - "@aws-sdk/credential-provider-http", - "@aws-sdk/credential-provider-ini@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0______@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/credential-provider-process", - "@aws-sdk/credential-provider-sso@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0", - "@aws-sdk/credential-provider-web-identity@3.696.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/types", - "@smithy/credential-provider-imds", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-node@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-MmEmNDo1bBtTgRmdNfdQksXu4uXe66s0p1hi1YPrn1h59Q605eq/xiWbGL6/3KdkViH6eGUuABeV2ODld86ylg==", - "dependencies": [ - "@aws-sdk/credential-provider-env", - "@aws-sdk/credential-provider-http", - "@aws-sdk/credential-provider-ini@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/credential-provider-process", - "@aws-sdk/credential-provider-sso@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/credential-provider-web-identity@3.696.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/types", - "@smithy/credential-provider-imds", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-node@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-MmEmNDo1bBtTgRmdNfdQksXu4uXe66s0p1hi1YPrn1h59Q605eq/xiWbGL6/3KdkViH6eGUuABeV2ODld86ylg==", - "dependencies": [ - "@aws-sdk/credential-provider-env", - "@aws-sdk/credential-provider-http", - "@aws-sdk/credential-provider-ini@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0______@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/credential-provider-process", - "@aws-sdk/credential-provider-sso@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/credential-provider-web-identity@3.696.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/types", - "@smithy/credential-provider-imds", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-process@3.696.0": { - "integrity": "sha512-mL1RcFDe9sfmyU5K1nuFkO8UiJXXxLX4JO1gVaDIOvPqwStpUAwi3A1BoeZhWZZNQsiKI810RnYGo0E0WB/hUA==", - "dependencies": [ - "@aws-sdk/core", - "@aws-sdk/types", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-sso@3.699.0": { - "integrity": "sha512-Ekp2cZG4pl9D8+uKWm4qO1xcm8/MeiI8f+dnlZm8aQzizeC+aXYy9GyoclSf6daK8KfRPiRfM7ZHBBL5dAfdMA==", - "dependencies": [ - "@aws-sdk/client-sso", - "@aws-sdk/core", - "@aws-sdk/token-providers@3.699.0", - "@aws-sdk/types", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-sso@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-Ekp2cZG4pl9D8+uKWm4qO1xcm8/MeiI8f+dnlZm8aQzizeC+aXYy9GyoclSf6daK8KfRPiRfM7ZHBBL5dAfdMA==", - "dependencies": [ - "@aws-sdk/client-sso", - "@aws-sdk/core", - "@aws-sdk/token-providers@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/types", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-sso@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0": { - "integrity": "sha512-Ekp2cZG4pl9D8+uKWm4qO1xcm8/MeiI8f+dnlZm8aQzizeC+aXYy9GyoclSf6daK8KfRPiRfM7ZHBBL5dAfdMA==", - "dependencies": [ - "@aws-sdk/client-sso", - "@aws-sdk/core", - "@aws-sdk/token-providers@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0", - "@aws-sdk/types", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-sso@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-Ekp2cZG4pl9D8+uKWm4qO1xcm8/MeiI8f+dnlZm8aQzizeC+aXYy9GyoclSf6daK8KfRPiRfM7ZHBBL5dAfdMA==", - "dependencies": [ - "@aws-sdk/client-sso", - "@aws-sdk/core", - "@aws-sdk/token-providers@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/types", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-sso@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0": { - "integrity": "sha512-Ekp2cZG4pl9D8+uKWm4qO1xcm8/MeiI8f+dnlZm8aQzizeC+aXYy9GyoclSf6daK8KfRPiRfM7ZHBBL5dAfdMA==", - "dependencies": [ - "@aws-sdk/client-sso", - "@aws-sdk/core", - "@aws-sdk/token-providers@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0", - "@aws-sdk/types", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-sso@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0______@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0": { - "integrity": "sha512-Ekp2cZG4pl9D8+uKWm4qO1xcm8/MeiI8f+dnlZm8aQzizeC+aXYy9GyoclSf6daK8KfRPiRfM7ZHBBL5dAfdMA==", - "dependencies": [ - "@aws-sdk/client-sso", - "@aws-sdk/core", - "@aws-sdk/token-providers@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0______@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0", - "@aws-sdk/types", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-web-identity@3.696.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0": { - "integrity": "sha512-XJ/CVlWChM0VCoc259vWguFUjJDn/QwDqHwbx+K9cg3v6yrqXfK5ai+p/6lx0nQpnk4JzPVeYYxWRpaTsGC9rg==", - "dependencies": [ - "@aws-sdk/client-sts@3.699.0", - "@aws-sdk/core", - "@aws-sdk/types", - "@smithy/property-provider", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-web-identity@3.696.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-XJ/CVlWChM0VCoc259vWguFUjJDn/QwDqHwbx+K9cg3v6yrqXfK5ai+p/6lx0nQpnk4JzPVeYYxWRpaTsGC9rg==", - "dependencies": [ - "@aws-sdk/client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0", - "@aws-sdk/core", - "@aws-sdk/types", - "@smithy/property-provider", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-web-identity@3.696.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-XJ/CVlWChM0VCoc259vWguFUjJDn/QwDqHwbx+K9cg3v6yrqXfK5ai+p/6lx0nQpnk4JzPVeYYxWRpaTsGC9rg==", - "dependencies": [ - "@aws-sdk/client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/core", - "@aws-sdk/types", - "@smithy/property-provider", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-web-identity@3.696.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-XJ/CVlWChM0VCoc259vWguFUjJDn/QwDqHwbx+K9cg3v6yrqXfK5ai+p/6lx0nQpnk4JzPVeYYxWRpaTsGC9rg==", - "dependencies": [ - "@aws-sdk/client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0", - "@aws-sdk/core", - "@aws-sdk/types", - "@smithy/property-provider", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-web-identity@3.696.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-XJ/CVlWChM0VCoc259vWguFUjJDn/QwDqHwbx+K9cg3v6yrqXfK5ai+p/6lx0nQpnk4JzPVeYYxWRpaTsGC9rg==", - "dependencies": [ - "@aws-sdk/client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/core", - "@aws-sdk/types", - "@smithy/property-provider", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-web-identity@3.696.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0______@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-XJ/CVlWChM0VCoc259vWguFUjJDn/QwDqHwbx+K9cg3v6yrqXfK5ai+p/6lx0nQpnk4JzPVeYYxWRpaTsGC9rg==", - "dependencies": [ - "@aws-sdk/client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/core", - "@aws-sdk/types", - "@smithy/property-provider", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/credential-provider-web-identity@3.696.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0______@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-XJ/CVlWChM0VCoc259vWguFUjJDn/QwDqHwbx+K9cg3v6yrqXfK5ai+p/6lx0nQpnk4JzPVeYYxWRpaTsGC9rg==", - "dependencies": [ - "@aws-sdk/client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/core", - "@aws-sdk/types", - "@smithy/property-provider", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/lib-storage@3.705.0_@aws-sdk+client-s3@3.705.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0______@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-jucPgdO5RCQAki8+CcEi3ZQxBUpq6iVcurtMkLS1xGbe/VOhxzNOt44V/4WqjUu7ra3on8DD0DOqd9523BqOzA==", - "dependencies": [ - "@aws-sdk/client-s3@3.705.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0", - "@smithy/abort-controller", - "@smithy/middleware-endpoint", - "@smithy/smithy-client", - "buffer", - "events", - "stream-browserify", - "tslib" - ] - }, - "@aws-sdk/middleware-bucket-endpoint@3.696.0": { - "integrity": "sha512-V07jishKHUS5heRNGFpCWCSTjRJyQLynS/ncUeE8ZYtG66StOOQWftTwDfFOSoXlIqrXgb4oT9atryzXq7Z4LQ==", - "dependencies": [ - "@aws-sdk/types", - "@aws-sdk/util-arn-parser", - "@smithy/node-config-provider", - "@smithy/protocol-http", - "@smithy/types", - "@smithy/util-config-provider", - "tslib" - ] - }, - "@aws-sdk/middleware-expect-continue@3.696.0": { - "integrity": "sha512-vpVukqY3U2pb+ULeX0shs6L0aadNep6kKzjme/MyulPjtUDJpD3AekHsXRrCCGLmOqSKqRgQn5zhV9pQhHsb6Q==", - "dependencies": [ - "@aws-sdk/types", - "@smithy/protocol-http", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/middleware-flexible-checksums@3.701.0": { - "integrity": "sha512-adNaPCyTT+CiVM0ufDiO1Fe7nlRmJdI9Hcgj0M9S6zR7Dw70Ra5z8Lslkd7syAccYvZaqxLklGjPQH/7GNxwTA==", - "dependencies": [ - "@aws-crypto/crc32", - "@aws-crypto/crc32c", - "@aws-crypto/util", - "@aws-sdk/core", - "@aws-sdk/types", - "@smithy/is-array-buffer@3.0.0", - "@smithy/node-config-provider", - "@smithy/protocol-http", - "@smithy/types", - "@smithy/util-middleware", - "@smithy/util-stream", - "@smithy/util-utf8@3.0.0", - "tslib" - ] - }, - "@aws-sdk/middleware-host-header@3.696.0": { - "integrity": "sha512-zELJp9Ta2zkX7ELggMN9qMCgekqZhFC5V2rOr4hJDEb/Tte7gpfKSObAnw/3AYiVqt36sjHKfdkoTsuwGdEoDg==", - "dependencies": [ - "@aws-sdk/types", - "@smithy/protocol-http", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/middleware-location-constraint@3.696.0": { - "integrity": "sha512-FgH12OB0q+DtTrP2aiDBddDKwL4BPOrm7w3VV9BJrSdkqQCNBPz8S1lb0y5eVH4tBG+2j7gKPlOv1wde4jF/iw==", - "dependencies": [ - "@aws-sdk/types", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/middleware-logger@3.696.0": { - "integrity": "sha512-KhkHt+8AjCxcR/5Zp3++YPJPpFQzxpr+jmONiT/Jw2yqnSngZ0Yspm5wGoRx2hS1HJbyZNuaOWEGuJoxLeBKfA==", - "dependencies": [ - "@aws-sdk/types", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/middleware-recursion-detection@3.696.0": { - "integrity": "sha512-si/maV3Z0hH7qa99f9ru2xpS5HlfSVcasRlNUXKSDm611i7jFMWwGNLUOXFAOLhXotPX5G3Z6BLwL34oDeBMug==", - "dependencies": [ - "@aws-sdk/types", - "@smithy/protocol-http", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/middleware-sdk-s3@3.696.0": { - "integrity": "sha512-M7fEiAiN7DBMHflzOFzh1I2MNSlLpbiH2ubs87bdRc2wZsDPSbs4l3v6h3WLhxoQK0bq6vcfroudrLBgvCuX3Q==", - "dependencies": [ - "@aws-sdk/core", - "@aws-sdk/types", - "@aws-sdk/util-arn-parser", - "@smithy/core", - "@smithy/node-config-provider", - "@smithy/protocol-http", - "@smithy/signature-v4", - "@smithy/smithy-client", - "@smithy/types", - "@smithy/util-config-provider", - "@smithy/util-middleware", - "@smithy/util-stream", - "@smithy/util-utf8@3.0.0", - "tslib" - ] - }, - "@aws-sdk/middleware-ssec@3.696.0": { - "integrity": "sha512-w/d6O7AOZ7Pg3w2d3BxnX5RmGNWb5X4RNxF19rJqcgu/xqxxE/QwZTNd5a7eTsqLXAUIfbbR8hh0czVfC1pJLA==", - "dependencies": [ - "@aws-sdk/types", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/middleware-user-agent@3.696.0": { - "integrity": "sha512-Lvyj8CTyxrHI6GHd2YVZKIRI5Fmnugt3cpJo0VrKKEgK5zMySwEZ1n4dqPK6czYRWKd5+WnYHYAuU+Wdk6Jsjw==", - "dependencies": [ - "@aws-sdk/core", - "@aws-sdk/types", - "@aws-sdk/util-endpoints", - "@smithy/core", - "@smithy/protocol-http", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/region-config-resolver@3.696.0": { - "integrity": "sha512-7EuH142lBXjI8yH6dVS/CZeiK/WZsmb/8zP6bQbVYpMrppSTgB3MzZZdxVZGzL5r8zPQOU10wLC4kIMy0qdBVQ==", - "dependencies": [ - "@aws-sdk/types", - "@smithy/node-config-provider", - "@smithy/types", - "@smithy/util-config-provider", - "@smithy/util-middleware", - "tslib" - ] - }, - "@aws-sdk/s3-request-presigner@3.705.0": { - "integrity": "sha512-dAQiXv/TqjEUCoEeiKqQGI8LJ3g8Xv+XJL4W9CwsB6ZHHDq0Q05ulpDSkhhCf52ySXf5dJ33e1o/VeUDY3q0pw==", - "dependencies": [ - "@aws-sdk/signature-v4-multi-region", - "@aws-sdk/types", - "@aws-sdk/util-format-url", - "@smithy/middleware-endpoint", - "@smithy/protocol-http", - "@smithy/smithy-client", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/signature-v4-multi-region@3.696.0": { - "integrity": "sha512-ijPkoLjXuPtgxAYlDoYls8UaG/VKigROn9ebbvPL/orEY5umedd3iZTcS9T+uAf4Ur3GELLxMQiERZpfDKaz3g==", - "dependencies": [ - "@aws-sdk/middleware-sdk-s3", - "@aws-sdk/types", - "@smithy/protocol-http", - "@smithy/signature-v4", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/token-providers@3.699.0": { - "integrity": "sha512-kuiEW9DWs7fNos/SM+y58HCPhcIzm1nEZLhe2/7/6+TvAYLuEWURYsbK48gzsxXlaJ2k/jGY3nIsA7RptbMOwA==" - }, - "@aws-sdk/token-providers@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-kuiEW9DWs7fNos/SM+y58HCPhcIzm1nEZLhe2/7/6+TvAYLuEWURYsbK48gzsxXlaJ2k/jGY3nIsA7RptbMOwA==", - "dependencies": [ - "@aws-sdk/client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0", - "@aws-sdk/types", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/token-providers@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0": { - "integrity": "sha512-kuiEW9DWs7fNos/SM+y58HCPhcIzm1nEZLhe2/7/6+TvAYLuEWURYsbK48gzsxXlaJ2k/jGY3nIsA7RptbMOwA==", - "dependencies": [ - "@aws-sdk/client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0", - "@aws-sdk/types", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/token-providers@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0": { - "integrity": "sha512-kuiEW9DWs7fNos/SM+y58HCPhcIzm1nEZLhe2/7/6+TvAYLuEWURYsbK48gzsxXlaJ2k/jGY3nIsA7RptbMOwA==", - "dependencies": [ - "@aws-sdk/client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/types", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/token-providers@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0": { - "integrity": "sha512-kuiEW9DWs7fNos/SM+y58HCPhcIzm1nEZLhe2/7/6+TvAYLuEWURYsbK48gzsxXlaJ2k/jGY3nIsA7RptbMOwA==", - "dependencies": [ - "@aws-sdk/client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/types", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/token-providers@3.699.0_@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0______@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0": { - "integrity": "sha512-kuiEW9DWs7fNos/SM+y58HCPhcIzm1nEZLhe2/7/6+TvAYLuEWURYsbK48gzsxXlaJ2k/jGY3nIsA7RptbMOwA==", - "dependencies": [ - "@aws-sdk/client-sso-oidc@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0_@aws-sdk+client-sts@3.699.0__@aws-sdk+client-sso-oidc@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0_____@aws-sdk+client-sts@3.699.0___@aws-sdk+client-sts@3.699.0____@aws-sdk+client-sso-oidc@3.699.0_____@aws-sdk+client-sts@3.699.0______@aws-sdk+client-sso-oidc@3.699.0____@aws-sdk+client-sso-oidc@3.699.0", - "@aws-sdk/types", - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/types@3.696.0": { - "integrity": "sha512-9rTvUJIAj5d3//U5FDPWGJ1nFJLuWb30vugGOrWk7aNZ6y9tuA3PI7Cc9dP8WEXKVyK1vuuk8rSFP2iqXnlgrw==", - "dependencies": [ - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/util-arn-parser@3.693.0": { - "integrity": "sha512-WC8x6ca+NRrtpAH64rWu+ryDZI3HuLwlEr8EU6/dbC/pt+r/zC0PBoC15VEygUaBA+isppCikQpGyEDu0Yj7gQ==", - "dependencies": [ - "tslib" - ] - }, - "@aws-sdk/util-endpoints@3.696.0": { - "integrity": "sha512-T5s0IlBVX+gkb9g/I6CLt4yAZVzMSiGnbUqWihWsHvQR1WOoIcndQy/Oz/IJXT9T2ipoy7a80gzV6a5mglrioA==", - "dependencies": [ - "@aws-sdk/types", - "@smithy/types", - "@smithy/util-endpoints", - "tslib" - ] - }, - "@aws-sdk/util-format-url@3.696.0": { - "integrity": "sha512-R6yK1LozUD1GdAZRPhNsIow6VNFJUTyyoIar1OCWaknlucBMcq7musF3DN3TlORBwfFMj5buHc2ET9OtMtzvuA==", - "dependencies": [ - "@aws-sdk/types", - "@smithy/querystring-builder", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/util-locate-window@3.693.0": { - "integrity": "sha512-ttrag6haJLWABhLqtg1Uf+4LgHWIMOVSYL+VYZmAp2v4PUGOwWmWQH0Zk8RM7YuQcLfH/EoR72/Yxz6A4FKcuw==", - "dependencies": [ - "tslib" - ] - }, - "@aws-sdk/util-user-agent-browser@3.696.0": { - "integrity": "sha512-Z5rVNDdmPOe6ELoM5AhF/ja5tSjbe6ctSctDPb0JdDf4dT0v2MfwhJKzXju2RzX8Es/77Glh7MlaXLE0kCB9+Q==", - "dependencies": [ - "@aws-sdk/types", - "@smithy/types", - "bowser", - "tslib" - ] - }, - "@aws-sdk/util-user-agent-node@3.696.0": { - "integrity": "sha512-KhKqcfyXIB0SCCt+qsu4eJjsfiOrNzK5dCV7RAW2YIpp+msxGUUX0NdRE9rkzjiv+3EMktgJm3eEIS+yxtlVdQ==", - "dependencies": [ - "@aws-sdk/middleware-user-agent", - "@aws-sdk/types", - "@smithy/node-config-provider", - "@smithy/types", - "tslib" - ] - }, - "@aws-sdk/xml-builder@3.696.0": { - "integrity": "sha512-dn1mX+EeqivoLYnY7p2qLrir0waPnCgS/0YdRCAVU2x14FgfUYCH6Im3w3oi2dMwhxfKY5lYVB5NKvZu7uI9lQ==", - "dependencies": [ - "@smithy/types", - "tslib" - ] - }, - "@smithy/abort-controller@3.1.8": { - "integrity": "sha512-+3DOBcUn5/rVjlxGvUPKc416SExarAQ+Qe0bqk30YSUjbepwpS7QN0cyKUSifvLJhdMZ0WPzPP5ymut0oonrpQ==", - "dependencies": [ - "@smithy/types", - "tslib" - ] - }, - "@smithy/chunked-blob-reader-native@3.0.1": { - "integrity": "sha512-VEYtPvh5rs/xlyqpm5NRnfYLZn+q0SRPELbvBV+C/G7IQ+ouTuo+NKKa3ShG5OaFR8NYVMXls9hPYLTvIKKDrQ==", - "dependencies": [ - "@smithy/util-base64", - "tslib" - ] - }, - "@smithy/chunked-blob-reader@4.0.0": { - "integrity": "sha512-jSqRnZvkT4egkq/7b6/QRCNXmmYVcHwnJldqJ3IhVpQE2atObVJ137xmGeuGFhjFUr8gCEVAOKwSY79OvpbDaQ==", - "dependencies": [ - "tslib" - ] - }, - "@smithy/config-resolver@3.0.12": { - "integrity": "sha512-YAJP9UJFZRZ8N+UruTeq78zkdjUHmzsY62J4qKWZ4SXB4QXJ/+680EfXXgkYA2xj77ooMqtUY9m406zGNqwivQ==", - "dependencies": [ - "@smithy/node-config-provider", - "@smithy/types", - "@smithy/util-config-provider", - "@smithy/util-middleware", - "tslib" - ] - }, - "@smithy/core@2.5.4": { - "integrity": "sha512-iFh2Ymn2sCziBRLPuOOxRPkuCx/2gBdXtBGuCUFLUe6bWYjKnhHyIPqGeNkLZ5Aco/5GjebRTBFiWID3sDbrKw==", - "dependencies": [ - "@smithy/middleware-serde", - "@smithy/protocol-http", - "@smithy/types", - "@smithy/util-body-length-browser", - "@smithy/util-middleware", - "@smithy/util-stream", - "@smithy/util-utf8@3.0.0", - "tslib" - ] - }, - "@smithy/credential-provider-imds@3.2.7": { - "integrity": "sha512-cEfbau+rrWF8ylkmmVAObOmjbTIzKyUC5TkBL58SbLywD0RCBC4JAUKbmtSm2w5KUJNRPGgpGFMvE2FKnuNlWQ==", - "dependencies": [ - "@smithy/node-config-provider", - "@smithy/property-provider", - "@smithy/types", - "@smithy/url-parser", - "tslib" - ] - }, - "@smithy/eventstream-codec@3.1.9": { - "integrity": "sha512-F574nX0hhlNOjBnP+noLtsPFqXnWh2L0+nZKCwcu7P7J8k+k+rdIDs+RMnrMwrzhUE4mwMgyN0cYnEn0G8yrnQ==", - "dependencies": [ - "@aws-crypto/crc32", - "@smithy/types", - "@smithy/util-hex-encoding", - "tslib" - ] - }, - "@smithy/eventstream-serde-browser@3.0.13": { - "integrity": "sha512-Nee9m+97o9Qj6/XeLz2g2vANS2SZgAxV4rDBMKGHvFJHU/xz88x2RwCkwsvEwYjSX4BV1NG1JXmxEaDUzZTAtw==", - "dependencies": [ - "@smithy/eventstream-serde-universal", - "@smithy/types", - "tslib" - ] - }, - "@smithy/eventstream-serde-config-resolver@3.0.10": { - "integrity": "sha512-K1M0x7P7qbBUKB0UWIL5KOcyi6zqV5mPJoL0/o01HPJr0CSq3A9FYuJC6e11EX6hR8QTIR++DBiGrYveOu6trw==", - "dependencies": [ - "@smithy/types", - "tslib" - ] - }, - "@smithy/eventstream-serde-node@3.0.12": { - "integrity": "sha512-kiZymxXvZ4tnuYsPSMUHe+MMfc4FTeFWJIc0Q5wygJoUQM4rVHNghvd48y7ppuulNMbuYt95ah71pYc2+o4JOA==", - "dependencies": [ - "@smithy/eventstream-serde-universal", - "@smithy/types", - "tslib" - ] - }, - "@smithy/eventstream-serde-universal@3.0.12": { - "integrity": "sha512-1i8ifhLJrOZ+pEifTlF0EfZzMLUGQggYQ6WmZ4d5g77zEKf7oZ0kvh1yKWHPjofvOwqrkwRDVuxuYC8wVd662A==", - "dependencies": [ - "@smithy/eventstream-codec", - "@smithy/types", - "tslib" - ] - }, - "@smithy/fetch-http-handler@4.1.1": { - "integrity": "sha512-bH7QW0+JdX0bPBadXt8GwMof/jz0H28I84hU1Uet9ISpzUqXqRQ3fEZJ+ANPOhzSEczYvANNl3uDQDYArSFDtA==", - "dependencies": [ - "@smithy/protocol-http", - "@smithy/querystring-builder", - "@smithy/types", - "@smithy/util-base64", - "tslib" - ] - }, - "@smithy/hash-blob-browser@3.1.9": { - "integrity": "sha512-wOu78omaUuW5DE+PVWXiRKWRZLecARyP3xcq5SmkXUw9+utgN8HnSnBfrjL2B/4ZxgqPjaAJQkC/+JHf1ITVaQ==", - "dependencies": [ - "@smithy/chunked-blob-reader", - "@smithy/chunked-blob-reader-native", - "@smithy/types", - "tslib" - ] - }, - "@smithy/hash-node@3.0.10": { - "integrity": "sha512-3zWGWCHI+FlJ5WJwx73Mw2llYR8aflVyZN5JhoqLxbdPZi6UyKSdCeXAWJw9ja22m6S6Tzz1KZ+kAaSwvydi0g==", - "dependencies": [ - "@smithy/types", - "@smithy/util-buffer-from@3.0.0", - "@smithy/util-utf8@3.0.0", - "tslib" - ] - }, - "@smithy/hash-stream-node@3.1.9": { - "integrity": "sha512-3XfHBjSP3oDWxLmlxnt+F+FqXpL3WlXs+XXaB6bV9Wo8BBu87fK1dSEsyH7Z4ZHRmwZ4g9lFMdf08m9hoX1iRA==", - "dependencies": [ - "@smithy/types", - "@smithy/util-utf8@3.0.0", - "tslib" - ] - }, - "@smithy/invalid-dependency@3.0.10": { - "integrity": "sha512-Lp2L65vFi+cj0vFMu2obpPW69DU+6O5g3086lmI4XcnRCG8PxvpWC7XyaVwJCxsZFzueHjXnrOH/E0pl0zikfA==", - "dependencies": [ - "@smithy/types", - "tslib" - ] - }, - "@smithy/is-array-buffer@2.2.0": { - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "dependencies": [ - "tslib" - ] - }, - "@smithy/is-array-buffer@3.0.0": { - "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", - "dependencies": [ - "tslib" - ] - }, - "@smithy/md5-js@3.0.10": { - "integrity": "sha512-m3bv6dApflt3fS2Y1PyWPUtRP7iuBlvikEOGwu0HsCZ0vE7zcIX+dBoh3e+31/rddagw8nj92j0kJg2TfV+SJA==", - "dependencies": [ - "@smithy/types", - "@smithy/util-utf8@3.0.0", - "tslib" - ] - }, - "@smithy/middleware-content-length@3.0.12": { - "integrity": "sha512-1mDEXqzM20yywaMDuf5o9ue8OkJ373lSPbaSjyEvkWdqELhFMyNNgKGWL/rCSf4KME8B+HlHKuR8u9kRj8HzEQ==", - "dependencies": [ - "@smithy/protocol-http", - "@smithy/types", - "tslib" - ] - }, - "@smithy/middleware-endpoint@3.2.4": { - "integrity": "sha512-TybiW2LA3kYVd3e+lWhINVu1o26KJbBwOpADnf0L4x/35vLVica77XVR5hvV9+kWeTGeSJ3IHTcYxbRxlbwhsg==", - "dependencies": [ - "@smithy/core", - "@smithy/middleware-serde", - "@smithy/node-config-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "@smithy/url-parser", - "@smithy/util-middleware", - "tslib" - ] - }, - "@smithy/middleware-retry@3.0.28": { - "integrity": "sha512-vK2eDfvIXG1U64FEUhYxoZ1JSj4XFbYWkK36iz02i3pFwWiDz1Q7jKhGTBCwx/7KqJNk4VS7d7cDLXFOvP7M+g==", - "dependencies": [ - "@smithy/node-config-provider", - "@smithy/protocol-http", - "@smithy/service-error-classification", - "@smithy/smithy-client", - "@smithy/types", - "@smithy/util-middleware", - "@smithy/util-retry", - "tslib", - "uuid" - ] - }, - "@smithy/middleware-serde@3.0.10": { - "integrity": "sha512-MnAuhh+dD14F428ubSJuRnmRsfOpxSzvRhaGVTvd/lrUDE3kxzCCmH8lnVTvoNQnV2BbJ4c15QwZ3UdQBtFNZA==", - "dependencies": [ - "@smithy/types", - "tslib" - ] - }, - "@smithy/middleware-stack@3.0.10": { - "integrity": "sha512-grCHyoiARDBBGPyw2BeicpjgpsDFWZZxptbVKb3CRd/ZA15F/T6rZjCCuBUjJwdck1nwUuIxYtsS4H9DDpbP5w==", - "dependencies": [ - "@smithy/types", - "tslib" - ] - }, - "@smithy/node-config-provider@3.1.11": { - "integrity": "sha512-URq3gT3RpDikh/8MBJUB+QGZzfS7Bm6TQTqoh4CqE8NBuyPkWa5eUXj0XFcFfeZVgg3WMh1u19iaXn8FvvXxZw==", - "dependencies": [ - "@smithy/property-provider", - "@smithy/shared-ini-file-loader", - "@smithy/types", - "tslib" - ] - }, - "@smithy/node-http-handler@3.3.1": { - "integrity": "sha512-fr+UAOMGWh6bn4YSEezBCpJn9Ukp9oR4D32sCjCo7U81evE11YePOQ58ogzyfgmjIO79YeOdfXXqr0jyhPQeMg==", - "dependencies": [ - "@smithy/abort-controller", - "@smithy/protocol-http", - "@smithy/querystring-builder", - "@smithy/types", - "tslib" - ] - }, - "@smithy/property-provider@3.1.10": { - "integrity": "sha512-n1MJZGTorTH2DvyTVj+3wXnd4CzjJxyXeOgnTlgNVFxaaMeT4OteEp4QrzF8p9ee2yg42nvyVK6R/awLCakjeQ==", - "dependencies": [ - "@smithy/types", - "tslib" - ] - }, - "@smithy/protocol-http@4.1.7": { - "integrity": "sha512-FP2LepWD0eJeOTm0SjssPcgqAlDFzOmRXqXmGhfIM52G7Lrox/pcpQf6RP4F21k0+O12zaqQt5fCDOeBtqY6Cg==", - "dependencies": [ - "@smithy/types", - "tslib" - ] - }, - "@smithy/querystring-builder@3.0.10": { - "integrity": "sha512-nT9CQF3EIJtIUepXQuBFb8dxJi3WVZS3XfuDksxSCSn+/CzZowRLdhDn+2acbBv8R6eaJqPupoI/aRFIImNVPQ==", - "dependencies": [ - "@smithy/types", - "@smithy/util-uri-escape", - "tslib" - ] - }, - "@smithy/querystring-parser@3.0.10": { - "integrity": "sha512-Oa0XDcpo9SmjhiDD9ua2UyM3uU01ZTuIrNdZvzwUTykW1PM8o2yJvMh1Do1rY5sUQg4NDV70dMi0JhDx4GyxuQ==", - "dependencies": [ - "@smithy/types", - "tslib" - ] - }, - "@smithy/service-error-classification@3.0.10": { - "integrity": "sha512-zHe642KCqDxXLuhs6xmHVgRwy078RfqxP2wRDpIyiF8EmsWXptMwnMwbVa50lw+WOGNrYm9zbaEg0oDe3PTtvQ==", - "dependencies": [ - "@smithy/types" - ] - }, - "@smithy/shared-ini-file-loader@3.1.11": { - "integrity": "sha512-AUdrIZHFtUgmfSN4Gq9nHu3IkHMa1YDcN+s061Nfm+6pQ0mJy85YQDB0tZBCmls0Vuj22pLwDPmL92+Hvfwwlg==", - "dependencies": [ - "@smithy/types", - "tslib" - ] - }, - "@smithy/signature-v4@4.2.3": { - "integrity": "sha512-pPSQQ2v2vu9vc8iew7sszLd0O09I5TRc5zhY71KA+Ao0xYazIG+uLeHbTJfIWGO3BGVLiXjUr3EEeCcEQLjpWQ==", - "dependencies": [ - "@smithy/is-array-buffer@3.0.0", - "@smithy/protocol-http", - "@smithy/types", - "@smithy/util-hex-encoding", - "@smithy/util-middleware", - "@smithy/util-uri-escape", - "@smithy/util-utf8@3.0.0", - "tslib" - ] - }, - "@smithy/smithy-client@3.4.5": { - "integrity": "sha512-k0sybYT9zlP79sIKd1XGm4TmK0AS1nA2bzDHXx7m0nGi3RQ8dxxQUs4CPkSmQTKAo+KF9aINU3KzpGIpV7UoMw==", - "dependencies": [ - "@smithy/core", - "@smithy/middleware-endpoint", - "@smithy/middleware-stack", - "@smithy/protocol-http", - "@smithy/types", - "@smithy/util-stream", - "tslib" - ] - }, - "@smithy/types@3.7.1": { - "integrity": "sha512-XKLcLXZY7sUQgvvWyeaL/qwNPp6V3dWcUjqrQKjSb+tzYiCy340R/c64LV5j+Tnb2GhmunEX0eou+L+m2hJNYA==", - "dependencies": [ - "tslib" - ] - }, - "@smithy/url-parser@3.0.10": { - "integrity": "sha512-j90NUalTSBR2NaZTuruEgavSdh8MLirf58LoGSk4AtQfyIymogIhgnGUU2Mga2bkMkpSoC9gxb74xBXL5afKAQ==", - "dependencies": [ - "@smithy/querystring-parser", - "@smithy/types", - "tslib" - ] - }, - "@smithy/util-base64@3.0.0": { - "integrity": "sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==", - "dependencies": [ - "@smithy/util-buffer-from@3.0.0", - "@smithy/util-utf8@3.0.0", - "tslib" - ] - }, - "@smithy/util-body-length-browser@3.0.0": { - "integrity": "sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==", - "dependencies": [ - "tslib" - ] - }, - "@smithy/util-body-length-node@3.0.0": { - "integrity": "sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==", - "dependencies": [ - "tslib" - ] - }, - "@smithy/util-buffer-from@2.2.0": { - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "dependencies": [ - "@smithy/is-array-buffer@2.2.0", - "tslib" - ] - }, - "@smithy/util-buffer-from@3.0.0": { - "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", - "dependencies": [ - "@smithy/is-array-buffer@3.0.0", - "tslib" - ] - }, - "@smithy/util-config-provider@3.0.0": { - "integrity": "sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==", - "dependencies": [ - "tslib" - ] - }, - "@smithy/util-defaults-mode-browser@3.0.28": { - "integrity": "sha512-6bzwAbZpHRFVJsOztmov5PGDmJYsbNSoIEfHSJJyFLzfBGCCChiO3od9k7E/TLgrCsIifdAbB9nqbVbyE7wRUw==", - "dependencies": [ - "@smithy/property-provider", - "@smithy/smithy-client", - "@smithy/types", - "bowser", - "tslib" - ] - }, - "@smithy/util-defaults-mode-node@3.0.28": { - "integrity": "sha512-78ENJDorV1CjOQselGmm3+z7Yqjj5HWCbjzh0Ixuq736dh1oEnD9sAttSBNSLlpZsX8VQnmERqA2fEFlmqWn8w==", - "dependencies": [ - "@smithy/config-resolver", - "@smithy/credential-provider-imds", - "@smithy/node-config-provider", - "@smithy/property-provider", - "@smithy/smithy-client", - "@smithy/types", - "tslib" - ] - }, - "@smithy/util-endpoints@2.1.6": { - "integrity": "sha512-mFV1t3ndBh0yZOJgWxO9J/4cHZVn5UG1D8DeCc6/echfNkeEJWu9LD7mgGH5fHrEdR7LDoWw7PQO6QiGpHXhgA==", - "dependencies": [ - "@smithy/node-config-provider", - "@smithy/types", - "tslib" - ] - }, - "@smithy/util-hex-encoding@3.0.0": { - "integrity": "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==", - "dependencies": [ - "tslib" - ] - }, - "@smithy/util-middleware@3.0.10": { - "integrity": "sha512-eJO+/+RsrG2RpmY68jZdwQtnfsxjmPxzMlQpnHKjFPwrYqvlcT+fHdT+ZVwcjlWSrByOhGr9Ff2GG17efc192A==", - "dependencies": [ - "@smithy/types", - "tslib" - ] - }, - "@smithy/util-retry@3.0.10": { - "integrity": "sha512-1l4qatFp4PiU6j7UsbasUHL2VU023NRB/gfaa1M0rDqVrRN4g3mCArLRyH3OuktApA4ye+yjWQHjdziunw2eWA==", - "dependencies": [ - "@smithy/service-error-classification", - "@smithy/types", - "tslib" - ] - }, - "@smithy/util-stream@3.3.1": { - "integrity": "sha512-Ff68R5lJh2zj+AUTvbAU/4yx+6QPRzg7+pI7M1FbtQHcRIp7xvguxVsQBKyB3fwiOwhAKu0lnNyYBaQfSW6TNw==", - "dependencies": [ - "@smithy/fetch-http-handler", - "@smithy/node-http-handler", - "@smithy/types", - "@smithy/util-base64", - "@smithy/util-buffer-from@3.0.0", - "@smithy/util-hex-encoding", - "@smithy/util-utf8@3.0.0", - "tslib" - ] - }, - "@smithy/util-uri-escape@3.0.0": { - "integrity": "sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==", - "dependencies": [ - "tslib" - ] - }, - "@smithy/util-utf8@2.3.0": { - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "dependencies": [ - "@smithy/util-buffer-from@2.2.0", - "tslib" - ] - }, - "@smithy/util-utf8@3.0.0": { - "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", - "dependencies": [ - "@smithy/util-buffer-from@3.0.0", - "tslib" - ] - }, - "@smithy/util-waiter@3.1.9": { - "integrity": "sha512-/aMXPANhMOlMPjfPtSrDfPeVP8l56SJlz93xeiLmhLe5xvlXA5T3abZ2ilEsDEPeY9T/wnN/vNGn9wa1SbufWA==", - "dependencies": [ - "@smithy/abort-controller", - "@smithy/types", - "tslib" - ] - }, - "aws4fetch@1.0.20": { - "integrity": "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==" - }, - "base64-js@1.5.1": { - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "bowser@2.11.0": { - "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" - }, - "buffer@5.6.0": { - "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", - "dependencies": [ - "base64-js", - "ieee754" - ] - }, - "events@3.3.0": { - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" - }, - "fast-xml-parser@4.4.1": { - "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", - "dependencies": [ - "strnum" - ] - }, - "ieee754@1.2.1": { - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "inherits@2.0.4": { - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "jose@4.15.9": { - "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==" - }, - "jose@5.2.3": { - "integrity": "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA==" - }, - "lru-cache@6.0.0": { - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": [ - "yallist" - ] - }, - "object-hash@2.2.0": { - "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==" - }, - "oidc-token-hash@5.0.3": { - "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==" - }, - "openid-client@5.6.4": { - "integrity": "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA==", - "dependencies": [ - "jose@4.15.9", - "lru-cache", - "object-hash", - "oidc-token-hash" - ] - }, - "readable-stream@3.6.2": { - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": [ - "inherits", - "string_decoder", - "util-deprecate" - ] - }, - "safe-buffer@5.2.1": { - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "sst-darwin-arm64@3.3.47": { - "integrity": "sha512-81enlQkm7ZzvYQVS410HqwAyQEt3LmODRVrf0Y4vKjr5x1SvPNrHlSGbQ+lVyJgifn+PMoEQHGS4kOuyVgnTqA==" - }, - "sst-darwin-x64@3.3.47": { - "integrity": "sha512-9qppOAEPpzB1mxcttjQmDnX2W7vNCa4PsauzGIm2UXStsswHNd/DhDcqUiwkGyloI7yy7bEdgImrAXiNW8W5Eg==" - }, - "sst-linux-arm64@3.3.47": { - "integrity": "sha512-ITLhEfizeDbipcwN0YjNT8246/WIcEilaUoTV3GLK8MkJQEZ1nNRkCFpQNzusoXUvQoRoheWU3WrqMOODz54dw==" - }, - "sst-linux-x64@3.3.47": { - "integrity": "sha512-WJMuwDpju1zdZZqmbPIt+YszmO6VqCojn+3fiaUPSUg8Pm+DEUKAEggjlrBgdUkOOqNBYkIXq7N0YM8cs7tw6Q==" - }, - "sst-linux-x86@3.3.47": { - "integrity": "sha512-Zdpd7fjA3vs6rB7s5qSNWGdvDV96gpBq91YO1i1UrtJHUAZ1KehOqLm3M1YGkTYxaTR7k53J2rUAuZQoTdGQBQ==" - }, - "sst@3.3.47": { - "integrity": "sha512-7P2LbYv9WxW1y9l9cVKI547oUihKjvIwJ7Vy0xnmmSSnOp1AuD2BJQqzpldH5UgB2fWZND0MsXkMR15dut4zyw==", - "dependencies": [ - "aws4fetch", - "jose@5.2.3", - "openid-client", - "sst-darwin-arm64", - "sst-darwin-x64", - "sst-linux-arm64", - "sst-linux-x64", - "sst-linux-x86" - ] - }, - "stream-browserify@3.0.0": { - "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", - "dependencies": [ - "inherits", - "readable-stream" - ] - }, - "string_decoder@1.3.0": { - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": [ - "safe-buffer" - ] - }, - "strnum@1.0.5": { - "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" - }, - "tslib@2.8.1": { - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, - "util-deprecate@1.0.2": { - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "uuid@9.0.1": { - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" - }, - "yallist@4.0.0": { - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - }, - "workspace": { - "dependencies": [ - "jsr:@std/assert@1", - "npm:@aws-sdk/client-s3@^3.705.0", - "npm:@aws-sdk/lib-storage@^3.705.0", - "npm:@aws-sdk/s3-request-presigner@^3.705.0", - "npm:sst@^3.3.47" - ] - } -} diff --git a/examples/aws-drizzle-migrations/package.json b/examples/aws-drizzle-migrations/package.json index dcd89f97a6..1e6443a3ed 100644 --- a/examples/aws-drizzle-migrations/package.json +++ b/examples/aws-drizzle-migrations/package.json @@ -11,6 +11,6 @@ "drizzle-kit": "^0.26.2", "drizzle-orm": "^0.35.1", "pg": "^8.13.0", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-drizzle-migrations/sst-env.d.ts b/examples/aws-drizzle-migrations/sst-env.d.ts deleted file mode 100644 index abf1144ab8..0000000000 --- a/examples/aws-drizzle-migrations/sst-env.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyApi": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyPostgres": { - "database": string - "host": string - "password": string - "port": number - "type": "sst.aws.Postgres" - "username": string - } - "MyVpc": { - "bastion": string - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-drizzle/package.json b/examples/aws-drizzle/package.json index 4b5b6382cd..fde9d0b941 100644 --- a/examples/aws-drizzle/package.json +++ b/examples/aws-drizzle/package.json @@ -11,6 +11,6 @@ "drizzle-kit": "^0.26.2", "drizzle-orm": "^0.35.1", "pg": "^8.13.0", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-drizzle/sst-env.d.ts b/examples/aws-drizzle/sst-env.d.ts deleted file mode 100644 index abf1144ab8..0000000000 --- a/examples/aws-drizzle/sst-env.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyApi": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyPostgres": { - "database": string - "host": string - "password": string - "port": number - "type": "sst.aws.Postgres" - "username": string - } - "MyVpc": { - "bastion": string - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-planetscale-drizzle/.gitignore b/examples/aws-dsql-drizzle/.gitignore similarity index 100% rename from examples/aws-planetscale-drizzle/.gitignore rename to examples/aws-dsql-drizzle/.gitignore diff --git a/examples/aws-dsql-drizzle/drizzle.config.ts b/examples/aws-dsql-drizzle/drizzle.config.ts new file mode 100644 index 0000000000..ba08a3f2c4 --- /dev/null +++ b/examples/aws-dsql-drizzle/drizzle.config.ts @@ -0,0 +1,18 @@ +import { Resource } from "sst"; +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + dialect: "postgresql", + schema: ["./src/**/*.sql.ts"], + out: "./migrations", + dbCredentials: { + host: Resource.MyCluster.endpoint, + port: 5432, + user: "admin", + password: process.env.DSQL_TOKEN!, + database: "postgres", + ssl: { + rejectUnauthorized: false, + }, + }, +}); diff --git a/examples/aws-dsql-drizzle/package.json b/examples/aws-dsql-drizzle/package.json new file mode 100644 index 0000000000..6b24eb27a5 --- /dev/null +++ b/examples/aws-dsql-drizzle/package.json @@ -0,0 +1,18 @@ +{ + "name": "aws-dsql-drizzle", + "version": "0.0.0", + "type": "module", + "scripts": { + "db": "sst shell drizzle-kit" + }, + "dependencies": { + "@aws-sdk/credential-providers": "^3.844.0", + "@aws-sdk/dsql-signer": "^3.844.0", + "@aws/aurora-dsql-node-postgres-connector": "^0.1.8", + "@types/aws-lambda": "^8.10.142", + "@types/pg": "^8.11.10", + "drizzle-kit": "^0.26.2", + "drizzle-orm": "^0.35.1", + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-dsql-drizzle/push.ts b/examples/aws-dsql-drizzle/push.ts new file mode 100644 index 0000000000..8481e544ad --- /dev/null +++ b/examples/aws-dsql-drizzle/push.ts @@ -0,0 +1,18 @@ +import { DsqlSigner } from "@aws-sdk/dsql-signer"; +import { Resource } from "sst"; +import { execSync } from "child_process"; + +const signer = new DsqlSigner({ + region: Resource.MyCluster.region, + hostname: Resource.MyCluster.endpoint, +}); + +const token = await signer.getDbConnectAdminAuthToken(); + +execSync("bunx drizzle-kit push", { + stdio: "inherit", + env: { + ...process.env, + DSQL_TOKEN: token, + }, +}); diff --git a/examples/aws-dsql-drizzle/src/api.ts b/examples/aws-dsql-drizzle/src/api.ts new file mode 100644 index 0000000000..1cfb345fc0 --- /dev/null +++ b/examples/aws-dsql-drizzle/src/api.ts @@ -0,0 +1,27 @@ +import { db } from "./drizzle"; +import { todo } from "./todo.sql"; +import { APIGatewayProxyEventV2 } from "aws-lambda"; + +export const handler = async (evt: APIGatewayProxyEventV2) => { + if (evt.requestContext.http.method === "GET") { + const result = await db.select().from(todo).execute(); + + return { + statusCode: 200, + body: JSON.stringify(result, null, 2), + }; + } + + if (evt.requestContext.http.method === "POST") { + const result = await db + .insert(todo) + .values({ title: "Todo", description: crypto.randomUUID() }) + .returning() + .execute(); + + return { + statusCode: 200, + body: JSON.stringify(result), + }; + } +}; diff --git a/examples/aws-dsql-drizzle/src/drizzle.ts b/examples/aws-dsql-drizzle/src/drizzle.ts new file mode 100644 index 0000000000..705e8e91e7 --- /dev/null +++ b/examples/aws-dsql-drizzle/src/drizzle.ts @@ -0,0 +1,11 @@ +import { drizzle } from "drizzle-orm/node-postgres"; +import { AuroraDSQLPool } from "@aws/aurora-dsql-node-postgres-connector"; +import { Resource } from "sst"; +import * as schema from "./todo.sql"; + +const pool = new AuroraDSQLPool({ + host: Resource.MyCluster.endpoint, + user: "admin", +}); + +export const db = drizzle(pool, { schema }); diff --git a/examples/aws-dsql-drizzle/src/todo.sql.ts b/examples/aws-dsql-drizzle/src/todo.sql.ts new file mode 100644 index 0000000000..b04e165a5c --- /dev/null +++ b/examples/aws-dsql-drizzle/src/todo.sql.ts @@ -0,0 +1,7 @@ +import { text, bigint, pgTable } from "drizzle-orm/pg-core"; + +export const todo = pgTable("todo", { + id: bigint("id", { mode: "number" }).primaryKey().generatedAlwaysAsIdentity(), + title: text("title").notNull(), + description: text("description"), +}); diff --git a/examples/aws-dsql-drizzle/sst.config.ts b/examples/aws-dsql-drizzle/sst.config.ts new file mode 100644 index 0000000000..4edaefc62a --- /dev/null +++ b/examples/aws-dsql-drizzle/sst.config.ts @@ -0,0 +1,66 @@ +/// + +/** + * ## AWS Aurora DSQL with Drizzle + * + * In this example, we use Drizzle ORM with an Aurora DSQL cluster. + * + * ```ts title="sst.config.ts" + * const cluster = new sst.aws.Dsql("MyCluster"); + * ``` + * + * And link it to a Lambda function. + * + * ```ts title="sst.config.ts" {4} + * new sst.aws.Function("MyApi", { + * handler: "src/api.handler", + * link: [cluster], + * url: true, + * }); + * ``` + * + * Push the Drizzle schema to the database. + * + * ```bash + * sst shell -- bun run push.ts + * ``` + * + * Now in the function we can connect to the cluster using Drizzle with the DSQL connector. + * Learn more about [DSQL Node.js connectors](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/SECTION_Node-js-connectors.html). + * + * ```ts title="src/drizzle.ts" + * import { drizzle } from "drizzle-orm/node-postgres"; + * import { AuroraDSQLPool } from "@aws/aurora-dsql-node-postgres-connector"; + * import { Resource } from "sst"; + * + * const pool = new AuroraDSQLPool({ + * host: Resource.MyCluster.endpoint, + * user: "admin", + * }); + * + * export const db = drizzle(pool, { schema }); + * ``` + */ +export default $config({ + app(input) { + return { + name: "aws-dsql-drizzle", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + }; + }, + async run() { + const cluster = new sst.aws.Dsql("MyCluster"); + + new sst.aws.Function("MyApi", { + handler: "src/api.handler", + link: [cluster], + url: true, + }); + + return { + endpoint: cluster.endpoint, + region: cluster.region, + }; + }, +}); diff --git a/examples/aws-dsql-drizzle/tsconfig.json b/examples/aws-dsql-drizzle/tsconfig.json new file mode 100644 index 0000000000..aee0ec940f --- /dev/null +++ b/examples/aws-dsql-drizzle/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "strict": true + } +} diff --git a/examples/aws-dsql-multiregion/.gitignore b/examples/aws-dsql-multiregion/.gitignore new file mode 100644 index 0000000000..2060062401 --- /dev/null +++ b/examples/aws-dsql-multiregion/.gitignore @@ -0,0 +1,3 @@ +# sst +.sst +bun.lock \ No newline at end of file diff --git a/examples/aws-dsql-multiregion/lambda.ts b/examples/aws-dsql-multiregion/lambda.ts new file mode 100644 index 0000000000..28e5441dd8 --- /dev/null +++ b/examples/aws-dsql-multiregion/lambda.ts @@ -0,0 +1,41 @@ +import { AuroraDSQLClient } from "@aws/aurora-dsql-node-postgres-connector"; +import { Resource } from "sst"; + +async function connectToCluster(endpoint: string) { + const client = new AuroraDSQLClient({ + host: endpoint, + user: "admin", + }); + + await client.connect(); + const result = await client.query("SELECT NOW() as now"); + await client.end(); + + return result.rows[0].now; +} + +export const handler = async () => { + try { + const usEast1Time = await connectToCluster(Resource.MultiRegion.endpoint); + + const usEast2Time = await connectToCluster(Resource.MultiRegion.peer.endpoint); + + return { + statusCode: 200, + body: JSON.stringify({ + message: "Successfully connected to both DSQL clusters.", + usEast1Time, + usEast2Time, + }), + }; + } catch (error) { + console.error("Error accessing DSQL clusters:", error); + return { + statusCode: 500, + body: JSON.stringify({ + error: "Failed to access DSQL clusters", + details: error instanceof Error ? error.message : String(error), + }), + }; + } +}; diff --git a/examples/aws-dsql-multiregion/package.json b/examples/aws-dsql-multiregion/package.json new file mode 100644 index 0000000000..ad0e8cdc63 --- /dev/null +++ b/examples/aws-dsql-multiregion/package.json @@ -0,0 +1,19 @@ +{ + "name": "aws-dsql-multiregion", + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "sst dev", + "deploy": "sst deploy", + "remove": "sst remove" + }, + "dependencies": { + "@aws-sdk/credential-providers": "^3.844.0", + "@aws/aurora-dsql-node-postgres-connector": "^0.1.8", + "sst": "file:../../sdk/js" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.133", + "@types/pg": "^8.10.9" + } +} diff --git a/examples/aws-dsql-multiregion/sst.config.ts b/examples/aws-dsql-multiregion/sst.config.ts new file mode 100644 index 0000000000..8d09ff4867 --- /dev/null +++ b/examples/aws-dsql-multiregion/sst.config.ts @@ -0,0 +1,82 @@ +/// + +/** + * ## AWS Aurora DSQL Multi-Region + * + * In this example, we deploy a multi-region Aurora DSQL cluster and connect to both + * clusters from a Lambda function. + * + * :::note + * Multi-region with VPCs is not currently supported. + * ::: + * + * Create the cluster with a witness region and a peer region. The witness must differ + * from both cluster regions. + * + * ```ts title="sst.config.ts" + * const cluster = new sst.aws.Dsql("MultiRegion", { + * regions: { + * witness: "us-west-2", + * peer: "us-east-2", + * }, + * }); + * ``` + * + * Connect to both clusters from your function using the DSQL connector. + * Learn more about [DSQL Node.js connectors](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/SECTION_Node-js-connectors.html). + * + * ```ts title="lambda.ts" + * import { AuroraDSQLClient } from "@aws/aurora-dsql-node-postgres-connector"; + * import { Resource } from "sst"; + * + * async function connectToCluster(endpoint: string) { + * const client = new AuroraDSQLClient({ host: endpoint, user: "admin" }); + * await client.connect(); + * return client; + * } + * + * // Cluster in us-east-1 + * const usEast1 = await connectToCluster(Resource.MultiRegion.endpoint); + * + * // Cluster in us-east-2 + * const usEast2 = await connectToCluster(Resource.MultiRegion.peer.endpoint); + * ``` + */ + +export default $config({ + app(input) { + return { + name: "aws-dsql-multiregion", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + providers: { + aws: { region: "us-east-1" }, + }, + }; + }, + async run() { + const cluster = new sst.aws.Dsql("MultiRegion", { + backup: true, + regions: { + witness: "us-west-2", + peer: "us-east-2", + }, + }); + + const fn = new sst.aws.Function("MyFunction", { + handler: "lambda.handler", + link: [cluster], + url: true, + }); + + return { + url: fn.url, + arn: cluster.arn, + endpoint: cluster.endpoint, + region: cluster.region, + peerArn: cluster.peer.arn, + peerEndpoint: cluster.peer.endpoint, + peerRegion: cluster.peer.region, + }; + }, +}); diff --git a/examples/aws-dsql-vpc/.gitignore b/examples/aws-dsql-vpc/.gitignore new file mode 100644 index 0000000000..2060062401 --- /dev/null +++ b/examples/aws-dsql-vpc/.gitignore @@ -0,0 +1,3 @@ +# sst +.sst +bun.lock \ No newline at end of file diff --git a/examples/aws-dsql-vpc/lambda.ts b/examples/aws-dsql-vpc/lambda.ts new file mode 100644 index 0000000000..6c8c676be8 --- /dev/null +++ b/examples/aws-dsql-vpc/lambda.ts @@ -0,0 +1,32 @@ +import { AuroraDSQLClient } from "@aws/aurora-dsql-node-postgres-connector"; +import { Resource } from "sst"; + +export const handler = async () => { + try { + const client = new AuroraDSQLClient({ + host: Resource.MyCluster.endpoint, + user: "admin", + }); + + await client.connect(); + const now = await client.query("SELECT NOW() as now"); + await client.end(); + + return { + statusCode: 200, + body: JSON.stringify({ + message: "Successfully connected to DSQL cluster.", + now: now.rows[0].now, + }), + }; + } catch (error) { + console.error("Error accessing DSQL cluster:", error); + return { + statusCode: 500, + body: JSON.stringify({ + error: "Failed to access DSQL cluster", + details: error instanceof Error ? error.message : String(error), + }), + }; + } +}; diff --git a/examples/aws-dsql-vpc/package.json b/examples/aws-dsql-vpc/package.json new file mode 100644 index 0000000000..fe05e2ec1b --- /dev/null +++ b/examples/aws-dsql-vpc/package.json @@ -0,0 +1,19 @@ +{ + "name": "aws-dsql-vpc", + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "sst dev", + "deploy": "sst deploy", + "remove": "sst remove" + }, + "dependencies": { + "@aws-sdk/credential-providers": "^3.844.0", + "@aws/aurora-dsql-node-postgres-connector": "^0.1.8", + "sst": "file:../../sdk/js" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.133", + "@types/pg": "^8.10.9" + } +} diff --git a/examples/aws-dsql-vpc/sst.config.ts b/examples/aws-dsql-vpc/sst.config.ts new file mode 100644 index 0000000000..390c868187 --- /dev/null +++ b/examples/aws-dsql-vpc/sst.config.ts @@ -0,0 +1,84 @@ +/// + +/** + * ## AWS Aurora DSQL in a VPC + * + * In this example, we connect to an Aurora DSQL cluster privately from a Lambda + * function using VPC endpoints, without routing traffic over the public internet. + * + * Create a VPC, then create the cluster with a connection endpoint inside it. + * + * ```ts title="sst.config.ts" + * const vpc = new sst.aws.Vpc("MyVpc"); + * + * const cluster = new sst.aws.Dsql("MyCluster", { + * vpc: { + * instance: vpc, + * endpoints: { connection: true }, + * }, + * }); + * ``` + * + * Link the cluster to a function that's also in the VPC. The linked `endpoint` will + * automatically resolve to the private VPC endpoint hostname instead of the public one. + * + * ```ts title="sst.config.ts" + * new sst.aws.Function("MyFunction", { + * handler: "lambda.handler", + * vpc, + * link: [cluster], + * }); + * ``` + * + * Connect from your function using the DSQL connector β€” no config changes needed. + * Learn more about [DSQL Node.js connectors](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/SECTION_Node-js-connectors.html). + * + * ```ts title="lambda.ts" + * import { AuroraDSQLClient } from "@aws/aurora-dsql-node-postgres-connector"; + * import { Resource } from "sst"; + * + * const client = new AuroraDSQLClient({ + * host: Resource.MyCluster.endpoint, + * user: "admin", + * }); + * + * await client.connect(); + * const result = await client.query("SELECT NOW()"); + * await client.end(); + * ``` + */ + +export default $config({ + app(input) { + return { + name: "aws-dsql-vpc", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + }; + }, + async run() { + const vpc = new sst.aws.Vpc("singleClusterVpc"); + + const cluster = new sst.aws.Dsql("MyCluster", { + vpc: { + instance: vpc, + endpoints: { + connection: true, + management: false, + }, + }, + }); + + const fn = new sst.aws.Function("MyFunction", { + handler: "lambda.handler", + vpc, + link: [cluster], + url: true, + }); + + return { + endpoint: cluster.endpoint, + region: cluster.region, + }; + }, +}); diff --git a/examples/aws-dsql/.gitignore b/examples/aws-dsql/.gitignore new file mode 100644 index 0000000000..2060062401 --- /dev/null +++ b/examples/aws-dsql/.gitignore @@ -0,0 +1,3 @@ +# sst +.sst +bun.lock \ No newline at end of file diff --git a/examples/aws-dsql/lambda.ts b/examples/aws-dsql/lambda.ts new file mode 100644 index 0000000000..6c8c676be8 --- /dev/null +++ b/examples/aws-dsql/lambda.ts @@ -0,0 +1,32 @@ +import { AuroraDSQLClient } from "@aws/aurora-dsql-node-postgres-connector"; +import { Resource } from "sst"; + +export const handler = async () => { + try { + const client = new AuroraDSQLClient({ + host: Resource.MyCluster.endpoint, + user: "admin", + }); + + await client.connect(); + const now = await client.query("SELECT NOW() as now"); + await client.end(); + + return { + statusCode: 200, + body: JSON.stringify({ + message: "Successfully connected to DSQL cluster.", + now: now.rows[0].now, + }), + }; + } catch (error) { + console.error("Error accessing DSQL cluster:", error); + return { + statusCode: 500, + body: JSON.stringify({ + error: "Failed to access DSQL cluster", + details: error instanceof Error ? error.message : String(error), + }), + }; + } +}; diff --git a/examples/aws-dsql/package.json b/examples/aws-dsql/package.json new file mode 100644 index 0000000000..3038223d17 --- /dev/null +++ b/examples/aws-dsql/package.json @@ -0,0 +1,19 @@ +{ + "name": "aws-dsql", + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "sst dev", + "deploy": "sst deploy", + "remove": "sst remove" + }, + "dependencies": { + "@aws-sdk/credential-providers": "^3.844.0", + "@aws/aurora-dsql-node-postgres-connector": "^0.1.8", + "sst": "file:../../sdk/js" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.133", + "@types/pg": "^8.10.9" + } +} diff --git a/examples/aws-dsql/sst.config.ts b/examples/aws-dsql/sst.config.ts new file mode 100644 index 0000000000..cc3477e4f3 --- /dev/null +++ b/examples/aws-dsql/sst.config.ts @@ -0,0 +1,62 @@ +/// + +/** + * ## AWS Aurora DSQL + * + * In this example, we deploy an Aurora DSQL cluster. + * + * ```ts title="sst.config.ts" + * const cluster = new sst.aws.Dsql("MyCluster"); + * ``` + * + * And link it to a Lambda function. + * + * ```ts title="sst.config.ts" {4} + * new sst.aws.Function("MyFunction", { + * handler: "lambda.handler", + * link: [cluster], + * url: true, + * }); + * ``` + * + * Now in the function we can connect to the cluster using the DSQL connector. + * Learn more about [DSQL Node.js connectors](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/SECTION_Node-js-connectors.html). + * + * ```ts title="lambda.ts" + * import { AuroraDSQLClient } from "@aws/aurora-dsql-node-postgres-connector"; + * import { Resource } from "sst"; + * + * const client = new AuroraDSQLClient({ + * host: Resource.MyCluster.endpoint, + * user: "admin", + * }); + * + * await client.connect(); + * const result = await client.query("SELECT NOW()"); + * await client.end(); + * ``` + */ + +export default $config({ + app(input) { + return { + name: "aws-dsql", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + }; + }, + async run() { + const cluster = new sst.aws.Dsql("MyCluster", {}); + + const fn = new sst.aws.Function("MyFunction", { + handler: "lambda.handler", + link: [cluster], + url: true, + }); + + return { + endpoint: cluster.endpoint, + region: cluster.region, + }; + }, +}); \ No newline at end of file diff --git a/examples/aws-dynamo-composite-keys/creator.ts b/examples/aws-dynamo-composite-keys/creator.ts new file mode 100644 index 0000000000..4a9365b223 --- /dev/null +++ b/examples/aws-dynamo-composite-keys/creator.ts @@ -0,0 +1,23 @@ +import { Resource } from "sst"; +import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; +const client = new DynamoDBClient(); + +export const handler = async () => { + await client.send( + new PutItemCommand({ + TableName: Resource.MyTable.name, + Item: { + userId: { S: "user1" }, + noteId: { S: Date.now().toString() }, + region: { S: "us-east" }, + category: { S: "notes" }, + createdAt: { N: Date.now().toString() }, + }, + }) + ); + + return { + statusCode: 200, + body: JSON.stringify({ status: "sent" }, null, 2), + }; +}; diff --git a/examples/aws-dynamo-composite-keys/package.json b/examples/aws-dynamo-composite-keys/package.json new file mode 100644 index 0000000000..1269793242 --- /dev/null +++ b/examples/aws-dynamo-composite-keys/package.json @@ -0,0 +1,21 @@ +{ + "name": "aws-dynamo-composite-keys", + "version": "1.0.0", + "description": "", + "type": "module", + "main": "index.js", + "scripts": { + "deploy": "go run ../../cmd/sst deploy", + "remove": "go run ../../cmd/sst remove", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "sst": "file:../../sdk/js" + }, + "dependencies": { + "@aws-sdk/client-dynamodb": "^3.515.0" + } +} diff --git a/examples/aws-dynamo-composite-keys/reader.ts b/examples/aws-dynamo-composite-keys/reader.ts new file mode 100644 index 0000000000..dfb2b1527b --- /dev/null +++ b/examples/aws-dynamo-composite-keys/reader.ts @@ -0,0 +1,26 @@ +import { Resource } from "sst"; +import { DynamoDBClient, QueryCommand } from "@aws-sdk/client-dynamodb"; +const client = new DynamoDBClient(); + +export const handler = async () => { + const result = await client.send( + new QueryCommand({ + TableName: Resource.MyTable.name, + IndexName: "RegionCategoryIndex", + KeyConditionExpression: "#r = :region AND #c = :category", + ExpressionAttributeNames: { + "#r": "region", + "#c": "category", + }, + ExpressionAttributeValues: { + ":region": { S: "us-east" }, + ":category": { S: "notes" }, + }, + }) + ); + + return { + statusCode: 200, + body: JSON.stringify(result.Items, null, 2), + }; +}; diff --git a/examples/aws-dynamo-composite-keys/sst.config.ts b/examples/aws-dynamo-composite-keys/sst.config.ts new file mode 100644 index 0000000000..dfea2ec245 --- /dev/null +++ b/examples/aws-dynamo-composite-keys/sst.config.ts @@ -0,0 +1,52 @@ +/// + +/** + * ## DynamoDB composite keys + * + * Create a DynamoDB table with multi-attribute composite keys in a global secondary index. + */ +export default $config({ + app(input) { + return { + name: "aws-dynamo-composite-keys", + home: "aws", + removal: input?.stage === "production" ? "retain" : "remove", + }; + }, + async run() { + const table = new sst.aws.Dynamo("MyTable", { + fields: { + userId: "string", + noteId: "string", + region: "string", + category: "string", + createdAt: "number", + }, + primaryIndex: { hashKey: "userId", rangeKey: "noteId" }, + globalIndexes: { + RegionCategoryIndex: { + hashKey: ["region", "category"], + rangeKey: "createdAt", + }, + }, + }); + + const creator = new sst.aws.Function("MyCreator", { + handler: "creator.handler", + link: [table], + url: true, + }); + + const reader = new sst.aws.Function("MyReader", { + handler: "reader.handler", + link: [table], + url: true, + }); + + return { + creator: creator.url, + reader: reader.url, + table: table.name, + }; + }, +}); diff --git a/examples/aws-dynamo/publisher.ts b/examples/aws-dynamo/creator.ts similarity index 100% rename from examples/aws-dynamo/publisher.ts rename to examples/aws-dynamo/creator.ts diff --git a/examples/aws-dynamo/package.json b/examples/aws-dynamo/package.json index 23a4f70743..fd944c5262 100644 --- a/examples/aws-dynamo/package.json +++ b/examples/aws-dynamo/package.json @@ -13,7 +13,7 @@ "author": "", "license": "ISC", "devDependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" }, "dependencies": { "@aws-sdk/client-dynamodb": "^3.515.0" diff --git a/examples/aws-dynamo/reader.ts b/examples/aws-dynamo/reader.ts new file mode 100644 index 0000000000..fd68d74ea7 --- /dev/null +++ b/examples/aws-dynamo/reader.ts @@ -0,0 +1,16 @@ +import { Resource } from "sst"; +import { DynamoDBClient, ScanCommand } from "@aws-sdk/client-dynamodb"; +const client = new DynamoDBClient(); + +export const handler = async () => { + const result = await client.send( + new ScanCommand({ + TableName: Resource.MyTable.name, + }) + ); + + return { + statusCode: 200, + body: JSON.stringify(result.Items, null, 2), + }; +}; diff --git a/examples/aws-dynamo/sst.config.ts b/examples/aws-dynamo/sst.config.ts index 6b73785da1..9d9ff5d5b2 100644 --- a/examples/aws-dynamo/sst.config.ts +++ b/examples/aws-dynamo/sst.config.ts @@ -35,14 +35,21 @@ export default $config({ ], }); - const app = new sst.aws.Function("MyApp", { - handler: "publisher.handler", + const creator = new sst.aws.Function("MyCreator", { + handler: "creator.handler", + link: [table], + url: true, + }); + + const reader = new sst.aws.Function("MyReader", { + handler: "reader.handler", link: [table], url: true, }); return { - app: app.url, + creator: creator.url, + reader: reader.url, table: table.name, }; }, diff --git a/examples/aws-ec2-pulumi/package.json b/examples/aws-ec2-pulumi/package.json index e0179bb9cc..b4ad3bd51e 100644 --- a/examples/aws-ec2-pulumi/package.json +++ b/examples/aws-ec2-pulumi/package.json @@ -13,7 +13,7 @@ "author": "", "license": "ISC", "devDependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" }, "dependencies": { } diff --git a/examples/aws-efs-sqlite/sst-env.d.ts b/examples/aws-efs-sqlite/sst-env.d.ts deleted file mode 100644 index 373236038f..0000000000 --- a/examples/aws-efs-sqlite/sst-env.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyFunction": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-efs-surrealdb/package.json b/examples/aws-efs-surrealdb/package.json index bc8587fe5e..adccacafad 100644 --- a/examples/aws-efs-surrealdb/package.json +++ b/examples/aws-efs-surrealdb/package.json @@ -10,7 +10,7 @@ "author": "", "license": "ISC", "dependencies": { - "sst": "^3", + "sst": "file:../../sdk/js", "surrealdb": "^1.0.1" } } diff --git a/examples/aws-efs-surrealdb/sst-env.d.ts b/examples/aws-efs-surrealdb/sst-env.d.ts deleted file mode 100644 index fe9d6e973f..0000000000 --- a/examples/aws-efs-surrealdb/sst-env.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyApp": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyConfig": { - "database": string - "host": string - "namespace": string - "password": string - "port": number - "type": "sst.sst.Linkable" - "username": string - } - "MyService": { - "service": string - "type": "sst.aws.Service" - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-efs/sst-env.d.ts b/examples/aws-efs/sst-env.d.ts deleted file mode 100644 index 7832718ac0..0000000000 --- a/examples/aws-efs/sst-env.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyFunction": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-email-domain/package.json b/examples/aws-email-domain/package.json new file mode 100644 index 0000000000..ceb433a533 --- /dev/null +++ b/examples/aws-email-domain/package.json @@ -0,0 +1,20 @@ +{ + "name": "aws-email-domain", + "version": "1.0.0", + "description": "", + "type": "module", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@aws-sdk/client-sesv2": "^3.515.0", + "sst": "file:../../sdk/js" + }, + "devDependencies": { + "@types/aws-lambda": "8.10.142" + } +} diff --git a/examples/aws-email-domain/sender.ts b/examples/aws-email-domain/sender.ts new file mode 100644 index 0000000000..4b85f92e7d --- /dev/null +++ b/examples/aws-email-domain/sender.ts @@ -0,0 +1,32 @@ +import { Resource } from "sst"; +import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2"; + +const client = new SESv2Client(); + +export const handler = async () => { + await client.send( + new SendEmailCommand({ + FromEmailAddress: `no-reply@${Resource.MyEmail.sender}`, + Destination: { + ToAddresses: [`jhon@${Resource.MyEmail.sender}`], + }, + Content: { + Simple: { + Subject: { + Data: "Hello World!", + }, + Body: { + Text: { + Data: "Sent from my SST app.", + }, + }, + }, + }, + }) + ); + + return { + statusCode: 200, + body: "Sent!" + }; +}; diff --git a/examples/aws-email-domain/sst.config.ts b/examples/aws-email-domain/sst.config.ts new file mode 100644 index 0000000000..b5a111560c --- /dev/null +++ b/examples/aws-email-domain/sst.config.ts @@ -0,0 +1,29 @@ +/// + +export default $config({ + app(input) { + return { + name: "aws-email-domain", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + }; + }, + async run() { + const email = new sst.aws.Email("MyEmail", { + sender: "example.com", + mailFrom: { + domain: "mail.example.com", + }, + }); + + const api = new sst.aws.Function("MyApi", { + handler: "sender.handler", + link: [email], + url: true, + }); + + return { + url: api.url, + }; + }, +}); diff --git a/examples/aws-planetscale-drizzle/tsconfig.json b/examples/aws-email-domain/tsconfig.json similarity index 100% rename from examples/aws-planetscale-drizzle/tsconfig.json rename to examples/aws-email-domain/tsconfig.json diff --git a/examples/aws-email/package.json b/examples/aws-email/package.json index 3b1b97e3f8..c24cf0273c 100644 --- a/examples/aws-email/package.json +++ b/examples/aws-email/package.json @@ -12,7 +12,7 @@ "license": "ISC", "dependencies": { "@aws-sdk/client-sesv2": "^3.515.0", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "8.10.142" diff --git a/examples/aws-express-redis/package.json b/examples/aws-express-redis/package.json index 733b6fd134..376035cecd 100644 --- a/examples/aws-express-redis/package.json +++ b/examples/aws-express-redis/package.json @@ -12,7 +12,7 @@ "dependencies": { "express": "^4.21.0", "ioredis": "^5.4.1", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "8.10.145" diff --git a/examples/aws-express-redis/sst-env.d.ts b/examples/aws-express-redis/sst-env.d.ts deleted file mode 100644 index 261f14f5b2..0000000000 --- a/examples/aws-express-redis/sst-env.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyRedis": { - "host": string - "password": string - "port": number - "type": "sst.aws.Redis" - "username": string - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "bastion": string - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-express/package.json b/examples/aws-express/package.json index 634aaefc0a..ad4e1fbccb 100644 --- a/examples/aws-express/package.json +++ b/examples/aws-express/package.json @@ -15,7 +15,7 @@ "@aws-sdk/s3-request-presigner": "^3.705.0", "express": "^4.21.1", "multer": "^1.4.5-lts.1", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "8.10.146" diff --git a/examples/aws-express/sst-env.d.ts b/examples/aws-express/sst-env.d.ts deleted file mode 100644 index ff1d425c12..0000000000 --- a/examples/aws-express/sst-env.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-fastapi/README.md b/examples/aws-fastapi/README.md deleted file mode 100644 index fe0525afb0..0000000000 --- a/examples/aws-fastapi/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# ❍ Python Example - -Deploy python applications using sst. - -SST uses [uv](https://github.com/astral-sh/uv) to manage your python runtime. If you do not have uv installed, you can install it [here](https://docs.astral.sh/uv/getting-started/installation/). Any sst workspace package can be built and deployed to aws lambda using sst. In this example we deploy an API handler to lambda from the `functions` directory. The handler depends on shared code from the `shared` directory using uv's workspaces feature. (Note: builds currently do not tree shake so lots of workspaces can make larger builds than necessary.) - -Python functions can be deployed just like other SST functions, the only difference is that the functions themselves must be configured within a uv workspace, there is no drop-in-mode. - -```typescript title="sst.config.ts" -const python = new sst.aws.Function("MyPythonFunction", { - handler: "functions/src/functions/api.handler", - runtime: "python3.11", - url: true -}); -``` - - -If you are using live lambdas for your python functions, it is recommended to specify your python version to match your Lambda runtime otherwise you may encounter issues with dependencies. - -```toml title="src/pyproject.toml" -[project] -name = "aws-python" -version = "0.1.0" -description = "A SST app" -authors = [ - {name = "", email = "" }, -] -requires-python = "==3.11.*" -``` - -Live lambda will locally run your python code by building the workspace and running the specified handler. You can have multiple handlers in the same workspace and have multiple workspaces in the same project. - -```markdown -. -β”œβ”€β”€ workspace_a -β”‚ β”œβ”€β”€ pyproject.toml -β”‚ └── src -β”‚ └── workspace_a -β”‚ β”œβ”€β”€ __init__.py -β”‚ β”œβ”€β”€ api_a.py -β”‚ └── api_b.py -└── workspace_b - β”œβ”€β”€ pyproject.toml - └── src - └── workspace_b - β”œβ”€β”€ __init__.py - └── index.py -``` - -Keep in mind that AWS Lambda zip archives have limits and python does use native extensions for certain packages, if you are using large dependencies such as numpy, pandas, and others found in the SciPy stack, you may want to use the container mode to deploy your python code. diff --git a/examples/aws-fastapi/core/sst.pyi b/examples/aws-fastapi/core/sst.pyi deleted file mode 100644 index 18f014ac3c..0000000000 --- a/examples/aws-fastapi/core/sst.pyi +++ /dev/null @@ -1,17 +0,0 @@ -# Automatically generated by SST -# pylint: disable=all -# ruff: noqa -from typing import Any - -class Resource: - class App: - name: str - stage: str - class FastAPI: - name: str - type: str - url: str - class MyLinkableValue: - foo: str - type: str - diff --git a/examples/aws-fastapi/functions/pyproject.toml b/examples/aws-fastapi/functions/pyproject.toml index 59ddf3d06a..ac5a78c4b8 100644 --- a/examples/aws-fastapi/functions/pyproject.toml +++ b/examples/aws-fastapi/functions/pyproject.toml @@ -2,7 +2,7 @@ name = "functions" version = "0.1.0" description = "Lambda function handlers" -dependencies = ["core", "sst", "fastapi==0.115.8", "mangum==0.19.0"] +dependencies = ["core", "sst-sdk", "fastapi==0.115.8", "mangum==0.19.0"] requires-python = "==3.11.*" [build-system] @@ -11,4 +11,4 @@ build-backend = "hatchling.build" [tool.uv.sources] core = { workspace = true } -sst = { git = "https://github.com/sst/sst.git", branch = "dev", subdirectory = "sdk/python" } +sst-sdk = { git = "https://github.com/anomalyco/sst.git", branch = "dev", subdirectory = "sdk/python" } diff --git a/examples/aws-fastapi/functions/sst.pyi b/examples/aws-fastapi/functions/sst.pyi deleted file mode 100644 index 18f014ac3c..0000000000 --- a/examples/aws-fastapi/functions/sst.pyi +++ /dev/null @@ -1,17 +0,0 @@ -# Automatically generated by SST -# pylint: disable=all -# ruff: noqa -from typing import Any - -class Resource: - class App: - name: str - stage: str - class FastAPI: - name: str - type: str - url: str - class MyLinkableValue: - foo: str - type: str - diff --git a/examples/aws-fastapi/package.json b/examples/aws-fastapi/package.json index 9f7e6bd2f3..ff07a387c7 100644 --- a/examples/aws-fastapi/package.json +++ b/examples/aws-fastapi/package.json @@ -10,6 +10,6 @@ "author": "", "license": "ISC", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-fastapi/pyproject.toml b/examples/aws-fastapi/pyproject.toml index 8decc2198d..c7a1678eeb 100644 --- a/examples/aws-fastapi/pyproject.toml +++ b/examples/aws-fastapi/pyproject.toml @@ -13,4 +13,4 @@ requires-python = "==3.11.*" members = ["functions", "core"] [tool.uv.sources] -sst = { git = "https://github.com/sst/sst.git", subdirectory = "sdk/python", branch = "dev" } +sst-sdk = { git = "https://github.com/anomalyco/sst.git", subdirectory = "sdk/python", branch = "dev" } diff --git a/examples/aws-fastapi/sst-env.d.ts b/examples/aws-fastapi/sst-env.d.ts deleted file mode 100644 index ee9adf502c..0000000000 --- a/examples/aws-fastapi/sst-env.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -declare module "sst" { - export interface Resource { - "FastAPI": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyLinkableValue": { - "foo": string - "type": "sst.sst.Linkable" - } - } -} -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-fastapi/sst.pyi b/examples/aws-fastapi/sst.pyi deleted file mode 100644 index 18f014ac3c..0000000000 --- a/examples/aws-fastapi/sst.pyi +++ /dev/null @@ -1,17 +0,0 @@ -# Automatically generated by SST -# pylint: disable=all -# ruff: noqa -from typing import Any - -class Resource: - class App: - name: str - stage: str - class FastAPI: - name: str - type: str - url: str - class MyLinkableValue: - foo: str - type: str - diff --git a/examples/aws-fastapi/uv.lock b/examples/aws-fastapi/uv.lock deleted file mode 100644 index ad5cfd098a..0000000000 --- a/examples/aws-fastapi/uv.lock +++ /dev/null @@ -1,303 +0,0 @@ -version = 1 -requires-python = "==3.11.*" - -[manifest] -members = [ - "aws-fastapi", - "core", - "functions", -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, -] - -[[package]] -name = "anyio" -version = "4.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "sniffio" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, -] - -[[package]] -name = "aws-fastapi" -version = "0.1.0" -source = { virtual = "." } - -[[package]] -name = "certifi" -version = "2024.8.30" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, -] - -[[package]] -name = "cffi" -version = "1.17.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, - { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, - { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, - { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, - { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, - { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, - { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, - { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, - { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, - { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, - { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, - { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, - { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, - { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, - { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, - { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, -] - -[[package]] -name = "core" -version = "0.1.0" -source = { editable = "core" } -dependencies = [ - { name = "requests" }, -] - -[package.metadata] -requires-dist = [{ name = "requests", specifier = ">=2.32.3" }] - -[[package]] -name = "cryptography" -version = "44.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/67/545c79fe50f7af51dbad56d16b23fe33f63ee6a5d956b3cb68ea110cbe64/cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14", size = 710819 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/27/5e3524053b4c8889da65cf7814a9d0d8514a05194a25e1e34f46852ee6eb/cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009", size = 6642022 }, - { url = "https://files.pythonhosted.org/packages/34/b9/4d1fa8d73ae6ec350012f89c3abfbff19fc95fe5420cf972e12a8d182986/cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f", size = 3943865 }, - { url = "https://files.pythonhosted.org/packages/6e/57/371a9f3f3a4500807b5fcd29fec77f418ba27ffc629d88597d0d1049696e/cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2", size = 4162562 }, - { url = "https://files.pythonhosted.org/packages/c5/1d/5b77815e7d9cf1e3166988647f336f87d5634a5ccecec2ffbe08ef8dd481/cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911", size = 3951923 }, - { url = "https://files.pythonhosted.org/packages/28/01/604508cd34a4024467cd4105887cf27da128cba3edd435b54e2395064bfb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69", size = 3685194 }, - { url = "https://files.pythonhosted.org/packages/c6/3d/d3c55d4f1d24580a236a6753902ef6d8aafd04da942a1ee9efb9dc8fd0cb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026", size = 4187790 }, - { url = "https://files.pythonhosted.org/packages/ea/a6/44d63950c8588bfa8594fd234d3d46e93c3841b8e84a066649c566afb972/cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd", size = 3951343 }, - { url = "https://files.pythonhosted.org/packages/c1/17/f5282661b57301204cbf188254c1a0267dbd8b18f76337f0a7ce1038888c/cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0", size = 4187127 }, - { url = "https://files.pythonhosted.org/packages/f3/68/abbae29ed4f9d96596687f3ceea8e233f65c9645fbbec68adb7c756bb85a/cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf", size = 4070666 }, - { url = "https://files.pythonhosted.org/packages/0f/10/cf91691064a9e0a88ae27e31779200b1505d3aee877dbe1e4e0d73b4f155/cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864", size = 4288811 }, - { url = "https://files.pythonhosted.org/packages/38/78/74ea9eb547d13c34e984e07ec8a473eb55b19c1451fe7fc8077c6a4b0548/cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a", size = 2771882 }, - { url = "https://files.pythonhosted.org/packages/cf/6c/3907271ee485679e15c9f5e93eac6aa318f859b0aed8d369afd636fafa87/cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00", size = 3206989 }, - { url = "https://files.pythonhosted.org/packages/9f/f1/676e69c56a9be9fd1bffa9bc3492366901f6e1f8f4079428b05f1414e65c/cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008", size = 6643714 }, - { url = "https://files.pythonhosted.org/packages/ba/9f/1775600eb69e72d8f9931a104120f2667107a0ee478f6ad4fe4001559345/cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862", size = 3943269 }, - { url = "https://files.pythonhosted.org/packages/25/ba/e00d5ad6b58183829615be7f11f55a7b6baa5a06910faabdc9961527ba44/cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3", size = 4166461 }, - { url = "https://files.pythonhosted.org/packages/b3/45/690a02c748d719a95ab08b6e4decb9d81e0ec1bac510358f61624c86e8a3/cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7", size = 3950314 }, - { url = "https://files.pythonhosted.org/packages/e6/50/bf8d090911347f9b75adc20f6f6569ed6ca9b9bff552e6e390f53c2a1233/cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a", size = 3686675 }, - { url = "https://files.pythonhosted.org/packages/e1/e7/cfb18011821cc5f9b21efb3f94f3241e3a658d267a3bf3a0f45543858ed8/cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c", size = 4190429 }, - { url = "https://files.pythonhosted.org/packages/07/ef/77c74d94a8bfc1a8a47b3cafe54af3db537f081742ee7a8a9bd982b62774/cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62", size = 3950039 }, - { url = "https://files.pythonhosted.org/packages/6d/b9/8be0ff57c4592382b77406269b1e15650c9f1a167f9e34941b8515b97159/cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41", size = 4189713 }, - { url = "https://files.pythonhosted.org/packages/78/e1/4b6ac5f4100545513b0847a4d276fe3c7ce0eacfa73e3b5ebd31776816ee/cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b", size = 4071193 }, - { url = "https://files.pythonhosted.org/packages/3d/cb/afff48ceaed15531eab70445abe500f07f8f96af2bb35d98af6bfa89ebd4/cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7", size = 4289566 }, - { url = "https://files.pythonhosted.org/packages/30/6f/4eca9e2e0f13ae459acd1ca7d9f0257ab86e68f44304847610afcb813dc9/cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9", size = 2772371 }, - { url = "https://files.pythonhosted.org/packages/d2/05/5533d30f53f10239616a357f080892026db2d550a40c393d0a8a7af834a9/cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f", size = 3207303 }, -] - -[[package]] -name = "fastapi" -version = "0.115.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a2/b2/5a5dc4affdb6661dea100324e19a7721d5dc524b464fe8e366c093fd7d87/fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9", size = 295403 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/7d/2d6ce181d7a5f51dedb8c06206cbf0ec026a99bf145edd309f9e17c3282f/fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf", size = 94814 }, -] - -[[package]] -name = "functions" -version = "0.1.0" -source = { editable = "functions" } -dependencies = [ - { name = "core" }, - { name = "fastapi" }, - { name = "mangum" }, - { name = "sst" }, -] - -[package.metadata] -requires-dist = [ - { name = "core", editable = "core" }, - { name = "fastapi", specifier = "==0.115.8" }, - { name = "mangum", specifier = "==0.19.0" }, - { name = "sst", git = "https://github.com/sst/sst.git?subdirectory=sdk%2Fpython&branch=dev" }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, -] - -[[package]] -name = "mangum" -version = "0.19.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/97/e0/6ee9bfa27226252a449cba12fc57d3f1c3ce661813377ab33e29245389a4/mangum-0.19.0.tar.gz", hash = "sha256:e388e7c491b7b67970f8234e46fd4a7b21ff87785848f418de08148f71cf0bd6", size = 85792 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/ec/dd1cae5f6b1b4a08c01de587b45e889036b2f8c06408621e0cb273909965/mangum-0.19.0-py3-none-any.whl", hash = "sha256:e500b35f495d5e68ac98bc97334896d6101523f2ee2c57ba6a61893b65266e59", size = 17083 }, -] - -[[package]] -name = "pycparser" -version = "2.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, -] - -[[package]] -name = "pydantic" -version = "2.10.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, -] - -[[package]] -name = "pydantic-core" -version = "2.27.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, - { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, - { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, - { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, - { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, - { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, - { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, - { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, - { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, - { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, - { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, - { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, - { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, - { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, -] - -[[package]] -name = "requests" -version = "2.32.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, -] - -[[package]] -name = "sst" -version = "0.2.0" -source = { git = "https://github.com/sst/sst.git?subdirectory=sdk%2Fpython&branch=dev#e335fd9a3f41ee9cf5e12db5d85fa49e00175cde" } -dependencies = [ - { name = "cryptography" }, -] - -[[package]] -name = "starlette" -version = "0.45.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ff/fb/2984a686808b89a6781526129a4b51266f678b2d2b97ab2d325e56116df8/starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f", size = 2574076 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/61/f2b52e107b1fc8944b33ef56bf6ac4ebbe16d91b94d2b87ce013bf63fb84/starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d", size = 71507 }, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, -] - -[[package]] -name = "urllib3" -version = "2.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, -] diff --git a/examples/aws-ffmpeg/package.json b/examples/aws-ffmpeg/package.json index e5e3c41c33..4bee839366 100644 --- a/examples/aws-ffmpeg/package.json +++ b/examples/aws-ffmpeg/package.json @@ -11,7 +11,7 @@ "license": "ISC", "dependencies": { "ffmpeg-static": "^5.2.0", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "8.10.145" diff --git a/examples/aws-ffmpeg/sst-env.d.ts b/examples/aws-ffmpeg/sst-env.d.ts deleted file mode 100644 index 3c71f13143..0000000000 --- a/examples/aws-ffmpeg/sst-env.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - "MyFunction": { - "name": string - "type": "sst.aws.Function" - "url": string - } - } -} -export {} diff --git a/examples/aws-flutter-web/README.md b/examples/aws-flutter-web/README.md deleted file mode 100644 index e1c1a6814d..0000000000 --- a/examples/aws-flutter-web/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# SST v3 Flutter Web - -This project demonstrates how to deploy a Flutter website to AWS CloudFront using SST v3. - -It's a simple setup where a Flutter-based web application (a demo counter app) is built and deployed directly through SST v3 with minimal configuration. - -## Prerequisites - -Before you begin, ensure you have the following installed: - -- AWS Credentials ([Guide on loading from a file](https://docs.sst.dev/advanced/iam-credentials#loading-from-a-file)) -- SST CLI ([Documentation](https://sst.dev/docs/reference/cli)) - -## Getting Started - -### Step 1: Create the Flutter Project - -Create a new Flutter project: - -```bash -flutter create aws_flutter_web -``` - -### Step 2: Initialize SST v3 - -Navigate to the root folder of your project and initialize SST v3, selecting AWS as your cloud provider: - -```bash -sst init -``` - -### Step 3: Configure the Project - -Edit the `sst.config.ts` file in your project's root directory to set up the deployment settings. Here's an example configuration: - -```typescript -/// - -export default $config({ - app(input) { - return { - name: "aws-flutter-web", - removal: input?.stage === "production" ? "retain" : "remove", - home: "aws", - }; - }, - async run() { - new sst.aws.StaticSite("MySite", { - build: { - command: "flutter build web", - output: "build/web", - }, - }); - }, -}); -``` - -A new instance of `sst.aws.StaticSite` has been added to the `run` method. This construct facilitates the configuration and management of static site deployments on AWS. -`"FlutterSite"` is the name given to this particular static site deployment. - -The build object is configured with key properties to define the build process for the Flutter web application: - -- `command`: The command to build the Flutter web application (`flutter build web`). -- `output`: The directory where the build outputs will be stored, which in this case is `build/web`. - -### Step 4: Deploy to CloudFront - -Deploy your application to CloudFront using the following command: - -```bash -sst deploy --stage production -``` - -Once deployed, your Flutter web application will be accessible through the URL provided by CloudFront. You can interact with your demo counter app directly via this URL. - -### Step 5: Remove - -Eventually remove the website using the following command: - -```bash -sst remove --stage production -``` diff --git a/examples/aws-flutter-web/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/examples/aws-flutter-web/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725b70..0000000000 --- a/examples/aws-flutter-web/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/examples/aws-go-api-gateway-v2/package.json b/examples/aws-go-api-gateway-v2/package.json index 470b8cfcd8..73e5fb2b94 100644 --- a/examples/aws-go-api-gateway-v2/package.json +++ b/examples/aws-go-api-gateway-v2/package.json @@ -10,7 +10,7 @@ "typescript": "^5.0.0" }, "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" }, "scripts": { "dev": "bun sst dev" diff --git a/examples/aws-go-lambda-bucket-presigned-url/package.json b/examples/aws-go-lambda-bucket-presigned-url/package.json index 635ad839c1..4c65464398 100644 --- a/examples/aws-go-lambda-bucket-presigned-url/package.json +++ b/examples/aws-go-lambda-bucket-presigned-url/package.json @@ -10,6 +10,6 @@ "typescript": "^5.0.0" }, "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-go-lambda-dynamo/package.json b/examples/aws-go-lambda-dynamo/package.json index a451d6b76b..d230289740 100644 --- a/examples/aws-go-lambda-dynamo/package.json +++ b/examples/aws-go-lambda-dynamo/package.json @@ -10,7 +10,7 @@ "typescript": "^5.0.0" }, "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" }, "scripts": { "dev": "bun sst dev" diff --git a/examples/aws-hono-container/README.md b/examples/aws-hono-container/README.md deleted file mode 100644 index e12b31db70..0000000000 --- a/examples/aws-hono-container/README.md +++ /dev/null @@ -1,8 +0,0 @@ -``` -npm install -npm run dev -``` - -``` -open http://localhost:3000 -``` diff --git a/examples/aws-hono-container/package.json b/examples/aws-hono-container/package.json index 2c75c4ef5a..565f26f302 100644 --- a/examples/aws-hono-container/package.json +++ b/examples/aws-hono-container/package.json @@ -11,7 +11,7 @@ "@aws-sdk/s3-request-presigner": "^3.701.0", "@hono/node-server": "^1.13.7", "hono": "^4.6.12", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "8.10.146", diff --git a/examples/aws-hono-container/sst-env.d.ts b/examples/aws-hono-container/sst-env.d.ts deleted file mode 100644 index ff1d425c12..0000000000 --- a/examples/aws-hono-container/sst-env.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-hono-redis/README.md b/examples/aws-hono-redis/README.md deleted file mode 100644 index e12b31db70..0000000000 --- a/examples/aws-hono-redis/README.md +++ /dev/null @@ -1,8 +0,0 @@ -``` -npm install -npm run dev -``` - -``` -open http://localhost:3000 -``` diff --git a/examples/aws-hono-redis/package.json b/examples/aws-hono-redis/package.json index d93c46c20a..7048149d5a 100644 --- a/examples/aws-hono-redis/package.json +++ b/examples/aws-hono-redis/package.json @@ -9,7 +9,7 @@ "@hono/node-server": "^1.13.2", "hono": "^4.6.5", "ioredis": "^5.4.1", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "8.10.145", diff --git a/examples/aws-hono-redis/sst-env.d.ts b/examples/aws-hono-redis/sst-env.d.ts deleted file mode 100644 index a089940b53..0000000000 --- a/examples/aws-hono-redis/sst-env.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyRedis": { - "host": string - "password": string - "port": number - "type": "sst.aws.Redis" - "username": string - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "bastion": string - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-hono-stream/index.ts b/examples/aws-hono-stream/index.ts index 3acbd1533c..5a9239a382 100644 --- a/examples/aws-hono-stream/index.ts +++ b/examples/aws-hono-stream/index.ts @@ -1,6 +1,6 @@ import { Hono } from "hono"; import { streamText } from "hono/streaming"; -import { handle, streamHandle } from "hono/aws-lambda"; +import { streamHandle } from "hono/aws-lambda"; const app = new Hono() .get("/", (c) => { @@ -11,4 +11,4 @@ const app = new Hono() }) }); -export const handler = process.env.SST_LIVE ? handle(app) : streamHandle(app); +export const handler = streamHandle(app); diff --git a/examples/aws-hono-stream/package.json b/examples/aws-hono-stream/package.json index d96c6d881d..51c7dd4fe7 100644 --- a/examples/aws-hono-stream/package.json +++ b/examples/aws-hono-stream/package.json @@ -11,6 +11,6 @@ "license": "ISC", "dependencies": { "hono": "^4.6.2", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-hono-stream/sst-env.d.ts b/examples/aws-hono-stream/sst-env.d.ts deleted file mode 100644 index ddd6bc93a3..0000000000 --- a/examples/aws-hono-stream/sst-env.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "Hono": { - "name": string - "type": "sst.aws.Function" - "url": string - } - } -} diff --git a/examples/aws-hono-stream/sst.config.ts b/examples/aws-hono-stream/sst.config.ts index 7cd97e2d2f..005d16ee1a 100644 --- a/examples/aws-hono-stream/sst.config.ts +++ b/examples/aws-hono-stream/sst.config.ts @@ -5,31 +5,23 @@ * * An example on how to enable streaming for Lambda functions using Hono. * - * While `sst dev` doesn't support streaming, we can conditionally enable it on deploy. - * * ```ts title="sst.config.ts" * { - * streaming: $dev ? false : true + * streaming: true * } * ``` * * ```ts title="index.ts" - * export const handler = process.env.SST_LIVE ? handle(app) : streamHandle(app); + * export const handler = streamHandle(app); * ``` * - * This will return the standard handler for `sst dev`. - * - * :::note - * Streaming is currently not supported in `sst dev`. - * ::: - * * To test this in your terminal, use the `curl` command with the `--no-buffer` option. * * ```bash "--no-buffer" * curl --no-buffer https://u3dyblk457ghskwbmzrbylpxoi0ayrbb.lambda-url.us-east-1.on.aws * ``` * - * Here we are using a Function URL directly because API Gateway doesn't support streaming. + * Streaming is also supported through API Gateway REST API. * */ export default $config({ @@ -43,7 +35,7 @@ export default $config({ async run() { const hono = new sst.aws.Function("Hono", { url: true, - streaming: $dev ? false : true, + streaming: true, timeout: "15 minutes", handler: "index.handler", }); diff --git a/examples/aws-hono/README.md b/examples/aws-hono/README.md deleted file mode 100644 index 5bb2accb27..0000000000 --- a/examples/aws-hono/README.md +++ /dev/null @@ -1,4 +0,0 @@ -``` -npm install -npm run deploy -``` diff --git a/examples/aws-hono/package.json b/examples/aws-hono/package.json index 064c2fcd12..6ed2c8e0c7 100644 --- a/examples/aws-hono/package.json +++ b/examples/aws-hono/package.json @@ -16,6 +16,6 @@ "@aws-sdk/client-s3": "^3.701.0", "@aws-sdk/s3-request-presigner": "^3.701.0", "hono": "^4.6.12", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-hono/sst-env.d.ts b/examples/aws-hono/sst-env.d.ts deleted file mode 100644 index c1b62bdd43..0000000000 --- a/examples/aws-hono/sst-env.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -declare module "sst" { - export interface Resource { - } -} -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-hono/sst.config.ts b/examples/aws-hono/sst.config.ts index eca8db4add..2c9f1db7b1 100644 --- a/examples/aws-hono/sst.config.ts +++ b/examples/aws-hono/sst.config.ts @@ -5,6 +5,7 @@ export default $config({ return { name: "aws-hono", removal: input?.stage === "production" ? "retain" : "remove", + protect: input?.stage === "production", home: "aws", }; }, diff --git a/examples/aws-import/package.json b/examples/aws-import/package.json index 28ed2be3ef..62f02b1660 100644 --- a/examples/aws-import/package.json +++ b/examples/aws-import/package.json @@ -2,6 +2,6 @@ "name": "aws-import", "version": "0.0.0", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-import/sst-env.d.ts b/examples/aws-import/sst-env.d.ts deleted file mode 100644 index f110f33043..0000000000 --- a/examples/aws-import/sst-env.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - } -} -export {} diff --git a/examples/aws-info/package.json b/examples/aws-info/package.json new file mode 100644 index 0000000000..9802a5872a --- /dev/null +++ b/examples/aws-info/package.json @@ -0,0 +1,7 @@ +{ + "name": "aws-info", + "version": "0.0.0", + "dependencies": { + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-info/sst.config.ts b/examples/aws-info/sst.config.ts index 7c9aac7c7a..3c09c1ee88 100644 --- a/examples/aws-info/sst.config.ts +++ b/examples/aws-info/sst.config.ts @@ -17,7 +17,7 @@ export default $config({ }, async run() { return { - region: aws.getRegionOutput().name, + region: aws.getRegionOutput().region, account: aws.getCallerIdentityOutput({}).accountId, }; }, diff --git a/examples/aws-jsx-email/README.md b/examples/aws-jsx-email/README.md deleted file mode 100644 index a41fc0ef75..0000000000 --- a/examples/aws-jsx-email/README.md +++ /dev/null @@ -1 +0,0 @@ -# aws-jsx-email diff --git a/examples/aws-jsx-email/package.json b/examples/aws-jsx-email/package.json index b6270ef12b..25b27b978c 100644 --- a/examples/aws-jsx-email/package.json +++ b/examples/aws-jsx-email/package.json @@ -11,7 +11,7 @@ "dependencies": { "@aws-sdk/client-sesv2": "^3.651.1", "jsx-email": "^1.10.11", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "8.10.145", diff --git a/examples/aws-jsx-email/sst-env.d.ts b/examples/aws-jsx-email/sst-env.d.ts deleted file mode 100644 index 04d80476f6..0000000000 --- a/examples/aws-jsx-email/sst-env.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyApi": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyEmail": { - "sender": string - "type": "sst.aws.Email" - } - } -} diff --git a/examples/aws-kinesis-stream/package.json b/examples/aws-kinesis-stream/package.json index 9a8e188382..5cf5cc605b 100644 --- a/examples/aws-kinesis-stream/package.json +++ b/examples/aws-kinesis-stream/package.json @@ -13,7 +13,7 @@ "author": "", "license": "ISC", "devDependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" }, "dependencies": { "@aws-sdk/client-kinesis": "^3.598.0" diff --git a/examples/aws-lambda-ai-stream/index.ts b/examples/aws-lambda-ai-stream/index.ts new file mode 100644 index 0000000000..07cbaf48d0 --- /dev/null +++ b/examples/aws-lambda-ai-stream/index.ts @@ -0,0 +1,15 @@ +import { streamText } from 'ai'; + +export const handler = awslambda.streamifyResponse(async (_event, responseStream) => { + const result = streamText({ + model: 'amazon/nova-micro', + prompt: 'Write a poem about clouds that is twenty paragraphs long.', + }); + + responseStream.setContentType('text/plain'); + for await (const chunk of result.textStream) { + responseStream.write(chunk); + process.stdout.write(chunk); + } + responseStream.end(); +}); diff --git a/examples/aws-lambda-ai-stream/package.json b/examples/aws-lambda-ai-stream/package.json new file mode 100644 index 0000000000..dbb40556da --- /dev/null +++ b/examples/aws-lambda-ai-stream/package.json @@ -0,0 +1,11 @@ +{ + "name": "aws-lambda-ai-stream", + "type": "module", + "dependencies": { + "ai": "^6.0.116", + "sst": "file:../../sdk/js" + }, + "devDependencies": { + "@types/aws-lambda": "8.10.145" + } +} diff --git a/examples/aws-lambda-ai-stream/sst.config.ts b/examples/aws-lambda-ai-stream/sst.config.ts new file mode 100644 index 0000000000..97e6c3feb7 --- /dev/null +++ b/examples/aws-lambda-ai-stream/sst.config.ts @@ -0,0 +1,79 @@ +/// + +/** + * ## AWS Lambda AI streaming + * + * An example on how to stream AI responses from a Lambda function using the + * [AI SDK](https://ai-sdk.dev). + * + * Uses `streamText` from the AI SDK to stream a response + * through a Lambda function URL. + * + * ```ts title="sst.config.ts" + * { + * streaming: true + * } + * ``` + * + * The handler uses `awslambda.streamifyResponse` to stream the AI response + * back to the client as it's generated. + * + * ```ts title="index.ts" + * export const handler = awslambda.streamifyResponse( + * async (_event, responseStream) => { + * const result = streamText({ + * model: "amazon/nova-micro", + * prompt: "Write a poem about clouds that is twenty paragraphs long.", + * }); + * + * responseStream.setContentType("text/plain"); + * for await (const chunk of result.textStream) { + * responseStream.write(chunk); + * } + * responseStream.end(); + * }, + * ); + * ``` + * + * Set the API key for the AI gateway. + * + * ```bash + * sst secret set AiGatewayApiKey your-api-key-here + * ``` + * + * Use the "Run Client" dev command in the multiplexer to invoke the server and see + * the streamed response. + * + */ +export default $config({ + app(input) { + return { + name: "aws-lambda-ai-stream", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + }; + }, + async run() { + const server = new sst.aws.Function("Server", { + url: true, + streaming: true, + timeout: "15 minutes", + handler: "index.handler", + environment: { + AI_GATEWAY_API_KEY: new sst.Secret("AiGatewayApiKey").value, + }, + }); + + new sst.x.DevCommand("Client", { + dev: { + autostart: false, + command: $interpolate`curl --no-buffer ${server.url}`, + title: "Run Client", + }, + }); + + return { + url: server.url, + }; + }, +}); diff --git a/examples/aws-lambda-cron/cron.ts b/examples/aws-lambda-cron/cron.ts new file mode 100644 index 0000000000..8fabb78be5 --- /dev/null +++ b/examples/aws-lambda-cron/cron.ts @@ -0,0 +1,4 @@ +export async function handler(event: any) { + console.log("Cron triggered", JSON.stringify(event)); + return { statusCode: 200 }; +} diff --git a/examples/aws-lambda-cron/package.json b/examples/aws-lambda-cron/package.json new file mode 100644 index 0000000000..1725914407 --- /dev/null +++ b/examples/aws-lambda-cron/package.json @@ -0,0 +1,7 @@ +{ + "name": "aws-cron-lambda", + "private": true, + "dependencies": { + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-lambda-cron/sst.config.ts b/examples/aws-lambda-cron/sst.config.ts new file mode 100644 index 0000000000..f3fc0b6ccc --- /dev/null +++ b/examples/aws-lambda-cron/sst.config.ts @@ -0,0 +1,27 @@ +/// + +export default $config({ + app(input) { + return { + name: "aws-lambda-cron", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + }; + }, + async run() { + const queue = new sst.aws.Queue("MyDLQ"); + + const cron = new sst.aws.CronV2("MyCron", { + schedule: "rate(1 minute)", + function: "cron.handler", + timezone: "America/New_York", + retries: 3, + dlq: queue.arn, + }); + + return { + function: cron.nodes.function.name, + dlq: queue.arn, + }; + }, +}); diff --git a/examples/aws-lambda-cron/tsconfig.json b/examples/aws-lambda-cron/tsconfig.json new file mode 100644 index 0000000000..6ec1c39406 --- /dev/null +++ b/examples/aws-lambda-cron/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true + } +} diff --git a/examples/aws-lambda-hook/package.json b/examples/aws-lambda-hook/package.json index 922ecb1e00..90bae21e53 100644 --- a/examples/aws-lambda-hook/package.json +++ b/examples/aws-lambda-hook/package.json @@ -11,7 +11,7 @@ "license": "ISC", "type": "commonjs", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "8.10.149" diff --git a/examples/aws-lambda-hook/sst-env.d.ts b/examples/aws-lambda-hook/sst-env.d.ts deleted file mode 100644 index d097ac85cf..0000000000 --- a/examples/aws-lambda-hook/sst-env.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -declare module "sst" { - export interface Resource { - "MyFunction": { - "name": string - "type": "sst.aws.Function" - "url": string - } - } -} -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-lambda-retry-with-queues/package.json b/examples/aws-lambda-retry-with-queues/package.json index 4cb89458f4..64cfdc9882 100644 --- a/examples/aws-lambda-retry-with-queues/package.json +++ b/examples/aws-lambda-retry-with-queues/package.json @@ -7,7 +7,7 @@ "@types/aws-lambda": "8.10.146" }, "dependencies": { - "sst": "^3", + "sst": "file:../../sdk/js", "@aws-sdk/client-lambda": "^3.714.0", "@aws-sdk/client-sqs": "^3.714.0", "zod": "^3.24.1" diff --git a/examples/aws-lambda-retry-with-queues/sst.config.ts b/examples/aws-lambda-retry-with-queues/sst.config.ts index acf1f2daf6..2f912c2147 100644 --- a/examples/aws-lambda-retry-with-queues/sst.config.ts +++ b/examples/aws-lambda-retry-with-queues/sst.config.ts @@ -50,7 +50,7 @@ * { * actions: ["lambda:GetFunction", "lambda:InvokeFunction"], * resources: [ - * $interpolate`arn:aws:lambda:${aws.getRegionOutput().name}:${ + * $interpolate`arn:aws:lambda:${aws.getRegionOutput().region}:${ * aws.getCallerIdentityOutput().accountId * }:function:*`, * ], @@ -199,7 +199,7 @@ export default $config({ { actions: ["lambda:GetFunction", "lambda:InvokeFunction"], resources: [ - $interpolate`arn:aws:lambda:${aws.getRegionOutput().name}:${ + $interpolate`arn:aws:lambda:${aws.getRegionOutput().region}:${ aws.getCallerIdentityOutput().accountId }:function:*`, ], diff --git a/examples/aws-lambda-rust-multiple-binaries/Cargo.lock b/examples/aws-lambda-rust-multiple-binaries/Cargo.lock deleted file mode 100644 index e9effe1c8c..0000000000 --- a/examples/aws-lambda-rust-multiple-binaries/Cargo.lock +++ /dev/null @@ -1,2753 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "anstream" -version = "0.6.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" - -[[package]] -name = "anstyle-parse" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" -dependencies = [ - "anstyle", - "once_cell", - "windows-sys 0.59.0", -] - -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "aws-config" -version = "1.5.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50236e4d60fe8458de90a71c0922c761e41755adf091b1b03de1cef537179915" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sdk-sso", - "aws-sdk-ssooidc", - "aws-sdk-sts", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "hex", - "http 0.2.12", - "ring", - "time", - "tokio", - "tracing", - "url", - "zeroize", -] - -[[package]] -name = "aws-credential-types" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60e8f6b615cb5fc60a98132268508ad104310f0cfb25a1c22eee76efdf9154da" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "zeroize", -] - -[[package]] -name = "aws-runtime" -version = "1.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76dd04d39cc12844c0994f2c9c5a6f5184c22e9188ec1ff723de41910a21dcad" -dependencies = [ - "aws-credential-types", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "http-body 0.4.6", - "once_cell", - "percent-encoding", - "pin-project-lite", - "tracing", - "uuid", -] - -[[package]] -name = "aws-rust-lambda" -version = "0.1.0" -dependencies = [ - "aws-config", - "aws-sdk-s3", - "lambda_runtime", - "serde", - "serde_json", - "sst_sdk", - "tokio", - "uuid", -] - -[[package]] -name = "aws-sdk-s3" -version = "1.73.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3978e0a211bdc5cddecfd91fb468665a662a27fbdaef39ddf36a2a18fef12cb4" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-checksums", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "bytes", - "fastrand", - "hex", - "hmac", - "http 0.2.12", - "http-body 0.4.6", - "lru", - "once_cell", - "percent-encoding", - "regex-lite", - "sha2", - "tracing", - "url", -] - -[[package]] -name = "aws-sdk-sso" -version = "1.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16ff718c9ee45cc1ebd4774a0e086bb80a6ab752b4902edf1c9f56b86ee1f770" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "http 0.2.12", - "once_cell", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-ssooidc" -version = "1.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5183e088715cc135d8d396fdd3bc02f018f0da4c511f53cb8d795b6a31c55809" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "http 0.2.12", - "once_cell", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-sts" -version = "1.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9f944ef032717596639cea4a2118a3a457268ef51bbb5fde9637e54c465da00" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-query", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "http 0.2.12", - "once_cell", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sigv4" -version = "1.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bc5bbd1e4a2648fd8c5982af03935972c24a2f9846b396de661d351ee3ce837" -dependencies = [ - "aws-credential-types", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "crypto-bigint 0.5.5", - "form_urlencoded", - "hex", - "hmac", - "http 0.2.12", - "http 1.2.0", - "once_cell", - "p256", - "percent-encoding", - "ring", - "sha2", - "subtle", - "time", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-smithy-async" -version = "1.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa59d1327d8b5053c54bf2eaae63bf629ba9e904434d0835a28ed3c0ed0a614e" -dependencies = [ - "futures-util", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "aws-smithy-checksums" -version = "0.62.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f45a1c384d7a393026bc5f5c177105aa9fa68e4749653b985707ac27d77295" -dependencies = [ - "aws-smithy-http", - "aws-smithy-types", - "bytes", - "crc32c", - "crc32fast", - "crc64fast-nvme", - "hex", - "http 0.2.12", - "http-body 0.4.6", - "md-5", - "pin-project-lite", - "sha1", - "sha2", - "tracing", -] - -[[package]] -name = "aws-smithy-eventstream" -version = "0.60.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b18559a41e0c909b77625adf2b8c50de480a8041e5e4a3f5f7d177db70abc5a" -dependencies = [ - "aws-smithy-types", - "bytes", - "crc32fast", -] - -[[package]] -name = "aws-smithy-http" -version = "0.60.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7809c27ad8da6a6a68c454e651d4962479e81472aa19ae99e59f9aba1f9713cc" -dependencies = [ - "aws-smithy-eventstream", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "bytes-utils", - "futures-core", - "http 0.2.12", - "http-body 0.4.6", - "once_cell", - "percent-encoding", - "pin-project-lite", - "pin-utils", - "tracing", -] - -[[package]] -name = "aws-smithy-json" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "623a51127f24c30776c8b374295f2df78d92517386f77ba30773f15a30ce1422" -dependencies = [ - "aws-smithy-types", -] - -[[package]] -name = "aws-smithy-query" -version = "0.60.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" -dependencies = [ - "aws-smithy-types", - "urlencoding", -] - -[[package]] -name = "aws-smithy-runtime" -version = "1.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d526a12d9ed61fadefda24abe2e682892ba288c2018bcb38b1b4c111d13f6d92" -dependencies = [ - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "fastrand", - "h2", - "http 0.2.12", - "http-body 0.4.6", - "http-body 1.0.1", - "httparse", - "hyper 0.14.32", - "hyper-rustls", - "once_cell", - "pin-project-lite", - "pin-utils", - "rustls", - "tokio", - "tracing", -] - -[[package]] -name = "aws-smithy-runtime-api" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92165296a47a812b267b4f41032ff8069ab7ff783696d217f0994a0d7ab585cd" -dependencies = [ - "aws-smithy-async", - "aws-smithy-types", - "bytes", - "http 0.2.12", - "http 1.2.0", - "pin-project-lite", - "tokio", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-smithy-types" -version = "1.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7b8a53819e42f10d0821f56da995e1470b199686a1809168db6ca485665f042" -dependencies = [ - "base64-simd", - "bytes", - "bytes-utils", - "futures-core", - "http 0.2.12", - "http 1.2.0", - "http-body 0.4.6", - "http-body 1.0.1", - "http-body-util", - "itoa", - "num-integer", - "pin-project-lite", - "pin-utils", - "ryu", - "serde", - "time", - "tokio", - "tokio-util", -] - -[[package]] -name = "aws-smithy-xml" -version = "0.60.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" -dependencies = [ - "xmlparser", -] - -[[package]] -name = "aws-types" -version = "1.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbd0a668309ec1f66c0f6bda4840dd6d4796ae26d699ebc266d7cc95c6d040f" -dependencies = [ - "aws-credential-types", - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "rustc_version", - "tracing", -] - -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets", -] - -[[package]] -name = "base16ct" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" -dependencies = [ - "outref", - "vsimd", -] - -[[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - -[[package]] -name = "bitflags" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bytes" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" - -[[package]] -name = "bytes-utils" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" -dependencies = [ - "bytes", - "either", -] - -[[package]] -name = "cbindgen" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fce8dd7fcfcbf3a0a87d8f515194b49d6135acab73e18bd380d1d93bb1a15eb" -dependencies = [ - "clap", - "heck", - "indexmap", - "log", - "proc-macro2", - "quote", - "serde", - "serde_json", - "syn", - "tempfile", - "toml", -] - -[[package]] -name = "cc" -version = "1.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755717a7de9ec452bf7f3f1a3099085deabd7f2962b861dae91ecd7a365903d2" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "clap" -version = "4.5.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" -dependencies = [ - "clap_builder", -] - -[[package]] -name = "clap_builder" -version = "4.5.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_lex" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" - -[[package]] -name = "colorchoice" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "crc32c" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47" -dependencies = [ - "rustc_version", -] - -[[package]] -name = "crc32fast" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crc64fast-nvme" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5e2ee08013e3f228d6d2394116c4549a6df77708442c62d887d83f68ef2ee37" -dependencies = [ - "cbindgen", - "crc", -] - -[[package]] -name = "crypto-bigint" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" -dependencies = [ - "generic-array", - "rand_core", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "rand_core", - "subtle", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "rand_core", - "typenum", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "der" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" -dependencies = [ - "const-oid", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ecdsa" -version = "0.14.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" -dependencies = [ - "der", - "elliptic-curve", - "rfc6979", - "signature", -] - -[[package]] -name = "either" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" - -[[package]] -name = "elliptic-curve" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" -dependencies = [ - "base16ct", - "crypto-bigint 0.4.9", - "der", - "digest", - "ff", - "generic-array", - "group", - "pkcs8", - "rand_core", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "errno" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "ff" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" -dependencies = [ - "rand_core", - "subtle", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets", -] - -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "group" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" -dependencies = [ - "ff", - "rand_core", - "subtle", -] - -[[package]] -name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http 1.2.0", -] - -[[package]] -name = "http-body-util" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" -dependencies = [ - "bytes", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "pin-project-lite", -] - -[[package]] -name = "http-serde" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f056c8559e3757392c8d091e796416e4649d8e49e88b8d76df6c002f05027fd" -dependencies = [ - "http 1.2.0", - "serde", -] - -[[package]] -name = "httparse" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "log", - "rustls", - "rustls-native-certs", - "tokio", - "tokio-rustls", -] - -[[package]] -name = "hyper-util" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "hyper 1.6.0", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "idna" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "inout" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" -dependencies = [ - "generic-array", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - -[[package]] -name = "itoa" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" - -[[package]] -name = "lambda_runtime" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed49669d6430292aead991e19bf13153135a884f916e68f32997c951af637ebe" -dependencies = [ - "async-stream", - "base64 0.22.1", - "bytes", - "futures", - "http 1.2.0", - "http-body 1.0.1", - "http-body-util", - "http-serde", - "hyper 1.6.0", - "hyper-util", - "lambda_runtime_api_client", - "pin-project", - "serde", - "serde_json", - "serde_path_to_error", - "tokio", - "tokio-stream", - "tower", - "tower-layer", - "tracing", -] - -[[package]] -name = "lambda_runtime_api_client" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c90a10f094475a34a04da2be11686c4dcfe214d93413162db9ffdff3d3af293a" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.6.0", - "hyper-util", - "tokio", - "tower", - "tower-service", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.169" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "litemap" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" - -[[package]] -name = "log" -version = "0.4.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" - -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown", -] - -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "miniz_oxide" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" -dependencies = [ - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - -[[package]] -name = "p256" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" -dependencies = [ - "ecdsa", - "elliptic-curve", - "sha2", -] - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pin-project" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs8" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "proc-macro2" -version = "1.0.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.15", -] - -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-lite" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" - -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - -[[package]] -name = "rfc6979" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" -dependencies = [ - "crypto-bigint 0.4.9", - "hmac", - "zeroize", -] - -[[package]] -name = "ring" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.15", - "libc", - "spin", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki", - "sct", -] - -[[package]] -name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe", - "rustls-pemfile", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "ryu" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" - -[[package]] -name = "schannel" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "sec1" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" - -[[package]] -name = "serde" -version = "1.0.217" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.217" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.138" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" -dependencies = [ - "itoa", - "serde", -] - -[[package]] -name = "serde_spanned" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" -dependencies = [ - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - -[[package]] -name = "signature" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" -dependencies = [ - "digest", - "rand_core", -] - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "socket2" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "spki" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "sst_sdk" -version = "0.1.0" -dependencies = [ - "aes-gcm", - "base64 0.21.7", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tempfile" -version = "3.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" -dependencies = [ - "cfg-if", - "fastrand", - "getrandom 0.3.1", - "once_cell", - "rustix", - "windows-sys 0.59.0", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thread_local" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "time" -version = "0.3.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tokio" -version = "1.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-macros" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-serde" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" -dependencies = [ - "serde", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" -dependencies = [ - "matchers", - "once_cell", - "regex", - "serde", - "serde_json", - "sharded-slab", - "thread_local", - "tracing", - "tracing-core", - "tracing-serde", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "unicode-ident" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" - -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common", - "subtle", -] - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" -dependencies = [ - "getrandom 0.3.1", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasi" -version = "0.13.3+wasi-0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" -dependencies = [ - "wit-bindgen-rt", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen-rt" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" -dependencies = [ - "bitflags", -] - -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - -[[package]] -name = "xmlparser" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" - -[[package]] -name = "yoke" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerofrom" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" - -[[package]] -name = "zerovec" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/examples/aws-lambda-rust-multiple-binaries/README.md b/examples/aws-lambda-rust-multiple-binaries/README.md deleted file mode 100644 index c1c2ca3952..0000000000 --- a/examples/aws-lambda-rust-multiple-binaries/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# SST Rust support - -SST uses [cargo lambda](https://www.cargo-lambda.info/) to build rust binaries and deploy them on AWS Lambda. You MUST install [cargo](https://rustup.rs/) and [cargo lambda](https://www.cargo-lambda.info/guide/installation.html) and [zig](https://ziglang.org/download/) (a cargo-lambda dependency which is automatically installed with cargo-lambda in most installation methods) on your own before using sst rust lambdas. - -## Setup with Github Actions - -An example to deploy using github actions with `arm64` architecture, feel free to configure as needed - -```yml -name: Deploy Prod - -on: - push: - branches: - - main - -jobs: - deploy-prod: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: pguyot/arm-runner-action@v2 - - - name: use Node.js - uses: actions/setup-node@v3 - with: - node-version: latest - - - name: use pnpm - uses: pnpm/action-setup@v4 - with: - version: latest - - - name: get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: setup pnpm cache - uses: actions/cache@v3 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: use Rust - uses: actions-rs/toolchain@v1 - - - name: use Rust cache - uses: Swatinem/rust-cache@v2 - - - name: use Zig - uses: korandoru/setup-zig@v1 - with: - zig-version: master - - - name: use Cargo Lambda - uses: jaxxstorm/action-install-gh-release@v1.9.0 - with: - repo: cargo-lambda/cargo-lambda - platform: linux - arch: aarch64 # | x86_64 - - - name: pnpm install - run: pnpm install --frozen-lockfile - - - name: sst install providers - run: | - set -euxo pipefail - pnpm sst install - - - name: sst deploy - run: | - set -euxo pipefail - pnpm sst deploy --stage prod - - env: - STAGE: prod - LOG_LEVEL: info - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} -``` diff --git a/examples/aws-lambda-stream/index.ts b/examples/aws-lambda-stream/index.ts index e4f53c1b41..fb8e0b89d2 100644 --- a/examples/aws-lambda-stream/index.ts +++ b/examples/aws-lambda-stream/index.ts @@ -1,19 +1,17 @@ -import { APIGatewayProxyEventV2 } from "aws-lambda"; -import { streamifyResponse, ResponseStream } from "lambda-stream"; +export const handler = awslambda.streamifyResponse( + async (event, stream) => { + stream = awslambda.HttpResponseStream.from(stream, { + statusCode: 200, + headers: { + "Content-Type": "text/plain; charset=UTF-8", + "X-Content-Type-Options": "nosniff", + }, + }); -export const handler = streamifyResponse(myHandler); + stream.write("Hello "); + await new Promise((resolve) => setTimeout(resolve, 3000)); + stream.write("World"); -async function myHandler( - _event: APIGatewayProxyEventV2, - responseStream: ResponseStream -): Promise { - return new Promise((resolve, _reject) => { - responseStream.setContentType('text/plain') - responseStream.write('Hello') - setTimeout(() => { - responseStream.write(' World') - responseStream.end() - resolve() - }, 3000) - }) -} + stream.end(); + }, +); diff --git a/examples/aws-lambda-stream/package.json b/examples/aws-lambda-stream/package.json index 09d9e394c4..bda03c194b 100644 --- a/examples/aws-lambda-stream/package.json +++ b/examples/aws-lambda-stream/package.json @@ -10,10 +10,9 @@ "author": "", "license": "ISC", "dependencies": { - "lambda-stream": "^0.5.0", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { - "@types/aws-lambda": "8.10.145" + "@types/aws-lambda": "^8.10.161" } } diff --git a/examples/aws-lambda-stream/sst-env.d.ts b/examples/aws-lambda-stream/sst-env.d.ts deleted file mode 100644 index 4a24b20b2e..0000000000 --- a/examples/aws-lambda-stream/sst-env.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyFunction": { - "name": string - "type": "sst.aws.Function" - "url": string - } - } -} diff --git a/examples/aws-lambda-stream/sst.config.ts b/examples/aws-lambda-stream/sst.config.ts index ef89d007d7..8bea35f559 100644 --- a/examples/aws-lambda-stream/sst.config.ts +++ b/examples/aws-lambda-stream/sst.config.ts @@ -11,51 +11,31 @@ * } * ``` * - * While `sst dev` doesn't support streaming, you can use the - * [`lambda-stream`](https://github.com/astuyve/lambda-stream) package to test locally. - * - * ```bash - * npm install lambda-stream - * ``` - * - * Then, you can use the `streamifyResponse` function to wrap your handler: + * Use the `awslambda.streamifyResponse` function to wrap your handler. The `awslambda` + * global is provided by the Lambda execution environment at runtime, and SST provides it + * automatically during `sst dev` as well. For TypeScript types, importing from + * `@types/aws-lambda` will augment the global namespace. * * ```ts title="index.ts" - * import { APIGatewayProxyEventV2 } from "aws-lambda"; - * import { streamifyResponse, ResponseStream } from "lambda-stream"; - * - * export const handler = streamifyResponse(myHandler); - * - * async function myHandler( - * _event: APIGatewayProxyEventV2, - * responseStream: ResponseStream - * ): Promise { - * return new Promise((resolve, _reject) => { - * responseStream.setContentType('text/plain') - * responseStream.write('Hello') - * setTimeout(() => { - * responseStream.write(' World') - * responseStream.end() - * resolve() - * }, 3000) - * }) - * } + * export const handler = awslambda.streamifyResponse( + * async (event, stream) => { + * stream = awslambda.HttpResponseStream.from(stream, { + * statusCode: 200, + * headers: { + * "Content-Type": "text/plain; charset=UTF-8", + * "X-Content-Type-Options": "nosniff", + * }, + * }); + * + * stream.write("Hello "); + * await new Promise((resolve) => setTimeout(resolve, 3000)); + * stream.write("World"); + * + * stream.end(); + * }, + * ); * ``` * - * When deployed, this will use the `awslambda.streamifyResponse`. - * - * :::note - * Streaming is currently not supported in `sst dev`. - * ::: - * - * To test this in your terminal, use the `curl` command with the `--no-buffer` option. - * - * ```bash "--no-buffer" - * curl --no-buffer https://u3dyblk457ghskwbmzrbylpxoi0ayrbb.lambda-url.us-east-1.on.aws - * ``` - * - * Here we are using a Function URL directly because API Gateway doesn't support streaming. - * */ export default $config({ app(input) { diff --git a/examples/aws-lambda-trpc-stream/package.json b/examples/aws-lambda-trpc-stream/package.json new file mode 100644 index 0000000000..94bc5f1505 --- /dev/null +++ b/examples/aws-lambda-trpc-stream/package.json @@ -0,0 +1,13 @@ +{ + "name": "aws-lambda-trpc-stream", + "type": "module", + "dependencies": { + "@trpc/client": "^11.11.0", + "@trpc/server": "^11.11.0", + "sst": "file:../../sdk/js", + "zod": "^4.3.6" + }, + "devDependencies": { + "@types/aws-lambda": "8.10.145" + } +} diff --git a/examples/aws-lambda-trpc-stream/sst.config.ts b/examples/aws-lambda-trpc-stream/sst.config.ts new file mode 100644 index 0000000000..0d59f8a635 --- /dev/null +++ b/examples/aws-lambda-trpc-stream/sst.config.ts @@ -0,0 +1,48 @@ +/// + +/** + * ## AWS Lambda tRPC streaming + * + * An example on how to use tRPC with Lambda streaming. + * + * Uses `@trpc/server`'s `awsLambdaStreamingRequestHandler` adapter to handle + * streaming responses through Lambda function URLs. + * + * The `trpc-server` function defines a tRPC router and streams responses. + * The `trpc-client` function invokes the server using `httpBatchStreamLink`. + * + * Streaming is supported in both `sst dev` and `sst deploy`. + * + */ +export default $config({ + app(input) { + return { + name: 'aws-lambda-trpc-stream', + removal: input?.stage === 'production' ? 'retain' : 'remove', + home: 'aws', + }; + }, + async run() { + const trpcServer = new sst.aws.Function('TrpcServer', { + handler: 'trpc-server.handler', + streaming: true, + url: true, + runtime: 'nodejs24.x', + }); + + new sst.x.DevCommand('Client', { + dev: { + autostart: false, + command: $interpolate`npx tsx trpc-client.ts`, + title: 'Run Client', + }, + environment: { + TRPC_SERVER_URL: trpcServer.url, + }, + }); + + return { + serverUrl: trpcServer.url, + }; + }, +}); diff --git a/examples/aws-lambda-trpc-stream/trpc-client.ts b/examples/aws-lambda-trpc-stream/trpc-client.ts new file mode 100644 index 0000000000..aec7000b10 --- /dev/null +++ b/examples/aws-lambda-trpc-stream/trpc-client.ts @@ -0,0 +1,40 @@ +import { + createTRPCClient, + httpBatchStreamLink, + TRPCClientError, +} from "@trpc/client"; +import { ApiRouter } from "./trpc-server"; + +const client = createTRPCClient({ + links: [ + httpBatchStreamLink({ + url: process.env.TRPC_SERVER_URL, + methodOverride: "POST", + }), + ], +}); + +export const main = async () => { + await Promise.all( + Array.from({ length: 10 }, async (_, idx) => { + const delay = Math.random() * 8_000; + console.log("sending request", idx); + try { + const res = await client.getById.query({ delay, idx }); + console.log("got response", res.idx); + } catch (err) { + if (isTrpcError(err)) { + console.error("received error", idx, err.shape.message); + } else { + console.error("received unexpecte error", idx); + } + } + }), + ); +}; + +const isTrpcError = (err: unknown): err is TRPCClientError => { + return !!err && err instanceof TRPCClientError; +}; + +main().catch(console.error); diff --git a/examples/aws-lambda-trpc-stream/trpc-server.ts b/examples/aws-lambda-trpc-stream/trpc-server.ts new file mode 100644 index 0000000000..6fb341e390 --- /dev/null +++ b/examples/aws-lambda-trpc-stream/trpc-server.ts @@ -0,0 +1,36 @@ +import { initTRPC, TRPCError } from '@trpc/server'; +import { awsLambdaStreamingRequestHandler } from '@trpc/server/adapters/aws-lambda'; +import z from 'zod'; + +const { router, procedure } = initTRPC.create(); + +const apiRouter = router({ + getById: procedure + .input( + z.object({ + delay: z.number(), + idx: z.number(), + }), + ) + .query(async ({ input }) => { + await new Promise((r) => setTimeout(r, input.delay)); + if (Math.random() < 0.5) { + console.log('sending response', input.delay); + return { id: Math.random(), idx: input.idx }; + } + console.log('sending error', input.idx); + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Random failure', + }); + }), +}); + +export type ApiRouter = typeof apiRouter; + +export const handler = awslambda.streamifyResponse( + awsLambdaStreamingRequestHandler({ + router: apiRouter, + allowMethodOverride: true, + }), +); diff --git a/examples/aws-lambda-vpc/package.json b/examples/aws-lambda-vpc/package.json index 9271e7a7c5..38b2e18d93 100644 --- a/examples/aws-lambda-vpc/package.json +++ b/examples/aws-lambda-vpc/package.json @@ -11,7 +11,7 @@ "license": "ISC", "dependencies": { "ioredis": "^5.4.1", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "8.10.145" diff --git a/examples/aws-lambda-vpc/sst-env.d.ts b/examples/aws-lambda-vpc/sst-env.d.ts deleted file mode 100644 index 22ad0aad58..0000000000 --- a/examples/aws-lambda-vpc/sst-env.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyFunction": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyRedis": { - "host": string - "password": string - "port": number - "type": "sst.aws.Redis" - "username": string - } - "MyVpc": { - "bastion": string - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/hyperdrive/.gitignore b/examples/aws-linkable-env/.gitignore similarity index 100% rename from examples/hyperdrive/.gitignore rename to examples/aws-linkable-env/.gitignore diff --git a/examples/aws-linkable-env/package.json b/examples/aws-linkable-env/package.json new file mode 100644 index 0000000000..9febdd34d5 --- /dev/null +++ b/examples/aws-linkable-env/package.json @@ -0,0 +1,7 @@ +{ + "name": "aws-linkable-env", + "version": "0.0.0", + "dependencies": { + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-linkable-env/sst.config.ts b/examples/aws-linkable-env/sst.config.ts new file mode 100644 index 0000000000..20acdfcd00 --- /dev/null +++ b/examples/aws-linkable-env/sst.config.ts @@ -0,0 +1,70 @@ +/// + +/** + * ## Linkable env vars + * + * Pass SST link env vars to a native `aws.ecs.TaskDefinition` container using + * `sst.Linkable.env()`. This lets `Resource.MyResource` work at runtime + * in compute not managed by SST. + * + */ +export default $config({ + app(input) { + return { + name: "aws-linkable-env", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + }; + }, + async run() { + // Create an SST bucket + const bucket = new sst.aws.Bucket("MyBucket"); + + // Create a custom linkable + const linkable = new sst.Linkable("MyLinkable", { + properties: { + foo: "bar", + }, + }); + + // Create VPC and ECS cluster using native AWS resources + const vpc = new aws.ec2.Vpc("Vpc", { cidrBlock: "10.0.0.0/16" }); + const subnet = new aws.ec2.Subnet("Subnet", { + vpcId: vpc.id, + cidrBlock: "10.0.0.0/24", + }); + const cluster = new aws.ecs.Cluster("Cluster"); + + // Linkable.env() returns a Record, but ECS expects + // environment as an array of { name, value } objects + const environment = sst.Linkable.env([bucket, linkable]).apply((env) => + Object.entries(env).map(([name, value]) => ({ name, value })), + ); + + const taskDefinition = new aws.ecs.TaskDefinition("TaskDefinition", { + family: $interpolate`${$app.name}-${$app.stage}`, + cpu: "256", + memory: "512", + networkMode: "awsvpc", + requiresCompatibilities: ["FARGATE"], + containerDefinitions: $jsonStringify([ + { + name: "app", + image: "public.ecr.aws/docker/library/node:20-slim", + essential: true, + environment, + }, + ]), + }); + + new aws.ecs.Service("Service", { + cluster: cluster.arn, + taskDefinition: taskDefinition.arn, + desiredCount: 0, + launchType: "FARGATE", + networkConfiguration: { + subnets: [subnet.id], + }, + }); + }, +}); diff --git a/examples/in-progress/aws-drizzle/tsconfig.json b/examples/aws-linkable-env/tsconfig.json similarity index 100% rename from examples/in-progress/aws-drizzle/tsconfig.json rename to examples/aws-linkable-env/tsconfig.json diff --git a/examples/aws-linkable/bun.lockb b/examples/aws-linkable/bun.lockb deleted file mode 100755 index 37e8a5aa60..0000000000 Binary files a/examples/aws-linkable/bun.lockb and /dev/null differ diff --git a/examples/aws-linkable/package.json b/examples/aws-linkable/package.json index c3f212b5c1..ccedf4fe7b 100644 --- a/examples/aws-linkable/package.json +++ b/examples/aws-linkable/package.json @@ -2,6 +2,6 @@ "name": "aws-linkable", "version": "0.0.0", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-linkable/sst-env.d.ts b/examples/aws-linkable/sst-env.d.ts deleted file mode 100644 index 89dd6ae08b..0000000000 --- a/examples/aws-linkable/sst-env.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - ExistingResources: { - bucketName: string - queueName: string - type: "sst.sst.Linkable" - } - Function: { - name: string - type: "sst.aws.Function" - url: string - } - Topic: { - arn: string - name: string - type: "aws.sns/topic.Topic" - } - } -} -export {} diff --git a/examples/aws-load-balancer-waf/package.json b/examples/aws-load-balancer-waf/package.json new file mode 100644 index 0000000000..668890f41c --- /dev/null +++ b/examples/aws-load-balancer-waf/package.json @@ -0,0 +1,7 @@ +{ + "name": "aws-load-balancer-waf", + "version": "0.0.0", + "dependencies": { + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-monorepo/.env.thdxr b/examples/aws-monorepo/.env.thdxr deleted file mode 100644 index 1bf6d8a1b4..0000000000 --- a/examples/aws-monorepo/.env.thdxr +++ /dev/null @@ -1 +0,0 @@ -BRO=1 diff --git a/examples/aws-monorepo/bun.lockb b/examples/aws-monorepo/bun.lockb deleted file mode 100755 index 953813e3f3..0000000000 Binary files a/examples/aws-monorepo/bun.lockb and /dev/null differ diff --git a/examples/aws-monorepo/package.json b/examples/aws-monorepo/package.json index b9f4421389..9f1076e6a4 100644 --- a/examples/aws-monorepo/package.json +++ b/examples/aws-monorepo/package.json @@ -12,6 +12,6 @@ "typescript": "5.3.3" }, "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-monorepo/packages/astro/README.md b/examples/aws-monorepo/packages/astro/README.md deleted file mode 100644 index 1db3fb3991..0000000000 --- a/examples/aws-monorepo/packages/astro/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# Astro Starter Kit: Basics - -```sh -npm create astro@latest -- --template basics -``` - -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) -[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) - -> πŸ§‘β€πŸš€ **Seasoned astronaut?** Delete this file. Have fun! - -![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) - -## πŸš€ Project Structure - -Inside of your Astro project, you'll see the following folders and files: - -```text -/ -β”œβ”€β”€ public/ -β”‚ └── favicon.svg -β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ components/ -β”‚ β”‚ └── Card.astro -β”‚ β”œβ”€β”€ layouts/ -β”‚ β”‚ └── Layout.astro -β”‚ └── pages/ -β”‚ └── index.astro -└── package.json -``` - -Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. - -There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. - -Any static assets, like images, can be placed in the `public/` directory. - -## 🧞 Commands - -All commands are run from the root of the project, from a terminal: - -| Command | Action | -| :------------------------ | :----------------------------------------------- | -| `npm install` | Installs dependencies | -| `npm run dev` | Starts local dev server at `localhost:4321` | -| `npm run build` | Build your production site to `./dist/` | -| `npm run preview` | Preview your build locally, before deploying | -| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | -| `npm run astro -- --help` | Get help using the Astro CLI | - -## πŸ‘€ Want to learn more? - -Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). diff --git a/examples/aws-monorepo/packages/astro/package.json b/examples/aws-monorepo/packages/astro/package.json index 140fb22e6b..677f362509 100644 --- a/examples/aws-monorepo/packages/astro/package.json +++ b/examples/aws-monorepo/packages/astro/package.json @@ -15,7 +15,7 @@ "@aws-sdk/s3-request-presigner": "^3.540.0", "astro": "^4.5.9", "astro-sst": "^2.41.2", - "sst": "^3", + "sst": "file:../../../../sdk/js", "typescript": "^5.4.3" } } diff --git a/examples/aws-monorepo/packages/astro/sst-env.d.ts b/examples/aws-monorepo/packages/astro/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/aws-monorepo/packages/astro/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-monorepo/packages/core/package.json b/examples/aws-monorepo/packages/core/package.json index 21280abec8..bfd1c917de 100644 --- a/examples/aws-monorepo/packages/core/package.json +++ b/examples/aws-monorepo/packages/core/package.json @@ -8,6 +8,6 @@ ] }, "devDependencies": { - "sst": "^3" + "sst": "file:../../../../sdk/js" } } diff --git a/examples/aws-monorepo/packages/core/src/example/index.ts b/examples/aws-monorepo/packages/core/src/example/index.ts index 53b1d6dd50..9cb1c03477 100644 --- a/examples/aws-monorepo/packages/core/src/example/index.ts +++ b/examples/aws-monorepo/packages/core/src/example/index.ts @@ -1,4 +1,4 @@ -export module Example { +export namespace Example { export function hello() { return "Hello, world!"; } diff --git a/examples/aws-monorepo/packages/core/sst-env.d.ts b/examples/aws-monorepo/packages/core/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/aws-monorepo/packages/core/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-monorepo/packages/frontend/package.json b/examples/aws-monorepo/packages/frontend/package.json index f7fbc12703..6a6424629f 100644 --- a/examples/aws-monorepo/packages/frontend/package.json +++ b/examples/aws-monorepo/packages/frontend/package.json @@ -8,6 +8,6 @@ }, "devDependencies": { "vite": "^5.2.8", - "sst": "^3" + "sst": "file:../../../../sdk/js" } } diff --git a/examples/aws-monorepo/packages/frontend/src/sst-env.d.ts b/examples/aws-monorepo/packages/frontend/src/sst-env.d.ts deleted file mode 100644 index 4addc8ef99..0000000000 --- a/examples/aws-monorepo/packages/frontend/src/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/// -interface ImportMetaEnv { - readonly VITE_API_URL: string -} -interface ImportMeta { - readonly env: ImportMetaEnv -} \ No newline at end of file diff --git a/examples/aws-monorepo/packages/frontend/sst-env.d.ts b/examples/aws-monorepo/packages/frontend/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/aws-monorepo/packages/frontend/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-monorepo/packages/functions/package.json b/examples/aws-monorepo/packages/functions/package.json index 965d615308..9fd4eb3af9 100644 --- a/examples/aws-monorepo/packages/functions/package.json +++ b/examples/aws-monorepo/packages/functions/package.json @@ -3,10 +3,10 @@ "version": "0.0.0", "dependencies": { "@aws-monorepo/core": "*", - "sst": "^3" + "sst": "file:../../../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "^8.10.137", - "sst": "^3" + "sst": "file:../../../../sdk/js" } } diff --git a/examples/aws-monorepo/packages/functions/sst-env.d.ts b/examples/aws-monorepo/packages/functions/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/aws-monorepo/packages/functions/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-monorepo/packages/scripts/package.json b/examples/aws-monorepo/packages/scripts/package.json index c5d9268a7d..c8b78ff82a 100644 --- a/examples/aws-monorepo/packages/scripts/package.json +++ b/examples/aws-monorepo/packages/scripts/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "dependencies": { "@aws-monorepo/core": "*", - "sst": "^3" + "sst": "file:../../../../sdk/js" }, "scripts": { "shell": "sst shell tsx" diff --git a/examples/aws-monorepo/packages/scripts/sst-env.d.ts b/examples/aws-monorepo/packages/scripts/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/aws-monorepo/packages/scripts/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-monorepo/sst-env.d.ts b/examples/aws-monorepo/sst-env.d.ts deleted file mode 100644 index 1738812bf8..0000000000 --- a/examples/aws-monorepo/sst-env.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -declare module "sst" { - export interface Resource { - "Api": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "Astro": { - "type": "sst.aws.Astro" - "url": string - } - "Database": { - "name": string - "type": "sst.aws.Dynamo" - } - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "StaticSite": { - "type": "sst.aws.StaticSite" - "url": string - } - } -} -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-multi-region/package.json b/examples/aws-multi-region/package.json index bf56f650f1..b2370c5519 100644 --- a/examples/aws-multi-region/package.json +++ b/examples/aws-multi-region/package.json @@ -10,7 +10,7 @@ "author": "", "license": "ISC", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "^8.10.145", diff --git a/examples/aws-multi-region/sst-env.d.ts b/examples/aws-multi-region/sst-env.d.ts deleted file mode 100644 index 5ff61f5628..0000000000 --- a/examples/aws-multi-region/sst-env.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyEastFunction": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyWestFunction": { - "name": string - "type": "sst.aws.Function" - "url": string - } - } -} diff --git a/examples/aws-mysql-local/package.json b/examples/aws-mysql-local/package.json index 74788b8706..10eb83dc38 100644 --- a/examples/aws-mysql-local/package.json +++ b/examples/aws-mysql-local/package.json @@ -11,7 +11,7 @@ "license": "ISC", "dependencies": { "mysql2": "3.14.0", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "8.10.145" diff --git a/examples/aws-mysql/package.json b/examples/aws-mysql/package.json index c2788572c7..cdf175cd3c 100644 --- a/examples/aws-mysql/package.json +++ b/examples/aws-mysql/package.json @@ -11,6 +11,6 @@ "description": "", "dependencies": { "mysql2": "3.14.0", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-nestjs-container/README.md b/examples/aws-nestjs-container/README.md deleted file mode 100644 index c17103c312..0000000000 --- a/examples/aws-nestjs-container/README.md +++ /dev/null @@ -1,99 +0,0 @@ -

- Nest Logo -

- -[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 -[circleci-url]: https://circleci.com/gh/nestjs/nest - -

A progressive Node.js framework for building efficient and scalable server-side applications.

-

-NPM Version -Package License -NPM Downloads -CircleCI -Coverage -Discord -Backers on Open Collective -Sponsors on Open Collective - Donate us - Support us - Follow us on Twitter -

- - -## Description - -[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. - -## Project setup - -```bash -$ npm install -``` - -## Compile and run the project - -```bash -# development -$ npm run start - -# watch mode -$ npm run start:dev - -# production mode -$ npm run start:prod -``` - -## Run tests - -```bash -# unit tests -$ npm run test - -# e2e tests -$ npm run test:e2e - -# test coverage -$ npm run test:cov -``` - -## Deployment - -When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information. - -If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps: - -```bash -$ npm install -g mau -$ mau deploy -``` - -With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure. - -## Resources - -Check out a few resources that may come in handy when working with NestJS: - -- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework. -- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy). -- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/). -- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks. -- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com). -- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com). -- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs). -- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com). - -## Support - -Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). - -## Stay in touch - -- Author - [Kamil MyΕ›liwiec](https://twitter.com/kammysliwiec) -- Website - [https://nestjs.com](https://nestjs.com/) -- Twitter - [@nestframework](https://twitter.com/nestframework) - -## License - -Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE). diff --git a/examples/aws-nestjs-container/package.json b/examples/aws-nestjs-container/package.json index c0e45874c7..0beae8372e 100644 --- a/examples/aws-nestjs-container/package.json +++ b/examples/aws-nestjs-container/package.json @@ -28,7 +28,7 @@ "@nestjs/platform-express": "^10.0.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@nestjs/cli": "^10.0.0", diff --git a/examples/aws-nestjs-container/sst-env.d.ts b/examples/aws-nestjs-container/sst-env.d.ts deleted file mode 100644 index ff1d425c12..0000000000 --- a/examples/aws-nestjs-container/sst-env.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-nestjs-redis/README.md b/examples/aws-nestjs-redis/README.md deleted file mode 100644 index c997880623..0000000000 --- a/examples/aws-nestjs-redis/README.md +++ /dev/null @@ -1,85 +0,0 @@ -

- Nest Logo -

- -[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 -[circleci-url]: https://circleci.com/gh/nestjs/nest - -

A progressive Node.js framework for building efficient and scalable server-side applications.

-

-NPM Version -Package License -NPM Downloads -CircleCI -Coverage -Discord -Backers on Open Collective -Sponsors on Open Collective - Donate us - Support us - Follow us on Twitter -

- - -## Description - -[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. - -## Project setup - -```bash -$ npm install -``` - -## Compile and run the project - -```bash -# development -$ npm run start - -# watch mode -$ npm run start:dev - -# production mode -$ npm run start:prod -``` - -## Run tests - -```bash -# unit tests -$ npm run test - -# e2e tests -$ npm run test:e2e - -# test coverage -$ npm run test:cov -``` - -## Resources - -Check out a few resources that may come in handy when working with NestJS: - -- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework. -- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy). -- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/). -- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com). -- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com). -- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs). -- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com). - -## Support - -Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). - -## Stay in touch - -- Author - [Kamil MyΕ›liwiec](https://twitter.com/kammysliwiec) -- Website - [https://nestjs.com](https://nestjs.com/) -- Twitter - [@nestframework](https://twitter.com/nestframework) - -## License - -Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE). diff --git a/examples/aws-nestjs-redis/package.json b/examples/aws-nestjs-redis/package.json index c9a4763393..83fae61d50 100644 --- a/examples/aws-nestjs-redis/package.json +++ b/examples/aws-nestjs-redis/package.json @@ -26,7 +26,7 @@ "ioredis": "^5.4.1", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@nestjs/cli": "^10.0.0", diff --git a/examples/aws-nestjs-redis/sst-env.d.ts b/examples/aws-nestjs-redis/sst-env.d.ts deleted file mode 100644 index e973cf25d9..0000000000 --- a/examples/aws-nestjs-redis/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - } -} diff --git a/examples/aws-nextjs-add-behavior/README.md b/examples/aws-nextjs-add-behavior/README.md deleted file mode 100644 index e215bc4ccf..0000000000 --- a/examples/aws-nextjs-add-behavior/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/examples/aws-nextjs-add-behavior/package.json b/examples/aws-nextjs-add-behavior/package.json index ab80cf0b23..aae5dbed9e 100644 --- a/examples/aws-nextjs-add-behavior/package.json +++ b/examples/aws-nextjs-add-behavior/package.json @@ -12,7 +12,7 @@ "next": "15.0.1", "react": "19.0.0-rc-69d4b800-20241021", "react-dom": "19.0.0-rc-69d4b800-20241021", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/node": "^20", diff --git a/examples/aws-nextjs-add-behavior/sst-env.d.ts b/examples/aws-nextjs-add-behavior/sst-env.d.ts deleted file mode 100644 index 694280d7bc..0000000000 --- a/examples/aws-nextjs-add-behavior/sst-env.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyWeb": { - "type": "sst.aws.Nextjs" - "url": string - } - } -} diff --git a/examples/aws-nextjs-basic-auth/README.md b/examples/aws-nextjs-basic-auth/README.md deleted file mode 100644 index e215bc4ccf..0000000000 --- a/examples/aws-nextjs-basic-auth/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/examples/aws-nextjs-basic-auth/package.json b/examples/aws-nextjs-basic-auth/package.json index f1c9fde8c4..f9239740de 100644 --- a/examples/aws-nextjs-basic-auth/package.json +++ b/examples/aws-nextjs-basic-auth/package.json @@ -12,7 +12,7 @@ "next": "14.2.9", "react": "^18", "react-dom": "^18", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/node": "^20", diff --git a/examples/aws-nextjs-basic-auth/sst-env.d.ts b/examples/aws-nextjs-basic-auth/sst-env.d.ts deleted file mode 100644 index 9da7e45271..0000000000 --- a/examples/aws-nextjs-basic-auth/sst-env.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - "MyWeb": { - "type": "sst.aws.Nextjs" - "url": string - } - "PASSWORD": { - "type": "sst.sst.Secret" - "value": string - } - "USERNAME": { - "type": "sst.sst.Secret" - "value": string - } - } -} -export {} diff --git a/examples/aws-nextjs-container/README.md b/examples/aws-nextjs-container/README.md deleted file mode 100644 index e215bc4ccf..0000000000 --- a/examples/aws-nextjs-container/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/examples/aws-nextjs-container/package.json b/examples/aws-nextjs-container/package.json index 47786843f7..9e89772fb6 100644 --- a/examples/aws-nextjs-container/package.json +++ b/examples/aws-nextjs-container/package.json @@ -14,7 +14,7 @@ "next": "15.0.3", "react": "19.0.0-rc-66855b96-20241106", "react-dom": "19.0.0-rc-66855b96-20241106", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/node": "^20", diff --git a/examples/aws-nextjs-container/sst-env.d.ts b/examples/aws-nextjs-container/sst-env.d.ts deleted file mode 100644 index ff1d425c12..0000000000 --- a/examples/aws-nextjs-container/sst-env.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-nextjs-redis/README.md b/examples/aws-nextjs-redis/README.md deleted file mode 100644 index e215bc4ccf..0000000000 --- a/examples/aws-nextjs-redis/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/examples/aws-nextjs-redis/package.json b/examples/aws-nextjs-redis/package.json index 4b4b174c28..cc3f62615b 100644 --- a/examples/aws-nextjs-redis/package.json +++ b/examples/aws-nextjs-redis/package.json @@ -13,7 +13,7 @@ "next": "14.2.14", "react": "^18", "react-dom": "^18", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/node": "^20", diff --git a/examples/aws-nextjs-redis/sst-env.d.ts b/examples/aws-nextjs-redis/sst-env.d.ts deleted file mode 100644 index 261f14f5b2..0000000000 --- a/examples/aws-nextjs-redis/sst-env.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyRedis": { - "host": string - "password": string - "port": number - "type": "sst.aws.Redis" - "username": string - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "bastion": string - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-nextjs-stream/README.md b/examples/aws-nextjs-stream/README.md deleted file mode 100644 index e215bc4ccf..0000000000 --- a/examples/aws-nextjs-stream/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/examples/aws-nextjs-stream/package.json b/examples/aws-nextjs-stream/package.json index d66751a62e..324de7960c 100644 --- a/examples/aws-nextjs-stream/package.json +++ b/examples/aws-nextjs-stream/package.json @@ -12,7 +12,7 @@ "next": "14.2.12", "react": "^18", "react-dom": "^18", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/node": "^20", diff --git a/examples/aws-nextjs-stream/sst-env.d.ts b/examples/aws-nextjs-stream/sst-env.d.ts deleted file mode 100644 index e2aa6798e3..0000000000 --- a/examples/aws-nextjs-stream/sst-env.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyWeb": { - "type": "sst.aws.Nextjs" - "url": string - } - } -} diff --git a/examples/aws-nextjs/README.md b/examples/aws-nextjs/README.md deleted file mode 100644 index e215bc4ccf..0000000000 --- a/examples/aws-nextjs/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/examples/aws-nextjs/package.json b/examples/aws-nextjs/package.json index ac42409862..5c3a68a25a 100644 --- a/examples/aws-nextjs/package.json +++ b/examples/aws-nextjs/package.json @@ -14,7 +14,7 @@ "next": "14.2.15", "react": "^18", "react-dom": "^18", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/node": "^20", diff --git a/examples/aws-nextjs/sst-env.d.ts b/examples/aws-nextjs/sst-env.d.ts deleted file mode 100644 index 38a4813c1b..0000000000 --- a/examples/aws-nextjs/sst-env.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyWeb": { - "type": "sst.aws.Nextjs" - "url": string - } - } -} diff --git a/examples/aws-nuxt-container/README.md b/examples/aws-nuxt-container/README.md deleted file mode 100644 index f5db2a2dbf..0000000000 --- a/examples/aws-nuxt-container/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# Nuxt 3 Minimal Starter - -Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. - -## Setup - -Make sure to install the dependencies: - -```bash -# npm -npm install - -# pnpm -pnpm install - -# yarn -yarn install - -# bun -bun install -``` - -## Development Server - -Start the development server on `http://localhost:3000`: - -```bash -# npm -npm run dev - -# pnpm -pnpm run dev - -# yarn -yarn dev - -# bun -bun run dev -``` - -## Production - -Build the application for production: - -```bash -# npm -npm run build - -# pnpm -pnpm run build - -# yarn -yarn build - -# bun -bun run build -``` - -Locally preview production build: - -```bash -# npm -npm run preview - -# pnpm -pnpm run preview - -# yarn -yarn preview - -# bun -bun run preview -``` - -Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. diff --git a/examples/aws-nuxt-container/package.json b/examples/aws-nuxt-container/package.json index dabb830025..962195505a 100644 --- a/examples/aws-nuxt-container/package.json +++ b/examples/aws-nuxt-container/package.json @@ -12,7 +12,7 @@ "dependencies": { "ioredis": "^5.4.1", "nuxt": "^3.13.0", - "sst": "^3", + "sst": "file:../../sdk/js", "vue": "latest", "vue-router": "latest" }, diff --git a/examples/aws-nuxt-container/sst-env.d.ts b/examples/aws-nuxt-container/sst-env.d.ts deleted file mode 100644 index 261f14f5b2..0000000000 --- a/examples/aws-nuxt-container/sst-env.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyRedis": { - "host": string - "password": string - "port": number - "type": "sst.aws.Redis" - "username": string - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "bastion": string - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/internal/playground/sites/nuxt/.gitignore b/examples/aws-nuxt-stream/.gitignore similarity index 100% rename from examples/internal/playground/sites/nuxt/.gitignore rename to examples/aws-nuxt-stream/.gitignore diff --git a/examples/aws-nuxt-stream/app.vue b/examples/aws-nuxt-stream/app.vue new file mode 100644 index 0000000000..c7e6100872 --- /dev/null +++ b/examples/aws-nuxt-stream/app.vue @@ -0,0 +1,30 @@ + + + diff --git a/examples/aws-nuxt-stream/nuxt.config.ts b/examples/aws-nuxt-stream/nuxt.config.ts new file mode 100644 index 0000000000..c67efbbe32 --- /dev/null +++ b/examples/aws-nuxt-stream/nuxt.config.ts @@ -0,0 +1,12 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + ignore: ['**/.sst/**'], + compatibilityDate: '2025-05-15', + nitro: { + preset: 'aws-lambda', + awsLambda: { + streaming: true + } + }, + devtools: { enabled: true } +}) diff --git a/examples/aws-nuxt-stream/package.json b/examples/aws-nuxt-stream/package.json new file mode 100644 index 0000000000..34569ea7a4 --- /dev/null +++ b/examples/aws-nuxt-stream/package.json @@ -0,0 +1,18 @@ +{ + "name": "nuxt-app", + "private": true, + "type": "module", + "scripts": { + "build": "nuxt build", + "dev": "nuxt dev", + "generate": "nuxt generate", + "postinstall": "nuxt prepare", + "preview": "nuxt preview" + }, + "dependencies": { + "nuxt": "^3.15.5", + "sst": "file:../../sdk/js", + "vue": "latest", + "vue-router": "latest" + } +} diff --git a/examples/internal/playground/sites/nuxt/public/favicon.ico b/examples/aws-nuxt-stream/public/favicon.ico similarity index 100% rename from examples/internal/playground/sites/nuxt/public/favicon.ico rename to examples/aws-nuxt-stream/public/favicon.ico diff --git a/examples/internal/playground/sites/nuxt/public/robots.txt b/examples/aws-nuxt-stream/public/robots.txt similarity index 100% rename from examples/internal/playground/sites/nuxt/public/robots.txt rename to examples/aws-nuxt-stream/public/robots.txt diff --git a/examples/aws-nuxt-stream/server/api/streaming.ts b/examples/aws-nuxt-stream/server/api/streaming.ts new file mode 100644 index 0000000000..d237103432 --- /dev/null +++ b/examples/aws-nuxt-stream/server/api/streaming.ts @@ -0,0 +1,20 @@ +export default defineEventHandler(async (event) => { + const eventStream = createEventStream(event); + + eventStream.push("Start\n\n"); + + const interval = setInterval(async () => { + await eventStream.push(`Random: ${Math.random().toFixed(5)} `); + }, 1000); + + setTimeout(async () => { + clearInterval(interval); + await eventStream.close(); + }, 10000); + + eventStream.onClosed(() => { + clearInterval(interval); + }); + + return eventStream.send(); +}) diff --git a/examples/internal/playground/sites/nuxt/server/tsconfig.json b/examples/aws-nuxt-stream/server/tsconfig.json similarity index 100% rename from examples/internal/playground/sites/nuxt/server/tsconfig.json rename to examples/aws-nuxt-stream/server/tsconfig.json diff --git a/examples/aws-nuxt-stream/sst.config.ts b/examples/aws-nuxt-stream/sst.config.ts new file mode 100644 index 0000000000..cb1615d890 --- /dev/null +++ b/examples/aws-nuxt-stream/sst.config.ts @@ -0,0 +1,86 @@ +/// + +/** + * ## AWS Nuxt streaming + * + * An example of how to use streaming with Nuxt.js. Uses `createEventStream` to stream data from a server API. + * + * ```ts title="server/api/streaming.ts" + * export default defineEventHandler(async (event) => { + * const eventStream = createEventStream(event); + * eventStream.push("Start\n\n"); + * + * // Send a message every second + * const interval = setInterval(async () => { + * await eventStream.push(`Random: ${Math.random().toFixed(5)} `); + * }, 1000); + * } + * ``` + * + * The client uses the Fetch API to consume the stream. + * + * ```vue title="app.vue" + * + * + * + * ``` + * + * Make sure to have your nuxt.config.ts set up to handle the streaming API correctly. + * + * ```ts title="nuxt.config.ts" {4-6} + * export default defineNuxtConfig({ + * nitro: { + * preset: 'aws-lambda', + * awsLambda: { + * streaming: true + * } + * } + * }); + * ``` + * + * You should see random numbers streamed to the page every second for 10 seconds. + * + * :::note + * Safari handles streaming differently than other browsers. + * ::: + * + * Safari uses a [different heuristic](https://bugs.webkit.org/show_bug.cgi?id=252413) to + * determine when to stream data. You need to render _enough_ initial HTML to trigger streaming. + * This is typically only a problem for demo apps. + */ +export default $config({ + app(input) { + return { + name: "aws-nuxt-stream", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + }; + }, + async run() { + new sst.aws.Nuxt("MyWeb"); + }, +}); diff --git a/examples/internal/playground/sites/nuxt/tsconfig.json b/examples/aws-nuxt-stream/tsconfig.json similarity index 100% rename from examples/internal/playground/sites/nuxt/tsconfig.json rename to examples/aws-nuxt-stream/tsconfig.json diff --git a/examples/aws-nuxt/README.md b/examples/aws-nuxt/README.md deleted file mode 100644 index f5db2a2dbf..0000000000 --- a/examples/aws-nuxt/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# Nuxt 3 Minimal Starter - -Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. - -## Setup - -Make sure to install the dependencies: - -```bash -# npm -npm install - -# pnpm -pnpm install - -# yarn -yarn install - -# bun -bun install -``` - -## Development Server - -Start the development server on `http://localhost:3000`: - -```bash -# npm -npm run dev - -# pnpm -pnpm run dev - -# yarn -yarn dev - -# bun -bun run dev -``` - -## Production - -Build the application for production: - -```bash -# npm -npm run build - -# pnpm -pnpm run build - -# yarn -yarn build - -# bun -bun run build -``` - -Locally preview production build: - -```bash -# npm -npm run preview - -# pnpm -pnpm run preview - -# yarn -yarn preview - -# bun -bun run preview -``` - -Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. diff --git a/examples/aws-nuxt/bun.lockb b/examples/aws-nuxt/bun.lockb deleted file mode 100755 index dc00272888..0000000000 Binary files a/examples/aws-nuxt/bun.lockb and /dev/null differ diff --git a/examples/aws-nuxt/package.json b/examples/aws-nuxt/package.json index a64af4b471..2001ffcfb3 100644 --- a/examples/aws-nuxt/package.json +++ b/examples/aws-nuxt/package.json @@ -13,7 +13,7 @@ "@aws-sdk/client-s3": "^3.651.1", "@aws-sdk/s3-request-presigner": "^3.651.1", "nuxt": "^3.13.0", - "sst": "^3", + "sst": "file:../../sdk/js", "vue": "latest", "vue-router": "latest" } diff --git a/examples/aws-nuxt/sst-env.d.ts b/examples/aws-nuxt/sst-env.d.ts deleted file mode 100644 index 2614770503..0000000000 --- a/examples/aws-nuxt/sst-env.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyWeb": { - "type": "sst.aws.Nuxt" - "url": string - } - } -} diff --git a/examples/aws-open-search-local/package.json b/examples/aws-open-search-local/package.json index cf785d183d..599559797b 100644 --- a/examples/aws-open-search-local/package.json +++ b/examples/aws-open-search-local/package.json @@ -11,7 +11,7 @@ "license": "ISC", "dependencies": { "@opensearch-project/opensearch": "^3.5.1", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "8.10.145" diff --git a/examples/aws-open-search-local/sst-env.d.ts b/examples/aws-open-search-local/sst-env.d.ts deleted file mode 100644 index c1b62bdd43..0000000000 --- a/examples/aws-open-search-local/sst-env.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -declare module "sst" { - export interface Resource { - } -} -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-open-search/package.json b/examples/aws-open-search/package.json index d624496ed0..8776398850 100644 --- a/examples/aws-open-search/package.json +++ b/examples/aws-open-search/package.json @@ -11,6 +11,6 @@ "description": "", "dependencies": { "@opensearch-project/opensearch": "^3.5.1", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/in-progress/aws-drizzle/.gitignore b/examples/aws-planetscale-drizzle-mysql/.gitignore similarity index 100% rename from examples/in-progress/aws-drizzle/.gitignore rename to examples/aws-planetscale-drizzle-mysql/.gitignore diff --git a/examples/aws-planetscale-drizzle-mysql/drizzle.config.ts b/examples/aws-planetscale-drizzle-mysql/drizzle.config.ts new file mode 100644 index 0000000000..1fd6c1a4d3 --- /dev/null +++ b/examples/aws-planetscale-drizzle-mysql/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "drizzle-kit"; +import { Resource } from "sst"; + +export default defineConfig({ + dialect: "mysql", + dbCredentials: { + url: `mysql://${Resource.Database.username}:${Resource.Database.password}@${Resource.Database.host}/${Resource.Database.database}?ssl={"rejectUnauthorized":true}`, + }, + schema: ["./src/schema.ts"], +}); diff --git a/examples/aws-planetscale-drizzle-mysql/package.json b/examples/aws-planetscale-drizzle-mysql/package.json new file mode 100644 index 0000000000..760c01bdce --- /dev/null +++ b/examples/aws-planetscale-drizzle-mysql/package.json @@ -0,0 +1,15 @@ +{ + "name": "aws-planetscale-drizzle-mysql", + "version": "0.0.0", + "scripts": { + "db": "sst shell drizzle-kit", + "db:push": "sst shell drizzle-kit push", + "db:studio": "sst shell drizzle-kit studio" + }, + "dependencies": { + "@planetscale/database": "^1.19.0", + "drizzle-kit": "^0.31.10", + "drizzle-orm": "^0.45.1", + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-planetscale-drizzle/src/api.ts b/examples/aws-planetscale-drizzle-mysql/src/api.ts similarity index 100% rename from examples/aws-planetscale-drizzle/src/api.ts rename to examples/aws-planetscale-drizzle-mysql/src/api.ts diff --git a/examples/aws-planetscale-drizzle/src/drizzle.ts b/examples/aws-planetscale-drizzle-mysql/src/drizzle.ts similarity index 100% rename from examples/aws-planetscale-drizzle/src/drizzle.ts rename to examples/aws-planetscale-drizzle-mysql/src/drizzle.ts diff --git a/examples/aws-planetscale-drizzle/src/schema.ts b/examples/aws-planetscale-drizzle-mysql/src/schema.ts similarity index 100% rename from examples/aws-planetscale-drizzle/src/schema.ts rename to examples/aws-planetscale-drizzle-mysql/src/schema.ts diff --git a/examples/aws-planetscale-drizzle-mysql/sst.config.ts b/examples/aws-planetscale-drizzle-mysql/sst.config.ts new file mode 100644 index 0000000000..1778249823 --- /dev/null +++ b/examples/aws-planetscale-drizzle-mysql/sst.config.ts @@ -0,0 +1,122 @@ +/// + +/** + * ## AWS PlanetScale Drizzle MySQL + * + * In this example, we use PlanetScale with a branch-per-stage pattern. Every stage gets its own + * database branch β€” so each PR can have an isolated database. + * + * ```ts title="sst.config.ts" + * const db = planetscale.getDatabaseVitessOutput({ + * id: "mydb", + * organization: "myorg", + * }); + * + * const branch = + * $app.stage === "production" + * ? planetscale.getVitessBranchOutput({ + * id: db.defaultBranch, + * organization: db.organization, + * database: db.name, + * }) + * : new planetscale.VitessBranch("DatabaseBranch", { + * database: db.name, + * organization: db.organization, + * name: $app.stage, + * parentBranch: db.defaultBranch, + * }); + * ``` + * + * We then create a password and wrap it in a `Linkable` to link it to a function. + * + * ```ts title="sst.config.ts" {3} + * new sst.aws.Function("Api", { + * handler: "src/api.handler", + * link: [database], + * url: true, + * }); + * ``` + * + * You can push your Drizzle schema changes to PlanetScale with: + * + * ```bash + * bun run db:push + * ``` + * + * In the function we use [Drizzle ORM](https://orm.drizzle.team) with the + * [`Resource`](/docs/reference/sdk/#resource) helper. + * + * ```ts title="src/drizzle.ts" + * import { drizzle } from "drizzle-orm/planetscale-serverless"; + * import { Resource } from "sst"; + * + * export const db = drizzle({ + * connection: { + * host: Resource.Database.host, + * username: Resource.Database.username, + * password: Resource.Database.password, + * }, + * }); + * ``` + * + */ +export default $config({ + app(input) { + return { + name: "aws-planetscale-drizzle-mysql", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + providers: { + planetscale: "1.0.0", + }, + }; + }, + async run() { + const db = planetscale.getDatabaseVitessOutput({ + id: "example", + organization: "vimtor", + }); + + const branch = + $app.stage === "production" + ? planetscale.getVitessBranchOutput({ + id: db.defaultBranch, + organization: db.organization, + database: db.name, + }) + : new planetscale.VitessBranch("DatabaseBranch", { + database: db.name, + organization: db.organization, + name: $app.stage, + parentBranch: db.defaultBranch, + }); + + const password = new planetscale.VitessBranchPassword("DatabasePassword", { + database: db.name, + organization: db.organization, + branch: branch.name, + role: "admin", + name: `${$app.name}-${$app.stage}`, + }); + + const database = new sst.Linkable("Database", { + properties: { + host: password.accessHostUrl, + username: password.username, + password: password.plainText, + database: db.name, + port: 3306, + }, + }); + + const api = new sst.aws.Function("Api", { + handler: "src/api.handler", + link: [database], + url: true, + }); + + return { + url: api.url, + }; + }, +}); diff --git a/examples/in-progress/cloudflare-drizzle/tsconfig.json b/examples/aws-planetscale-drizzle-mysql/tsconfig.json similarity index 100% rename from examples/in-progress/cloudflare-drizzle/tsconfig.json rename to examples/aws-planetscale-drizzle-mysql/tsconfig.json diff --git a/examples/in-progress/cloudflare-drizzle/.gitignore b/examples/aws-planetscale-drizzle-postgres/.gitignore similarity index 100% rename from examples/in-progress/cloudflare-drizzle/.gitignore rename to examples/aws-planetscale-drizzle-postgres/.gitignore diff --git a/examples/aws-planetscale-drizzle-postgres/drizzle.config.ts b/examples/aws-planetscale-drizzle-postgres/drizzle.config.ts new file mode 100644 index 0000000000..03f4e900e9 --- /dev/null +++ b/examples/aws-planetscale-drizzle-postgres/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "drizzle-kit"; +import { Resource } from "sst"; + +export default defineConfig({ + dialect: "postgresql", + dbCredentials: { + url: `postgresql://${Resource.Database.username}:${Resource.Database.password}@${Resource.Database.host}/${Resource.Database.database}?sslmode=require`, + }, + schema: ["./src/schema.ts"], +}); diff --git a/examples/aws-planetscale-drizzle-postgres/package.json b/examples/aws-planetscale-drizzle-postgres/package.json new file mode 100644 index 0000000000..8c4f0ecd74 --- /dev/null +++ b/examples/aws-planetscale-drizzle-postgres/package.json @@ -0,0 +1,16 @@ +{ + "name": "aws-planetscale-drizzle-postgres", + "version": "0.0.0", + "scripts": { + "db": "sst shell drizzle-kit", + "db:push": "sst shell drizzle-kit push", + "db:studio": "sst shell drizzle-kit studio" + }, + "dependencies": { + "@neondatabase/serverless": "^1.0.2", + "drizzle-kit": "^0.31.10", + "drizzle-orm": "^0.45.1", + "postgres": "^3.4.5", + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-planetscale-drizzle-postgres/src/api.ts b/examples/aws-planetscale-drizzle-postgres/src/api.ts new file mode 100644 index 0000000000..025826383a --- /dev/null +++ b/examples/aws-planetscale-drizzle-postgres/src/api.ts @@ -0,0 +1,26 @@ +import { db } from "./drizzle"; +import { todosTable } from "./schema"; +import { APIGatewayProxyHandlerV2 } from "aws-lambda"; + +export const handler: APIGatewayProxyHandlerV2 = async (evt) => { + if (evt.requestContext.http.method === "GET") { + const result = await db.select().from(todosTable).execute(); + return { + statusCode: 200, + body: JSON.stringify(result), + }; + } + + if (evt.requestContext.http.method === "POST") { + await db.insert(todosTable).values({ title: "new todosTable" }).execute(); + return { + statusCode: 200, + body: "created", + }; + } + + return { + statusCode: 404, + body: "not found", + }; +}; diff --git a/examples/aws-planetscale-drizzle-postgres/src/drizzle.ts b/examples/aws-planetscale-drizzle-postgres/src/drizzle.ts new file mode 100644 index 0000000000..a521bdc43f --- /dev/null +++ b/examples/aws-planetscale-drizzle-postgres/src/drizzle.ts @@ -0,0 +1,11 @@ +import { Resource } from "sst"; +import { neon, neonConfig } from "@neondatabase/serverless"; +import { drizzle } from "drizzle-orm/neon-http"; + +// Required for PlanetScale Postgres connections +neonConfig.fetchEndpoint = (host) => `https://${host}/sql`; +const sql = neon( + `postgresql://${Resource.Database.username}:${Resource.Database.password}@${Resource.Database.host}:${Resource.Database.port}/postgres?sslmode=verify-full`, +); + +export const db = drizzle({ client: sql }); diff --git a/examples/aws-planetscale-drizzle-postgres/src/schema.ts b/examples/aws-planetscale-drizzle-postgres/src/schema.ts new file mode 100644 index 0000000000..f75c5cd19e --- /dev/null +++ b/examples/aws-planetscale-drizzle-postgres/src/schema.ts @@ -0,0 +1,7 @@ +import { pgTable, serial, text, varchar } from "drizzle-orm/pg-core"; + +export const todosTable = pgTable("todo", { + id: serial("id").primaryKey(), + title: varchar("title", { length: 255 }).notNull(), + description: text("description"), +}); diff --git a/examples/aws-planetscale-drizzle-postgres/sst.config.ts b/examples/aws-planetscale-drizzle-postgres/sst.config.ts new file mode 100644 index 0000000000..3128227068 --- /dev/null +++ b/examples/aws-planetscale-drizzle-postgres/sst.config.ts @@ -0,0 +1,128 @@ +/// + +/** + * ## AWS PlanetScale Drizzle Postgres + * + * In this example, we use PlanetScale Postgres with a branch-per-stage pattern. Every stage + * gets its own database branch β€” so each PR can have an isolated database. + * + * ```ts title="sst.config.ts" + * const db = planetscale.getDatabasePostgresOutput({ + * id: "mydb", + * organization: "myorg", + * }); + * + * const branch = + * $app.stage === "production" + * ? planetscale.getPostgresBranchOutput({ + * id: db.defaultBranch, + * organization: db.organization, + * database: db.name, + * }) + * : new planetscale.PostgresBranch("DatabaseBranch", { + * database: db.name, + * organization: db.organization, + * name: $app.stage, + * parentBranch: db.defaultBranch, + * }); + * ``` + * + * We then create a role and wrap it in a `Linkable` to link it to a function. + * + * ```ts title="sst.config.ts" {3} + * new sst.aws.Function("Api", { + * handler: "src/api.handler", + * link: [database], + * url: true, + * }); + * ``` + * + * You can push your Drizzle schema changes to PlanetScale with: + * + * ```bash + * bun run db:push + * ``` + * + * In the function we use [Drizzle ORM](https://orm.drizzle.team) with the + * [`Resource`](/docs/reference/sdk/#resource) helper. + * + * ```ts title="src/drizzle.ts" + * import { drizzle } from "drizzle-orm/postgres-js"; + * import { Resource } from "sst"; + * import postgres from "postgres"; + * + * const client = postgres({ + * host: Resource.Database.host, + * username: Resource.Database.username, + * password: Resource.Database.password, + * database: Resource.Database.database, + * }); + * + * export const db = drizzle(client); + * ``` + * + */ +export default $config({ + app(input) { + return { + name: "aws-planetscale-drizzle-postgres", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + providers: { + planetscale: "1.0.0", + }, + }; + }, + async run() { + const db = planetscale.getDatabasePostgresOutput({ + id: "mydb", + organization: "myorg", + }); + + const branch = + $app.stage === "production" + ? planetscale.getPostgresBranchOutput({ + id: db.defaultBranch, + organization: db.organization, + database: db.name, + }) + : new planetscale.PostgresBranch("DatabaseBranch", { + database: db.name, + organization: db.organization, + name: $app.stage, + parentBranch: db.defaultBranch, + }); + + const role = new planetscale.PostgresBranchRole("DatabaseRole", { + database: db.name, + organization: db.organization, + branch: branch.name, + name: `${$app.name}-${$app.stage}`, + inheritedRoles: [ + "pg_read_all_data", + "pg_write_all_data", + "postgres", // Only needed for pushing schema changes + ], + }); + + const database = new sst.Linkable("Database", { + properties: { + host: role.accessHostUrl, + username: role.username, + password: role.password, + database: role.databaseName, + port: 6432, // Use 5432 for direct connection instead of PgBouncer + }, + }); + + const api = new sst.aws.Function("Api", { + handler: "src/api.handler", + link: [database], + url: true, + }); + + return { + url: api.url, + }; + }, +}); diff --git a/examples/internal/big/tsconfig.json b/examples/aws-planetscale-drizzle-postgres/tsconfig.json similarity index 100% rename from examples/internal/big/tsconfig.json rename to examples/aws-planetscale-drizzle-postgres/tsconfig.json diff --git a/examples/aws-planetscale-drizzle/drizzle.config.ts b/examples/aws-planetscale-drizzle/drizzle.config.ts deleted file mode 100644 index 498e89fe67..0000000000 --- a/examples/aws-planetscale-drizzle/drizzle.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from "drizzle-kit"; -import { Resource } from "sst"; - -export default defineConfig({ - dialect: "mysql", - dbCredentials: { - host: Resource.Database.host, - user: Resource.Database.username, - password: Resource.Database.password, - database: Resource.Database.database, - }, - schema: ["./src/schema.ts"], -}); diff --git a/examples/aws-planetscale-drizzle/package.json b/examples/aws-planetscale-drizzle/package.json deleted file mode 100644 index 06ef2625a7..0000000000 --- a/examples/aws-planetscale-drizzle/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "aws-planetscale-drizzle", - "version": "0.0.0", - "scripts": { - "db": "sst shell drizzle-kit", - "db:push": "sst shell drizzle-kit push", - "db:studio": "sst shell drizzle-kit studio" - }, - "dependencies": { - "@planetscale/database": "^1.19.0", - "drizzle-kit": "^0.30.4", - "drizzle-orm": "^0.38.4", - "sst": "^3" - } -} diff --git a/examples/aws-planetscale-drizzle/sst-env.d.ts b/examples/aws-planetscale-drizzle/sst-env.d.ts deleted file mode 100644 index 7e164f54b6..0000000000 --- a/examples/aws-planetscale-drizzle/sst-env.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - Database: { - host: string - username: string - password: string - database: string - port: number - type: "sst.sst.Linkable" - } - Api: { - name: string - type: "sst.aws.Function" - url: string - } - } -} -export {} diff --git a/examples/aws-planetscale-drizzle/sst.config.ts b/examples/aws-planetscale-drizzle/sst.config.ts deleted file mode 100644 index 5b729c65c7..0000000000 --- a/examples/aws-planetscale-drizzle/sst.config.ts +++ /dev/null @@ -1,122 +0,0 @@ -/// - -/** - * ## AWS PlanetScale Drizzle - * - * In this example, we use PlanetScale with a branch-per-stage pattern. Every stage gets its own - * database branch β€” so each PR can have an isolated database. - * - * ```ts title="sst.config.ts" - * const db = planetscale.getDatabaseOutput({ - * name: "mydb", - * organization: "myorg", - * }); - * - * const branch = - * $app.stage === "production" - * ? planetscale.getBranchOutput({ - * name: "production", - * organization: db.organization, - * database: db.name, - * }) - * : new planetscale.Branch("DatabaseBranch", { - * database: db.name, - * organization: db.organization, - * name: $app.stage, - * parentBranch: "production", - * }); - * ``` - * - * We then create a password and wrap it in a `Linkable` to link it to a function. - * - * ```ts title="sst.config.ts" {3} - * new sst.aws.Function("Api", { - * handler: "src/api.handler", - * link: [database], - * url: true, - * }); - * ``` - * - * You can push your Drizzle schema changes to PlanetScale with: - * - * ```bash - * bun run db:push - * ``` - * - * In the function we use [Drizzle ORM](https://orm.drizzle.team) with the - * [`Resource`](/docs/reference/sdk/#resource) helper. - * - * ```ts title="src/drizzle.ts" - * import { drizzle } from "drizzle-orm/planetscale-serverless"; - * import { Resource } from "sst"; - * - * export const db = drizzle({ - * connection: { - * host: Resource.Database.host, - * username: Resource.Database.username, - * password: Resource.Database.password, - * }, - * }); - * ``` - * - */ -export default $config({ - app(input) { - return { - name: "aws-planetscale-drizzle", - removal: input?.stage === "production" ? "retain" : "remove", - home: "aws", - providers: { - planetscale: "0.4.1", - }, - }; - }, - async run() { - const db = planetscale.getDatabaseOutput({ - name: "mydb", - organization: "myorg", - }); - - const branch = - $app.stage === "production" - ? planetscale.getBranchOutput({ - name: "production", - organization: db.organization, - database: db.name, - }) - : new planetscale.Branch("DatabaseBranch", { - database: db.name, - organization: db.organization, - name: $app.stage, - parentBranch: "production", - }); - - const password = new planetscale.Password("DatabasePassword", { - database: db.name, - organization: db.organization, - branch: branch.name, - role: "admin", - name: `${$app.name}-${$app.stage}`, - }); - - const database = new sst.Linkable("Database", { - properties: { - host: password.accessHostUrl, - username: password.username, - password: password.plaintext, - database: db.name, - port: 3306, - }, - }); - - const api = new sst.aws.Function("Api", { - handler: "src/api.handler", - link: [database], - url: true, - }); - - return { - url: api.url, - }; - }, -}); diff --git a/examples/aws-policy-pack/package.json b/examples/aws-policy-pack/package.json index 9903249056..b984f65082 100644 --- a/examples/aws-policy-pack/package.json +++ b/examples/aws-policy-pack/package.json @@ -10,6 +10,6 @@ "license": "ISC", "description": "", "dependencies": { - "sst": "latest" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-policy-pack/policy-pack/sst-env.d.ts b/examples/aws-policy-pack/policy-pack/sst-env.d.ts deleted file mode 100644 index 3b8cffd4fd..0000000000 --- a/examples/aws-policy-pack/policy-pack/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-policy-pack/sst-env.d.ts b/examples/aws-policy-pack/sst-env.d.ts deleted file mode 100644 index 30db5f41ac..0000000000 --- a/examples/aws-policy-pack/sst-env.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -declare module "sst" { - export interface Resource { - } -} -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-postgres-local/package.json b/examples/aws-postgres-local/package.json index 82e6f07d6a..b5d4a4644c 100644 --- a/examples/aws-postgres-local/package.json +++ b/examples/aws-postgres-local/package.json @@ -11,7 +11,7 @@ "license": "ISC", "dependencies": { "pg": "^8.13.1", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "8.10.145", diff --git a/examples/aws-postgres-local/sst-env.d.ts b/examples/aws-postgres-local/sst-env.d.ts deleted file mode 100644 index 0d337506a1..0000000000 --- a/examples/aws-postgres-local/sst-env.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyFunction": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyPostgres": { - "database": string - "host": string - "password": string - "port": number - "type": "sst.aws.Postgres" - "username": string - } - "MyVpc": { - "bastion": string - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-postgres-shared-parameter-group/sst.config.ts b/examples/aws-postgres-shared-parameter-group/sst.config.ts new file mode 100644 index 0000000000..ab0e7dc8e6 --- /dev/null +++ b/examples/aws-postgres-shared-parameter-group/sst.config.ts @@ -0,0 +1,72 @@ +/// + +export default $config({ + app(input) { + return { + name: "aws-postgres-shared-pg", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + }; + }, + async run() { + const vpc = new sst.aws.Vpc("MyVpc"); + + // Alternative: create a standalone parameter group and reference it + // + // const sharedParameterGroup = new aws.rds.ParameterGroup( + // "SharedParameterGroup", + // { + // name: "shared-parameter-group", + // family: "postgres17", + // parameters: [ + // { name: "rds.force_ssl", value: "0" }, + // { name: "log_min_duration_statement", value: "1000" }, + // ], + // }, + // ); + // + // Then use in each Postgres: + // transform: { + // instance: { parameterGroupName: sharedParameterGroup.name }, + // } + + // First database with custom parameters + const db1 = new sst.aws.Postgres("Database1", { + vpc, + transform: { + parameterGroup: { + parameters: [ + { + name: "rds.force_ssl", + value: "0", + }, + { + name: "rds.logical_replication", + value: "1", + applyMethod: "pending-reboot", + }, + { + name: "log_min_duration_statement", + value: "1000", + }, + ], + }, + }, + }); + + // Second database reuses db1's parameter group + const db2 = new sst.aws.Postgres("Database2", { + vpc, + transform: { + instance: { + parameterGroupName: db1.nodes.instance.parameterGroupName, + }, + }, + }); + + return { + db1Host: db1.host, + db2Host: db2.host, + }; + }, +}); diff --git a/examples/aws-postgres/package.json b/examples/aws-postgres/package.json index 2618158c81..b90c7345e2 100644 --- a/examples/aws-postgres/package.json +++ b/examples/aws-postgres/package.json @@ -11,6 +11,6 @@ "description": "", "dependencies": { "pg": "^8.13.0", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-postgres/sst-env.d.ts b/examples/aws-postgres/sst-env.d.ts deleted file mode 100644 index 15bd24573e..0000000000 --- a/examples/aws-postgres/sst-env.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyApp": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyPostgres": { - "database": string - "host": string - "password": string - "port": number - "type": "sst.aws.Postgres" - "username": string - } - "MyVpc": { - "bastion": string - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/internal/big/.gitignore b/examples/aws-pothos-graphql/.gitignore similarity index 100% rename from examples/internal/big/.gitignore rename to examples/aws-pothos-graphql/.gitignore diff --git a/examples/aws-pothos-graphql/client.ts b/examples/aws-pothos-graphql/client.ts new file mode 100644 index 0000000000..4216224d56 --- /dev/null +++ b/examples/aws-pothos-graphql/client.ts @@ -0,0 +1,22 @@ +import { Resource } from "sst"; +import { createClient } from "./graphql/genql"; + +const client = createClient({ + url: `${Resource.Api.url}/graphql`, +}); + +export async function handler() { + const createGiraffe = await client.mutation({ + createGiraffe: { + __args: { + name: "Jonny", + }, + name: true, + }, + }); + + return { + statusCode: 200, + body: createGiraffe, + }; +} diff --git a/examples/aws-pothos-graphql/graphql/genql/index.ts b/examples/aws-pothos-graphql/graphql/genql/index.ts new file mode 100644 index 0000000000..d6b343103f --- /dev/null +++ b/examples/aws-pothos-graphql/graphql/genql/index.ts @@ -0,0 +1,65 @@ +// @ts-nocheck +import type { + QueryGenqlSelection, + Query, + MutationGenqlSelection, + Mutation, +} from './schema' +import { + linkTypeMap, + createClient as createClientOriginal, + generateGraphqlOperation, + type FieldsSelection, + type GraphqlOperation, + type ClientOptions, + GenqlError, +} from './runtime' +export type { FieldsSelection } from './runtime' +export { GenqlError } + +import types from './types' +export * from './schema' +const typeMap = linkTypeMap(types as any) + +export interface Client { + query( + request: R & { __name?: string }, + ): Promise> + + mutation( + request: R & { __name?: string }, + ): Promise> +} + +export const createClient = function (options?: ClientOptions): Client { + return createClientOriginal({ + url: undefined, + + ...options, + queryRoot: typeMap.Query!, + mutationRoot: typeMap.Mutation!, + subscriptionRoot: typeMap.Subscription!, + }) as any +} + +export const everything = { + __scalar: true, +} + +export type QueryResult = FieldsSelection< + Query, + fields +> +export const generateQueryOp: ( + fields: QueryGenqlSelection & { __name?: string }, +) => GraphqlOperation = function (fields) { + return generateGraphqlOperation('query', typeMap.Query!, fields as any) +} + +export type MutationResult = + FieldsSelection +export const generateMutationOp: ( + fields: MutationGenqlSelection & { __name?: string }, +) => GraphqlOperation = function (fields) { + return generateGraphqlOperation('mutation', typeMap.Mutation!, fields as any) +} diff --git a/examples/aws-pothos-graphql/graphql/genql/runtime/batcher.ts b/examples/aws-pothos-graphql/graphql/genql/runtime/batcher.ts new file mode 100644 index 0000000000..c092551010 --- /dev/null +++ b/examples/aws-pothos-graphql/graphql/genql/runtime/batcher.ts @@ -0,0 +1,275 @@ +// @ts-nocheck +import type { GraphqlOperation } from './generateGraphqlOperation' +import { GenqlError } from './error' + +type Variables = Record + +type QueryError = Error & { + message: string + + locations?: Array<{ + line: number + column: number + }> + path?: any + rid: string + details?: Record +} +type Result = { + data: Record + errors: Array +} +type Fetcher = ( + batchedQuery: GraphqlOperation | Array, +) => Promise> +type Options = { + batchInterval?: number + shouldBatch?: boolean + maxBatchSize?: number +} +type Queue = Array<{ + request: GraphqlOperation + resolve: (...args: Array) => any + reject: (...args: Array) => any +}> + +/** + * takes a list of requests (queue) and batches them into a single server request. + * It will then resolve each individual requests promise with the appropriate data. + * @private + * @param {QueryBatcher} client - the client to use + * @param {Queue} queue - the list of requests to batch + */ +function dispatchQueueBatch(client: QueryBatcher, queue: Queue): void { + let batchedQuery: any = queue.map((item) => item.request) + + if (batchedQuery.length === 1) { + batchedQuery = batchedQuery[0] + } + (() => { + try { + return client.fetcher(batchedQuery); + } catch(e) { + return Promise.reject(e); + } + })().then((responses: any) => { + if (queue.length === 1 && !Array.isArray(responses)) { + if (responses.errors && responses.errors.length) { + queue[0].reject( + new GenqlError(responses.errors, responses.data), + ) + return + } + + queue[0].resolve(responses) + return + } else if (responses.length !== queue.length) { + throw new Error('response length did not match query length') + } + + for (let i = 0; i < queue.length; i++) { + if (responses[i].errors && responses[i].errors.length) { + queue[i].reject( + new GenqlError(responses[i].errors, responses[i].data), + ) + } else { + queue[i].resolve(responses[i]) + } + } + }) + .catch((e) => { + for (let i = 0; i < queue.length; i++) { + queue[i].reject(e) + } + }); +} + +/** + * creates a list of requests to batch according to max batch size. + * @private + * @param {QueryBatcher} client - the client to create list of requests from from + * @param {Options} options - the options for the batch + */ +function dispatchQueue(client: QueryBatcher, options: Options): void { + const queue = client._queue + const maxBatchSize = options.maxBatchSize || 0 + client._queue = [] + + if (maxBatchSize > 0 && maxBatchSize < queue.length) { + for (let i = 0; i < queue.length / maxBatchSize; i++) { + dispatchQueueBatch( + client, + queue.slice(i * maxBatchSize, (i + 1) * maxBatchSize), + ) + } + } else { + dispatchQueueBatch(client, queue) + } +} +/** + * Create a batcher client. + * @param {Fetcher} fetcher - A function that can handle the network requests to graphql endpoint + * @param {Options} options - the options to be used by client + * @param {boolean} options.shouldBatch - should the client batch requests. (default true) + * @param {integer} options.batchInterval - duration (in MS) of each batch window. (default 6) + * @param {integer} options.maxBatchSize - max number of requests in a batch. (default 0) + * @param {boolean} options.defaultHeaders - default headers to include with every request + * + * @example + * const fetcher = batchedQuery => fetch('path/to/graphql', { + * method: 'post', + * headers: { + * Accept: 'application/json', + * 'Content-Type': 'application/json', + * }, + * body: JSON.stringify(batchedQuery), + * credentials: 'include', + * }) + * .then(response => response.json()) + * + * const client = new QueryBatcher(fetcher, { maxBatchSize: 10 }) + */ + +export class QueryBatcher { + fetcher: Fetcher + _options: Options + _queue: Queue + + constructor( + fetcher: Fetcher, + { + batchInterval = 6, + shouldBatch = true, + maxBatchSize = 0, + }: Options = {}, + ) { + this.fetcher = fetcher + this._options = { + batchInterval, + shouldBatch, + maxBatchSize, + } + this._queue = [] + } + + /** + * Fetch will send a graphql request and return the parsed json. + * @param {string} query - the graphql query. + * @param {Variables} variables - any variables you wish to inject as key/value pairs. + * @param {[string]} operationName - the graphql operationName. + * @param {Options} overrides - the client options overrides. + * + * @return {promise} resolves to parsed json of server response + * + * @example + * client.fetch(` + * query getHuman($id: ID!) { + * human(id: $id) { + * name + * height + * } + * } + * `, { id: "1001" }, 'getHuman') + * .then(human => { + * // do something with human + * console.log(human); + * }); + */ + fetch( + query: string, + variables?: Variables, + operationName?: string, + overrides: Options = {}, + ): Promise { + const request: GraphqlOperation = { + query, + } + const options = Object.assign({}, this._options, overrides) + + if (variables) { + request.variables = variables + } + + if (operationName) { + request.operationName = operationName + } + + const promise = new Promise((resolve, reject) => { + this._queue.push({ + request, + resolve, + reject, + }) + + if (this._queue.length === 1) { + if (options.shouldBatch) { + setTimeout( + () => dispatchQueue(this, options), + options.batchInterval, + ) + } else { + dispatchQueue(this, options) + } + } + }) + return promise + } + + /** + * Fetch will send a graphql request and return the parsed json. + * @param {string} query - the graphql query. + * @param {Variables} variables - any variables you wish to inject as key/value pairs. + * @param {[string]} operationName - the graphql operationName. + * @param {Options} overrides - the client options overrides. + * + * @return {Promise>} resolves to parsed json of server response + * + * @example + * client.forceFetch(` + * query getHuman($id: ID!) { + * human(id: $id) { + * name + * height + * } + * } + * `, { id: "1001" }, 'getHuman') + * .then(human => { + * // do something with human + * console.log(human); + * }); + */ + forceFetch( + query: string, + variables?: Variables, + operationName?: string, + overrides: Options = {}, + ): Promise { + const request: GraphqlOperation = { + query, + } + const options = Object.assign({}, this._options, overrides, { + shouldBatch: false, + }) + + if (variables) { + request.variables = variables + } + + if (operationName) { + request.operationName = operationName + } + + const promise = new Promise((resolve, reject) => { + const client = new QueryBatcher(this.fetcher, this._options) + client._queue = [ + { + request, + resolve, + reject, + }, + ] + dispatchQueue(client, options) + }) + return promise + } +} diff --git a/examples/aws-pothos-graphql/graphql/genql/runtime/createClient.ts b/examples/aws-pothos-graphql/graphql/genql/runtime/createClient.ts new file mode 100644 index 0000000000..755617ed7a --- /dev/null +++ b/examples/aws-pothos-graphql/graphql/genql/runtime/createClient.ts @@ -0,0 +1,68 @@ +// @ts-nocheck + +import { type BatchOptions, createFetcher } from './fetcher' +import type { ExecutionResult, LinkedType } from './types' +import { + generateGraphqlOperation, + type GraphqlOperation, +} from './generateGraphqlOperation' + +export type Headers = + | HeadersInit + | (() => HeadersInit) + | (() => Promise) + +export type BaseFetcher = ( + operation: GraphqlOperation | GraphqlOperation[], +) => Promise + +export type ClientOptions = Omit & { + url?: string + batch?: BatchOptions | boolean + fetcher?: BaseFetcher + fetch?: Function + headers?: Headers +} + +export const createClient = ({ + queryRoot, + mutationRoot, + subscriptionRoot, + ...options +}: ClientOptions & { + queryRoot?: LinkedType + mutationRoot?: LinkedType + subscriptionRoot?: LinkedType +}) => { + const fetcher = createFetcher(options) + const client: { + query?: Function + mutation?: Function + } = {} + + if (queryRoot) { + client.query = (request: any) => { + if (!queryRoot) throw new Error('queryRoot argument is missing') + + const resultPromise = fetcher( + generateGraphqlOperation('query', queryRoot, request), + ) + + return resultPromise + } + } + if (mutationRoot) { + client.mutation = (request: any) => { + if (!mutationRoot) + throw new Error('mutationRoot argument is missing') + + const resultPromise = fetcher( + generateGraphqlOperation('mutation', mutationRoot, request), + ) + + return resultPromise + } + } + + return client as any +} diff --git a/examples/aws-pothos-graphql/graphql/genql/runtime/error.ts b/examples/aws-pothos-graphql/graphql/genql/runtime/error.ts new file mode 100644 index 0000000000..d9039ebe0c --- /dev/null +++ b/examples/aws-pothos-graphql/graphql/genql/runtime/error.ts @@ -0,0 +1,29 @@ +// @ts-nocheck +export class GenqlError extends Error { + errors: Array = [] + /** + * Partial data returned by the server + */ + data?: any + constructor(errors: any[], data: any) { + let message = Array.isArray(errors) + ? errors.map((x) => x?.message || '').join('\n') + : '' + if (!message) { + message = 'GraphQL error' + } + super(message) + this.errors = errors + this.data = data + } +} + +interface GraphqlError { + message: string + locations?: Array<{ + line: number + column: number + }> + path?: string[] + extensions?: Record +} diff --git a/examples/aws-pothos-graphql/graphql/genql/runtime/fetcher.ts b/examples/aws-pothos-graphql/graphql/genql/runtime/fetcher.ts new file mode 100644 index 0000000000..74e6d4ce9a --- /dev/null +++ b/examples/aws-pothos-graphql/graphql/genql/runtime/fetcher.ts @@ -0,0 +1,97 @@ +// @ts-nocheck +import { QueryBatcher } from './batcher' + +import type { ClientOptions } from './createClient' +import type { GraphqlOperation } from './generateGraphqlOperation' +import { GenqlError } from './error' + +export interface Fetcher { + (gql: GraphqlOperation): Promise +} + +export type BatchOptions = { + batchInterval?: number // ms + maxBatchSize?: number +} + +const DEFAULT_BATCH_OPTIONS = { + maxBatchSize: 10, + batchInterval: 40, +} + +export const createFetcher = ({ + url, + headers = {}, + fetcher, + fetch: _fetch, + batch = false, + ...rest +}: ClientOptions): Fetcher => { + if (!url && !fetcher) { + throw new Error('url or fetcher is required') + } + + fetcher = fetcher || (async (body) => { + let headersObject = + typeof headers == 'function' ? await headers() : headers + headersObject = headersObject || {} + if (typeof fetch === 'undefined' && !_fetch) { + throw new Error( + 'Global `fetch` function is not available, pass a fetch polyfill to Genql `createClient`', + ) + } + let fetchImpl = _fetch || fetch + const res = await fetchImpl(url!, { + headers: { + 'Content-Type': 'application/json', + ...headersObject, + }, + method: 'POST', + body: JSON.stringify(body), + ...rest, + }) + if (!res.ok) { + throw new Error(`${res.statusText}: ${await res.text()}`) + } + const json = await res.json() + return json + }) + + if (!batch) { + return async (body) => { + const json = await fetcher!(body) + if (Array.isArray(json)) { + return json.map((json) => { + if (json?.errors?.length) { + throw new GenqlError(json.errors || [], json.data) + } + return json.data + }) + } else { + if (json?.errors?.length) { + throw new GenqlError(json.errors || [], json.data) + } + return json.data + } + } + } + + const batcher = new QueryBatcher( + async (batchedQuery) => { + // console.log(batchedQuery) // [{ query: 'query{user{age}}', variables: {} }, ...] + const json = await fetcher!(batchedQuery) + return json as any + }, + batch === true ? DEFAULT_BATCH_OPTIONS : batch, + ) + + return async ({ query, variables }) => { + const json = await batcher.fetch(query, variables) + if (json?.data) { + return json.data + } + throw new Error( + 'Genql batch fetcher returned unexpected result ' + JSON.stringify(json), + ) + } +} diff --git a/examples/aws-pothos-graphql/graphql/genql/runtime/generateGraphqlOperation.ts b/examples/aws-pothos-graphql/graphql/genql/runtime/generateGraphqlOperation.ts new file mode 100644 index 0000000000..c618019ec8 --- /dev/null +++ b/examples/aws-pothos-graphql/graphql/genql/runtime/generateGraphqlOperation.ts @@ -0,0 +1,225 @@ +// @ts-nocheck +import type { LinkedField, LinkedType } from './types' + +export interface Args { + [arg: string]: any | undefined +} + +export interface Fields { + [field: string]: Request +} + +export type Request = boolean | number | Fields + +export interface Variables { + [name: string]: { + value: any + typing: [LinkedType, string] + } +} + +export interface Context { + root: LinkedType + varCounter: number + variables: Variables + fragmentCounter: number + fragments: string[] +} + +export interface GraphqlOperation { + query: string + variables?: { [name: string]: any } + operationName?: string +} + +const parseRequest = ( + request: Request | undefined, + ctx: Context, + path: string[], +): string => { + if (typeof request === 'object' && '__args' in request) { + const args: any = request.__args + let fields: Request | undefined = { ...request } + delete fields.__args + const argNames = Object.keys(args) + + if (argNames.length === 0) { + return parseRequest(fields, ctx, path) + } + + const field = getFieldFromPath(ctx.root, path) + + const argStrings = argNames.map((argName) => { + ctx.varCounter++ + const varName = `v${ctx.varCounter}` + + const typing = field.args && field.args[argName] // typeMap used here, .args + + if (!typing) { + throw new Error( + `no typing defined for argument \`${argName}\` in path \`${path.join( + '.', + )}\``, + ) + } + + ctx.variables[varName] = { + value: args[argName], + typing, + } + + return `${argName}:$${varName}` + }) + return `(${argStrings})${parseRequest(fields, ctx, path)}` + } else if (typeof request === 'object' && Object.keys(request).length > 0) { + const fields = request + const fieldNames = Object.keys(fields).filter((k) => Boolean(fields[k])) + + if (fieldNames.length === 0) { + throw new Error( + `field selection should not be empty: ${path.join('.')}`, + ) + } + + const type = + path.length > 0 ? getFieldFromPath(ctx.root, path).type : ctx.root + const scalarFields = type.scalar + + let scalarFieldsFragment: string | undefined + + if (fieldNames.includes('__scalar')) { + const falsyFieldNames = new Set( + Object.keys(fields).filter((k) => !Boolean(fields[k])), + ) + if (scalarFields?.length) { + ctx.fragmentCounter++ + scalarFieldsFragment = `f${ctx.fragmentCounter}` + + ctx.fragments.push( + `fragment ${scalarFieldsFragment} on ${ + type.name + }{${scalarFields + .filter((f) => !falsyFieldNames.has(f)) + .join(',')}}`, + ) + } + } + + const fieldsSelection = fieldNames + .filter((f) => !['__scalar', '__name'].includes(f)) + .map((f) => { + const parsed = parseRequest(fields[f], ctx, [...path, f]) + + if (f.startsWith('on_')) { + ctx.fragmentCounter++ + const implementationFragment = `f${ctx.fragmentCounter}` + + const typeMatch = f.match(/^on_(.+)/) + + if (!typeMatch || !typeMatch[1]) + throw new Error('match failed') + + ctx.fragments.push( + `fragment ${implementationFragment} on ${typeMatch[1]}${parsed}`, + ) + + return `...${implementationFragment}` + } else { + return `${f}${parsed}` + } + }) + .concat(scalarFieldsFragment ? [`...${scalarFieldsFragment}`] : []) + .join(',') + + return `{${fieldsSelection}}` + } else { + return '' + } +} + +export const generateGraphqlOperation = ( + operation: 'query' | 'mutation' | 'subscription', + root: LinkedType, + fields?: Fields, +): GraphqlOperation => { + const ctx: Context = { + root: root, + varCounter: 0, + variables: {}, + fragmentCounter: 0, + fragments: [], + } + const result = parseRequest(fields, ctx, []) + + const varNames = Object.keys(ctx.variables) + + const varsString = + varNames.length > 0 + ? `(${varNames.map((v) => { + const variableType = ctx.variables[v].typing[1] + return `$${v}:${variableType}` + })})` + : '' + + const operationName = fields?.__name || '' + + return { + query: [ + `${operation} ${operationName}${varsString}${result}`, + ...ctx.fragments, + ].join(','), + variables: Object.keys(ctx.variables).reduce<{ [name: string]: any }>( + (r, v) => { + r[v] = ctx.variables[v].value + return r + }, + {}, + ), + ...(operationName ? { operationName: operationName.toString() } : {}), + } +} + +export const getFieldFromPath = ( + root: LinkedType | undefined, + path: string[], +) => { + let current: LinkedField | undefined + + if (!root) throw new Error('root type is not provided') + + if (path.length === 0) throw new Error(`path is empty`) + + path.forEach((f) => { + const type = current ? current.type : root + + if (!type.fields) + throw new Error(`type \`${type.name}\` does not have fields`) + + const possibleTypes = Object.keys(type.fields) + .filter((i) => i.startsWith('on_')) + .reduce( + (types, fieldName) => { + const field = type.fields && type.fields[fieldName] + if (field) types.push(field.type) + return types + }, + [type], + ) + + let field: LinkedField | null = null + + possibleTypes.forEach((type) => { + const found = type.fields && type.fields[f] + if (found) field = found + }) + + if (!field) + throw new Error( + `type \`${type.name}\` does not have a field \`${f}\``, + ) + + current = field + }) + + return current as LinkedField +} diff --git a/examples/aws-pothos-graphql/graphql/genql/runtime/index.ts b/examples/aws-pothos-graphql/graphql/genql/runtime/index.ts new file mode 100644 index 0000000000..130ed4bf79 --- /dev/null +++ b/examples/aws-pothos-graphql/graphql/genql/runtime/index.ts @@ -0,0 +1,13 @@ +// @ts-nocheck +export { createClient } from './createClient' +export type { ClientOptions } from './createClient' +export type { FieldsSelection } from './typeSelection' +export { generateGraphqlOperation } from './generateGraphqlOperation' +export type { GraphqlOperation } from './generateGraphqlOperation' +export { linkTypeMap } from './linkTypeMap' +// export { Observable } from 'zen-observable-ts' +export { createFetcher } from './fetcher' +export { GenqlError } from './error' +export const everything = { + __scalar: true, +} diff --git a/examples/aws-pothos-graphql/graphql/genql/runtime/linkTypeMap.ts b/examples/aws-pothos-graphql/graphql/genql/runtime/linkTypeMap.ts new file mode 100644 index 0000000000..3e12c54598 --- /dev/null +++ b/examples/aws-pothos-graphql/graphql/genql/runtime/linkTypeMap.ts @@ -0,0 +1,156 @@ +// @ts-nocheck +import type { + CompressedType, + CompressedTypeMap, + LinkedArgMap, + LinkedField, + LinkedType, + LinkedTypeMap, +} from './types' + +export interface PartialLinkedFieldMap { + [field: string]: { + type: string + args?: LinkedArgMap + } +} + +export const linkTypeMap = ( + typeMap: CompressedTypeMap, +): LinkedTypeMap => { + const indexToName: Record = Object.assign( + {}, + ...Object.keys(typeMap.types).map((k, i) => ({ [i]: k })), + ) + + let intermediaryTypeMap = Object.assign( + {}, + ...Object.keys(typeMap.types || {}).map( + (k): Record => { + const type: CompressedType = typeMap.types[k]! + const fields = type || {} + return { + [k]: { + name: k, + // type scalar properties + scalar: Object.keys(fields).filter((f) => { + const [type] = fields[f] || [] + + const isScalar = + type && typeMap.scalars.includes(type) + if (!isScalar) { + return false + } + const args = fields[f]?.[1] + const argTypes = Object.values(args || {}) + .map((x) => x?.[1]) + .filter(Boolean) + + const hasRequiredArgs = argTypes.some( + (str) => str && str.endsWith('!'), + ) + if (hasRequiredArgs) { + return false + } + return true + }), + // fields with corresponding `type` and `args` + fields: Object.assign( + {}, + ...Object.keys(fields).map( + (f): PartialLinkedFieldMap => { + const [typeIndex, args] = fields[f] || [] + if (typeIndex == null) { + return {} + } + return { + [f]: { + // replace index with type name + type: indexToName[typeIndex], + args: Object.assign( + {}, + ...Object.keys(args || {}).map( + (k) => { + // if argTypeString == argTypeName, argTypeString is missing, need to readd it + if (!args || !args[k]) { + return + } + const [ + argTypeName, + argTypeString, + ] = args[k] as any + return { + [k]: [ + indexToName[ + argTypeName + ], + argTypeString || + indexToName[ + argTypeName + ], + ], + } + }, + ), + ), + }, + } + }, + ), + ), + }, + } + }, + ), + ) + const res = resolveConcreteTypes(intermediaryTypeMap) + return res +} + +// replace typename with concrete type +export const resolveConcreteTypes = (linkedTypeMap: LinkedTypeMap) => { + Object.keys(linkedTypeMap).forEach((typeNameFromKey) => { + const type: LinkedType = linkedTypeMap[typeNameFromKey]! + // type.name = typeNameFromKey + if (!type.fields) { + return + } + + const fields = type.fields + + Object.keys(fields).forEach((f) => { + const field: LinkedField = fields[f]! + + if (field.args) { + const args = field.args + Object.keys(args).forEach((key) => { + const arg = args[key] + + if (arg) { + const [typeName] = arg + + if (typeof typeName === 'string') { + if (!linkedTypeMap[typeName]) { + linkedTypeMap[typeName] = { name: typeName } + } + + arg[0] = linkedTypeMap[typeName]! + } + } + }) + } + + const typeName = field.type as LinkedType | string + + if (typeof typeName === 'string') { + if (!linkedTypeMap[typeName]) { + linkedTypeMap[typeName] = { name: typeName } + } + + field.type = linkedTypeMap[typeName]! + } + }) + }) + + return linkedTypeMap +} diff --git a/examples/aws-pothos-graphql/graphql/genql/runtime/typeSelection.ts b/examples/aws-pothos-graphql/graphql/genql/runtime/typeSelection.ts new file mode 100644 index 0000000000..a021d00bf5 --- /dev/null +++ b/examples/aws-pothos-graphql/graphql/genql/runtime/typeSelection.ts @@ -0,0 +1,95 @@ +// @ts-nocheck +////////////////////////////////////////////////// + +// SOME THINGS TO KNOW BEFORE DIVING IN +/* +0. DST is the request type, SRC is the response type + +1. FieldsSelection uses an object because currently is impossible to make recursive types + +2. FieldsSelection is a recursive type that makes a type based on request type and fields + +3. HandleObject handles object types + +4. Handle__scalar adds all scalar properties excluding non scalar props +*/ + +export type FieldsSelection | undefined, DST> = { + scalar: SRC + union: Handle__isUnion + object: HandleObject + array: SRC extends Nil + ? never + : SRC extends Array + ? Array> + : never + __scalar: Handle__scalar + never: never +}[DST extends Nil + ? 'never' + : DST extends false | 0 + ? 'never' + : SRC extends Scalar + ? 'scalar' + : SRC extends any[] + ? 'array' + : SRC extends { __isUnion?: any } + ? 'union' + : DST extends { __scalar?: any } + ? '__scalar' + : DST extends {} + ? 'object' + : 'never'] + +type HandleObject, DST> = DST extends boolean + ? SRC + : SRC extends Nil + ? never + : Pick< + { + // using keyof SRC to maintain ?: relations of SRC type + [Key in keyof SRC]: Key extends keyof DST + ? FieldsSelection> + : SRC[Key] + }, + Exclude + // { + // // remove falsy values + // [Key in keyof DST]: DST[Key] extends false | 0 ? never : Key + // }[keyof DST] + > + +type Handle__scalar, DST> = SRC extends Nil + ? never + : Pick< + // continue processing fields that are in DST, directly pass SRC type if not in DST + { + [Key in keyof SRC]: Key extends keyof DST + ? FieldsSelection + : SRC[Key] + }, + // remove fields that are not scalars or are not in DST + { + [Key in keyof SRC]: SRC[Key] extends Nil + ? never + : Key extends FieldsToRemove + ? never + : SRC[Key] extends Scalar + ? Key + : Key extends keyof DST + ? Key + : never + }[keyof SRC] + > + +type Handle__isUnion, DST> = SRC extends Nil + ? never + : Omit // just return the union type + +type Scalar = string | number | Date | boolean | null | undefined + +type Anify = { [P in keyof T]?: any } + +type FieldsToRemove = '__isUnion' | '__scalar' | '__name' | '__args' + +type Nil = undefined | null diff --git a/examples/aws-pothos-graphql/graphql/genql/runtime/types.ts b/examples/aws-pothos-graphql/graphql/genql/runtime/types.ts new file mode 100644 index 0000000000..3f0bc30b9e --- /dev/null +++ b/examples/aws-pothos-graphql/graphql/genql/runtime/types.ts @@ -0,0 +1,69 @@ +// @ts-nocheck + +export interface ExecutionResult { + errors?: Array + data?: TData | null +} + +export interface ArgMap { + [arg: string]: [keyType, string] | [keyType] | undefined +} + +export type CompressedField = [ + type: keyType, + args?: ArgMap, +] + +export interface CompressedFieldMap { + [field: string]: CompressedField | undefined +} + +export type CompressedType = CompressedFieldMap + +export interface CompressedTypeMap { + scalars: Array + types: { + [type: string]: CompressedType | undefined + } +} + +// normal types +export type Field = { + type: keyType + args?: ArgMap +} + +export interface FieldMap { + [field: string]: Field | undefined +} + +export type Type = FieldMap + +export interface TypeMap { + scalars: Array + types: { + [type: string]: Type | undefined + } +} + +export interface LinkedArgMap { + [arg: string]: [LinkedType, string] | undefined +} +export interface LinkedField { + type: LinkedType + args?: LinkedArgMap +} + +export interface LinkedFieldMap { + [field: string]: LinkedField | undefined +} + +export interface LinkedType { + name: string + fields?: LinkedFieldMap + scalar?: string[] +} + +export interface LinkedTypeMap { + [type: string]: LinkedType | undefined +} diff --git a/examples/aws-pothos-graphql/graphql/genql/schema.graphql b/examples/aws-pothos-graphql/graphql/genql/schema.graphql new file mode 100644 index 0000000000..017afa4426 --- /dev/null +++ b/examples/aws-pothos-graphql/graphql/genql/schema.graphql @@ -0,0 +1,12 @@ +"""Long necks, cool patterns, taller than you.""" +type Giraffe { + name: String! +} + +type Mutation { + createGiraffe(name: String!): Giraffe! +} + +type Query { + giraffe: Giraffe! +} \ No newline at end of file diff --git a/examples/aws-pothos-graphql/graphql/genql/schema.ts b/examples/aws-pothos-graphql/graphql/genql/schema.ts new file mode 100644 index 0000000000..9677818f0e --- /dev/null +++ b/examples/aws-pothos-graphql/graphql/genql/schema.ts @@ -0,0 +1,70 @@ +// @ts-nocheck +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type Scalars = { + String: string, + Boolean: boolean, +} + + +/** Long necks, cool patterns, taller than you. */ +export interface Giraffe { + name: Scalars['String'] + __typename: 'Giraffe' +} + +export interface Mutation { + createGiraffe: Giraffe + __typename: 'Mutation' +} + +export interface Query { + giraffe: Giraffe + __typename: 'Query' +} + + +/** Long necks, cool patterns, taller than you. */ +export interface GiraffeGenqlSelection{ + name?: boolean | number + __typename?: boolean | number + __scalar?: boolean | number +} + +export interface MutationGenqlSelection{ + createGiraffe?: (GiraffeGenqlSelection & { __args: {name: Scalars['String']} }) + __typename?: boolean | number + __scalar?: boolean | number +} + +export interface QueryGenqlSelection{ + giraffe?: GiraffeGenqlSelection + __typename?: boolean | number + __scalar?: boolean | number +} + + + const Giraffe_possibleTypes: string[] = ['Giraffe'] + export const isGiraffe = (obj?: { __typename?: any } | null): obj is Giraffe => { + if (!obj?.__typename) throw new Error('__typename is missing in "isGiraffe"') + return Giraffe_possibleTypes.includes(obj.__typename) + } + + + + const Mutation_possibleTypes: string[] = ['Mutation'] + export const isMutation = (obj?: { __typename?: any } | null): obj is Mutation => { + if (!obj?.__typename) throw new Error('__typename is missing in "isMutation"') + return Mutation_possibleTypes.includes(obj.__typename) + } + + + + const Query_possibleTypes: string[] = ['Query'] + export const isQuery = (obj?: { __typename?: any } | null): obj is Query => { + if (!obj?.__typename) throw new Error('__typename is missing in "isQuery"') + return Query_possibleTypes.includes(obj.__typename) + } + \ No newline at end of file diff --git a/examples/aws-pothos-graphql/graphql/genql/types.ts b/examples/aws-pothos-graphql/graphql/genql/types.ts new file mode 100644 index 0000000000..8ddc017cce --- /dev/null +++ b/examples/aws-pothos-graphql/graphql/genql/types.ts @@ -0,0 +1,40 @@ +export default { + "scalars": [ + 1, + 4 + ], + "types": { + "Giraffe": { + "name": [ + 1 + ], + "__typename": [ + 1 + ] + }, + "String": {}, + "Mutation": { + "createGiraffe": [ + 0, + { + "name": [ + 1, + "String!" + ] + } + ], + "__typename": [ + 1 + ] + }, + "Query": { + "giraffe": [ + 0 + ], + "__typename": [ + 1 + ] + }, + "Boolean": {} + } +} \ No newline at end of file diff --git a/examples/aws-pothos-graphql/graphql/schema.graphql b/examples/aws-pothos-graphql/graphql/schema.graphql new file mode 100644 index 0000000000..017afa4426 --- /dev/null +++ b/examples/aws-pothos-graphql/graphql/schema.graphql @@ -0,0 +1,12 @@ +"""Long necks, cool patterns, taller than you.""" +type Giraffe { + name: String! +} + +type Mutation { + createGiraffe(name: String!): Giraffe! +} + +type Query { + giraffe: Giraffe! +} \ No newline at end of file diff --git a/examples/aws-pothos-graphql/graphql/urql.ts b/examples/aws-pothos-graphql/graphql/urql.ts new file mode 100644 index 0000000000..60833df69e --- /dev/null +++ b/examples/aws-pothos-graphql/graphql/urql.ts @@ -0,0 +1,98 @@ +import { + useQuery, + useClient, + createRequest, + RequestPolicy, + OperationResult, + UseMutationState, + OperationContext, + UseMutationResponse, +} from "urql"; +import { useEffect, useState, useCallback, useRef } from "react"; +import { + QueryResult, + QueryGenqlSelection, + MutationResult, + MutationGenqlSelection, + generateQueryOp, + generateMutationOp, +} from "./genql"; + +import { pipe, toPromise } from "wonka"; + +export function useTypedQuery(opts: { + query: Query; + pause?: boolean; + requestPolicy?: RequestPolicy; + context?: Partial; +}) { + const { query, variables } = generateQueryOp(opts.query); + return useQuery>({ + ...opts, + query, + variables, + }); +} + +const initialState = { + stale: false, + fetching: false, + data: undefined, + error: undefined, + operation: undefined, + extensions: undefined, +}; + +export function useTypedMutation< + Variables extends Record, + Mutation extends MutationGenqlSelection, + Data extends MutationResult +>( + builder: (vars: Variables) => Mutation, + opts?: Partial +): UseMutationResponse { + const client = useClient(); + const isMounted = useRef(true); + const [state, setState] = + useState>(initialState); + const executeMutation = useCallback( + ( + vars?: Variables, + context?: Partial + ): Promise> => { + setState({ ...initialState, fetching: true }); + const buildArgs = vars || ({} as Variables); + const built = builder(buildArgs); + const { query, variables } = generateMutationOp(built); + return pipe( + client.executeMutation( + createRequest(query, variables as Variables), + { ...opts, ...context } + ), + toPromise + ).then((result: OperationResult) => { + if (isMounted.current) { + setState({ + fetching: false, + stale: !!result.stale, + data: result.data, + error: result.error, + extensions: result.extensions, + operation: result.operation, + }); + } + return result; + }); + }, + [state, setState] + ); + + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); + + return [state, executeMutation]; +} diff --git a/examples/aws-pothos-graphql/package.json b/examples/aws-pothos-graphql/package.json new file mode 100644 index 0000000000..cb287214e0 --- /dev/null +++ b/examples/aws-pothos-graphql/package.json @@ -0,0 +1,17 @@ +{ + "name": "aws-pothos-graphql", + "dependencies": { + "@pothos/core": "^3.41.1", + "@urql/core": "^5.0.4", + "graphql-yoga": "^5.3.1", + "react": "^18.3.1", + "sst": "file:../../sdk/js", + "urql": "^4.1.0", + "wonka": "^6.3.4" + }, + "devDependencies": { + "@genql/cli": "^6.3.3", + "@types/aws-lambda": "8.10.138", + "@types/react": "^18.3.3" + } +} diff --git a/examples/aws-pothos-graphql/pothos/builder.ts b/examples/aws-pothos-graphql/pothos/builder.ts new file mode 100644 index 0000000000..3dc1195588 --- /dev/null +++ b/examples/aws-pothos-graphql/pothos/builder.ts @@ -0,0 +1,6 @@ +import SchemaBuilder from "@pothos/core"; + +export const builder = new SchemaBuilder({}); + +builder.queryType({}); +builder.mutationType({}); diff --git a/examples/aws-pothos-graphql/pothos/extract.ts b/examples/aws-pothos-graphql/pothos/extract.ts new file mode 100644 index 0000000000..0326d4a3fa --- /dev/null +++ b/examples/aws-pothos-graphql/pothos/extract.ts @@ -0,0 +1,38 @@ +import { lexicographicSortSchema, printSchema } from "graphql"; +import path from "path"; +import { schema } from "./schema"; + +async function extract() { + const schemaAsString = printSchema(lexicographicSortSchema(schema)); + + await Bun.write("./graphql/schema.graphql", schemaAsString); + + const proc = Bun.spawn( + [ + "bun", + "x", + "@genql/cli", + "--output", + "./genql", + "--schema", + "./schema.graphql", + "--esm", + ], + { + cwd: "./graphql", + } + ); + + const exitCode = await proc.exited; + if (exitCode !== 0) { + throw Error(`Genegration faild with code ${exitCode}`); + } +} + +extract() + .then(() => { + console.log("Pothos schema extracted successfully."); + }) + .catch((error) => { + console.error("Failed to extract pothos schema the database:", error); + }); diff --git a/examples/aws-pothos-graphql/pothos/graphql.ts b/examples/aws-pothos-graphql/pothos/graphql.ts new file mode 100644 index 0000000000..a11cb0cfb7 --- /dev/null +++ b/examples/aws-pothos-graphql/pothos/graphql.ts @@ -0,0 +1,6 @@ +import { schema } from "./schema"; +import { awsLambdaRequestHandler } from "./server"; + +export const handler = awsLambdaRequestHandler({ + schema, +}); diff --git a/examples/aws-pothos-graphql/pothos/schema.ts b/examples/aws-pothos-graphql/pothos/schema.ts new file mode 100644 index 0000000000..fb2921ffe5 --- /dev/null +++ b/examples/aws-pothos-graphql/pothos/schema.ts @@ -0,0 +1,5 @@ +import { builder } from "./builder"; + +import "./types/giraffe"; + +export const schema = builder.toSchema({}); diff --git a/examples/aws-pothos-graphql/pothos/server.ts b/examples/aws-pothos-graphql/pothos/server.ts new file mode 100644 index 0000000000..56536f2bc6 --- /dev/null +++ b/examples/aws-pothos-graphql/pothos/server.ts @@ -0,0 +1,53 @@ +import { + APIGatewayProxyEventV2, + APIGatewayProxyResult, + Context, +} from "aws-lambda"; +import { createYoga, YogaServerOptions } from "graphql-yoga"; + +type ServerContext = { + event: APIGatewayProxyEventV2; + context: Context; +}; + +export function awsLambdaRequestHandler( + options: YogaServerOptions +) { + const yoga = createYoga({ + ...options, + }); + + return async ( + event: APIGatewayProxyEventV2, + lambdaContext: Context + ): Promise => { + const parameters = new URLSearchParams( + (event.queryStringParameters as Record) || {} + ).toString(); + + const url = `${event.rawPath}?${parameters}`; + + const request: RequestInit = { + method: event.requestContext.http.method, + headers: event.headers as HeadersInit, + body: event.body + ? Buffer.from(event.body, event.isBase64Encoded ? "base64" : "utf8") + : undefined, + }; + + const serverContext: ServerContext = { + event, + context: lambdaContext, + }; + + const response = await yoga.fetch(url, request, serverContext); + const responseHeaders = Object.fromEntries(response.headers.entries()); + + return { + statusCode: response.status, + headers: responseHeaders, + body: await response.text(), + isBase64Encoded: false, + }; + }; +} diff --git a/examples/aws-pothos-graphql/pothos/types/giraffe.ts b/examples/aws-pothos-graphql/pothos/types/giraffe.ts new file mode 100644 index 0000000000..d0dbeab188 --- /dev/null +++ b/examples/aws-pothos-graphql/pothos/types/giraffe.ts @@ -0,0 +1,38 @@ +import { builder } from "../builder"; + +export class Giraffe { + name: string; + + constructor(name: string) { + this.name = name; + } +} + +builder.objectType(Giraffe, { + name: "Giraffe", + description: "Long necks, cool patterns, taller than you.", + fields: (t) => ({ + name: t.exposeString("name", {}), + }), +}); + +builder.queryFields((t) => ({ + giraffe: t.field({ + type: Giraffe, + resolve: () => new Giraffe("James"), + }), +})); + +builder.mutationFields((t) => ({ + createGiraffe: t.field({ + type: Giraffe, + args: { + name: t.arg.string({ required: true }), + }, + resolve: async (root, args) => { + const giraffe = { name: args.name }; + + return giraffe; + }, + }), +})); diff --git a/examples/aws-pothos-graphql/sst.config.ts b/examples/aws-pothos-graphql/sst.config.ts new file mode 100644 index 0000000000..ef154806d8 --- /dev/null +++ b/examples/aws-pothos-graphql/sst.config.ts @@ -0,0 +1,35 @@ +/// + +export default $config({ + app(input) { + return { + name: "aws-pothos-graphql", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + }; + }, + async run() { + if ($dev) { + new sst.x.DevCommand("PothosGraphqlExtractor", { + dev: { + command: "bun --watch run pothos/extract.ts", + autostart: true, + }, + }); + } + + const api = new sst.aws.ApiGatewayV2("Api"); + api.route("POST /graphql", "pothos/graphql.handler"); + + const client = new sst.aws.Function("Client", { + url: true, + link: [api], + handler: "client.handler", + }); + + return { + api: api.url, + client: client.url, + }; + }, +}); diff --git a/examples/aws-pothos-graphql/tsconfig.json b/examples/aws-pothos-graphql/tsconfig.json new file mode 100644 index 0000000000..aee0ec940f --- /dev/null +++ b/examples/aws-pothos-graphql/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "strict": true + } +} diff --git a/examples/aws-prisma-lambda/package.json b/examples/aws-prisma-lambda/package.json index 79d53d9615..423d683d52 100644 --- a/examples/aws-prisma-lambda/package.json +++ b/examples/aws-prisma-lambda/package.json @@ -19,6 +19,6 @@ }, "dependencies": { "@prisma/client": "^5.19.1", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-prisma/package.json b/examples/aws-prisma/package.json index 611010ee5d..6be9028453 100644 --- a/examples/aws-prisma/package.json +++ b/examples/aws-prisma/package.json @@ -19,6 +19,6 @@ "dependencies": { "@prisma/client": "^5.21.1", "express": "^4.21.1", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-prisma/sst-env.d.ts b/examples/aws-prisma/sst-env.d.ts deleted file mode 100644 index b462661ce6..0000000000 --- a/examples/aws-prisma/sst-env.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyPostgres": { - "database": string - "host": string - "password": string - "port": number - "type": "sst.aws.Postgres" - "username": string - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "bastion": string - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-puppeteer/package.json b/examples/aws-puppeteer/package.json index d09db393ba..892b488539 100644 --- a/examples/aws-puppeteer/package.json +++ b/examples/aws-puppeteer/package.json @@ -12,7 +12,7 @@ "dependencies": { "@sparticuz/chromium": "^127.0.0", "puppeteer-core": "^23.1.1", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "8.10.145" diff --git a/examples/aws-python-container/README.md b/examples/aws-python-container/README.md deleted file mode 100644 index da6518bfcc..0000000000 --- a/examples/aws-python-container/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# ❍ Python Example with Containers - -Deploy python applications using sst. - -SST uses [uv](https://github.com/astral-sh/uv) to manage your python runtime. If you do not have uv installed, you can install it [here](https://docs.astral.sh/uv/getting-started/installation/). Any sst workspace package can be built and deployed to aws lambda using sst. In this example we deploy an API handler to lambda from the `functions` directory. The handler depends on shared code from the `shared` directory using uv's workspaces feature. (Note: builds currently do not tree shake so lots of workspaces can make larger builds than necessary.) We also deploy another function from `custom-dockerfile` to show how you can use a custom Dockerfile to deploy your python code. - -Python functions can be deployed just like other SST functions, the only difference is that the functions themselves must be configured within a uv workspace, there is no drop-in-mode. - -```typescript title="sst.config.ts" -const python = new sst.aws.Function("MyPythonFunction", { - python: { container: true }, - handler: "functions/src/functions/api.handler", - runtime: "python3.11", - url: true -}); -``` - -If you are using live lambdas for your python functions, it is recommended to specify your python version to match your Lambda runtime otherwise you may encounter issues with dependencies. - -```toml title="src/pyproject.toml" -[project] -name = "aws-python" -version = "0.1.0" -description = "A SST app" -authors = [ - {name = "", email = "" }, -] -requires-python = "==3.11.*" -``` - -Live lambda will locally run your python code by building the workspace and running the specified handler. You can have multiple handlers in the same workspace and have multiple workspaces in the same project. - -```markdown -. -β”œβ”€β”€ workspace_a -β”‚ β”œβ”€β”€ pyproject.toml -β”‚ └── src -β”‚ └── workspace_a -β”‚ β”œβ”€β”€ __init__.py -β”‚ β”œβ”€β”€ api_a.py -β”‚ └── api_b.py -└── workspace_b - β”œβ”€β”€ pyproject.toml - └── src - └── workspace_b - β”œβ”€β”€ __init__.py - └── index.py -``` \ No newline at end of file diff --git a/examples/aws-python-container/core/sst.pyi b/examples/aws-python-container/core/sst.pyi deleted file mode 100644 index 0e0781a67c..0000000000 --- a/examples/aws-python-container/core/sst.pyi +++ /dev/null @@ -1,21 +0,0 @@ -# Automatically generated by SST -# pylint: disable=all -# ruff: noqa -from typing import Any - -class Resource: - class App: - name: str - stage: str - class MyLinkableValue: - foo: str - type: str - class PythonFn: - name: str - type: str - url: str - class PythonFnCustom: - name: str - type: str - url: str - diff --git a/examples/aws-python-container/custom_dockerfile/Dockerfile b/examples/aws-python-container/custom_dockerfile/Dockerfile index b8a66e3b4b..a8383a707c 100644 --- a/examples/aws-python-container/custom_dockerfile/Dockerfile +++ b/examples/aws-python-container/custom_dockerfile/Dockerfile @@ -5,21 +5,23 @@ ARG PYTHON_VERSION=3.11 FROM public.ecr.aws/lambda/python:${PYTHON_VERSION} # # Ensure git is installed so we can install git based dependencies (such as sst) -RUN dnf update -y && \ - dnf install -y git gcc && \ - dnf clean all +RUN if command -v dnf > /dev/null 2>&1; then \ + dnf update -y && dnf install -y git gcc && dnf clean all; \ + elif command -v yum > /dev/null 2>&1; then \ + yum install -y git gcc && yum clean all; \ + fi # Install UV to manage your python runtime COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv +# Copy everything first so workspace packages (referenced as ./pkg in requirements.txt) +# are available during dependency installation +COPY . ${LAMBDA_TASK_ROOT} + # Install the dependencies to the lambda runtime -COPY requirements.txt ${LAMBDA_TASK_ROOT}/requirements.txt RUN uv pip install -r requirements.txt --target ${LAMBDA_TASK_ROOT} --system # Perform any steps that you want here: # Example: pre-bake in model weights from huggingface to image -# Copy the rest of the code -COPY . ${LAMBDA_TASK_ROOT} - # No need to configure the handler or entrypoint - SST will do that diff --git a/examples/aws-python-container/custom_dockerfile/pyproject.toml b/examples/aws-python-container/custom_dockerfile/pyproject.toml index 95cd1259ec..feae2e5b61 100644 --- a/examples/aws-python-container/custom_dockerfile/pyproject.toml +++ b/examples/aws-python-container/custom_dockerfile/pyproject.toml @@ -2,7 +2,7 @@ name = "custom_dockerfile" version = "0.1.0" description = "Custom Dockerfile" -dependencies = ["core", "sst"] +dependencies = ["core", "sst-sdk"] requires-python = "==3.11.*" [build-system] @@ -11,4 +11,4 @@ build-backend = "hatchling.build" [tool.uv.sources] core = { workspace = true } -sst = { git = "https://github.com/sst/sst.git", branch = "dev", subdirectory = "sdk/python" } +sst-sdk = { git = "https://github.com/anomalyco/sst.git", branch = "dev", subdirectory = "sdk/python" } diff --git a/examples/aws-python-container/custom_dockerfile/sst.pyi b/examples/aws-python-container/custom_dockerfile/sst.pyi deleted file mode 100644 index 0e0781a67c..0000000000 --- a/examples/aws-python-container/custom_dockerfile/sst.pyi +++ /dev/null @@ -1,21 +0,0 @@ -# Automatically generated by SST -# pylint: disable=all -# ruff: noqa -from typing import Any - -class Resource: - class App: - name: str - stage: str - class MyLinkableValue: - foo: str - type: str - class PythonFn: - name: str - type: str - url: str - class PythonFnCustom: - name: str - type: str - url: str - diff --git a/examples/aws-python-container/functions/pyproject.toml b/examples/aws-python-container/functions/pyproject.toml index 08486392c4..8eefe8e19d 100644 --- a/examples/aws-python-container/functions/pyproject.toml +++ b/examples/aws-python-container/functions/pyproject.toml @@ -2,7 +2,7 @@ name = "functions" version = "0.1.0" description = "Lambda function (container-mode)handlers" -dependencies = ["core", "sst"] +dependencies = ["core", "sst-sdk"] requires-python = "==3.11.*" [build-system] @@ -11,4 +11,4 @@ build-backend = "hatchling.build" [tool.uv.sources] core = { workspace = true } -sst = { git = "https://github.com/sst/sst.git", branch = "dev", subdirectory = "sdk/python" } +sst-sdk = { git = "https://github.com/anomalyco/sst.git", branch = "dev", subdirectory = "sdk/python" } diff --git a/examples/aws-python-container/functions/sst.pyi b/examples/aws-python-container/functions/sst.pyi deleted file mode 100644 index 0e0781a67c..0000000000 --- a/examples/aws-python-container/functions/sst.pyi +++ /dev/null @@ -1,21 +0,0 @@ -# Automatically generated by SST -# pylint: disable=all -# ruff: noqa -from typing import Any - -class Resource: - class App: - name: str - stage: str - class MyLinkableValue: - foo: str - type: str - class PythonFn: - name: str - type: str - url: str - class PythonFnCustom: - name: str - type: str - url: str - diff --git a/examples/aws-python-container/pyproject.toml b/examples/aws-python-container/pyproject.toml index a87e9dd1f5..4cd1c70628 100644 --- a/examples/aws-python-container/pyproject.toml +++ b/examples/aws-python-container/pyproject.toml @@ -13,4 +13,4 @@ requires-python = "==3.11.*" members = ["functions", "core", "custom_dockerfile"] [tool.uv.sources] -sst = { git = "https://github.com/sst/sst.git", subdirectory = "sdk/python", branch = "dev" } +sst-sdk = { git = "https://github.com/anomalyco/sst.git", subdirectory = "sdk/python", branch = "dev" } diff --git a/examples/aws-python-container/sst-env.d.ts b/examples/aws-python-container/sst-env.d.ts deleted file mode 100644 index 360ef54296..0000000000 --- a/examples/aws-python-container/sst-env.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - "MyPythonFunction": { - "name": string - "type": "sst.aws.Function" - "url": string - } - } -} -export {} diff --git a/examples/aws-python-container/sst.pyi b/examples/aws-python-container/sst.pyi deleted file mode 100644 index 0e0781a67c..0000000000 --- a/examples/aws-python-container/sst.pyi +++ /dev/null @@ -1,21 +0,0 @@ -# Automatically generated by SST -# pylint: disable=all -# ruff: noqa -from typing import Any - -class Resource: - class App: - name: str - stage: str - class MyLinkableValue: - foo: str - type: str - class PythonFn: - name: str - type: str - url: str - class PythonFnCustom: - name: str - type: str - url: str - diff --git a/examples/aws-python-container/uv.lock b/examples/aws-python-container/uv.lock deleted file mode 100644 index 94d4fc62e2..0000000000 --- a/examples/aws-python-container/uv.lock +++ /dev/null @@ -1,191 +0,0 @@ -version = 1 -requires-python = "==3.11.*" - -[manifest] -members = [ - "aws-python-container", - "core", - "custom-dockerfile", - "functions", -] - -[[package]] -name = "aws-python-container" -version = "0.1.0" -source = { virtual = "." } - -[[package]] -name = "certifi" -version = "2024.8.30" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, -] - -[[package]] -name = "cffi" -version = "1.17.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, - { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, - { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, - { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, - { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, - { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, - { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, - { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, - { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, - { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, - { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, - { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, - { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, - { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, - { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, - { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, -] - -[[package]] -name = "core" -version = "0.1.0" -source = { editable = "core" } -dependencies = [ - { name = "requests" }, -] - -[package.metadata] -requires-dist = [{ name = "requests", specifier = ">=2.32.3" }] - -[[package]] -name = "cryptography" -version = "43.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", size = 686989 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/f3/01fdf26701a26f4b4dbc337a26883ad5bccaa6f1bbbdd29cd89e22f18a1c/cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", size = 6225303 }, - { url = "https://files.pythonhosted.org/packages/a3/01/4896f3d1b392025d4fcbecf40fdea92d3df8662123f6835d0af828d148fd/cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", size = 3760905 }, - { url = "https://files.pythonhosted.org/packages/0a/be/f9a1f673f0ed4b7f6c643164e513dbad28dd4f2dcdf5715004f172ef24b6/cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", size = 3977271 }, - { url = "https://files.pythonhosted.org/packages/4e/49/80c3a7b5514d1b416d7350830e8c422a4d667b6d9b16a9392ebfd4a5388a/cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", size = 3746606 }, - { url = "https://files.pythonhosted.org/packages/0e/16/a28ddf78ac6e7e3f25ebcef69ab15c2c6be5ff9743dd0709a69a4f968472/cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", size = 3986484 }, - { url = "https://files.pythonhosted.org/packages/01/f5/69ae8da70c19864a32b0315049866c4d411cce423ec169993d0434218762/cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", size = 3852131 }, - { url = "https://files.pythonhosted.org/packages/fd/db/e74911d95c040f9afd3612b1f732e52b3e517cb80de8bf183be0b7d413c6/cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", size = 4075647 }, - { url = "https://files.pythonhosted.org/packages/56/48/7b6b190f1462818b324e674fa20d1d5ef3e24f2328675b9b16189cbf0b3c/cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", size = 2623873 }, - { url = "https://files.pythonhosted.org/packages/eb/b1/0ebff61a004f7f89e7b65ca95f2f2375679d43d0290672f7713ee3162aff/cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", size = 3068039 }, - { url = "https://files.pythonhosted.org/packages/30/d5/c8b32c047e2e81dd172138f772e81d852c51f0f2ad2ae8a24f1122e9e9a7/cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", size = 6222984 }, - { url = "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", size = 3762968 }, - { url = "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", size = 3977754 }, - { url = "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", size = 3749458 }, - { url = "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", size = 3988220 }, - { url = "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", size = 3853898 }, - { url = "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", size = 4076592 }, - { url = "https://files.pythonhosted.org/packages/81/1e/ffcc41b3cebd64ca90b28fd58141c5f68c83d48563c88333ab660e002cd3/cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", size = 2623145 }, - { url = "https://files.pythonhosted.org/packages/87/5c/3dab83cc4aba1f4b0e733e3f0c3e7d4386440d660ba5b1e3ff995feb734d/cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", size = 3068026 }, -] - -[[package]] -name = "custom-dockerfile" -version = "0.1.0" -source = { editable = "custom_dockerfile" } -dependencies = [ - { name = "core" }, - { name = "sst" }, -] - -[package.metadata] -requires-dist = [ - { name = "core", editable = "core" }, - { name = "sst", git = "https://github.com/sst/sst.git?subdirectory=sdk%2Fpython&branch=dev" }, -] - -[[package]] -name = "functions" -version = "0.1.0" -source = { editable = "functions" } -dependencies = [ - { name = "core" }, - { name = "sst" }, -] - -[package.metadata] -requires-dist = [ - { name = "core", editable = "core" }, - { name = "sst", git = "https://github.com/sst/sst.git?subdirectory=sdk%2Fpython&branch=dev" }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, -] - -[[package]] -name = "pycparser" -version = "2.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, -] - -[[package]] -name = "requests" -version = "2.32.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, -] - -[[package]] -name = "sst" -version = "0.2.0" -source = { git = "https://github.com/sst/sst.git?subdirectory=sdk%2Fpython&branch=dev#e335fd9a3f41ee9cf5e12db5d85fa49e00175cde" } -dependencies = [ - { name = "cryptography" }, -] - -[[package]] -name = "urllib3" -version = "2.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, -] diff --git a/examples/aws-python-huggingface/README.md b/examples/aws-python-huggingface/README.md deleted file mode 100644 index f505e0430f..0000000000 --- a/examples/aws-python-huggingface/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# ❍ Using Hugging Face Models with Python - -Deploy lightweight huggingface models using sst on AWS Lambda. - -This example uses the [transformers](https://github.com/huggingface/transformers) library to generate text using the [TinyStories-33M](https://huggingface.co/roneneldan/TinyStories-33M) model. The backend is the pytorch cpu runtime. This example also shows how it is possible to use custom index resolution to get dependencies from a private pypi server such as the pytorch cpu link. This example also shows how to use a custom Dockerfile to handle complex builds such as installing pytorch and pruning the build size. - -Note that this is not a production ready example. \ No newline at end of file diff --git a/examples/aws-python-huggingface/functions/Dockerfile b/examples/aws-python-huggingface/functions/Dockerfile index 4d2de865da..b7f8c97a6e 100644 --- a/examples/aws-python-huggingface/functions/Dockerfile +++ b/examples/aws-python-huggingface/functions/Dockerfile @@ -22,13 +22,13 @@ ENV PATH="/root/.cargo/bin:${PATH}" # Install UV to manage your python runtime COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv +# Copy everything first so workspace package references (e.g., ./functions) +# in requirements.txt resolve correctly during installation +COPY . ${LAMBDA_TASK_ROOT} + # Install the dependencies to the lambda runtime -COPY requirements.txt ${LAMBDA_TASK_ROOT}/requirements.txt RUN uv pip install -r requirements.txt --target ${LAMBDA_TASK_ROOT} --system --no-verify-hashes -# Copy the rest of the code -COPY . ${LAMBDA_TASK_ROOT} - FROM public.ecr.aws/lambda/python:${PYTHON_VERSION} ENV HF_HOME=/tmp/transformers_cache diff --git a/examples/aws-python-huggingface/functions/sst.pyi b/examples/aws-python-huggingface/functions/sst.pyi deleted file mode 100644 index 5476298f36..0000000000 --- a/examples/aws-python-huggingface/functions/sst.pyi +++ /dev/null @@ -1,14 +0,0 @@ -# Automatically generated by SST -# pylint: disable=all -# ruff: noqa -from typing import Any - -class Resource: - class App: - name: str - stage: str - class MyPythonFunction: - name: str - type: str - url: str - diff --git a/examples/aws-python-huggingface/pyproject.toml b/examples/aws-python-huggingface/pyproject.toml index ac79a5eddb..fb335d9b8f 100644 --- a/examples/aws-python-huggingface/pyproject.toml +++ b/examples/aws-python-huggingface/pyproject.toml @@ -13,4 +13,4 @@ requires-python = "==3.12.*" members = ["functions"] [tool.uv.sources] -sst = { git = "https://github.com/sst/sst.git", subdirectory = "sdk/python", branch = "dev" } +sst-sdk = { git = "https://github.com/anomalyco/sst.git", subdirectory = "sdk/python", branch = "dev" } diff --git a/examples/aws-python-huggingface/sst-env.d.ts b/examples/aws-python-huggingface/sst-env.d.ts deleted file mode 100644 index 360ef54296..0000000000 --- a/examples/aws-python-huggingface/sst-env.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - "MyPythonFunction": { - "name": string - "type": "sst.aws.Function" - "url": string - } - } -} -export {} diff --git a/examples/aws-python-huggingface/sst.config.ts b/examples/aws-python-huggingface/sst.config.ts index 2b89b56097..14d3b52c6e 100644 --- a/examples/aws-python-huggingface/sst.config.ts +++ b/examples/aws-python-huggingface/sst.config.ts @@ -10,7 +10,7 @@ * generate text using the * [TinyStories-33M](https://huggingface.co/roneneldan/TinyStories-33M) model. The * backend is the pytorch cpu runtime. - * + * * :::note * This is not a production ready example. * ::: @@ -21,22 +21,23 @@ * as installing pytorch and pruning the build size. */ export default $config({ - app(input) { - return { - name: "aws-python-huggingface", - removal: input?.stage === "production" ? "retain" : "remove", - home: "aws", - }; - }, - async run() { - new sst.aws.Function("MyPythonFunction", { - python: { - container: true, - }, - handler: "functions/src/functions/api.handler", - runtime: "python3.12", - timeout: "60 seconds", - url: true, - }); - }, + app(input) { + return { + name: "aws-python-huggingface", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + }; + }, + async run() { + new sst.aws.Function("PythonFunction", { + python: { + container: true, + }, + handler: "functions/src/functions/api.handler", + runtime: "python3.12", + memory: "2048 MB", + timeout: "120 seconds", + url: true, + }); + }, }); diff --git a/examples/aws-python-huggingface/sst.pyi b/examples/aws-python-huggingface/sst.pyi deleted file mode 100644 index 5476298f36..0000000000 --- a/examples/aws-python-huggingface/sst.pyi +++ /dev/null @@ -1,14 +0,0 @@ -# Automatically generated by SST -# pylint: disable=all -# ruff: noqa -from typing import Any - -class Resource: - class App: - name: str - stage: str - class MyPythonFunction: - name: str - type: str - url: str - diff --git a/examples/aws-python-huggingface/uv.lock b/examples/aws-python-huggingface/uv.lock deleted file mode 100644 index b71eb74496..0000000000 --- a/examples/aws-python-huggingface/uv.lock +++ /dev/null @@ -1,547 +0,0 @@ -version = 1 -requires-python = "==3.12.*" -resolution-markers = [ - "platform_system != 'Darwin'", - "platform_system == 'Darwin'", -] - -[manifest] -members = [ - "aws-python-huggingface", - "functions", -] - -[[package]] -name = "aws-python-huggingface" -version = "0.1.0" -source = { virtual = "." } - -[[package]] -name = "certifi" -version = "2024.8.30" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, - { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, - { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, - { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, - { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, - { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, - { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, - { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, - { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, - { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, - { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, - { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, - { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, - { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, - { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, - { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, -] - -[[package]] -name = "filelock" -version = "3.16.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, -] - -[[package]] -name = "fsspec" -version = "2024.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a0/52/f16a068ebadae42526484c31f4398e62962504e5724a8ba5dc3409483df2/fsspec-2024.10.0.tar.gz", hash = "sha256:eda2d8a4116d4f2429db8550f2457da57279247dd930bb12f821b58391359493", size = 286853 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/b2/454d6e7f0158951d8a78c2e1eb4f69ae81beb8dca5fee9809c6c99e9d0d0/fsspec-2024.10.0-py3-none-any.whl", hash = "sha256:03b9a6785766a4de40368b88906366755e2819e758b83705c88cd7cb5fe81871", size = 179641 }, -] - -[[package]] -name = "functions" -version = "0.1.0" -source = { editable = "functions" } -dependencies = [ - { name = "torch", version = "2.3.1", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "platform_system != 'Darwin'" }, - { name = "torch", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "platform_system == 'Darwin'" }, - { name = "transformers" }, -] - -[package.metadata] -requires-dist = [ - { name = "torch", marker = "platform_system != 'Darwin'", specifier = "==2.3.1", index = "https://download.pytorch.org/whl/cpu" }, - { name = "torch", marker = "platform_system == 'Darwin'", specifier = "==2.3.1" }, - { name = "transformers", specifier = ">=4.44.2" }, -] - -[[package]] -name = "huggingface-hub" -version = "0.26.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "fsspec" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "tqdm" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d5/a8/882ae5d1cfa7c9c5be32feee4cee56d9873078913953423e47a756da110d/huggingface_hub-0.26.2.tar.gz", hash = "sha256:b100d853465d965733964d123939ba287da60a547087783ddff8a323f340332b", size = 375621 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/bf/cea0b9720c32fa01b0c4ec4b16b9f4ae34ca106b202ebbae9f03ab98cd8f/huggingface_hub-0.26.2-py3-none-any.whl", hash = "sha256:98c2a5a8e786c7b2cb6fdeb2740893cba4d53e312572ed3d8afafda65b128c46", size = 447536 }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, -] - -[[package]] -name = "intel-openmp" -version = "2021.4.0" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/45/18/527f247d673ff84c38e0b353b6901539b99e83066cd505be42ad341ab16d/intel_openmp-2021.4.0-py2.py3-none-win32.whl", hash = "sha256:6e863d8fd3d7e8ef389d52cf97a50fe2afe1a19247e8c0d168ce021546f96fc9", size = 1860605 }, - { url = "https://files.pythonhosted.org/packages/6f/21/b590c0cc3888b24f2ac9898c41d852d7454a1695fbad34bee85dba6dc408/intel_openmp-2021.4.0-py2.py3-none-win_amd64.whl", hash = "sha256:eef4c8bcc8acefd7f5cd3b9384dbf73d59e2c99fc56545712ded913f43c4a94f", size = 3516906 }, -] - -[[package]] -name = "jinja2" -version = "3.1.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, -] - -[[package]] -name = "markupsafe" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, -] - -[[package]] -name = "mkl" -version = "2021.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "intel-openmp", marker = "platform_system != 'Darwin'" }, - { name = "tbb", marker = "platform_system != 'Darwin'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/c6/892fe3bc91e811b78e4f85653864f2d92541d5e5c306b0cb3c2311e9ca64/mkl-2021.4.0-py2.py3-none-win32.whl", hash = "sha256:439c640b269a5668134e3dcbcea4350459c4a8bc46469669b2d67e07e3d330e8", size = 129048357 }, - { url = "https://files.pythonhosted.org/packages/fe/1c/5f6dbf18e8b73e0a5472466f0ea8d48ce9efae39bd2ff38cebf8dce61259/mkl-2021.4.0-py2.py3-none-win_amd64.whl", hash = "sha256:ceef3cafce4c009dd25f65d7ad0d833a0fbadc3d8903991ec92351fe5de1e718", size = 228499609 }, -] - -[[package]] -name = "mpmath" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, -] - -[[package]] -name = "networkx" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263 }, -] - -[[package]] -name = "numpy" -version = "2.1.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/ca/1166b75c21abd1da445b97bf1fa2f14f423c6cfb4fc7c4ef31dccf9f6a94/numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", size = 20166090 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/f0/385eb9970309643cbca4fc6eebc8bb16e560de129c91258dfaa18498da8b/numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e", size = 20849658 }, - { url = "https://files.pythonhosted.org/packages/54/4a/765b4607f0fecbb239638d610d04ec0a0ded9b4951c56dc68cef79026abf/numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958", size = 13492258 }, - { url = "https://files.pythonhosted.org/packages/bd/a7/2332679479c70b68dccbf4a8eb9c9b5ee383164b161bee9284ac141fbd33/numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8", size = 5090249 }, - { url = "https://files.pythonhosted.org/packages/c1/67/4aa00316b3b981a822c7a239d3a8135be2a6945d1fd11d0efb25d361711a/numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564", size = 6621704 }, - { url = "https://files.pythonhosted.org/packages/5e/da/1a429ae58b3b6c364eeec93bf044c532f2ff7b48a52e41050896cf15d5b1/numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512", size = 13606089 }, - { url = "https://files.pythonhosted.org/packages/9e/3e/3757f304c704f2f0294a6b8340fcf2be244038be07da4cccf390fa678a9f/numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b", size = 16043185 }, - { url = "https://files.pythonhosted.org/packages/43/97/75329c28fea3113d00c8d2daf9bc5828d58d78ed661d8e05e234f86f0f6d/numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc", size = 16410751 }, - { url = "https://files.pythonhosted.org/packages/ad/7a/442965e98b34e0ae9da319f075b387bcb9a1e0658276cc63adb8c9686f7b/numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0", size = 14082705 }, - { url = "https://files.pythonhosted.org/packages/ac/b6/26108cf2cfa5c7e03fb969b595c93131eab4a399762b51ce9ebec2332e80/numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9", size = 6239077 }, - { url = "https://files.pythonhosted.org/packages/a6/84/fa11dad3404b7634aaab50733581ce11e5350383311ea7a7010f464c0170/numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a", size = 12566858 }, -] - -[[package]] -name = "nvidia-cublas-cu12" -version = "12.1.3.1" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/37/6d/121efd7382d5b0284239f4ab1fc1590d86d34ed4a4a2fdb13b30ca8e5740/nvidia_cublas_cu12-12.1.3.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ee53ccca76a6fc08fb9701aa95b6ceb242cdaab118c3bb152af4e579af792728", size = 410594774 }, -] - -[[package]] -name = "nvidia-cuda-cupti-cu12" -version = "12.1.105" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/00/6b218edd739ecfc60524e585ba8e6b00554dd908de2c9c66c1af3e44e18d/nvidia_cuda_cupti_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:e54fde3983165c624cb79254ae9818a456eb6e87a7fd4d56a2352c24ee542d7e", size = 14109015 }, -] - -[[package]] -name = "nvidia-cuda-nvrtc-cu12" -version = "12.1.105" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/9f/c64c03f49d6fbc56196664d05dba14e3a561038a81a638eeb47f4d4cfd48/nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:339b385f50c309763ca65456ec75e17bbefcbbf2893f462cb8b90584cd27a1c2", size = 23671734 }, -] - -[[package]] -name = "nvidia-cuda-runtime-cu12" -version = "12.1.105" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/d5/c68b1d2cdfcc59e72e8a5949a37ddb22ae6cade80cd4a57a84d4c8b55472/nvidia_cuda_runtime_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:6e258468ddf5796e25f1dc591a31029fa317d97a0a94ed93468fc86301d61e40", size = 823596 }, -] - -[[package]] -name = "nvidia-cudnn-cu12" -version = "8.9.2.26" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nvidia-cublas-cu12", marker = "platform_system != 'Darwin'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/74/a2e2be7fb83aaedec84f391f082cf765dfb635e7caa9b49065f73e4835d8/nvidia_cudnn_cu12-8.9.2.26-py3-none-manylinux1_x86_64.whl", hash = "sha256:5ccb288774fdfb07a7e7025ffec286971c06d8d7b4fb162525334616d7629ff9", size = 731725872 }, -] - -[[package]] -name = "nvidia-cufft-cu12" -version = "11.0.2.54" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/86/94/eb540db023ce1d162e7bea9f8f5aa781d57c65aed513c33ee9a5123ead4d/nvidia_cufft_cu12-11.0.2.54-py3-none-manylinux1_x86_64.whl", hash = "sha256:794e3948a1aa71fd817c3775866943936774d1c14e7628c74f6f7417224cdf56", size = 121635161 }, -] - -[[package]] -name = "nvidia-curand-cu12" -version = "10.3.2.106" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/31/4890b1c9abc496303412947fc7dcea3d14861720642b49e8ceed89636705/nvidia_curand_cu12-10.3.2.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:9d264c5036dde4e64f1de8c50ae753237c12e0b1348738169cd0f8a536c0e1e0", size = 56467784 }, -] - -[[package]] -name = "nvidia-cusolver-cu12" -version = "11.4.5.107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nvidia-cublas-cu12", marker = "platform_system != 'Darwin'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_system != 'Darwin'" }, - { name = "nvidia-nvjitlink-cu12", marker = "platform_system != 'Darwin'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/1d/8de1e5c67099015c834315e333911273a8c6aaba78923dd1d1e25fc5f217/nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd", size = 124161928 }, -] - -[[package]] -name = "nvidia-cusparse-cu12" -version = "12.1.0.106" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "platform_system != 'Darwin'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/65/5b/cfaeebf25cd9fdec14338ccb16f6b2c4c7fa9163aefcf057d86b9cc248bb/nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c", size = 195958278 }, -] - -[[package]] -name = "nvidia-nccl-cu12" -version = "2.20.5" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/bb/d09dda47c881f9ff504afd6f9ca4f502ded6d8fc2f572cacc5e39da91c28/nvidia_nccl_cu12-2.20.5-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1fc150d5c3250b170b29410ba682384b14581db722b2531b0d8d33c595f33d01", size = 176238458 }, - { url = "https://files.pythonhosted.org/packages/4b/2a/0a131f572aa09f741c30ccd45a8e56316e8be8dfc7bc19bf0ab7cfef7b19/nvidia_nccl_cu12-2.20.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:057f6bf9685f75215d0c53bf3ac4a10b3e6578351de307abad9e18a99182af56", size = 176249402 }, -] - -[[package]] -name = "nvidia-nvjitlink-cu12" -version = "12.6.85" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/d7/c5383e47c7e9bf1c99d5bd2a8c935af2b6d705ad831a7ec5c97db4d82f4f/nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:eedc36df9e88b682efe4309aa16b5b4e78c2407eac59e8c10a6a47535164369a", size = 19744971 }, - { url = "https://files.pythonhosted.org/packages/31/db/dc71113d441f208cdfe7ae10d4983884e13f464a6252450693365e166dcf/nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cf4eaa7d4b6b543ffd69d6abfb11efdeb2db48270d94dfd3a452c24150829e41", size = 19270338 }, -] - -[[package]] -name = "nvidia-nvtx-cu12" -version = "12.1.105" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/d3/8057f0587683ed2fcd4dbfbdfdfa807b9160b809976099d36b8f60d08f03/nvidia_nvtx_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:dc21cf308ca5691e7c04d962e213f8a4aa9bbfa23d95412f452254c2caeb09e5", size = 99138 }, -] - -[[package]] -name = "packaging" -version = "24.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, -] - -[[package]] -name = "regex" -version = "2024.11.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, - { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, - { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, - { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, - { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, - { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, - { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, - { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, - { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, - { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, - { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, - { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, - { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, - { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, - { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, -] - -[[package]] -name = "requests" -version = "2.32.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, -] - -[[package]] -name = "safetensors" -version = "0.4.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cb/46/a1c56ed856c6ac3b1a8b37abe5be0cac53219367af1331e721b04d122577/safetensors-0.4.5.tar.gz", hash = "sha256:d73de19682deabb02524b3d5d1f8b3aaba94c72f1bbfc7911b9b9d5d391c0310", size = 65702 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/ac/5a63082f931e99200db95fd46fb6734f050bb6e96bf02521904c6518b7aa/safetensors-0.4.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:473300314e026bd1043cef391bb16a8689453363381561b8a3e443870937cc1e", size = 392015 }, - { url = "https://files.pythonhosted.org/packages/73/95/ab32aa6e9bdc832ff87784cdf9da26192b93de3ef82b8d1ada8f345c5044/safetensors-0.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:801183a0f76dc647f51a2d9141ad341f9665602a7899a693207a82fb102cc53e", size = 381774 }, - { url = "https://files.pythonhosted.org/packages/d6/6c/7e04b7626809fc63f3698f4c50e43aff2864b40089aa4506c918a75b8eed/safetensors-0.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1524b54246e422ad6fb6aea1ac71edeeb77666efa67230e1faf6999df9b2e27f", size = 441134 }, - { url = "https://files.pythonhosted.org/packages/58/2b/ffe7c86a277e6c1595fbdf415cfe2903f253f574a5405e93fda8baaa582c/safetensors-0.4.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3139098e3e8b2ad7afbca96d30ad29157b50c90861084e69fcb80dec7430461", size = 438467 }, - { url = "https://files.pythonhosted.org/packages/67/9c/f271bd804e08c7fda954d17b70ff281228a88077337a9e70feace4f4cc93/safetensors-0.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65573dc35be9059770808e276b017256fa30058802c29e1038eb1c00028502ea", size = 476566 }, - { url = "https://files.pythonhosted.org/packages/4c/ad/4cf76a3e430a8a26108407fa6cb93e6f80d996a5cb75d9540c8fe3862990/safetensors-0.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd33da8e9407559f8779c82a0448e2133737f922d71f884da27184549416bfed", size = 492253 }, - { url = "https://files.pythonhosted.org/packages/d9/40/a6f75ea449a9647423ec8b6f72c16998d35aa4b43cb38536ac060c5c7bf5/safetensors-0.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3685ce7ed036f916316b567152482b7e959dc754fcc4a8342333d222e05f407c", size = 434769 }, - { url = "https://files.pythonhosted.org/packages/52/47/d4b49b1231abf3131f7bb0bc60ebb94b27ee33e0a1f9569da05f8ac65dee/safetensors-0.4.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dde2bf390d25f67908278d6f5d59e46211ef98e44108727084d4637ee70ab4f1", size = 457166 }, - { url = "https://files.pythonhosted.org/packages/c3/cd/006468b03b0fa42ff82d795d47c4193e99001e96c3f08bd62ef1b5cab586/safetensors-0.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7469d70d3de970b1698d47c11ebbf296a308702cbaae7fcb993944751cf985f4", size = 619280 }, - { url = "https://files.pythonhosted.org/packages/22/4d/b6208d918e83daa84b424c0ac3191ae61b44b3191613a3a5a7b38f94b8ad/safetensors-0.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a6ba28118636a130ccbb968bc33d4684c48678695dba2590169d5ab03a45646", size = 605390 }, - { url = "https://files.pythonhosted.org/packages/e8/20/bf0e01825dc01ed75538021a98b9a046e60ead63c6c6700764c821a8c873/safetensors-0.4.5-cp312-none-win32.whl", hash = "sha256:c859c7ed90b0047f58ee27751c8e56951452ed36a67afee1b0a87847d065eec6", size = 273250 }, - { url = "https://files.pythonhosted.org/packages/f1/5f/ab6b6cec85b40789801f35b7d2fb579ae242d8193929974a106d5ff5c835/safetensors-0.4.5-cp312-none-win_amd64.whl", hash = "sha256:b5a8810ad6a6f933fff6c276eae92c1da217b39b4d8b1bc1c0b8af2d270dc532", size = 286307 }, -] - -[[package]] -name = "sympy" -version = "1.13.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mpmath" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ca/99/5a5b6f19ff9f083671ddf7b9632028436167cd3d33e11015754e41b249a4/sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f", size = 7533040 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/fe/81695a1aa331a842b582453b605175f419fe8540355886031328089d840a/sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8", size = 6189177 }, -] - -[[package]] -name = "tbb" -version = "2021.13.1" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/8a/5062b00c378c051e26507e5eca8d3b5c91ed63f8a2139f6f0f422be84b02/tbb-2021.13.1-py3-none-win32.whl", hash = "sha256:00f5e5a70051650ddd0ab6247c0549521968339ec21002e475cd23b1cbf46d66", size = 248994 }, - { url = "https://files.pythonhosted.org/packages/9b/24/84ce997e8ae6296168a74d0d9c4dde572d90fb23fd7c0b219c30ff71e00e/tbb-2021.13.1-py3-none-win_amd64.whl", hash = "sha256:cbf024b2463fdab3ebe3fa6ff453026358e6b903839c80d647e08ad6d0796ee9", size = 286908 }, -] - -[[package]] -name = "tokenizers" -version = "0.20.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "huggingface-hub" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/25/b1681c1c30ea3ea6e584ae3fffd552430b12faa599b558c4c4783f56d7ff/tokenizers-0.20.3.tar.gz", hash = "sha256:2278b34c5d0dd78e087e1ca7f9b1dcbf129d80211afa645f214bd6e051037539", size = 340513 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/00/92a08af2a6b0c88c50f1ab47d7189e695722ad9714b0ee78ea5e1e2e1def/tokenizers-0.20.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:49d12a32e190fad0e79e5bdb788d05da2f20d8e006b13a70859ac47fecf6ab2f", size = 2667951 }, - { url = "https://files.pythonhosted.org/packages/ec/9a/e17a352f0bffbf415cf7d73756f5c73a3219225fc5957bc2f39d52c61684/tokenizers-0.20.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:282848cacfb9c06d5e51489f38ec5aa0b3cd1e247a023061945f71f41d949d73", size = 2555167 }, - { url = "https://files.pythonhosted.org/packages/27/37/d108df55daf4f0fcf1f58554692ff71687c273d870a34693066f0847be96/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abe4e08c7d0cd6154c795deb5bf81d2122f36daf075e0c12a8b050d824ef0a64", size = 2898389 }, - { url = "https://files.pythonhosted.org/packages/b2/27/32f29da16d28f59472fa7fb38e7782069748c7e9ab9854522db20341624c/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca94fc1b73b3883c98f0c88c77700b13d55b49f1071dfd57df2b06f3ff7afd64", size = 2795866 }, - { url = "https://files.pythonhosted.org/packages/29/4e/8a9a3c89e128c4a40f247b501c10279d2d7ade685953407c4d94c8c0f7a7/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef279c7e239f95c8bdd6ff319d9870f30f0d24915b04895f55b1adcf96d6c60d", size = 3085446 }, - { url = "https://files.pythonhosted.org/packages/b4/3b/a2a7962c496ebcd95860ca99e423254f760f382cd4bd376f8895783afaf5/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16384073973f6ccbde9852157a4fdfe632bb65208139c9d0c0bd0176a71fd67f", size = 3094378 }, - { url = "https://files.pythonhosted.org/packages/1f/f4/a8a33f0192a1629a3bd0afcad17d4d221bbf9276da4b95d226364208d5eb/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:312d522caeb8a1a42ebdec87118d99b22667782b67898a76c963c058a7e41d4f", size = 3385755 }, - { url = "https://files.pythonhosted.org/packages/9e/65/c83cb3545a65a9eaa2e13b22c93d5e00bd7624b354a44adbdc93d5d9bd91/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2b7cb962564785a83dafbba0144ecb7f579f1d57d8c406cdaa7f32fe32f18ad", size = 2997679 }, - { url = "https://files.pythonhosted.org/packages/55/e9/a80d4e592307688a67c7c59ab77e03687b6a8bd92eb5db763a2c80f93f57/tokenizers-0.20.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:124c5882ebb88dadae1fc788a582299fcd3a8bd84fc3e260b9918cf28b8751f5", size = 8989296 }, - { url = "https://files.pythonhosted.org/packages/90/af/60c957af8d2244321124e893828f1a4817cde1a2d08d09d423b73f19bd2f/tokenizers-0.20.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2b6e54e71f84c4202111a489879005cb14b92616a87417f6c102c833af961ea2", size = 9303621 }, - { url = "https://files.pythonhosted.org/packages/be/a9/96172310ee141009646d63a1ca267c099c462d747fe5ef7e33f74e27a683/tokenizers-0.20.3-cp312-none-win32.whl", hash = "sha256:83d9bfbe9af86f2d9df4833c22e94d94750f1d0cd9bfb22a7bb90a86f61cdb1c", size = 2188979 }, - { url = "https://files.pythonhosted.org/packages/bd/68/61d85ae7ae96dde7d0974ff3538db75d5cdc29be2e4329cd7fc51a283e22/tokenizers-0.20.3-cp312-none-win_amd64.whl", hash = "sha256:44def74cee574d609a36e17c8914311d1b5dbcfe37c55fd29369d42591b91cf2", size = 2380725 }, -] - -[[package]] -name = "torch" -version = "2.3.1" -source = { registry = "https://download.pytorch.org/whl/cpu" } -resolution-markers = [ - "platform_system != 'Darwin'", -] -dependencies = [ - { name = "filelock", marker = "platform_system != 'Darwin'" }, - { name = "fsspec", marker = "platform_system != 'Darwin'" }, - { name = "jinja2", marker = "platform_system != 'Darwin'" }, - { name = "mkl", marker = "platform_system == 'Windows'" }, - { name = "networkx", marker = "platform_system != 'Darwin'" }, - { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "sympy", marker = "platform_system != 'Darwin'" }, - { name = "typing-extensions", marker = "platform_system != 'Darwin'" }, -] -wheels = [ - { url = "https://download.pytorch.org/whl/cpu/torch-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77d2de1a495a1c07f592c338a0d592e55cc0b9d2f800309e46a0ea2c0e3a2919" }, -] - -[[package]] -name = "torch" -version = "2.3.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "platform_system == 'Darwin'", -] -dependencies = [ - { name = "filelock", marker = "platform_system == 'Darwin'" }, - { name = "fsspec", marker = "platform_system == 'Darwin'" }, - { name = "jinja2", marker = "platform_system == 'Darwin'" }, - { name = "networkx", marker = "platform_system == 'Darwin'" }, - { name = "sympy", marker = "platform_system == 'Darwin'" }, - { name = "typing-extensions", marker = "platform_system == 'Darwin'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/b6/1a2e3d43d4bc4ad7a4575b3745d707a68d5ed00ba263b205b6281bdd0921/torch-2.3.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:3c333dc2ebc189561514eda06e81df22bf8fb64e2384746b2cb9f04f96d1d4c8", size = 60978559 }, -] - -[[package]] -name = "tqdm" -version = "4.67.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, -] - -[[package]] -name = "transformers" -version = "4.46.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "huggingface-hub" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "regex" }, - { name = "requests" }, - { name = "safetensors" }, - { name = "tokenizers" }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/37/5a/58f96c83e566f907ae39f16d4401bbefd8bb85c60bd1e6a95c419752ab90/transformers-4.46.3.tar.gz", hash = "sha256:8ee4b3ae943fe33e82afff8e837f4b052058b07ca9be3cb5b729ed31295f72cc", size = 8627944 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/51/b87caa939fedf307496e4dbf412f4b909af3d9ca8b189fc3b65c1faa456f/transformers-4.46.3-py3-none-any.whl", hash = "sha256:a12ef6f52841fd190a3e5602145b542d03507222f2c64ebb7ee92e8788093aef", size = 10034536 }, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, -] - -[[package]] -name = "urllib3" -version = "2.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, -] diff --git a/examples/aws-python/README.md b/examples/aws-python/README.md deleted file mode 100644 index fe0525afb0..0000000000 --- a/examples/aws-python/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# ❍ Python Example - -Deploy python applications using sst. - -SST uses [uv](https://github.com/astral-sh/uv) to manage your python runtime. If you do not have uv installed, you can install it [here](https://docs.astral.sh/uv/getting-started/installation/). Any sst workspace package can be built and deployed to aws lambda using sst. In this example we deploy an API handler to lambda from the `functions` directory. The handler depends on shared code from the `shared` directory using uv's workspaces feature. (Note: builds currently do not tree shake so lots of workspaces can make larger builds than necessary.) - -Python functions can be deployed just like other SST functions, the only difference is that the functions themselves must be configured within a uv workspace, there is no drop-in-mode. - -```typescript title="sst.config.ts" -const python = new sst.aws.Function("MyPythonFunction", { - handler: "functions/src/functions/api.handler", - runtime: "python3.11", - url: true -}); -``` - - -If you are using live lambdas for your python functions, it is recommended to specify your python version to match your Lambda runtime otherwise you may encounter issues with dependencies. - -```toml title="src/pyproject.toml" -[project] -name = "aws-python" -version = "0.1.0" -description = "A SST app" -authors = [ - {name = "", email = "" }, -] -requires-python = "==3.11.*" -``` - -Live lambda will locally run your python code by building the workspace and running the specified handler. You can have multiple handlers in the same workspace and have multiple workspaces in the same project. - -```markdown -. -β”œβ”€β”€ workspace_a -β”‚ β”œβ”€β”€ pyproject.toml -β”‚ └── src -β”‚ └── workspace_a -β”‚ β”œβ”€β”€ __init__.py -β”‚ β”œβ”€β”€ api_a.py -β”‚ └── api_b.py -└── workspace_b - β”œβ”€β”€ pyproject.toml - └── src - └── workspace_b - β”œβ”€β”€ __init__.py - └── index.py -``` - -Keep in mind that AWS Lambda zip archives have limits and python does use native extensions for certain packages, if you are using large dependencies such as numpy, pandas, and others found in the SciPy stack, you may want to use the container mode to deploy your python code. diff --git a/examples/aws-python/core/sst.pyi b/examples/aws-python/core/sst.pyi deleted file mode 100644 index 24547a9a39..0000000000 --- a/examples/aws-python/core/sst.pyi +++ /dev/null @@ -1,17 +0,0 @@ -# Automatically generated by SST -# pylint: disable=all -# ruff: noqa -from typing import Any - -class Resource: - class App: - name: str - stage: str - class MyLinkableValue: - foo: str - type: str - class MyPythonFunction: - name: str - type: str - url: str - diff --git a/examples/aws-python/functions/pyproject.toml b/examples/aws-python/functions/pyproject.toml index 8f724cece5..2d8dfe7819 100644 --- a/examples/aws-python/functions/pyproject.toml +++ b/examples/aws-python/functions/pyproject.toml @@ -2,7 +2,7 @@ name = "functions" version = "0.1.0" description = "Lambda function handlers" -dependencies = ["core", "sst"] +dependencies = ["core", "sst-sdk"] requires-python = "==3.11.*" [build-system] @@ -11,4 +11,4 @@ build-backend = "hatchling.build" [tool.uv.sources] core = { workspace = true } -sst = { git = "https://github.com/sst/sst.git", branch = "dev", subdirectory = "sdk/python" } +sst-sdk = { git = "https://github.com/anomalyco/sst.git", branch = "dev", subdirectory = "sdk/python" } diff --git a/examples/aws-python/functions/sst.pyi b/examples/aws-python/functions/sst.pyi deleted file mode 100644 index 24547a9a39..0000000000 --- a/examples/aws-python/functions/sst.pyi +++ /dev/null @@ -1,17 +0,0 @@ -# Automatically generated by SST -# pylint: disable=all -# ruff: noqa -from typing import Any - -class Resource: - class App: - name: str - stage: str - class MyLinkableValue: - foo: str - type: str - class MyPythonFunction: - name: str - type: str - url: str - diff --git a/examples/aws-python/pyproject.toml b/examples/aws-python/pyproject.toml index 7e7dc5f5d1..177fb56105 100644 --- a/examples/aws-python/pyproject.toml +++ b/examples/aws-python/pyproject.toml @@ -13,4 +13,4 @@ requires-python = "==3.11.*" members = ["functions", "core"] [tool.uv.sources] -sst = { git = "https://github.com/sst/sst.git", subdirectory = "sdk/python", branch = "dev" } +sst-sdk = { git = "https://github.com/anomalyco/sst.git", subdirectory = "sdk/python", branch = "dev" } diff --git a/examples/aws-python/sst.config.ts b/examples/aws-python/sst.config.ts index 091b3fd46f..fc3f7c2cc0 100644 --- a/examples/aws-python/sst.config.ts +++ b/examples/aws-python/sst.config.ts @@ -11,10 +11,10 @@ * is currently not supported. * * :::note - * Builds currently do not tree shake so lots of workspaces can make the build - * larger than necessary. + * Each function is packaged with only the dependencies from its own `pyproject.toml` + * and any workspace packages it depends on. * ::: - * + * * In this example we deploy a handler from the `functions/` directory. It depends * on shared code from another uv workspace in the `core/` directory. * @@ -36,9 +36,9 @@ * * The `handler` is the path to the handler file and the name of the handler function * in it. - * + * * ```ts title="sst.config.ts" {2} - * new sst.aws.Function("MyPythonFunction", { + * new sst.aws.Function("PythonFunction", { * handler: "functions/src/functions/api.handler", * runtime: "python3.11", * link: [linkableValue], @@ -58,16 +58,19 @@ * print(Resource.MyLinkableValue.foo) * ``` * - * Where the `sst` package can be added to your `pyproject.toml`. + * Where the `sst-sdk` package can be added to your `pyproject.toml`. * * ```toml title="functions/pyproject.toml" + * [project] + * dependencies = ["sst-sdk"] + * * [tool.uv.sources] - * sst = { git = "https://github.com/sst/sst.git", subdirectory = "sdk/python", branch = "dev" } + * sst-sdk = { git = "https://github.com/anomalyco/sst.git", subdirectory = "sdk/python", branch = "dev" } * ``` * * You also want to set the Python version in your `pyproject.toml` to the same * version as the one in Lambda. - * + * * ```toml title="functions/pyproject.toml" * requires-python = "==3.11.*" * ``` @@ -75,25 +78,25 @@ * This makes sure that your functions work the same in `sst dev` as `sst deploy`. */ export default $config({ - app(input) { - return { - name: "aws-python", - removal: input?.stage === "production" ? "retain" : "remove", - home: "aws", - }; - }, - async run() { - const linkableValue = new sst.Linkable("MyLinkableValue", { - properties: { - foo: "Hello World", - }, - }); + app(input) { + return { + name: "aws-python", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + }; + }, + async run() { + const linkableValue = new sst.Linkable("MyLinkableValue", { + properties: { + foo: "Hello World", + }, + }); - new sst.aws.Function("MyPythonFunction", { - handler: "functions/src/functions/api.handler", - runtime: "python3.11", - link: [linkableValue], - url: true, - }); - }, + new sst.aws.Function("PythonFunction", { + handler: "functions/src/functions/api.handler", + runtime: "python3.11", + link: [linkableValue], + url: true, + }); + }, }); diff --git a/examples/aws-python/sst.pyi b/examples/aws-python/sst.pyi deleted file mode 100644 index 24547a9a39..0000000000 --- a/examples/aws-python/sst.pyi +++ /dev/null @@ -1,17 +0,0 @@ -# Automatically generated by SST -# pylint: disable=all -# ruff: noqa -from typing import Any - -class Resource: - class App: - name: str - stage: str - class MyLinkableValue: - foo: str - type: str - class MyPythonFunction: - name: str - type: str - url: str - diff --git a/examples/aws-python/uv.lock b/examples/aws-python/uv.lock deleted file mode 100644 index 0f6c8477e4..0000000000 --- a/examples/aws-python/uv.lock +++ /dev/null @@ -1,181 +0,0 @@ -version = 1 -requires-python = "==3.11.*" - -[manifest] -members = [ - "aws-python", - "core", - "functions", -] - -[[package]] -name = "aws-python" -version = "0.1.0" -source = { virtual = "." } - -[[package]] -name = "certifi" -version = "2024.8.30" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, -] - -[[package]] -name = "cffi" -version = "1.17.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, - { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, - { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, - { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, - { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, - { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, - { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, - { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, - { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, - { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, - { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, - { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, - { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, - { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, - { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, - { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, -] - -[[package]] -name = "core" -version = "0.1.0" -source = { editable = "core" } -dependencies = [ - { name = "requests" }, -] - -[package.metadata] -requires-dist = [{ name = "requests", specifier = ">=2.32.3" }] - -[[package]] -name = "cryptography" -version = "44.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/67/545c79fe50f7af51dbad56d16b23fe33f63ee6a5d956b3cb68ea110cbe64/cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14", size = 710819 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/27/5e3524053b4c8889da65cf7814a9d0d8514a05194a25e1e34f46852ee6eb/cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009", size = 6642022 }, - { url = "https://files.pythonhosted.org/packages/34/b9/4d1fa8d73ae6ec350012f89c3abfbff19fc95fe5420cf972e12a8d182986/cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f", size = 3943865 }, - { url = "https://files.pythonhosted.org/packages/6e/57/371a9f3f3a4500807b5fcd29fec77f418ba27ffc629d88597d0d1049696e/cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2", size = 4162562 }, - { url = "https://files.pythonhosted.org/packages/c5/1d/5b77815e7d9cf1e3166988647f336f87d5634a5ccecec2ffbe08ef8dd481/cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911", size = 3951923 }, - { url = "https://files.pythonhosted.org/packages/28/01/604508cd34a4024467cd4105887cf27da128cba3edd435b54e2395064bfb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69", size = 3685194 }, - { url = "https://files.pythonhosted.org/packages/c6/3d/d3c55d4f1d24580a236a6753902ef6d8aafd04da942a1ee9efb9dc8fd0cb/cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026", size = 4187790 }, - { url = "https://files.pythonhosted.org/packages/ea/a6/44d63950c8588bfa8594fd234d3d46e93c3841b8e84a066649c566afb972/cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd", size = 3951343 }, - { url = "https://files.pythonhosted.org/packages/c1/17/f5282661b57301204cbf188254c1a0267dbd8b18f76337f0a7ce1038888c/cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0", size = 4187127 }, - { url = "https://files.pythonhosted.org/packages/f3/68/abbae29ed4f9d96596687f3ceea8e233f65c9645fbbec68adb7c756bb85a/cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf", size = 4070666 }, - { url = "https://files.pythonhosted.org/packages/0f/10/cf91691064a9e0a88ae27e31779200b1505d3aee877dbe1e4e0d73b4f155/cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864", size = 4288811 }, - { url = "https://files.pythonhosted.org/packages/38/78/74ea9eb547d13c34e984e07ec8a473eb55b19c1451fe7fc8077c6a4b0548/cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a", size = 2771882 }, - { url = "https://files.pythonhosted.org/packages/cf/6c/3907271ee485679e15c9f5e93eac6aa318f859b0aed8d369afd636fafa87/cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00", size = 3206989 }, - { url = "https://files.pythonhosted.org/packages/9f/f1/676e69c56a9be9fd1bffa9bc3492366901f6e1f8f4079428b05f1414e65c/cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008", size = 6643714 }, - { url = "https://files.pythonhosted.org/packages/ba/9f/1775600eb69e72d8f9931a104120f2667107a0ee478f6ad4fe4001559345/cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862", size = 3943269 }, - { url = "https://files.pythonhosted.org/packages/25/ba/e00d5ad6b58183829615be7f11f55a7b6baa5a06910faabdc9961527ba44/cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3", size = 4166461 }, - { url = "https://files.pythonhosted.org/packages/b3/45/690a02c748d719a95ab08b6e4decb9d81e0ec1bac510358f61624c86e8a3/cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7", size = 3950314 }, - { url = "https://files.pythonhosted.org/packages/e6/50/bf8d090911347f9b75adc20f6f6569ed6ca9b9bff552e6e390f53c2a1233/cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a", size = 3686675 }, - { url = "https://files.pythonhosted.org/packages/e1/e7/cfb18011821cc5f9b21efb3f94f3241e3a658d267a3bf3a0f45543858ed8/cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c", size = 4190429 }, - { url = "https://files.pythonhosted.org/packages/07/ef/77c74d94a8bfc1a8a47b3cafe54af3db537f081742ee7a8a9bd982b62774/cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62", size = 3950039 }, - { url = "https://files.pythonhosted.org/packages/6d/b9/8be0ff57c4592382b77406269b1e15650c9f1a167f9e34941b8515b97159/cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41", size = 4189713 }, - { url = "https://files.pythonhosted.org/packages/78/e1/4b6ac5f4100545513b0847a4d276fe3c7ce0eacfa73e3b5ebd31776816ee/cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b", size = 4071193 }, - { url = "https://files.pythonhosted.org/packages/3d/cb/afff48ceaed15531eab70445abe500f07f8f96af2bb35d98af6bfa89ebd4/cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7", size = 4289566 }, - { url = "https://files.pythonhosted.org/packages/30/6f/4eca9e2e0f13ae459acd1ca7d9f0257ab86e68f44304847610afcb813dc9/cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9", size = 2772371 }, - { url = "https://files.pythonhosted.org/packages/d2/05/5533d30f53f10239616a357f080892026db2d550a40c393d0a8a7af834a9/cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f", size = 3207303 }, -] - -[[package]] -name = "functions" -version = "0.1.0" -source = { editable = "functions" } -dependencies = [ - { name = "core" }, - { name = "sst" }, -] - -[package.metadata] -requires-dist = [ - { name = "core", editable = "core" }, - { name = "sst", git = "https://github.com/sst/sst.git?subdirectory=sdk%2Fpython&branch=dev" }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, -] - -[[package]] -name = "pycparser" -version = "2.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, -] - -[[package]] -name = "requests" -version = "2.32.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, -] - -[[package]] -name = "sst" -version = "0.2.0" -source = { git = "https://github.com/sst/sst.git?subdirectory=sdk%2Fpython&branch=dev#e335fd9a3f41ee9cf5e12db5d85fa49e00175cde" } -dependencies = [ - { name = "cryptography" }, -] - -[[package]] -name = "urllib3" -version = "2.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, -] diff --git a/examples/aws-queue/package.json b/examples/aws-queue/package.json index 8977e062b9..def9afd4f2 100644 --- a/examples/aws-queue/package.json +++ b/examples/aws-queue/package.json @@ -14,7 +14,7 @@ "license": "ISC", "devDependencies": { "@types/aws-lambda": "^8.10.149", - "sst": "^3" + "sst": "file:../../sdk/js" }, "dependencies": { "@aws-sdk/client-sqs": "^3.515.0" diff --git a/examples/aws-queue/sst-env.d.ts b/examples/aws-queue/sst-env.d.ts deleted file mode 100644 index 30db5f41ac..0000000000 --- a/examples/aws-queue/sst-env.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -declare module "sst" { - export interface Resource { - } -} -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-quota-increase/package.json b/examples/aws-quota-increase/package.json index 792902d9c7..ddfcda6b92 100644 --- a/examples/aws-quota-increase/package.json +++ b/examples/aws-quota-increase/package.json @@ -2,6 +2,6 @@ "name": "aws-quota-increase", "version": "0.0.0", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-rails/README.md b/examples/aws-rails/README.md deleted file mode 100644 index 7db80e4ca1..0000000000 --- a/examples/aws-rails/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# README - -This README would normally document whatever steps are necessary to get the -application up and running. - -Things you may want to cover: - -* Ruby version - -* System dependencies - -* Configuration - -* Database creation - -* Database initialization - -* How to run the test suite - -* Services (job queues, cache servers, search engines, etc.) - -* Deployment instructions - -* ... diff --git a/examples/aws-rds-instance-mysql-public/package.json b/examples/aws-rds-instance-mysql-public/package.json index 037037eed2..041968a701 100644 --- a/examples/aws-rds-instance-mysql-public/package.json +++ b/examples/aws-rds-instance-mysql-public/package.json @@ -9,7 +9,7 @@ "author": "", "license": "ISC", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { } diff --git a/examples/aws-rds-instance-mysql-public/sst-env.d.ts b/examples/aws-rds-instance-mysql-public/sst-env.d.ts deleted file mode 100644 index efbbe5f0ad..0000000000 --- a/examples/aws-rds-instance-mysql-public/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - - } -} -export {} diff --git a/examples/aws-react-router/README.md b/examples/aws-react-router/README.md deleted file mode 100644 index 5c4780a269..0000000000 --- a/examples/aws-react-router/README.md +++ /dev/null @@ -1,87 +0,0 @@ -# Welcome to React Router! - -A modern, production-ready template for building full-stack React applications using React Router. - -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default) - -## Features - -- πŸš€ Server-side rendering -- ⚑️ Hot Module Replacement (HMR) -- πŸ“¦ Asset bundling and optimization -- πŸ”„ Data loading and mutations -- πŸ”’ TypeScript by default -- πŸŽ‰ TailwindCSS for styling -- πŸ“– [React Router docs](https://reactrouter.com/) - -## Getting Started - -### Installation - -Install the dependencies: - -```bash -npm install -``` - -### Development - -Start the development server with HMR: - -```bash -npm run dev -``` - -Your application will be available at `http://localhost:5173`. - -## Building for Production - -Create a production build: - -```bash -npm run build -``` - -## Deployment - -### Docker Deployment - -To build and run using Docker: - -```bash -docker build -t my-app . - -# Run the container -docker run -p 3000:3000 my-app -``` - -The containerized application can be deployed to any platform that supports Docker, including: - -- AWS ECS -- Google Cloud Run -- Azure Container Apps -- Digital Ocean App Platform -- Fly.io -- Railway - -### DIY Deployment - -If you're familiar with deploying Node applications, the built-in app server is production-ready. - -Make sure to deploy the output of `npm run build` - -``` -β”œβ”€β”€ package.json -β”œβ”€β”€ package-lock.json (or pnpm-lock.yaml, or bun.lockb) -β”œβ”€β”€ build/ -β”‚ β”œβ”€β”€ client/ # Static assets -β”‚ └── server/ # Server-side code -``` - -## Styling - -This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. - ---- - -Built with ❀️ using React Router. diff --git a/examples/aws-react-router/package.json b/examples/aws-react-router/package.json index 2fbd7fff3a..eb7861a131 100644 --- a/examples/aws-react-router/package.json +++ b/examples/aws-react-router/package.json @@ -17,7 +17,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-router": "^7.5.2", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@react-router/dev": "^7.5.2", diff --git a/examples/aws-react-router/sst-env.d.ts b/examples/aws-react-router/sst-env.d.ts deleted file mode 100644 index c1b62bdd43..0000000000 --- a/examples/aws-react-router/sst-env.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -declare module "sst" { - export interface Resource { - } -} -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-realtime-nextjs/README.md b/examples/aws-realtime-nextjs/README.md deleted file mode 100644 index c4033664f8..0000000000 --- a/examples/aws-realtime-nextjs/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/examples/aws-realtime-nextjs/package.json b/examples/aws-realtime-nextjs/package.json index 932a938b48..3cce662c8b 100644 --- a/examples/aws-realtime-nextjs/package.json +++ b/examples/aws-realtime-nextjs/package.json @@ -13,7 +13,7 @@ "next": "14.2.3", "react": "^18", "react-dom": "^18", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/node": "^20", diff --git a/examples/aws-realtime-nextjs/sst-env.d.ts b/examples/aws-realtime-nextjs/sst-env.d.ts deleted file mode 100644 index 74b085439a..0000000000 --- a/examples/aws-realtime-nextjs/sst-env.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyRealtime": { - "authorizer": string - "endpoint": string - "type": "sst.aws.Realtime" - } - "MyWeb": { - "type": "sst.aws.Nextjs" - "url": string - } - } -} diff --git a/examples/aws-realtime/package.json b/examples/aws-realtime/package.json index ba9cce433d..5d2271a4b7 100644 --- a/examples/aws-realtime/package.json +++ b/examples/aws-realtime/package.json @@ -14,7 +14,7 @@ "author": "", "license": "ISC", "devDependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" }, "dependencies": { "@aws-sdk/client-iot-data-plane": "^3.569.0" diff --git a/examples/aws-redis-local/package.json b/examples/aws-redis-local/package.json index 63a08b3f55..128e5439f7 100644 --- a/examples/aws-redis-local/package.json +++ b/examples/aws-redis-local/package.json @@ -12,6 +12,6 @@ "license": "ISC", "dependencies": { "ioredis": "^5.4.1", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-redis-local/sst-env.d.ts b/examples/aws-redis-local/sst-env.d.ts deleted file mode 100644 index 56dd05fe42..0000000000 --- a/examples/aws-redis-local/sst-env.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyApp": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyRedis": { - "host": string - "password": string - "port": number - "type": "sst.aws.Redis" - "username": string - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-redis-local/sst.config.ts b/examples/aws-redis-local/sst.config.ts index 7a2993d490..b32f069188 100644 --- a/examples/aws-redis-local/sst.config.ts +++ b/examples/aws-redis-local/sst.config.ts @@ -34,7 +34,7 @@ * By providing the `dev` prop for Redis, SST will use the local Redis server and * not deploy a new Redis ElastiCache cluster when running `sst dev`. * - * It also allows us to access Redis through a Reosurce `link`. + * It also allows us to access Redis through a Resource `link`. * * ```ts title="index.ts" * const client = Resource.MyRedis.host === "localhost" diff --git a/examples/aws-redis/package.json b/examples/aws-redis/package.json index dc4b3272dc..b888afbfcc 100644 --- a/examples/aws-redis/package.json +++ b/examples/aws-redis/package.json @@ -14,6 +14,6 @@ "license": "ISC", "dependencies": { "ioredis": "^5.4.1", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-redis/sst-env.d.ts b/examples/aws-redis/sst-env.d.ts deleted file mode 100644 index 522504d36a..0000000000 --- a/examples/aws-redis/sst-env.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyApp": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyRedis": { - "clusterArn": string - "host": string - "password": string - "port": number - "type": "sst.aws.Redis" - "username": string - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-remix-container/README.md b/examples/aws-remix-container/README.md deleted file mode 100644 index 6c4d2168fa..0000000000 --- a/examples/aws-remix-container/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# Welcome to Remix! - -- πŸ“– [Remix docs](https://remix.run/docs) - -## Development - -Run the dev server: - -```shellscript -npm run dev -``` - -## Deployment - -First, build your app for production: - -```sh -npm run build -``` - -Then run the app in production mode: - -```sh -npm start -``` - -Now you'll need to pick a host to deploy it to. - -### DIY - -If you're familiar with deploying Node applications, the built-in Remix app server is production-ready. - -Make sure to deploy the output of `npm run build` - -- `build/server` -- `build/client` - -## Styling - -This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever css framework you prefer. See the [Vite docs on css](https://vitejs.dev/guide/features.html#css) for more information. diff --git a/examples/aws-remix-container/package.json b/examples/aws-remix-container/package.json index 4f05fadf32..8fdad703fa 100644 --- a/examples/aws-remix-container/package.json +++ b/examples/aws-remix-container/package.json @@ -19,7 +19,7 @@ "isbot": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@remix-run/dev": "^2.15.0", diff --git a/examples/aws-remix-container/sst-env.d.ts b/examples/aws-remix-container/sst-env.d.ts deleted file mode 100644 index ff1d425c12..0000000000 --- a/examples/aws-remix-container/sst-env.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-remix-redis/README.md b/examples/aws-remix-redis/README.md deleted file mode 100644 index 6c4d2168fa..0000000000 --- a/examples/aws-remix-redis/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# Welcome to Remix! - -- πŸ“– [Remix docs](https://remix.run/docs) - -## Development - -Run the dev server: - -```shellscript -npm run dev -``` - -## Deployment - -First, build your app for production: - -```sh -npm run build -``` - -Then run the app in production mode: - -```sh -npm start -``` - -Now you'll need to pick a host to deploy it to. - -### DIY - -If you're familiar with deploying Node applications, the built-in Remix app server is production-ready. - -Make sure to deploy the output of `npm run build` - -- `build/server` -- `build/client` - -## Styling - -This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever css framework you prefer. See the [Vite docs on css](https://vitejs.dev/guide/features.html#css) for more information. diff --git a/examples/aws-remix-redis/package.json b/examples/aws-remix-redis/package.json index 291673688d..22a6d322b2 100644 --- a/examples/aws-remix-redis/package.json +++ b/examples/aws-remix-redis/package.json @@ -18,7 +18,7 @@ "isbot": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@remix-run/dev": "^2.12.1", diff --git a/examples/aws-remix-redis/sst-env.d.ts b/examples/aws-remix-redis/sst-env.d.ts deleted file mode 100644 index 261f14f5b2..0000000000 --- a/examples/aws-remix-redis/sst-env.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyRedis": { - "host": string - "password": string - "port": number - "type": "sst.aws.Redis" - "username": string - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "bastion": string - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-remix-stream/README.md b/examples/aws-remix-stream/README.md deleted file mode 100644 index 6417d506d2..0000000000 --- a/examples/aws-remix-stream/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Welcome to Remix + Vite! - -πŸ“– See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/guides/vite) for details on supported features. - -## Development - -Run the Vite dev server: - -```shellscript -npm run dev -``` - -## Deployment - -First, build your app for production: - -```sh -npm run build -``` - -Then run the app in production mode: - -```sh -npm start -``` - -Now you'll need to pick a host to deploy it to. - -### DIY - -If you're familiar with deploying Node applications, the built-in Remix app server is production-ready. - -Make sure to deploy the output of `npm run build` - -- `build/server` -- `build/client` diff --git a/examples/aws-remix-stream/package.json b/examples/aws-remix-stream/package.json index daf4634115..b3f71965b1 100644 --- a/examples/aws-remix-stream/package.json +++ b/examples/aws-remix-stream/package.json @@ -17,7 +17,7 @@ "isbot": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@remix-run/dev": "^2.12.1", diff --git a/examples/aws-remix-stream/sst-env.d.ts b/examples/aws-remix-stream/sst-env.d.ts deleted file mode 100644 index db40e891a8..0000000000 --- a/examples/aws-remix-stream/sst-env.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyWeb": { - "type": "sst.aws.Remix" - "url": string - } - } -} diff --git a/examples/aws-remix/README.md b/examples/aws-remix/README.md deleted file mode 100644 index 6c4d2168fa..0000000000 --- a/examples/aws-remix/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# Welcome to Remix! - -- πŸ“– [Remix docs](https://remix.run/docs) - -## Development - -Run the dev server: - -```shellscript -npm run dev -``` - -## Deployment - -First, build your app for production: - -```sh -npm run build -``` - -Then run the app in production mode: - -```sh -npm start -``` - -Now you'll need to pick a host to deploy it to. - -### DIY - -If you're familiar with deploying Node applications, the built-in Remix app server is production-ready. - -Make sure to deploy the output of `npm run build` - -- `build/server` -- `build/client` - -## Styling - -This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever css framework you prefer. See the [Vite docs on css](https://vitejs.dev/guide/features.html#css) for more information. diff --git a/examples/aws-remix/package.json b/examples/aws-remix/package.json index cfaa00eb6d..b0cc925225 100644 --- a/examples/aws-remix/package.json +++ b/examples/aws-remix/package.json @@ -19,7 +19,7 @@ "isbot": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@remix-run/dev": "^2.15.0", diff --git a/examples/aws-remix/sst-env.d.ts b/examples/aws-remix/sst-env.d.ts deleted file mode 100644 index c1327bc96b..0000000000 --- a/examples/aws-remix/sst-env.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyWeb": { - "type": "sst.aws.Remix" - "url": string - } - } -} diff --git a/examples/aws-router-bucket/package.json b/examples/aws-router-bucket/package.json new file mode 100644 index 0000000000..8870a7c91c --- /dev/null +++ b/examples/aws-router-bucket/package.json @@ -0,0 +1,7 @@ +{ + "name": "aws-router-bucket", + "version": "0.0.0", + "dependencies": { + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-router-protection/api.ts b/examples/aws-router-protection/api.ts new file mode 100644 index 0000000000..bb310ecba1 --- /dev/null +++ b/examples/aws-router-protection/api.ts @@ -0,0 +1,9 @@ +export const handler = async (event: any) => { + return { + statusCode: 200, + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(event.headers), + }; +}; diff --git a/examples/aws-router-protection/package.json b/examples/aws-router-protection/package.json new file mode 100644 index 0000000000..6e962323f7 --- /dev/null +++ b/examples/aws-router-protection/package.json @@ -0,0 +1,7 @@ +{ + "name": "aws-router-protection", + "version": "0.0.0", + "dependencies": { + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-router-protection/sst.config.ts b/examples/aws-router-protection/sst.config.ts new file mode 100644 index 0000000000..ee813c3267 --- /dev/null +++ b/examples/aws-router-protection/sst.config.ts @@ -0,0 +1,34 @@ +/// + +/** + * ## Router protection with OAC + * + * Creates a router with Origin Access Control (OAC) to secure Lambda function URLs + * behind CloudFront. Direct access to the Lambda URL returns 403. + */ +export default $config({ + app(input) { + return { + name: "aws-router-protection", + home: "aws", + removal: input?.stage === "production" ? "retain" : "remove", + }; + }, + async run() { + const router = new sst.aws.Router("MyRouter", { + protection: "oac", + }); + + const api = new sst.aws.Function("MyApi", { + handler: "api.handler", + url: { + router: { instance: router, path: "/api" }, + }, + }); + + return { + router: router.url, + api: api.url, + }; + }, +}); diff --git a/examples/aws-router-waf/api.ts b/examples/aws-router-waf/api.ts new file mode 100644 index 0000000000..fb693b1801 --- /dev/null +++ b/examples/aws-router-waf/api.ts @@ -0,0 +1,9 @@ +export const handler = async (event: any) => { + return { + statusCode: 200, + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ message: "Hello from WAF-protected Router!" }), + }; +}; diff --git a/examples/aws-router-waf/sst.config.ts b/examples/aws-router-waf/sst.config.ts new file mode 100644 index 0000000000..793edf49ff --- /dev/null +++ b/examples/aws-router-waf/sst.config.ts @@ -0,0 +1,47 @@ +/// + +/** + * ## Router with WAF + * + * Enable WAF (Web Application Firewall) for a Router to protect against common + * web exploits and bots. + * + * WAF includes rate limiting per IP, and AWS managed rules for core rule set, + * known bad inputs, and SQL injection protection. + * + * You can also enable WAF logging to CloudWatch to monitor requests. + */ +export default $config({ + app(input) { + return { + name: "aws-router-waf", + home: "aws", + removal: input?.stage === "production" ? "retain" : "remove", + }; + }, + async run() { + const api = new sst.aws.Function("MyApi", { + handler: "api.handler", + url: true, + }); + + const router = new sst.aws.Router("MyRouter", { + routes: { + "/*": api.url, + }, + waf: { + rateLimitPerIp: 1000, + managedRules: { + coreRuleSet: true, + knownBadInputs: true, + sqlInjection: true, + }, + logging: true, + }, + }); + + return { + url: router.url, + }; + }, +}); diff --git a/examples/aws-router/package.json b/examples/aws-router/package.json new file mode 100644 index 0000000000..87a5d86ce2 --- /dev/null +++ b/examples/aws-router/package.json @@ -0,0 +1,7 @@ +{ + "name": "aws-router", + "version": "0.0.0", + "dependencies": { + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-router/sst-env.d.ts b/examples/aws-router/sst-env.d.ts deleted file mode 100644 index de5aafffb4..0000000000 --- a/examples/aws-router/sst-env.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - MyApi: { - name: string - type: "sst.aws.Function" - url: string - } - MyBucket: { - name: string - type: "sst.aws.Bucket" - } - MyRouter: { - type: "sst.aws.Router" - url: string - } - } -} -export {} \ No newline at end of file diff --git a/examples/aws-router/sst.config.ts b/examples/aws-router/sst.config.ts index a314b27ead..5db6f2d75c 100644 --- a/examples/aws-router/sst.config.ts +++ b/examples/aws-router/sst.config.ts @@ -22,7 +22,6 @@ export default $config({ access: "public", }); const router = new sst.aws.Router("MyRouter", { - domain: "router.ion.dev.sst.dev", routes: { "/api/*": api.url, "/*": $interpolate`https://${bucket.domain}`, diff --git a/examples/aws-rust-api/Cargo.lock b/examples/aws-rust-api/Cargo.lock deleted file mode 100644 index 1cf1402682..0000000000 --- a/examples/aws-rust-api/Cargo.lock +++ /dev/null @@ -1,2927 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "anstream" -version = "0.6.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" - -[[package]] -name = "anstyle-parse" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" -dependencies = [ - "anstyle", - "once_cell", - "windows-sys 0.59.0", -] - -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "aws-config" -version = "1.5.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50236e4d60fe8458de90a71c0922c761e41755adf091b1b03de1cef537179915" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sdk-sso", - "aws-sdk-ssooidc", - "aws-sdk-sts", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "hex", - "http 0.2.12", - "ring", - "time", - "tokio", - "tracing", - "url", - "zeroize", -] - -[[package]] -name = "aws-credential-types" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60e8f6b615cb5fc60a98132268508ad104310f0cfb25a1c22eee76efdf9154da" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "zeroize", -] - -[[package]] -name = "aws-runtime" -version = "1.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76dd04d39cc12844c0994f2c9c5a6f5184c22e9188ec1ff723de41910a21dcad" -dependencies = [ - "aws-credential-types", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "http-body 0.4.6", - "once_cell", - "percent-encoding", - "pin-project-lite", - "tracing", - "uuid", -] - -[[package]] -name = "aws-rust-lambda" -version = "0.1.0" -dependencies = [ - "aws-config", - "aws-sdk-s3", - "axum", - "lambda_http", - "serde", - "serde_json", - "sst_sdk", - "tokio", - "uuid", -] - -[[package]] -name = "aws-sdk-s3" -version = "1.73.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3978e0a211bdc5cddecfd91fb468665a662a27fbdaef39ddf36a2a18fef12cb4" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-checksums", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "bytes", - "fastrand", - "hex", - "hmac", - "http 0.2.12", - "http-body 0.4.6", - "lru", - "once_cell", - "percent-encoding", - "regex-lite", - "sha2", - "tracing", - "url", -] - -[[package]] -name = "aws-sdk-sso" -version = "1.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16ff718c9ee45cc1ebd4774a0e086bb80a6ab752b4902edf1c9f56b86ee1f770" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "http 0.2.12", - "once_cell", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-ssooidc" -version = "1.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5183e088715cc135d8d396fdd3bc02f018f0da4c511f53cb8d795b6a31c55809" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "http 0.2.12", - "once_cell", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-sts" -version = "1.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9f944ef032717596639cea4a2118a3a457268ef51bbb5fde9637e54c465da00" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-query", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "http 0.2.12", - "once_cell", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sigv4" -version = "1.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bc5bbd1e4a2648fd8c5982af03935972c24a2f9846b396de661d351ee3ce837" -dependencies = [ - "aws-credential-types", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "crypto-bigint 0.5.5", - "form_urlencoded", - "hex", - "hmac", - "http 0.2.12", - "http 1.2.0", - "once_cell", - "p256", - "percent-encoding", - "ring", - "sha2", - "subtle", - "time", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-smithy-async" -version = "1.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa59d1327d8b5053c54bf2eaae63bf629ba9e904434d0835a28ed3c0ed0a614e" -dependencies = [ - "futures-util", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "aws-smithy-checksums" -version = "0.62.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f45a1c384d7a393026bc5f5c177105aa9fa68e4749653b985707ac27d77295" -dependencies = [ - "aws-smithy-http", - "aws-smithy-types", - "bytes", - "crc32c", - "crc32fast", - "crc64fast-nvme", - "hex", - "http 0.2.12", - "http-body 0.4.6", - "md-5", - "pin-project-lite", - "sha1", - "sha2", - "tracing", -] - -[[package]] -name = "aws-smithy-eventstream" -version = "0.60.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b18559a41e0c909b77625adf2b8c50de480a8041e5e4a3f5f7d177db70abc5a" -dependencies = [ - "aws-smithy-types", - "bytes", - "crc32fast", -] - -[[package]] -name = "aws-smithy-http" -version = "0.60.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7809c27ad8da6a6a68c454e651d4962479e81472aa19ae99e59f9aba1f9713cc" -dependencies = [ - "aws-smithy-eventstream", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "bytes-utils", - "futures-core", - "http 0.2.12", - "http-body 0.4.6", - "once_cell", - "percent-encoding", - "pin-project-lite", - "pin-utils", - "tracing", -] - -[[package]] -name = "aws-smithy-json" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "623a51127f24c30776c8b374295f2df78d92517386f77ba30773f15a30ce1422" -dependencies = [ - "aws-smithy-types", -] - -[[package]] -name = "aws-smithy-query" -version = "0.60.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" -dependencies = [ - "aws-smithy-types", - "urlencoding", -] - -[[package]] -name = "aws-smithy-runtime" -version = "1.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d526a12d9ed61fadefda24abe2e682892ba288c2018bcb38b1b4c111d13f6d92" -dependencies = [ - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "fastrand", - "h2", - "http 0.2.12", - "http-body 0.4.6", - "http-body 1.0.1", - "httparse", - "hyper 0.14.32", - "hyper-rustls", - "once_cell", - "pin-project-lite", - "pin-utils", - "rustls", - "tokio", - "tracing", -] - -[[package]] -name = "aws-smithy-runtime-api" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92165296a47a812b267b4f41032ff8069ab7ff783696d217f0994a0d7ab585cd" -dependencies = [ - "aws-smithy-async", - "aws-smithy-types", - "bytes", - "http 0.2.12", - "http 1.2.0", - "pin-project-lite", - "tokio", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-smithy-types" -version = "1.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7b8a53819e42f10d0821f56da995e1470b199686a1809168db6ca485665f042" -dependencies = [ - "base64-simd", - "bytes", - "bytes-utils", - "futures-core", - "http 0.2.12", - "http 1.2.0", - "http-body 0.4.6", - "http-body 1.0.1", - "http-body-util", - "itoa", - "num-integer", - "pin-project-lite", - "pin-utils", - "ryu", - "serde", - "time", - "tokio", - "tokio-util", -] - -[[package]] -name = "aws-smithy-xml" -version = "0.60.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" -dependencies = [ - "xmlparser", -] - -[[package]] -name = "aws-types" -version = "1.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbd0a668309ec1f66c0f6bda4840dd6d4796ae26d699ebc266d7cc95c6d040f" -dependencies = [ - "aws-credential-types", - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "rustc_version", - "tracing", -] - -[[package]] -name = "aws_lambda_events" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7319a086b79c3ff026a33a61e80f04fd3885fbb73237981ea080d21944e1cb1c" -dependencies = [ - "base64 0.22.1", - "bytes", - "http 1.2.0", - "http-body 1.0.1", - "http-serde", - "query_map", - "serde", - "serde_json", -] - -[[package]] -name = "axum" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" -dependencies = [ - "axum-core", - "bytes", - "form_urlencoded", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.6.0", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower 0.5.2", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" -dependencies = [ - "bytes", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets", -] - -[[package]] -name = "base16ct" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" -dependencies = [ - "outref", - "vsimd", -] - -[[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - -[[package]] -name = "bitflags" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bytes" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" -dependencies = [ - "serde", -] - -[[package]] -name = "bytes-utils" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" -dependencies = [ - "bytes", - "either", -] - -[[package]] -name = "cbindgen" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fce8dd7fcfcbf3a0a87d8f515194b49d6135acab73e18bd380d1d93bb1a15eb" -dependencies = [ - "clap", - "heck", - "indexmap", - "log", - "proc-macro2", - "quote", - "serde", - "serde_json", - "syn", - "tempfile", - "toml", -] - -[[package]] -name = "cc" -version = "1.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755717a7de9ec452bf7f3f1a3099085deabd7f2962b861dae91ecd7a365903d2" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "clap" -version = "4.5.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" -dependencies = [ - "clap_builder", -] - -[[package]] -name = "clap_builder" -version = "4.5.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_lex" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" - -[[package]] -name = "colorchoice" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "crc32c" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47" -dependencies = [ - "rustc_version", -] - -[[package]] -name = "crc32fast" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crc64fast-nvme" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5e2ee08013e3f228d6d2394116c4549a6df77708442c62d887d83f68ef2ee37" -dependencies = [ - "cbindgen", - "crc", -] - -[[package]] -name = "crypto-bigint" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" -dependencies = [ - "generic-array", - "rand_core", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "rand_core", - "subtle", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "rand_core", - "typenum", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "der" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" -dependencies = [ - "const-oid", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ecdsa" -version = "0.14.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" -dependencies = [ - "der", - "elliptic-curve", - "rfc6979", - "signature", -] - -[[package]] -name = "either" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" - -[[package]] -name = "elliptic-curve" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" -dependencies = [ - "base16ct", - "crypto-bigint 0.4.9", - "der", - "digest", - "ff", - "generic-array", - "group", - "pkcs8", - "rand_core", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "errno" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "ff" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" -dependencies = [ - "rand_core", - "subtle", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets", -] - -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "group" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" -dependencies = [ - "ff", - "rand_core", - "subtle", -] - -[[package]] -name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http 1.2.0", -] - -[[package]] -name = "http-body-util" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" -dependencies = [ - "bytes", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "pin-project-lite", -] - -[[package]] -name = "http-serde" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f056c8559e3757392c8d091e796416e4649d8e49e88b8d76df6c002f05027fd" -dependencies = [ - "http 1.2.0", - "serde", -] - -[[package]] -name = "httparse" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "log", - "rustls", - "rustls-native-certs", - "tokio", - "tokio-rustls", -] - -[[package]] -name = "hyper-util" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "hyper 1.6.0", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "idna" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "inout" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" -dependencies = [ - "generic-array", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - -[[package]] -name = "itoa" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" - -[[package]] -name = "lambda_http" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fe279be7f89f5f72c97c3a96f45c43db8edab1007320ecc6a5741273b4d6db" -dependencies = [ - "aws_lambda_events", - "base64 0.22.1", - "bytes", - "encoding_rs", - "futures", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.6.0", - "lambda_runtime", - "mime", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "tokio-stream", - "url", -] - -[[package]] -name = "lambda_runtime" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed49669d6430292aead991e19bf13153135a884f916e68f32997c951af637ebe" -dependencies = [ - "async-stream", - "base64 0.22.1", - "bytes", - "futures", - "http 1.2.0", - "http-body 1.0.1", - "http-body-util", - "http-serde", - "hyper 1.6.0", - "hyper-util", - "lambda_runtime_api_client", - "pin-project", - "serde", - "serde_json", - "serde_path_to_error", - "tokio", - "tokio-stream", - "tower 0.4.13", - "tower-layer", - "tracing", -] - -[[package]] -name = "lambda_runtime_api_client" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c90a10f094475a34a04da2be11686c4dcfe214d93413162db9ffdff3d3af293a" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.6.0", - "hyper-util", - "tokio", - "tower 0.4.13", - "tower-service", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.169" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "litemap" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" - -[[package]] -name = "log" -version = "0.4.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" - -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown", -] - -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] - -[[package]] -name = "matchit" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "miniz_oxide" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" -dependencies = [ - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - -[[package]] -name = "p256" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" -dependencies = [ - "ecdsa", - "elliptic-curve", - "sha2", -] - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pin-project" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs8" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "proc-macro2" -version = "1.0.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "query_map" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eab6b8b1074ef3359a863758dae650c7c0c6027927a085b7af911c8e0bf3a15" -dependencies = [ - "form_urlencoded", - "serde", - "serde_derive", -] - -[[package]] -name = "quote" -version = "1.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.15", -] - -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-lite" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" - -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - -[[package]] -name = "rfc6979" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" -dependencies = [ - "crypto-bigint 0.4.9", - "hmac", - "zeroize", -] - -[[package]] -name = "ring" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.15", - "libc", - "spin", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki", - "sct", -] - -[[package]] -name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe", - "rustls-pemfile", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" - -[[package]] -name = "ryu" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" - -[[package]] -name = "schannel" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "sec1" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" - -[[package]] -name = "serde" -version = "1.0.217" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.217" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.138" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" -dependencies = [ - "itoa", - "serde", -] - -[[package]] -name = "serde_spanned" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - -[[package]] -name = "signature" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" -dependencies = [ - "digest", - "rand_core", -] - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "socket2" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "spki" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "sst_sdk" -version = "0.1.0" -dependencies = [ - "aes-gcm", - "base64 0.21.7", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" - -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tempfile" -version = "3.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" -dependencies = [ - "cfg-if", - "fastrand", - "getrandom 0.3.1", - "once_cell", - "rustix", - "windows-sys 0.59.0", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thread_local" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "time" -version = "0.3.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tokio" -version = "1.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-macros" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-serde" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" -dependencies = [ - "serde", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" -dependencies = [ - "matchers", - "once_cell", - "regex", - "serde", - "serde_json", - "sharded-slab", - "thread_local", - "tracing", - "tracing-core", - "tracing-serde", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "unicode-ident" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" - -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common", - "subtle", -] - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" -dependencies = [ - "getrandom 0.3.1", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasi" -version = "0.13.3+wasi-0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" -dependencies = [ - "wit-bindgen-rt", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen-rt" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" -dependencies = [ - "bitflags", -] - -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - -[[package]] -name = "xmlparser" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" - -[[package]] -name = "yoke" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerofrom" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" - -[[package]] -name = "zerovec" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/examples/aws-rust-api/README.md b/examples/aws-rust-api/README.md deleted file mode 100644 index c1c2ca3952..0000000000 --- a/examples/aws-rust-api/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# SST Rust support - -SST uses [cargo lambda](https://www.cargo-lambda.info/) to build rust binaries and deploy them on AWS Lambda. You MUST install [cargo](https://rustup.rs/) and [cargo lambda](https://www.cargo-lambda.info/guide/installation.html) and [zig](https://ziglang.org/download/) (a cargo-lambda dependency which is automatically installed with cargo-lambda in most installation methods) on your own before using sst rust lambdas. - -## Setup with Github Actions - -An example to deploy using github actions with `arm64` architecture, feel free to configure as needed - -```yml -name: Deploy Prod - -on: - push: - branches: - - main - -jobs: - deploy-prod: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: pguyot/arm-runner-action@v2 - - - name: use Node.js - uses: actions/setup-node@v3 - with: - node-version: latest - - - name: use pnpm - uses: pnpm/action-setup@v4 - with: - version: latest - - - name: get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: setup pnpm cache - uses: actions/cache@v3 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: use Rust - uses: actions-rs/toolchain@v1 - - - name: use Rust cache - uses: Swatinem/rust-cache@v2 - - - name: use Zig - uses: korandoru/setup-zig@v1 - with: - zig-version: master - - - name: use Cargo Lambda - uses: jaxxstorm/action-install-gh-release@v1.9.0 - with: - repo: cargo-lambda/cargo-lambda - platform: linux - arch: aarch64 # | x86_64 - - - name: pnpm install - run: pnpm install --frozen-lockfile - - - name: sst install providers - run: | - set -euxo pipefail - pnpm sst install - - - name: sst deploy - run: | - set -euxo pipefail - pnpm sst deploy --stage prod - - env: - STAGE: prod - LOG_LEVEL: info - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} -``` diff --git a/examples/aws-rust-cluster/README.md b/examples/aws-rust-cluster/README.md deleted file mode 100644 index 16b33e9129..0000000000 --- a/examples/aws-rust-cluster/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Rust Docker Service - -A simple example of running a slim rust docker image on ion. Of course the Dockerfile can be configured to do anything, but -in this example it utilizes [cargo chef](https://github.com/LukeMathWalker/cargo-chef) to make use of Docker layers to speed up -the builds. - -You will need docker running for this example to work. Also included is a `docker-compose.yml` for convenience of running the image locally, but it is not needed for deploying via sst. - -Simply using the typical -```sh -sst deploy --stage production -``` -will deploy the image \ No newline at end of file diff --git a/examples/aws-rust-cluster/package.json b/examples/aws-rust-cluster/package.json index e20fe14f90..3523728cc7 100644 --- a/examples/aws-rust-cluster/package.json +++ b/examples/aws-rust-cluster/package.json @@ -10,10 +10,10 @@ "license": "ISC", "devDependencies": { "@types/aws-lambda": "8.10.145", - "sst": "^3", - "sst-linux-x64": "^3.0.80" + "sst": "file:../../sdk/js", + "sst-linux-x64": "^4" }, "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-rust-cluster/sst-env.d.ts b/examples/aws-rust-cluster/sst-env.d.ts deleted file mode 100644 index f110f33043..0000000000 --- a/examples/aws-rust-cluster/sst-env.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - } -} -export {} diff --git a/examples/aws-rust-loco/Cargo.lock b/examples/aws-rust-loco/Cargo.lock deleted file mode 100644 index 5bafb69bf9..0000000000 --- a/examples/aws-rust-loco/Cargo.lock +++ /dev/null @@ -1,5349 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "getrandom", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "aliasable" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "allocator-api2" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "0.6.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" - -[[package]] -name = "anstyle-parse" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" -dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" -dependencies = [ - "anstyle", - "windows-sys 0.52.0", -] - -[[package]] -name = "anyhow" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" - -[[package]] -name = "argon2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" -dependencies = [ - "base64ct", - "blake2", - "cpufeatures", - "password-hash", -] - -[[package]] -name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" - -[[package]] -name = "async-attributes" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "async-channel" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -dependencies = [ - "concurrent-queue", - "event-listener 2.5.3", - "futures-core", -] - -[[package]] -name = "async-channel" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-compression" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa" -dependencies = [ - "brotli", - "flate2", - "futures-core", - "memchr", - "pin-project-lite", - "tokio", - "zstd", - "zstd-safe", -] - -[[package]] -name = "async-executor" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand 2.1.0", - "futures-lite 2.3.0", - "slab", -] - -[[package]] -name = "async-global-executor" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" -dependencies = [ - "async-channel 2.3.1", - "async-executor", - "async-io 2.3.3", - "async-lock 3.4.0", - "blocking", - "futures-lite 2.3.0", - "once_cell", - "tokio", -] - -[[package]] -name = "async-io" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.27", - "slab", - "socket2 0.4.10", - "waker-fn", -] - -[[package]] -name = "async-io" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" -dependencies = [ - "async-lock 3.4.0", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite 2.3.0", - "parking", - "polling 3.7.2", - "rustix 0.38.34", - "slab", - "tracing", - "windows-sys 0.52.0", -] - -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - -[[package]] -name = "async-lock" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" -dependencies = [ - "event-listener 5.3.1", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-std" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" -dependencies = [ - "async-attributes", - "async-channel 1.9.0", - "async-global-executor", - "async-io 1.13.0", - "async-lock 2.8.0", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite 1.13.0", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] - -[[package]] -name = "async-stream" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "atoi" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" -dependencies = [ - "num-traits", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "auto-future" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373" - -[[package]] -name = "autocfg" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" - -[[package]] -name = "axum" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" -dependencies = [ - "async-trait", - "axum-core", - "axum-macros", - "bytes", - "futures-util", - "http 1.1.0", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper 1.0.1", - "tokio", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 1.1.0", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper 0.1.2", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-extra" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733" -dependencies = [ - "axum", - "axum-core", - "bytes", - "cookie", - "futures-util", - "http 1.1.0", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "serde", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "axum-test" -version = "14.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167294800740b4b6bc7bfbccbf3a1d50a6c6e097342580ec4c11d1672e456292" -dependencies = [ - "anyhow", - "async-trait", - "auto-future", - "axum", - "bytes", - "cookie", - "http 1.1.0", - "http-body-util", - "hyper", - "hyper-util", - "mime", - "pretty_assertions", - "reserve-port", - "rust-multipart-rfc7578_2", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "tokio", - "tower", - "url", -] - -[[package]] -name = "backtrace" -version = "0.3.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "backtrace_printer" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8d28de81c708c843640982b66573df0f0168d87e42854b563971f326745aab7" -dependencies = [ - "btparse-stable", - "colored", - "regex", - "thiserror", -] - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - -[[package]] -name = "bb8" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b10cf871f3ff2ce56432fddc2615ac7acc3aa22ca321f8fea800846fbb32f188" -dependencies = [ - "async-trait", - "futures-util", - "parking_lot", - "tokio", -] - -[[package]] -name = "bigdecimal" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" -dependencies = [ - "serde", -] - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "blocking" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" -dependencies = [ - "async-channel 2.3.1", - "async-task", - "futures-io", - "futures-lite 2.3.0", - "piper", -] - -[[package]] -name = "borsh" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" -dependencies = [ - "borsh-derive", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" -dependencies = [ - "once_cell", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.72", - "syn_derive", -] - -[[package]] -name = "brotli" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "4.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bstr" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "btparse-stable" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d75b8252ed252f881d1dc4482ae3c3854df6ee8183c1906bac50ff358f4f89f" - -[[package]] -name = "bumpalo" -version = "3.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" - -[[package]] -name = "byte-unit" -version = "4.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da78b32057b8fdfc352504708feeba7216dcd65a2c9ab02978cbd288d1279b6c" -dependencies = [ - "serde", - "utf8-width", -] - -[[package]] -name = "bytecheck" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "simdutf8", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" - -[[package]] -name = "camino" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo-platform" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "cc" -version = "1.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" -dependencies = [ - "jobserver", - "libc", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-targets 0.52.6", -] - -[[package]] -name = "chrono-tz" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" -dependencies = [ - "chrono", - "chrono-tz-build", - "phf", -] - -[[package]] -name = "chrono-tz-build" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" -dependencies = [ - "parse-zoneinfo", - "phf", - "phf_codegen", -] - -[[package]] -name = "chumsky" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" -dependencies = [ - "hashbrown 0.14.5", - "stacker", -] - -[[package]] -name = "clap" -version = "4.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f6b81fb3c84f5563d509c59b5a48d935f689e993afa90fe39047f05adef9142" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca6706fd5224857d9ac5eb9355f6683563cc0541c7cd9d014043b57cbec78ac" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "clap_lex" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" - -[[package]] -name = "colorchoice" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" - -[[package]] -name = "colored" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" -dependencies = [ - "lazy_static", - "windows-sys 0.48.0", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "futures-core", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "console" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" -dependencies = [ - "encode_unicode", - "lazy_static", - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "cookie" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" - -[[package]] -name = "cpufeatures" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "crc32fast" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "cron" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8c3e73077b4b4a6ab1ea5047c37c57aee77657bc8ecd6f29b0af082d0b0c07" -dependencies = [ - "chrono", - "nom", - "once_cell", -] - -[[package]] -name = "cron_clock" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a8699d8ed16e3db689f8ae04d8dc3c6666a4ba7e724e5a157884b7cc385d16b" -dependencies = [ - "chrono", - "nom", - "once_cell", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" - -[[package]] -name = "crossterm" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" -dependencies = [ - "bitflags 1.3.2", - "crossterm_winapi", - "libc", - "mio 0.8.11", - "parking_lot", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", -] - -[[package]] -name = "cruet" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113a9e83d8f614be76de8df1f25bf9d0ea6e85ea573710a3d3f3abe1438ae49c" -dependencies = [ - "once_cell", - "regex", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "der" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", - "serde", -] - -[[package]] -name = "deunicode" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" - -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "subtle", -] - -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - -[[package]] -name = "duct" -version = "0.13.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ab5718d1224b63252cd0c6f74f6480f9ffeb117438a2e0f5cf6d9a4798929c" -dependencies = [ - "libc", - "once_cell", - "os_pipe", - "shared_child", -] - -[[package]] -name = "duct_sh" -version = "0.13.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6633cadba557545fbbe0299a2f9adc4bb2fc5fb238773f5e841e0c23d62146" -dependencies = [ - "duct", -] - -[[package]] -name = "educe" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4bd92664bf78c4d3dba9b7cdafce6fa15b13ed3ed16175218196942e99168a8" -dependencies = [ - "enum-ordinalize", - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "either" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" -dependencies = [ - "serde", -] - -[[package]] -name = "email-encoding" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60d1d33cdaede7e24091f039632eb5d3c7469fe5b066a985281a34fc70fa317f" -dependencies = [ - "base64 0.22.1", - "memchr", -] - -[[package]] -name = "email_address" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46b7a0ac6570e31bfe2c6cf575a576a55af9893d1a6b30b4444e6e90b216bb84" - -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - -[[package]] -name = "english-to-cron" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c16423ac933fee80f05a52b435a912d5b08edbbbfe936e0042ebb3accdf303da" -dependencies = [ - "lazy_static", - "regex", -] - -[[package]] -name = "enum-ordinalize" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" -dependencies = [ - "enum-ordinalize-derive", -] - -[[package]] -name = "enum-ordinalize-derive" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "errno" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "etcetera" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" -dependencies = [ - "cfg-if", - "home", - "windows-sys 0.48.0", -] - -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "event-listener" -version = "5.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" -dependencies = [ - "event-listener 5.3.1", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[package]] -name = "fastrand" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" - -[[package]] -name = "flate2" -version = "1.0.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "flume" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" -dependencies = [ - "futures-core", - "futures-sink", - "spin", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs-err" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" -dependencies = [ - "autocfg", -] - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" - -[[package]] -name = "futures-executor" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-intrusive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" -dependencies = [ - "futures-core", - "lock_api", - "parking_lot", -] - -[[package]] -name = "futures-io" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" - -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - -[[package]] -name = "futures-lite" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" -dependencies = [ - "fastrand 2.1.0", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "futures-sink" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" - -[[package]] -name = "futures-task" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" - -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" - -[[package]] -name = "futures-util" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "gethostname" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "gimli" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" - -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - -[[package]] -name = "globset" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" -dependencies = [ - "aho-corasick", - "bstr", - "log", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", -] - -[[package]] -name = "globwalk" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" -dependencies = [ - "bitflags 2.6.0", - "ignore", - "walkdir", -] - -[[package]] -name = "gloo-timers" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.8", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash 0.8.11", - "allocator-api2", -] - -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown 0.14.5", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "hostname" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" -dependencies = [ - "cfg-if", - "libc", - "windows", -] - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http 1.1.0", -] - -[[package]] -name = "http-body-util" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" -dependencies = [ - "bytes", - "futures-util", - "http 1.1.0", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "http-range-header" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a397c49fec283e3d6211adbe480be95aae5f304cfb923e9970e08956d5168a" - -[[package]] -name = "httparse" -version = "1.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "humansize" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" -dependencies = [ - "libm", -] - -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - -[[package]] -name = "hyper" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.1.0", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-util" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.1.0", - "http-body", - "hyper", - "pin-project-lite", - "socket2 0.5.7", - "tokio", - "tower", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "idna" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "if_chain" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" - -[[package]] -name = "ignore" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" -dependencies = [ - "crossbeam-deque", - "globset", - "log", - "memchr", - "regex-automata 0.4.7", - "same-file", - "walkdir", - "winapi-util", -] - -[[package]] -name = "include_dir" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" -dependencies = [ - "include_dir_macros", -] - -[[package]] -name = "include_dir_macros" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "indexmap" -version = "2.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" -dependencies = [ - "equivalent", - "hashbrown 0.14.5", -] - -[[package]] -name = "inherent" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0122b7114117e64a63ac49f752a5ca4624d534c7b1c7de796ac196381cd2d947" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "insta" -version = "1.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810ae6042d48e2c9e9215043563a58a80b877bc863228a74cf10c49d4620a6f5" -dependencies = [ - "console", - "lazy_static", - "linked-hash-map", - "pest", - "pest_derive", - "regex", - "serde", - "similar", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "ipnetwork" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" -dependencies = [ - "serde", -] - -[[package]] -name = "is-terminal" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" - -[[package]] -name = "jobserver" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" -dependencies = [ - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "jsonwebtoken" -version = "9.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" -dependencies = [ - "base64 0.21.7", - "js-sys", - "pem", - "ring", - "serde", - "serde_json", - "simple_asn1", -] - -[[package]] -name = "kv-log-macro" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -dependencies = [ - "log", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] - -[[package]] -name = "lettre" -version = "0.11.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a62049a808f1c4e2356a2a380bd5f2aca3b011b0b482cf3b914ba1731426969" -dependencies = [ - "async-trait", - "base64 0.22.1", - "chumsky", - "email-encoding", - "email_address", - "fastrand 2.1.0", - "futures-io", - "futures-util", - "hostname", - "httpdate", - "idna 0.5.0", - "mime", - "nom", - "percent-encoding", - "quoted_printable", - "rustls 0.23.12", - "rustls-pemfile 2.1.2", - "socket2 0.5.7", - "tokio", - "tokio-rustls", - "url", - "webpki-roots 0.26.3", -] - -[[package]] -name = "libc" -version = "0.2.155" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" - -[[package]] -name = "libm" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" - -[[package]] -name = "libredox" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" -dependencies = [ - "bitflags 2.6.0", - "libc", -] - -[[package]] -name = "libsqlite3-sys" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - -[[package]] -name = "linux-raw-sys" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" - -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "loco-rs" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92b58f8834452bee76b822ad42a9042c3d86367805d1cb4c8f3633c73bd4e73c" -dependencies = [ - "argon2", - "async-trait", - "axum", - "axum-extra", - "axum-test", - "backtrace_printer", - "bb8", - "byte-unit", - "bytes", - "cargo_metadata", - "cfg-if", - "chrono", - "clap", - "colored", - "duct", - "duct_sh", - "english-to-cron", - "fs-err", - "futures-util", - "hyper", - "include_dir", - "ipnetwork", - "jsonwebtoken", - "lazy_static", - "lettre", - "mime", - "moka", - "object_store", - "rand", - "regex", - "requestty", - "rrgen", - "rusty-sidekiq", - "sea-orm", - "sea-orm-migration", - "serde", - "serde_json", - "serde_variant", - "serde_yaml", - "tera", - "thiserror", - "tokio", - "tokio-cron-scheduler", - "tower", - "tower-http", - "tracing", - "tracing-appender", - "tracing-subscriber", - "uuid", - "validator", -] - -[[package]] -name = "log" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" -dependencies = [ - "value-bag", -] - -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] - -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "migration" -version = "0.1.0" -dependencies = [ - "async-std", - "loco-rs", - "sea-orm-migration", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", -] - -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - -[[package]] -name = "mio" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "wasi", - "windows-sys 0.52.0", -] - -[[package]] -name = "moka" -version = "0.12.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cf62eb4dd975d2dde76432fb1075c49e3ee2331cf36f1f8fd4b66550d32b6f" -dependencies = [ - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "once_cell", - "parking_lot", - "quanta", - "rustc_version", - "smallvec", - "tagptr", - "thiserror", - "triomphe", - "uuid", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-bigint-dig" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" -dependencies = [ - "byteorder", - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand", - "smallvec", - "zeroize", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi 0.3.9", - "libc", -] - -[[package]] -name = "object" -version = "0.36.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e" -dependencies = [ - "memchr", -] - -[[package]] -name = "object_store" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6da452820c715ce78221e8202ccc599b4a52f3e1eb3eedb487b680c81a8e3f3" -dependencies = [ - "async-trait", - "bytes", - "chrono", - "futures", - "humantime", - "itertools", - "parking_lot", - "percent-encoding", - "snafu", - "tokio", - "tracing", - "url", - "walkdir", -] - -[[package]] -name = "once_cell" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - -[[package]] -name = "ordered-float" -version = "3.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc" -dependencies = [ - "num-traits", -] - -[[package]] -name = "os_pipe" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29d73ba8daf8fac13b0501d1abeddcfe21ba7401ada61a819144b6c2a4f32209" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "ouroboros" -version = "0.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2ba07320d39dfea882faa70554b4bd342a5f273ed59ba7c1c6b4c840492c954" -dependencies = [ - "aliasable", - "ouroboros_macro", - "static_assertions", -] - -[[package]] -name = "ouroboros_macro" -version = "0.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec4c6225c69b4ca778c0aea097321a64c421cf4577b331c61b229267edabb6f8" -dependencies = [ - "heck 0.4.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "parking" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" - -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.3", - "smallvec", - "windows-targets 0.52.6", -] - -[[package]] -name = "parse-zoneinfo" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" -dependencies = [ - "regex", -] - -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core", - "subtle", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pem" -version = "3.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" -dependencies = [ - "base64 0.22.1", - "serde", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pest" -version = "2.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" -dependencies = [ - "memchr", - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "pest_meta" -version = "2.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" -dependencies = [ - "once_cell", - "pest", - "sha2", -] - -[[package]] -name = "phf" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_codegen" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_shared" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "piper" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" -dependencies = [ - "atomic-waker", - "fastrand 2.1.0", - "futures-io", -] - -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" - -[[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] - -[[package]] -name = "polling" -version = "3.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi 0.4.0", - "pin-project-lite", - "rustix 0.38.34", - "tracing", - "windows-sys 0.52.0", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "pretty_assertions" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" -dependencies = [ - "diff", - "yansi", -] - -[[package]] -name = "proc-macro-crate" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "psm" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" -dependencies = [ - "cc", -] - -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "quanta" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" -dependencies = [ - "crossbeam-utils", - "libc", - "once_cell", - "raw-cpuid", - "wasi", - "web-sys", - "winapi", -] - -[[package]] -name = "quote" -version = "1.0.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "quoted_printable" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "raw-cpuid" -version = "11.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb9ee317cfe3fbd54b36a511efc1edd42e216903c9cd575e686dd68a2ba90d8d" -dependencies = [ - "bitflags 2.6.0", -] - -[[package]] -name = "redis" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa8455fa3621f6b41c514946de66ea0531f57ca017b2e6c7cc368035ea5b46df" -dependencies = [ - "async-trait", - "bytes", - "combine", - "futures-util", - "itoa", - "percent-encoding", - "pin-project-lite", - "ryu", - "sha1_smol", - "tokio", - "tokio-util", - "url", -] - -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" -dependencies = [ - "bitflags 2.6.0", -] - -[[package]] -name = "redox_users" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" -dependencies = [ - "getrandom", - "libredox", - "thiserror", -] - -[[package]] -name = "regex" -version = "1.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax 0.8.4", -] - -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" - -[[package]] -name = "relative-path" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" - -[[package]] -name = "rend" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" -dependencies = [ - "bytecheck", -] - -[[package]] -name = "requestty" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa883a1f3e288e65187f653e6ba2e84fdf810fe02f4c8074f9c723d1aa26e2ae" -dependencies = [ - "requestty-ui", - "shell-words", - "smallvec", - "tempfile", - "winsplit", -] - -[[package]] -name = "requestty-ui" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7549bab39cf982b629b68e7ec191a5574e85086e95c0ebe514c02d3b42ffe225" -dependencies = [ - "bitflags 1.3.2", - "crossterm", - "once_cell", - "textwrap", - "unicode-segmentation", -] - -[[package]] -name = "reserve-port" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9838134a2bfaa8e1f40738fcc972ac799de6e0e06b5157acb95fc2b05a0ea283" -dependencies = [ - "lazy_static", - "thiserror", -] - -[[package]] -name = "ring" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" -dependencies = [ - "cc", - "cfg-if", - "getrandom", - "libc", - "spin", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rkyv" -version = "0.7.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0" -dependencies = [ - "bitvec", - "bytecheck", - "bytes", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "rrgen" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e40013551787f9f535e7dbc8dafc164591d941aeae48881a385d8b0393dd45f6" -dependencies = [ - "cruet", - "fs-err", - "glob", - "heck 0.4.1", - "regex", - "serde", - "serde_json", - "serde_regex", - "serde_yaml", - "tera", - "thiserror", -] - -[[package]] -name = "rsa" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core", - "signature", - "spki", - "subtle", - "zeroize", -] - -[[package]] -name = "rstest" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afd55a67069d6e434a95161415f5beeada95a01c7b815508a82dcb0e1593682" -dependencies = [ - "futures", - "futures-timer", - "rstest_macros", - "rustc_version", -] - -[[package]] -name = "rstest_macros" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4165dfae59a39dd41d8dec720d3cbfbc71f69744efb480a3920f5d4e0cc6798d" -dependencies = [ - "cfg-if", - "glob", - "proc-macro-crate", - "proc-macro2", - "quote", - "regex", - "relative-path", - "rustc_version", - "syn 2.0.72", - "unicode-ident", -] - -[[package]] -name = "rust-multipart-rfc7578_2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b748410c0afdef2ebbe3685a6a862e2ee937127cdaae623336a459451c8d57" -dependencies = [ - "bytes", - "futures-core", - "futures-util", - "http 0.2.12", - "mime", - "mime_guess", - "rand", - "thiserror", -] - -[[package]] -name = "rust_decimal" -version = "1.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1790d1c4c0ca81211399e0e0af16333276f375209e71a37b67698a373db5b47a" -dependencies = [ - "arrayvec", - "borsh", - "bytes", - "num-traits", - "rand", - "rkyv", - "serde", - "serde_json", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.37.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustix" -version = "0.38.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" -dependencies = [ - "bitflags 2.6.0", - "errno", - "libc", - "linux-raw-sys 0.4.14", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "ring", - "rustls-webpki 0.101.7", - "sct", -] - -[[package]] -name = "rustls" -version = "0.23.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" -dependencies = [ - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki 0.102.6", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - -[[package]] -name = "rustls-pemfile" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" -dependencies = [ - "base64 0.22.1", - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" - -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "rustls-webpki" -version = "0.102.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" - -[[package]] -name = "rusty-sidekiq" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a00db3916faeea070039864f98d4fd759d96fc07722571a4918d996fea5621" -dependencies = [ - "async-trait", - "bb8", - "chrono", - "cron_clock", - "gethostname", - "heck 0.4.1", - "hex", - "num_cpus", - "rand", - "redis", - "serde", - "serde_json", - "sha2", - "slog-term", - "thiserror", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "ryu" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scc" -version = "2.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fadf67e3cf23f8b11a6c8c48a16cb2437381503615acd91094ec7b4686a5a53" -dependencies = [ - "sdd", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "sdd" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85f05a494052771fc5bd0619742363b5e24e5ad72ab3111ec2e27925b8edc5f3" - -[[package]] -name = "sea-bae" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bd3534a9978d0aa7edd2808dc1f8f31c4d0ecd31ddf71d997b3c98e9f3c9114" -dependencies = [ - "heck 0.4.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "sea-orm" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d4ec1cdd8bdd3553d3c946079f58efa33fedc477f32603652652abcef96fe6" -dependencies = [ - "async-stream", - "async-trait", - "bigdecimal", - "chrono", - "futures", - "log", - "ouroboros", - "rust_decimal", - "sea-orm-macros", - "sea-query", - "sea-query-binder", - "serde", - "serde_json", - "sqlx", - "strum", - "thiserror", - "time", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "sea-orm-cli" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d525eee597817631f800857b0c2fe8645ec9c65b4304176a44bf14b93366009e" -dependencies = [ - "chrono", - "clap", - "dotenvy", - "glob", - "regex", - "sea-schema", - "tracing", - "tracing-subscriber", - "url", -] - -[[package]] -name = "sea-orm-macros" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f363ead48b625a6f8f905322a820464f728fa4fe4f1c222bed5234ccf8fb8555" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "sea-bae", - "syn 2.0.72", - "unicode-ident", -] - -[[package]] -name = "sea-orm-migration" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5df62369752f91b9295f8d71c146d84f818301a9ae7295106ebe8bbaa989a072" -dependencies = [ - "async-trait", - "clap", - "dotenvy", - "futures", - "sea-orm", - "sea-orm-cli", - "sea-schema", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "sea-query" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e5073b2cfed767511a57d18115f3b3d8bcb5690bf8c89518caec6cb22c0cd74" -dependencies = [ - "bigdecimal", - "chrono", - "educe", - "inherent", - "ordered-float", - "rust_decimal", - "sea-query-derive", - "serde_json", - "time", - "uuid", -] - -[[package]] -name = "sea-query-binder" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "754965d4aee6145bec25d0898e5c931e6c22859789ce62fd85a42a15ed5a8ce3" -dependencies = [ - "bigdecimal", - "chrono", - "rust_decimal", - "sea-query", - "serde_json", - "sqlx", - "time", - "uuid", -] - -[[package]] -name = "sea-query-derive" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a82fcb49253abcb45cdcb2adf92956060ec0928635eb21b4f7a6d8f25ab0bc" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "syn 2.0.72", - "thiserror", -] - -[[package]] -name = "sea-schema" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad52149fc81836ea7424c3425d8f6ed8ad448dd16d2e4f6a3907ba46f3f2fd78" -dependencies = [ - "futures", - "sea-query", - "sea-schema-derive", -] - -[[package]] -name = "sea-schema-derive" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "debdc8729c37fdbf88472f97fd470393089f997a909e535ff67c544d18cfccf0" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - -[[package]] -name = "semver" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" -dependencies = [ - "serde", -] - -[[package]] -name = "serde" -version = "1.0.204" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.204" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "serde_json" -version = "1.0.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" -dependencies = [ - "itoa", - "serde", -] - -[[package]] -name = "serde_regex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" -dependencies = [ - "regex", - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_variant" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a0068df419f9d9b6488fdded3f1c818522cdea328e02ce9d9f147380265a432" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "serial_test" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" -dependencies = [ - "futures", - "log", - "once_cell", - "parking_lot", - "scc", - "serial_test_derive", -] - -[[package]] -name = "serial_test_derive" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "server" -version = "0.1.0" -dependencies = [ - "async-trait", - "axum", - "chrono", - "include_dir", - "insta", - "loco-rs", - "migration", - "rstest", - "sea-orm", - "serde", - "serde_json", - "serial_test", - "tokio", - "tracing", - "tracing-subscriber", - "uuid", - "validator", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shared_child" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "shell-words" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" - -[[package]] -name = "signal-hook" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-mio" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" -dependencies = [ - "libc", - "mio 0.8.11", - "signal-hook", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core", -] - -[[package]] -name = "simdutf8" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" - -[[package]] -name = "similar" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" - -[[package]] -name = "simple_asn1" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror", - "time", -] - -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "slog" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" - -[[package]] -name = "slog-term" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6e022d0b998abfe5c3782c1f03551a596269450ccd677ea51c56f8b214610e8" -dependencies = [ - "is-terminal", - "slog", - "term", - "thread_local", - "time", -] - -[[package]] -name = "slug" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bd94acec9c8da640005f8e135a39fc0372e74535e6b368b7a04b875f784c8c4" -dependencies = [ - "deunicode", - "wasm-bindgen", -] - -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "smawk" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" - -[[package]] -name = "snafu" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" -dependencies = [ - "doc-comment", - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "socket2" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "sqlformat" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" -dependencies = [ - "nom", - "unicode_categories", -] - -[[package]] -name = "sqlx" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" -dependencies = [ - "sqlx-core", - "sqlx-macros", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", -] - -[[package]] -name = "sqlx-core" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" -dependencies = [ - "ahash 0.8.11", - "atoi", - "bigdecimal", - "byteorder", - "bytes", - "chrono", - "crc", - "crossbeam-queue", - "either", - "event-listener 2.5.3", - "futures-channel", - "futures-core", - "futures-intrusive", - "futures-io", - "futures-util", - "hashlink", - "hex", - "indexmap", - "log", - "memchr", - "once_cell", - "paste", - "percent-encoding", - "rust_decimal", - "rustls 0.21.12", - "rustls-pemfile 1.0.4", - "serde", - "serde_json", - "sha2", - "smallvec", - "sqlformat", - "thiserror", - "time", - "tokio", - "tokio-stream", - "tracing", - "url", - "uuid", - "webpki-roots 0.25.4", -] - -[[package]] -name = "sqlx-macros" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" -dependencies = [ - "proc-macro2", - "quote", - "sqlx-core", - "sqlx-macros-core", - "syn 1.0.109", -] - -[[package]] -name = "sqlx-macros-core" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" -dependencies = [ - "dotenvy", - "either", - "heck 0.4.1", - "hex", - "once_cell", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2", - "sqlx-core", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", - "syn 1.0.109", - "tempfile", - "tokio", - "url", -] - -[[package]] -name = "sqlx-mysql" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" -dependencies = [ - "atoi", - "base64 0.21.7", - "bigdecimal", - "bitflags 2.6.0", - "byteorder", - "bytes", - "chrono", - "crc", - "digest", - "dotenvy", - "either", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "generic-array", - "hex", - "hkdf", - "hmac", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "percent-encoding", - "rand", - "rsa", - "rust_decimal", - "serde", - "sha1", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror", - "time", - "tracing", - "uuid", - "whoami", -] - -[[package]] -name = "sqlx-postgres" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" -dependencies = [ - "atoi", - "base64 0.21.7", - "bigdecimal", - "bitflags 2.6.0", - "byteorder", - "chrono", - "crc", - "dotenvy", - "etcetera", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "hex", - "hkdf", - "hmac", - "home", - "itoa", - "log", - "md-5", - "memchr", - "num-bigint", - "once_cell", - "rand", - "rust_decimal", - "serde", - "serde_json", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror", - "time", - "tracing", - "uuid", - "whoami", -] - -[[package]] -name = "sqlx-sqlite" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" -dependencies = [ - "atoi", - "chrono", - "flume", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "libsqlite3-sys", - "log", - "percent-encoding", - "serde", - "sqlx-core", - "time", - "tracing", - "url", - "urlencoding", - "uuid", -] - -[[package]] -name = "stacker" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "winapi", -] - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn_derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - -[[package]] -name = "sync_wrapper" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" - -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "tempfile" -version = "3.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" -dependencies = [ - "cfg-if", - "fastrand 2.1.0", - "rustix 0.38.34", - "windows-sys 0.52.0", -] - -[[package]] -name = "tera" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" -dependencies = [ - "chrono", - "chrono-tz", - "globwalk", - "humansize", - "lazy_static", - "percent-encoding", - "pest", - "pest_derive", - "rand", - "regex", - "serde", - "serde_json", - "slug", - "unic-segment", -] - -[[package]] -name = "term" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" -dependencies = [ - "dirs-next", - "rustversion", - "winapi", -] - -[[package]] -name = "textwrap" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" -dependencies = [ - "smawk", - "unicode-linebreak", - "unicode-width", -] - -[[package]] -name = "thiserror" -version = "1.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "thread_local" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "time" -version = "0.3.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinyvec" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.39.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d040ac2b29ab03b09d4129c2f5bbd012a3ac2f79d38ff506a4bf8dd34b0eac8a" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio 1.0.1", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2 0.5.7", - "tokio-macros", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-cron-scheduler" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7b9480125554f0ace1c3c3797a24b5cc56c6a7cd82c739db35fb54c4dc046f3" -dependencies = [ - "chrono", - "cron", - "num-derive", - "num-traits", - "tokio", - "tracing", - "uuid", -] - -[[package]] -name = "tokio-macros" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" -dependencies = [ - "rustls 0.23.12", - "rustls-pki-types", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml_datetime" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" - -[[package]] -name = "toml_edit" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" -dependencies = [ - "indexmap", - "toml_datetime", - "winnow", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" -dependencies = [ - "async-compression", - "bitflags 2.6.0", - "bytes", - "futures-core", - "futures-util", - "http 1.1.0", - "http-body", - "http-body-util", - "http-range-header", - "httpdate", - "mime", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" - -[[package]] -name = "tower-service" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" - -[[package]] -name = "tracing" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-appender" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" -dependencies = [ - "crossbeam-channel", - "thiserror", - "time", - "tracing-subscriber", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "tracing-core" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-serde" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" -dependencies = [ - "serde", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex", - "serde", - "serde_json", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", - "tracing-serde", -] - -[[package]] -name = "triomphe" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "ucd-trie" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" - -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-segment" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" -dependencies = [ - "unic-ucd-segment", -] - -[[package]] -name = "unic-ucd-segment" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - -[[package]] -name = "unicase" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - -[[package]] -name = "unicode-normalization" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-properties" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" - -[[package]] -name = "unicode-segmentation" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" - -[[package]] -name = "unicode-width" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" - -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" -dependencies = [ - "form_urlencoded", - "idna 0.5.0", - "percent-encoding", -] - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "utf8-width" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" -dependencies = [ - "getrandom", - "rand", - "serde", -] - -[[package]] -name = "validator" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" -dependencies = [ - "idna 0.4.0", - "lazy_static", - "regex", - "serde", - "serde_derive", - "serde_json", - "url", - "validator_derive", -] - -[[package]] -name = "validator_derive" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af" -dependencies = [ - "if_chain", - "lazy_static", - "proc-macro-error", - "proc-macro2", - "quote", - "regex", - "syn 1.0.109", - "validator_types", -] - -[[package]] -name = "validator_types" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3" -dependencies = [ - "proc-macro2", - "syn 1.0.109", -] - -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - -[[package]] -name = "value-bag" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - -[[package]] -name = "wasm-bindgen" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.72", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" - -[[package]] -name = "web-sys" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "0.25.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" - -[[package]] -name = "webpki-roots" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "whoami" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" -dependencies = [ - "redox_syscall 0.4.1", - "wasite", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" -dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" -dependencies = [ - "windows-core", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "winsplit" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab703352da6a72f35c39a533526393725640575bb211f61987a2748323ad956" - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "yansi" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" - -[[package]] -name = "zstd" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.12+zstd.1.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/examples/aws-rust-loco/README.md b/examples/aws-rust-loco/README.md deleted file mode 100644 index 74f1ff01d1..0000000000 --- a/examples/aws-rust-loco/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# Welcome to Loco :train: - -[Loco](https://loco.rs) is a web and API framework running on Rust. - -This is the **Rest API starter** which includes a `User` model and authentication based on JWT. - -## Quick Start - -```sh -cargo loco start -``` - -```sh -$ cargo loco start -Finished dev [unoptimized + debuginfo] target(s) in 21.63s - Running `target/debug/myapp start` - - : - : - : - -controller/app_routes.rs:203: [Middleware] Adding log trace id - - β–„ β–€ - β–€ β–„ - β–„ β–€ β–„ β–„ β–„β–€ - β–„ β–€β–„β–„ - β–„ β–€ β–€ β–€β–„β–€β–ˆβ–„ - β–€β–ˆβ–„ -β–„β–„β–„β–„β–„β–„β–„ β–„β–„β–„β–„β–„β–„β–„β–„β–„ β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„β–„ β–„β–„β–„β–„β–„β–„β–„β–„β–„ β–€β–€β–ˆ - β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆ β–€β–ˆ - β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–€β–€β–€ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆ β–„β–ˆβ–„ - β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–„ - β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–„β–„β–„ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ - β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–€ - β–€β–€β–€β–ˆβ–ˆβ–„ β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€ β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€ β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€ β–ˆβ–ˆβ–€ - β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€ - https://loco.rs - -environment: development - database: automigrate - logger: debug -compilation: debug - modes: server - -listening on localhost:5150 -``` - -## Getting help - -Check out [a quick tour](https://loco.rs/docs/getting-started/tour/) or [the complete guide](https://loco.rs/docs/getting-started/guide/). diff --git a/examples/aws-service-discovery/lambda.ts b/examples/aws-service-discovery/lambda.ts index c8abf7626d..f7a871f838 100644 --- a/examples/aws-service-discovery/lambda.ts +++ b/examples/aws-service-discovery/lambda.ts @@ -1,12 +1,12 @@ import { Resource } from "sst"; export async function handler() { - const reponse = await fetch( + const response = await fetch( `http://${Resource.MyService.service}` ); return { statusCode: 200, - body: await reponse.text(), + body: await response.text(), }; } diff --git a/examples/aws-service-discovery/package.json b/examples/aws-service-discovery/package.json index 94774b1907..078810d63a 100644 --- a/examples/aws-service-discovery/package.json +++ b/examples/aws-service-discovery/package.json @@ -11,7 +11,7 @@ "license": "ISC", "dependencies": { "express": "^4.21.1", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "8.10.145" diff --git a/examples/aws-service-discovery/sst-env.d.ts b/examples/aws-service-discovery/sst-env.d.ts deleted file mode 100644 index 30594d1056..0000000000 --- a/examples/aws-service-discovery/sst-env.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyFunction": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyService": { - "service": string - "type": "sst.aws.Service" - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-service-discovery/sst.config.ts b/examples/aws-service-discovery/sst.config.ts index c2b282bec1..8e2fc28f1a 100644 --- a/examples/aws-service-discovery/sst.config.ts +++ b/examples/aws-service-discovery/sst.config.ts @@ -10,7 +10,7 @@ * the service's cloud map hostname. * * ```ts title="lambda.ts" - * const reponse = await fetch(`http://${Resource.MyService.service}`); + * const response = await fetch(`http://${Resource.MyService.service}`); * ``` * * Here we are accessing it through a Lambda function that's linked to the service and is diff --git a/examples/aws-service-transform-container/package.json b/examples/aws-service-transform-container/package.json index 43252e6ec3..7dad309412 100644 --- a/examples/aws-service-transform-container/package.json +++ b/examples/aws-service-transform-container/package.json @@ -11,6 +11,6 @@ "license": "ISC", "dependencies": { "express": "^4.21.1", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-service-transform-container/sst-env.d.ts b/examples/aws-service-transform-container/sst-env.d.ts deleted file mode 100644 index 30594d1056..0000000000 --- a/examples/aws-service-transform-container/sst-env.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyFunction": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyService": { - "service": string - "type": "sst.aws.Service" - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/internal/playground/images/sidecar/.dockerignore b/examples/aws-shared-alb-static/api/.dockerignore similarity index 100% rename from examples/internal/playground/images/sidecar/.dockerignore rename to examples/aws-shared-alb-static/api/.dockerignore diff --git a/examples/aws-shared-alb-static/api/Dockerfile b/examples/aws-shared-alb-static/api/Dockerfile new file mode 100644 index 0000000000..53718c02f4 --- /dev/null +++ b/examples/aws-shared-alb-static/api/Dockerfile @@ -0,0 +1,10 @@ +FROM node:18-bullseye-slim + +WORKDIR /app/ + +COPY package.json /app +RUN npm install + +COPY index.mjs /app + +ENTRYPOINT ["node", "index.mjs"] diff --git a/examples/aws-shared-alb-static/api/index.mjs b/examples/aws-shared-alb-static/api/index.mjs new file mode 100644 index 0000000000..68f1eb62b0 --- /dev/null +++ b/examples/aws-shared-alb-static/api/index.mjs @@ -0,0 +1,29 @@ +import express from "express"; + +const PORT = 3000; + +const app = express(); + +app.get("/", (req, res) => { + res.json({ status: "ok" }); +}); + +app.get("/api", (req, res) => { + res.json({ service: "api", message: "Hello from the API service" }); +}); + +app.get("/api/health", (req, res) => { + res.json({ status: "ok" }); +}); + +app.get("/api/greeting", (req, res) => { + res.json({ service: "api", message: "Hello from the API service" }); +}); + +app.get("/api/*", (req, res) => { + res.json({ service: "api", path: req.path }); +}); + +app.listen(PORT, () => { + console.log(`API service is running on http://localhost:${PORT}`); +}); diff --git a/examples/aws-shared-alb-static/api/package.json b/examples/aws-shared-alb-static/api/package.json new file mode 100644 index 0000000000..bf11689dd1 --- /dev/null +++ b/examples/aws-shared-alb-static/api/package.json @@ -0,0 +1,7 @@ +{ + "name": "api", + "version": "1.0.0", + "dependencies": { + "express": "^4.21.1" + } +} diff --git a/examples/aws-shared-alb-static/package.json b/examples/aws-shared-alb-static/package.json new file mode 100644 index 0000000000..138aaa3168 --- /dev/null +++ b/examples/aws-shared-alb-static/package.json @@ -0,0 +1,7 @@ +{ + "name": "aws-shared-alb-static", + "version": "1.0.0", + "dependencies": { + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-shared-alb-static/sst.config.ts b/examples/aws-shared-alb-static/sst.config.ts new file mode 100644 index 0000000000..87fd8c4550 --- /dev/null +++ b/examples/aws-shared-alb-static/sst.config.ts @@ -0,0 +1,80 @@ +/// + +/** + * ## AWS Shared ALB + * + * Creates a standalone ALB that is shared across stages. In dev, the ALB is + * referenced via `Alb.get()`. In production, it's created fresh. + * + * Uses the `$dev ? get : new` pattern to share infrastructure across stages. + */ +export default $config({ + app(input) { + return { + name: "aws-shared-alb", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + providers: { + aws: { + region: "us-east-1", + }, + }, + }; + }, + async run() { + const vpc = $dev + ? sst.aws.Vpc.get("MyVpc", "vpc-xxx") + : new sst.aws.Vpc("MyVpc"); + + const cluster = $dev + ? sst.aws.Cluster.get("MyCluster", { id: "cluster-xxx", vpc }) + : new sst.aws.Cluster("MyCluster", { vpc }); + + const alb = $dev + ? sst.aws.Alb.get("SharedAlb", "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/xxx") + : new sst.aws.Alb("SharedAlb", { + vpc, + listeners: [ + { port: 80, protocol: "http" }, + ], + }); + + if ($dev) { + new sst.aws.Service("Web", { + cluster, + image: { context: "web/" }, + loadBalancer: { + instance: alb, + rules: [ + { + listen: "80/http", + forward: "3000/http", + conditions: { path: "/app/*" }, + priority: 200, + }, + ], + }, + }); + } + + new sst.aws.Service("Api", { + cluster, + image: { context: "api/" }, + loadBalancer: { + instance: alb, + rules: [ + { + listen: "80/http", + forward: "3000/http", + conditions: { path: "/api/*" }, + priority: 100, + }, + ], + }, + }); + + return { + url: alb.url, + }; + }, +}); diff --git a/examples/internal/playground/images/task/.dockerignore b/examples/aws-shared-alb-static/web/.dockerignore similarity index 100% rename from examples/internal/playground/images/task/.dockerignore rename to examples/aws-shared-alb-static/web/.dockerignore diff --git a/examples/aws-shared-alb-static/web/Dockerfile b/examples/aws-shared-alb-static/web/Dockerfile new file mode 100644 index 0000000000..53718c02f4 --- /dev/null +++ b/examples/aws-shared-alb-static/web/Dockerfile @@ -0,0 +1,10 @@ +FROM node:18-bullseye-slim + +WORKDIR /app/ + +COPY package.json /app +RUN npm install + +COPY index.mjs /app + +ENTRYPOINT ["node", "index.mjs"] diff --git a/examples/aws-shared-alb-static/web/index.mjs b/examples/aws-shared-alb-static/web/index.mjs new file mode 100644 index 0000000000..0d64a1452d --- /dev/null +++ b/examples/aws-shared-alb-static/web/index.mjs @@ -0,0 +1,29 @@ +import express from "express"; + +const PORT = 3000; + +const app = express(); + +app.get("/", (req, res) => { + res.json({ status: "ok" }); +}); + +app.get("/app", (req, res) => { + res.send("

Web App

Hello from the Web service

"); +}); + +app.get("/app/health", (req, res) => { + res.json({ status: "ok" }); +}); + +app.get("/app/greeting", (req, res) => { + res.send("

Web App

Hello from the Web service

"); +}); + +app.get("/app/*", (req, res) => { + res.send(`

Web App

Path: ${req.path}

`); +}); + +app.listen(PORT, () => { + console.log(`Web service is running on http://localhost:${PORT}`); +}); diff --git a/examples/aws-shared-alb-static/web/package.json b/examples/aws-shared-alb-static/web/package.json new file mode 100644 index 0000000000..9c43fe9bb1 --- /dev/null +++ b/examples/aws-shared-alb-static/web/package.json @@ -0,0 +1,7 @@ +{ + "name": "web", + "version": "1.0.0", + "dependencies": { + "express": "^4.21.1" + } +} diff --git a/examples/internal/playground/images/web/.dockerignore b/examples/aws-shared-alb/api/.dockerignore similarity index 100% rename from examples/internal/playground/images/web/.dockerignore rename to examples/aws-shared-alb/api/.dockerignore diff --git a/examples/aws-shared-alb/api/Dockerfile b/examples/aws-shared-alb/api/Dockerfile new file mode 100644 index 0000000000..53718c02f4 --- /dev/null +++ b/examples/aws-shared-alb/api/Dockerfile @@ -0,0 +1,10 @@ +FROM node:18-bullseye-slim + +WORKDIR /app/ + +COPY package.json /app +RUN npm install + +COPY index.mjs /app + +ENTRYPOINT ["node", "index.mjs"] diff --git a/examples/aws-shared-alb/api/index.mjs b/examples/aws-shared-alb/api/index.mjs new file mode 100644 index 0000000000..d229500c93 --- /dev/null +++ b/examples/aws-shared-alb/api/index.mjs @@ -0,0 +1,21 @@ +import express from "express"; + +const PORT = 3000; + +const app = express(); + +app.get("/api", (req, res) => { + res.json({ service: "api", message: "Hello from the API service" }); +}); + +app.get("/api/health", (req, res) => { + res.json({ status: "ok" }); +}); + +app.get("/api/*", (req, res) => { + res.json({ service: "api", path: req.path }); +}); + +app.listen(PORT, () => { + console.log(`API service is running on http://localhost:${PORT}`); +}); diff --git a/examples/aws-shared-alb/api/package.json b/examples/aws-shared-alb/api/package.json new file mode 100644 index 0000000000..bf11689dd1 --- /dev/null +++ b/examples/aws-shared-alb/api/package.json @@ -0,0 +1,7 @@ +{ + "name": "api", + "version": "1.0.0", + "dependencies": { + "express": "^4.21.1" + } +} diff --git a/examples/aws-shared-alb/package.json b/examples/aws-shared-alb/package.json new file mode 100644 index 0000000000..0f75962a1a --- /dev/null +++ b/examples/aws-shared-alb/package.json @@ -0,0 +1,7 @@ +{ + "name": "aws-shared-alb", + "version": "1.0.0", + "dependencies": { + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-shared-alb/sst.config.ts b/examples/aws-shared-alb/sst.config.ts new file mode 100644 index 0000000000..34f2c072f1 --- /dev/null +++ b/examples/aws-shared-alb/sst.config.ts @@ -0,0 +1,124 @@ +/// + +/** + * ## AWS Shared ALB + * + * Creates a standalone ALB shared across multiple services. + * Shows advanced routing with path conditions, header conditions, and health checks. + * + * ```ts title="sst.config.ts" + * const alb = new sst.aws.Alb("SharedAlb", { + * vpc, + * listeners: [ + * { port: 80, protocol: "http" }, + * ], + * }); + * ``` + * + * Services can use header-based routing in addition to path-based: + * + * ```ts title="sst.config.ts" + * new sst.aws.Service("InternalApi", { + * cluster, + * image: { context: "api/" }, + * loadBalancer: { + * instance: alb, + * rules: [ + * { + * listen: "80/http", + * forward: "3000/http", + * conditions: { + * path: "/api/*", + * header: { name: "x-internal", values: ["true"] }, + * }, + * priority: 50, + * }, + * ], + * }, + * }); + * ``` + * + * This example creates: + * - A shared ALB with an HTTP listener + * - An API service with path-based routing and custom health check + * - A Web service with path-based routing + * - Both services share the same ALB + */ +export default $config({ + app(input) { + return { + name: "aws-shared-alb", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + providers: { + aws: { + region: "us-east-1", + }, + }, + }; + }, + async run() { + const vpc = new sst.aws.Vpc("MyVpc"); + const cluster = new sst.aws.Cluster("MyCluster", { vpc }); + + // Create a shared ALB with an HTTP listener + const alb = new sst.aws.Alb("SharedAlb", { + vpc, + listeners: [{ port: 80, protocol: "http" }], + }); + + // API service β€” handles /api/* with a custom health check path + new sst.aws.Service("Api", { + cluster, + image: { context: "api/" }, + loadBalancer: { + instance: alb, + rules: [ + { + listen: "80/http", + forward: "3000/http", + conditions: { path: "/api/*" }, + priority: 100, + }, + ], + health: { + "3000/http": { + path: "/api/health", + interval: "10 seconds", + timeout: "5 seconds", + healthyThreshold: 2, + unhealthyThreshold: 3, + }, + }, + }, + }); + + // Web service β€” handles everything else under /app/* + new sst.aws.Service("Web", { + cluster, + image: { context: "web/" }, + loadBalancer: { + instance: alb, + rules: [ + { + listen: "80/http", + forward: "3000/http", + conditions: { path: "/app/*" }, + priority: 200, + }, + ], + health: { + "3000/http": { + path: "/app/health", + interval: "10 seconds", + timeout: "5 seconds", + }, + }, + }, + }); + + return { + url: alb.url, + }; + }, +}); diff --git a/examples/aws-shared-alb/test.sh b/examples/aws-shared-alb/test.sh new file mode 100755 index 0000000000..2f7495c709 --- /dev/null +++ b/examples/aws-shared-alb/test.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Test the shared ALB example endpoints after deployment. +# +# Usage: +# ./test.sh [alb-url] +# +# If no URL is provided, attempts to read it from sst output. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Get ALB URL from argument or sst output +if [ -n "${1:-}" ]; then + ALB_URL="$1" +else + echo "Fetching ALB URL from sst output..." + ALB_URL=$(npx sst output url 2>/dev/null || true) + if [ -z "$ALB_URL" ]; then + echo "ERROR: Could not determine ALB URL." + echo "Usage: ./test.sh " + echo " e.g. ./test.sh http://SharedAlb-123456.us-east-1.elb.amazonaws.com" + exit 1 + fi +fi + +# Strip trailing slash +ALB_URL="${ALB_URL%/}" + +echo "=== Testing Shared ALB Example ===" +echo "ALB URL: $ALB_URL" +echo "" + +PASS=0 +FAIL=0 + +run_test() { + local name="$1" + local url="$2" + local expected="$3" + + echo -n "TEST: $name ... " + HTTP_CODE=$(curl -s -o /tmp/alb_test_response -w "%{http_code}" --max-time 10 "$url" 2>/dev/null || echo "000") + BODY=$(cat /tmp/alb_test_response 2>/dev/null || echo "") + + if [ "$HTTP_CODE" = "000" ]; then + echo "FAIL (connection error)" + FAIL=$((FAIL + 1)) + return + fi + + if echo "$BODY" | grep -q "$expected"; then + echo "OK (HTTP $HTTP_CODE) β†’ $BODY" + PASS=$((PASS + 1)) + else + echo "FAIL (HTTP $HTTP_CODE, expected '$expected')" + echo " Got: $BODY" + FAIL=$((FAIL + 1)) + fi +} + +# Note: ALB path pattern "/api/*" matches "/api/" but NOT "/api" itself. +# Tests use paths with a trailing segment to match the wildcard. + +# Test 1: API service β€” /api/health returns { "status": "ok" } +run_test "API health (/api/health)" "$ALB_URL/api/health" '"status":"ok"' + +# Test 2: API service β€” /api/users returns { "service": "api", "path": "/api/users" } +run_test "API routing (/api/users)" "$ALB_URL/api/users" '"service":"api"' + +# Test 3: API service β€” /api/greeting returns path info +run_test "API greeting (/api/greeting)" "$ALB_URL/api/greeting" "/api/greeting" + +# Test 4: Web service β€” /app/health returns { "status": "ok" } +run_test "Web health (/app/health)" "$ALB_URL/app/health" '"status":"ok"' + +# Test 5: Web service β€” /app/dashboard returns HTML with path +run_test "Web routing (/app/dashboard)" "$ALB_URL/app/dashboard" "/app/dashboard" + +# Test 6: Web service β€” /app/greeting returns path info +run_test "Web greeting (/app/greeting)" "$ALB_URL/app/greeting" "/app/greeting" + +# Test 7: Default action β€” / should return 404 (ALB default) +echo -n "TEST: Default action (/) ... " +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$ALB_URL/" 2>/dev/null || echo "000") +if [ "$HTTP_CODE" = "403" ]; then + echo "PASS (HTTP 403 as expected)" + PASS=$((PASS + 1)) +else + echo "FAIL (expected 403, got HTTP $HTTP_CODE)" + FAIL=$((FAIL + 1)) +fi + +echo "" +echo "=== Results: $PASS passed, $FAIL failed ===" + +if [ "$FAIL" -gt 0 ]; then + exit 1 +fi diff --git a/examples/aws-shared-alb/web/.dockerignore b/examples/aws-shared-alb/web/.dockerignore new file mode 100644 index 0000000000..d98fe17111 --- /dev/null +++ b/examples/aws-shared-alb/web/.dockerignore @@ -0,0 +1,3 @@ + +# sst +.sst \ No newline at end of file diff --git a/examples/aws-shared-alb/web/Dockerfile b/examples/aws-shared-alb/web/Dockerfile new file mode 100644 index 0000000000..53718c02f4 --- /dev/null +++ b/examples/aws-shared-alb/web/Dockerfile @@ -0,0 +1,10 @@ +FROM node:18-bullseye-slim + +WORKDIR /app/ + +COPY package.json /app +RUN npm install + +COPY index.mjs /app + +ENTRYPOINT ["node", "index.mjs"] diff --git a/examples/aws-shared-alb/web/index.mjs b/examples/aws-shared-alb/web/index.mjs new file mode 100644 index 0000000000..6640bec96f --- /dev/null +++ b/examples/aws-shared-alb/web/index.mjs @@ -0,0 +1,21 @@ +import express from "express"; + +const PORT = 3000; + +const app = express(); + +app.get("/app", (req, res) => { + res.send("

Web App

Hello from the Web service

"); +}); + +app.get("/app/health", (req, res) => { + res.json({ status: "ok" }); +}); + +app.get("/app/*", (req, res) => { + res.send(`

Web App

Path: ${req.path}

`); +}); + +app.listen(PORT, () => { + console.log(`Web service is running on http://localhost:${PORT}`); +}); diff --git a/examples/aws-shared-alb/web/package.json b/examples/aws-shared-alb/web/package.json new file mode 100644 index 0000000000..9c43fe9bb1 --- /dev/null +++ b/examples/aws-shared-alb/web/package.json @@ -0,0 +1,7 @@ +{ + "name": "web", + "version": "1.0.0", + "dependencies": { + "express": "^4.21.1" + } +} diff --git a/examples/aws-sharp/package.json b/examples/aws-sharp/package.json index b876053105..941e53db73 100644 --- a/examples/aws-sharp/package.json +++ b/examples/aws-sharp/package.json @@ -11,7 +11,7 @@ "license": "ISC", "dependencies": { "sharp": "^0.33.5", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "8.10.145" diff --git a/examples/aws-sharp/sst-env.d.ts b/examples/aws-sharp/sst-env.d.ts deleted file mode 100644 index f110f33043..0000000000 --- a/examples/aws-sharp/sst-env.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - } -} -export {} diff --git a/examples/aws-solid-container-ws/README.md b/examples/aws-solid-container-ws/README.md deleted file mode 100644 index a84af3943c..0000000000 --- a/examples/aws-solid-container-ws/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# SolidStart - -Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com); - -## Creating a project - -```bash -# create a new project in the current directory -npm init solid@latest - -# create a new project in my-app -npm init solid@latest my-app -``` - -## Developing - -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: - -```bash -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` - -## Building - -Solid apps are built with _presets_, which optimise your project for deployment to different environments. - -By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`. - -## This project was created with the [Solid CLI](https://solid-cli.netlify.app) diff --git a/examples/aws-solid-container-ws/package.json b/examples/aws-solid-container-ws/package.json index da1c50a3a6..27a783d39f 100644 --- a/examples/aws-solid-container-ws/package.json +++ b/examples/aws-solid-container-ws/package.json @@ -12,7 +12,7 @@ "@solidjs/router": "^0.14.7", "@solidjs/start": "^1.0.8", "solid-js": "^1.9.1", - "sst": "^3", + "sst": "file:../../sdk/js", "vinxi": "^0.4.3" }, "engines": { diff --git a/examples/aws-solid-container-ws/sst-env.d.ts b/examples/aws-solid-container-ws/sst-env.d.ts deleted file mode 100644 index 8beddcd4c3..0000000000 --- a/examples/aws-solid-container-ws/sst-env.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "bastion": string - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-solid-container/README.md b/examples/aws-solid-container/README.md deleted file mode 100644 index a84af3943c..0000000000 --- a/examples/aws-solid-container/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# SolidStart - -Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com); - -## Creating a project - -```bash -# create a new project in the current directory -npm init solid@latest - -# create a new project in my-app -npm init solid@latest my-app -``` - -## Developing - -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: - -```bash -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` - -## Building - -Solid apps are built with _presets_, which optimise your project for deployment to different environments. - -By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`. - -## This project was created with the [Solid CLI](https://solid-cli.netlify.app) diff --git a/examples/aws-solid-container/package.json b/examples/aws-solid-container/package.json index 5dac496be8..3f3f4c9222 100644 --- a/examples/aws-solid-container/package.json +++ b/examples/aws-solid-container/package.json @@ -13,7 +13,7 @@ "@solidjs/start": "^1.0.8", "ioredis": "^5.4.1", "solid-js": "^1.9.1", - "sst": "^3", + "sst": "file:../../sdk/js", "vinxi": "^0.4.3" }, "engines": { diff --git a/examples/aws-solid-container/sst-env.d.ts b/examples/aws-solid-container/sst-env.d.ts deleted file mode 100644 index 261f14f5b2..0000000000 --- a/examples/aws-solid-container/sst-env.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyRedis": { - "host": string - "password": string - "port": number - "type": "sst.aws.Redis" - "username": string - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "bastion": string - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-solid/README.md b/examples/aws-solid/README.md deleted file mode 100644 index a84af3943c..0000000000 --- a/examples/aws-solid/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# SolidStart - -Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com); - -## Creating a project - -```bash -# create a new project in the current directory -npm init solid@latest - -# create a new project in my-app -npm init solid@latest my-app -``` - -## Developing - -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: - -```bash -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` - -## Building - -Solid apps are built with _presets_, which optimise your project for deployment to different environments. - -By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`. - -## This project was created with the [Solid CLI](https://solid-cli.netlify.app) diff --git a/examples/aws-solid/package.json b/examples/aws-solid/package.json index df92e8eb0f..a9fa8b8ca3 100644 --- a/examples/aws-solid/package.json +++ b/examples/aws-solid/package.json @@ -14,7 +14,7 @@ "@solidjs/router": "^0.14.8", "@solidjs/start": "^1.0.8", "solid-js": "^1.9.1", - "sst": "^3", + "sst": "file:../../sdk/js", "vinxi": "^0.4.3" }, "engines": { diff --git a/examples/aws-solid/sst-env.d.ts b/examples/aws-solid/sst-env.d.ts deleted file mode 100644 index 5b7be3d793..0000000000 --- a/examples/aws-solid/sst-env.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyWeb": { - "type": "sst.aws.SolidStart" - "url": string - } - } -} diff --git a/examples/aws-static-site-basic-auth/package.json b/examples/aws-static-site-basic-auth/package.json index 42a97291be..da474b02d6 100644 --- a/examples/aws-static-site-basic-auth/package.json +++ b/examples/aws-static-site-basic-auth/package.json @@ -10,6 +10,6 @@ "license": "ISC", "description": "", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-static-site-basic-auth/sst-env.d.ts b/examples/aws-static-site-basic-auth/sst-env.d.ts deleted file mode 100644 index 4ce40f3fb5..0000000000 --- a/examples/aws-static-site-basic-auth/sst-env.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MySite": { - "type": "sst.aws.StaticSite" - "url": string - } - } -} diff --git a/examples/aws-static-site/package.json b/examples/aws-static-site/package.json index 889548b28f..5e4a51b0c2 100644 --- a/examples/aws-static-site/package.json +++ b/examples/aws-static-site/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "main": "index.js", "devDependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" }, "scripts": { "dev": "npx --yes serve ./site" diff --git a/examples/aws-static-site/site/404.html b/examples/aws-static-site/site/404.html new file mode 100644 index 0000000000..e1dc7559af --- /dev/null +++ b/examples/aws-static-site/site/404.html @@ -0,0 +1,7 @@ + + + 404 Not Found + +

404 - Page Not Found

+ + diff --git a/examples/aws-static-site/sst-env.d.ts b/examples/aws-static-site/sst-env.d.ts deleted file mode 100644 index 8df4df2ce6..0000000000 --- a/examples/aws-static-site/sst-env.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - MySite: { - type: "sst.aws.StaticSite" - url: string - } - } -} -export {} \ No newline at end of file diff --git a/examples/aws-static-site/sst.config.ts b/examples/aws-static-site/sst.config.ts index a27bdef4eb..2cde0b1837 100644 --- a/examples/aws-static-site/sst.config.ts +++ b/examples/aws-static-site/sst.config.ts @@ -17,6 +17,7 @@ export default $config({ async run() { new sst.aws.StaticSite("MySite", { path: "site", + errorPage: "404.html", }); }, }); diff --git a/examples/aws-step-functions-task-token/package.json b/examples/aws-step-functions-task-token/package.json index 6bbe959610..6f54d1065d 100644 --- a/examples/aws-step-functions-task-token/package.json +++ b/examples/aws-step-functions-task-token/package.json @@ -14,7 +14,7 @@ "license": "ISC", "dependencies": { "@aws-sdk/client-sfn": "^3.812.0", - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "^8.10.11" diff --git a/examples/aws-step-functions/package.json b/examples/aws-step-functions/package.json index 16a01d1f07..f1d60f0a31 100644 --- a/examples/aws-step-functions/package.json +++ b/examples/aws-step-functions/package.json @@ -13,6 +13,6 @@ "author": "", "license": "ISC", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-supabase-drizzle/package.json b/examples/aws-supabase-drizzle/package.json index 8864b2ffe3..9439872cd4 100644 --- a/examples/aws-supabase-drizzle/package.json +++ b/examples/aws-supabase-drizzle/package.json @@ -12,6 +12,6 @@ "drizzle-kit": "0.20.17-679add7", "drizzle-orm": "^0.30.9", "postgres": "^3.4.4", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-supabase-drizzle/sst-env.d.ts b/examples/aws-supabase-drizzle/sst-env.d.ts deleted file mode 100644 index e1afc776a9..0000000000 --- a/examples/aws-supabase-drizzle/sst-env.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst"; -declare module "sst" { - export interface Resource { - Database: { - database: string; - host: string; - password: string; - port: number; - type: "supabase.index/project.Project"; - user: string; - }; - } -} -export {}; diff --git a/examples/aws-svelte-container/README.md b/examples/aws-svelte-container/README.md deleted file mode 100644 index b5b295070b..0000000000 --- a/examples/aws-svelte-container/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# sv - -Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). - -## Creating a project - -If you're seeing this, you've probably already done this step. Congrats! - -```bash -# create a new project in the current directory -npx sv create - -# create a new project in my-app -npx sv create my-app -``` - -## Developing - -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: - -```bash -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` - -## Building - -To create a production version of your app: - -```bash -npm run build -``` - -You can preview the production build with `npm run preview`. - -> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/examples/aws-svelte-container/package.json b/examples/aws-svelte-container/package.json index 3deee46ffc..f62de0b433 100644 --- a/examples/aws-svelte-container/package.json +++ b/examples/aws-svelte-container/package.json @@ -22,7 +22,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.701.0", "@aws-sdk/s3-request-presigner": "^3.701.0", - "sst": "^3", + "sst": "file:../../sdk/js", "svelte-kit-sst": "2.43.5" } } diff --git a/examples/aws-svelte-container/sst-env.d.ts b/examples/aws-svelte-container/sst-env.d.ts deleted file mode 100644 index ff1d425c12..0000000000 --- a/examples/aws-svelte-container/sst-env.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-svelte-kit-remote-functions/README.md b/examples/aws-svelte-kit-remote-functions/README.md deleted file mode 100644 index 75842c404d..0000000000 --- a/examples/aws-svelte-kit-remote-functions/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# sv - -Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). - -## Creating a project - -If you're seeing this, you've probably already done this step. Congrats! - -```sh -# create a new project in the current directory -npx sv create - -# create a new project in my-app -npx sv create my-app -``` - -## Developing - -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: - -```sh -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` - -## Building - -To create a production version of your app: - -```sh -npm run build -``` - -You can preview the production build with `npm run preview`. - -> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/examples/aws-svelte-kit-remote-functions/bun.lock b/examples/aws-svelte-kit-remote-functions/bun.lock deleted file mode 100644 index 546547453a..0000000000 --- a/examples/aws-svelte-kit-remote-functions/bun.lock +++ /dev/null @@ -1,556 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "aws-svelte-kit-remote-functions", - "dependencies": { - "sst": "latest", - "svelte-kit-sst": "latest", - }, - "devDependencies": { - "@sveltejs/adapter-auto": "^6.1.0", - "@sveltejs/kit": "^2.43.2", - "@sveltejs/vite-plugin-svelte": "^6.2.0", - "@tailwindcss/vite": "^4.1.13", - "svelte": "^5.39.5", - "svelte-check": "^4.3.2", - "tailwindcss": "^4.1.13", - "typescript": "^5.9.2", - "vite": "^7.1.7", - }, - }, - }, - "packages": { - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.11", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.11", "", { "os": "android", "cpu": "arm" }, "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.11", "", { "os": "android", "cpu": "arm64" }, "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.11", "", { "os": "android", "cpu": "x64" }, "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.11", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.11", "", { "os": "linux", "cpu": "arm" }, "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.11", "", { "os": "linux", "cpu": "ia32" }, "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.11", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.11", "", { "os": "linux", "cpu": "s390x" }, "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.11", "", { "os": "linux", "cpu": "x64" }, "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.11", "", { "os": "none", "cpu": "arm64" }, "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.11", "", { "os": "none", "cpu": "x64" }, "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.11", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.11", "", { "os": "openbsd", "cpu": "x64" }, "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.11", "", { "os": "none", "cpu": "arm64" }, "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.11", "", { "os": "sunos", "cpu": "x64" }, "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.11", "", { "os": "win32", "cpu": "x64" }, "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA=="], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="], - - "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], - - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.5", "", { "os": "android", "cpu": "arm" }, "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.5", "", { "os": "android", "cpu": "arm64" }, "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.5", "", { "os": "linux", "cpu": "arm" }, "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.5", "", { "os": "linux", "cpu": "arm" }, "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q=="], - - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA=="], - - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.5", "", { "os": "linux", "cpu": "x64" }, "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.5", "", { "os": "linux", "cpu": "x64" }, "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.5", "", { "os": "none", "cpu": "arm64" }, "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.5", "", { "os": "win32", "cpu": "x64" }, "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg=="], - - "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], - - "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.6", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ=="], - - "@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@6.1.1", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-cBNt4jgH4KuaNO5gRSB2CZKkGtz+OCZ8lPjRQGjhvVUD4akotnj2weUia6imLl2v07K3IgsQRyM36909miSwoQ=="], - - "@sveltejs/kit": ["@sveltejs/kit@2.48.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-WIgVMGt+b9OxPDtu0Txow28RsBrLoV3wr2QoUxEHd4CHbpxbqKQf2SIEzd+SE+bqrUL2D5MxBeQBdY+t7o6n1A=="], - - "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", "deepmerge": "^4.3.1", "magic-string": "^0.30.17", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ=="], - - "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.1", "", { "dependencies": { "debug": "^4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA=="], - - "@tailwindcss/node": ["@tailwindcss/node@4.1.16", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.19", "source-map-js": "^1.2.1", "tailwindcss": "4.1.16" } }, "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw=="], - - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.16", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.16", "@tailwindcss/oxide-darwin-arm64": "4.1.16", "@tailwindcss/oxide-darwin-x64": "4.1.16", "@tailwindcss/oxide-freebsd-x64": "4.1.16", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.16", "@tailwindcss/oxide-linux-arm64-musl": "4.1.16", "@tailwindcss/oxide-linux-x64-gnu": "4.1.16", "@tailwindcss/oxide-linux-x64-musl": "4.1.16", "@tailwindcss/oxide-wasm32-wasi": "4.1.16", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.16", "@tailwindcss/oxide-win32-x64-msvc": "4.1.16" } }, "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg=="], - - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.16", "", { "os": "android", "cpu": "arm64" }, "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA=="], - - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA=="], - - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg=="], - - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.16", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg=="], - - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16", "", { "os": "linux", "cpu": "arm" }, "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw=="], - - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w=="], - - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ=="], - - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.16", "", { "os": "linux", "cpu": "x64" }, "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew=="], - - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.16", "", { "os": "linux", "cpu": "x64" }, "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw=="], - - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.16", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.7", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q=="], - - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A=="], - - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.16", "", { "os": "win32", "cpu": "x64" }, "sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg=="], - - "@tailwindcss/vite": ["@tailwindcss/vite@4.1.16", "", { "dependencies": { "@tailwindcss/node": "4.1.16", "@tailwindcss/oxide": "4.1.16", "tailwindcss": "4.1.16" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg=="], - - "@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="], - - "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - - "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - - "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], - - "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - - "aws-sdk": ["aws-sdk@2.1692.0", "", { "dependencies": { "buffer": "4.9.2", "events": "1.1.1", "ieee754": "1.1.13", "jmespath": "0.16.0", "querystring": "0.2.0", "sax": "1.2.1", "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", "xml2js": "0.6.2" } }, "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw=="], - - "aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="], - - "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], - - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - - "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], - - "buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="], - - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - - "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - - "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - - "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - - "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], - - "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - - "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - - "devalue": ["devalue@5.4.2", "", {}, "sha512-MwPZTKEPK2k8Qgfmqrd48ZKVvzSQjgW0lXLxiIBA8dQjtf/6mw6pggHNLcyDKyf+fI6eXxlQwPsfaCMTU5U+Bw=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - - "esbuild": ["esbuild@0.25.11", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.11", "@esbuild/android-arm": "0.25.11", "@esbuild/android-arm64": "0.25.11", "@esbuild/android-x64": "0.25.11", "@esbuild/darwin-arm64": "0.25.11", "@esbuild/darwin-x64": "0.25.11", "@esbuild/freebsd-arm64": "0.25.11", "@esbuild/freebsd-x64": "0.25.11", "@esbuild/linux-arm": "0.25.11", "@esbuild/linux-arm64": "0.25.11", "@esbuild/linux-ia32": "0.25.11", "@esbuild/linux-loong64": "0.25.11", "@esbuild/linux-mips64el": "0.25.11", "@esbuild/linux-ppc64": "0.25.11", "@esbuild/linux-riscv64": "0.25.11", "@esbuild/linux-s390x": "0.25.11", "@esbuild/linux-x64": "0.25.11", "@esbuild/netbsd-arm64": "0.25.11", "@esbuild/netbsd-x64": "0.25.11", "@esbuild/openbsd-arm64": "0.25.11", "@esbuild/openbsd-x64": "0.25.11", "@esbuild/openharmony-arm64": "0.25.11", "@esbuild/sunos-x64": "0.25.11", "@esbuild/win32-arm64": "0.25.11", "@esbuild/win32-ia32": "0.25.11", "@esbuild/win32-x64": "0.25.11" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q=="], - - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], - - "esrap": ["esrap@2.1.1", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-ebTT9B6lOtZGMgJ3o5r12wBacHctG7oEWazIda8UlPfA3HD/Wrv8FdXoVo73vzdpwCxNyXjPauyN2bbJzMkB9A=="], - - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - - "events": ["events@1.1.1", "", {}, "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw=="], - - "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], - - "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], - - "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], - - "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], - - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], - - "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], - - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - - "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="], - - "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], - - "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], - - "ieee754": ["ieee754@1.1.13", "", {}, "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - - "is-arguments": ["is-arguments@1.2.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="], - - "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], - - "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], - - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - - "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], - - "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], - - "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], - - "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], - - "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - - "jmespath": ["jmespath@0.16.0", "", {}, "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw=="], - - "jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="], - - "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], - - "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], - - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], - - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], - - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], - - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], - - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], - - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], - - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], - - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], - - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], - - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], - - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], - - "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], - - "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - - "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], - - "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], - - "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - - "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "oidc-token-hash": ["oidc-token-hash@5.1.1", "", {}, "sha512-D7EmwxJV6DsEB6vOFLrBM2OzsVgQzgPWyHlV2OOAVj772n+WTXpudC9e9u5BVKQnYwaD30Ivhi9b+4UeBcGu9g=="], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "opencontrol": ["opencontrol@0.0.6", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.6.1", "@tsconfig/bun": "1.0.7", "hono": "4.7.4", "zod": "3.24.2", "zod-to-json-schema": "3.24.3" }, "bin": { "opencontrol": "bin/index.mjs" } }, "sha512-QeCrpOK5D15QV8kjnGVeD/BHFLwcVr+sn4T6KKmP0WAMs2pww56e4h+eOGHb5iPOufUQXbdbBKi6WV2kk7tefQ=="], - - "openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="], - - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], - - "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], - - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - - "punycode": ["punycode@1.3.2", "", {}, "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw=="], - - "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], - - "querystring": ["querystring@0.2.0", "", {}, "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g=="], - - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - - "raw-body": ["raw-body@3.0.1", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.7.0", "unpipe": "1.0.0" } }, "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA=="], - - "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - - "rollup": ["rollup@4.52.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.5", "@rollup/rollup-android-arm64": "4.52.5", "@rollup/rollup-darwin-arm64": "4.52.5", "@rollup/rollup-darwin-x64": "4.52.5", "@rollup/rollup-freebsd-arm64": "4.52.5", "@rollup/rollup-freebsd-x64": "4.52.5", "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", "@rollup/rollup-linux-arm-musleabihf": "4.52.5", "@rollup/rollup-linux-arm64-gnu": "4.52.5", "@rollup/rollup-linux-arm64-musl": "4.52.5", "@rollup/rollup-linux-loong64-gnu": "4.52.5", "@rollup/rollup-linux-ppc64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-musl": "4.52.5", "@rollup/rollup-linux-s390x-gnu": "4.52.5", "@rollup/rollup-linux-x64-gnu": "4.52.5", "@rollup/rollup-linux-x64-musl": "4.52.5", "@rollup/rollup-openharmony-arm64": "4.52.5", "@rollup/rollup-win32-arm64-msvc": "4.52.5", "@rollup/rollup-win32-ia32-msvc": "4.52.5", "@rollup/rollup-win32-x64-gnu": "4.52.5", "@rollup/rollup-win32-x64-msvc": "4.52.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw=="], - - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - - "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="], - - "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], - - "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], - - "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], - - "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - - "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "sst": ["sst@3.17.21", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.21", "sst-darwin-x64": "3.17.21", "sst-linux-arm64": "3.17.21", "sst-linux-x64": "3.17.21", "sst-linux-x86": "3.17.21", "sst-win32-arm64": "3.17.21", "sst-win32-x64": "3.17.21", "sst-win32-x86": "3.17.21" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-cnzLtxY67kMneQ5h/dTSWBvvWOOp7viZ0VQzuzpET8hsHOzn450wbJbnGehGxsa9KdMHye0wZkKIkZmFz0YgDQ=="], - - "sst-darwin-arm64": ["sst-darwin-arm64@3.17.21", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vHFarepMF9Q4ThpjbuWwuWLyToRHCWnuKl5P+Nm5hDkT4hlcp+l/ElfYYCtI1uevfipzjdMxpssAUd1dzm6J/g=="], - - "sst-darwin-x64": ["sst-darwin-x64@3.17.21", "", { "os": "darwin", "cpu": "x64" }, "sha512-dehHJBLw1aKmc08x1cb1MGA/VujFhI0xi7MtU6yWEK1O04eSDvlEoW1inwVuVkzJw/9qSeBWf1Ixq+16ELu/Mw=="], - - "sst-linux-arm64": ["sst-linux-arm64@3.17.21", "", { "os": "linux", "cpu": "arm64" }, "sha512-jPcM6wpHEKWId7dchaTB1G016QjtN4/kKJko6Z1Byi0P1eUtlTtHN8INv+HWRKLX7ynd1lbRR9I10aHbaYmUMg=="], - - "sst-linux-x64": ["sst-linux-x64@3.17.21", "", { "os": "linux", "cpu": "x64" }, "sha512-tPdNR7KblS7Vn8knRdIo8yJ+dBL3Y3RBuDKEtrrhW01pDauyA5Ypt4FkWxQDG/0nxoCkQUDJpOoEEEgKtJG71Q=="], - - "sst-linux-x86": ["sst-linux-x86@3.17.21", "", { "os": "linux", "cpu": "none" }, "sha512-gh6MhTl2/f/lGeV4UcnmMuQ5TykjbTBrPX5hiv0sLyoI1w7oKh9SKKD388bPdcit+qqsoWyTP3Z+cjvH1RfucA=="], - - "sst-win32-arm64": ["sst-win32-arm64@3.17.21", "", { "os": "win32", "cpu": "arm64" }, "sha512-auOY7jxOrR5uqhPMKLnkjC0cFiQ89/FkVs51rGKX6ok2NvWME+QyshSw++nZKrIkWOvDZnnpGM+myvI80xb3kw=="], - - "sst-win32-x64": ["sst-win32-x64@3.17.21", "", { "os": "win32", "cpu": "x64" }, "sha512-CtZE+V3O7o89DAmJpN7f3zrRa4mar59xyiatVNxdAUDDZyYVaMKDCr42bIczDPxjcYnoE8NA0ac5tpCViJxJNw=="], - - "sst-win32-x86": ["sst-win32-x86@3.17.21", "", { "os": "win32", "cpu": "none" }, "sha512-Iq8PSKf7zcJhW9quD88V5qt3erPXDtmAQ/GqrwjJyfIVAU9Y1qZjyvDFVNeiUsdIyTP6smIIDwSHz+3Op9bY1A=="], - - "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], - - "svelte": ["svelte@5.42.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-+8dUmdJGvKSWEfbAgIaUmpD97s1bBAGxEf6s7wQonk+HNdMmrBZtpStzRypRqrYBFUmmhaUgBHUjraE8gLqWAw=="], - - "svelte-check": ["svelte-check@4.3.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg=="], - - "svelte-kit-sst": ["svelte-kit-sst@2.43.5", "", {}, "sha512-tvWj/YiPCL3scFcHo/RSYZWtXIuX6g6Mib4MXmDw0JrOtZHzqOCG4kWstTbjnWH4i4XyLPPvxrgSkm35muj4uw=="], - - "tailwindcss": ["tailwindcss@4.1.16", "", {}, "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA=="], - - "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], - - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - - "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], - - "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - - "url": ["url@0.10.3", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="], - - "util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="], - - "uuid": ["uuid@8.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - - "vite": ["vite@7.1.12", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug=="], - - "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], - - "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], - - "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], - - "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - - "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], - - "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], - - "zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.6.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.6.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], - - "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], - - "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "body-parser/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - - "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], - } -} diff --git a/examples/aws-svelte-kit-remote-functions/package.json b/examples/aws-svelte-kit-remote-functions/package.json index ee986606a0..2996b0ffc1 100644 --- a/examples/aws-svelte-kit-remote-functions/package.json +++ b/examples/aws-svelte-kit-remote-functions/package.json @@ -23,7 +23,7 @@ "vite": "^7.1.7" }, "dependencies": { - "sst": "^3", + "sst": "file:../../sdk/js", "svelte-kit-sst": "latest" } } diff --git a/examples/aws-svelte-kit-remote-functions/sst-env.d.ts b/examples/aws-svelte-kit-remote-functions/sst-env.d.ts deleted file mode 100644 index c1b62bdd43..0000000000 --- a/examples/aws-svelte-kit-remote-functions/sst-env.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -declare module "sst" { - export interface Resource { - } -} -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-svelte-kit/README.md b/examples/aws-svelte-kit/README.md deleted file mode 100644 index b5b295070b..0000000000 --- a/examples/aws-svelte-kit/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# sv - -Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). - -## Creating a project - -If you're seeing this, you've probably already done this step. Congrats! - -```bash -# create a new project in the current directory -npx sv create - -# create a new project in my-app -npx sv create my-app -``` - -## Developing - -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: - -```bash -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` - -## Building - -To create a production version of your app: - -```bash -npm run build -``` - -You can preview the production build with `npm run preview`. - -> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/examples/aws-svelte-kit/package.json b/examples/aws-svelte-kit/package.json index 95c968f26c..7db901eb05 100644 --- a/examples/aws-svelte-kit/package.json +++ b/examples/aws-svelte-kit/package.json @@ -21,7 +21,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.700.0", "@aws-sdk/s3-request-presigner": "^3.700.0", - "sst": "^3", + "sst": "file:../../sdk/js", "svelte-kit-sst": "2.43.5" } } diff --git a/examples/aws-svelte-kit/sst-env.d.ts b/examples/aws-svelte-kit/sst-env.d.ts deleted file mode 100644 index e973cf25d9..0000000000 --- a/examples/aws-svelte-kit/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - } -} diff --git a/examples/aws-svelte-redis/README.md b/examples/aws-svelte-redis/README.md deleted file mode 100644 index 5ce676612e..0000000000 --- a/examples/aws-svelte-redis/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# create-svelte - -Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte). - -## Creating a project - -If you're seeing this, you've probably already done this step. Congrats! - -```bash -# create a new project in the current directory -npm create svelte@latest - -# create a new project in my-app -npm create svelte@latest my-app -``` - -## Developing - -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: - -```bash -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` - -## Building - -To create a production version of your app: - -```bash -npm run build -``` - -You can preview the production build with `npm run preview`. - -> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. diff --git a/examples/aws-svelte-redis/package.json b/examples/aws-svelte-redis/package.json index 724a6f8d79..4eee5e0d29 100644 --- a/examples/aws-svelte-redis/package.json +++ b/examples/aws-svelte-redis/package.json @@ -22,7 +22,7 @@ "type": "module", "dependencies": { "ioredis": "^5.4.1", - "sst": "^3", + "sst": "file:../../sdk/js", "svelte-kit-sst": "2.43.5" } } diff --git a/examples/aws-svelte-redis/sst-env.d.ts b/examples/aws-svelte-redis/sst-env.d.ts deleted file mode 100644 index 261f14f5b2..0000000000 --- a/examples/aws-svelte-redis/sst-env.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyRedis": { - "host": string - "password": string - "port": number - "type": "sst.aws.Redis" - "username": string - } - "MyService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "MyVpc": { - "bastion": string - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-swift/README.md b/examples/aws-swift/README.md deleted file mode 100644 index cee9023202..0000000000 --- a/examples/aws-swift/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# ❍ Swift Example - -Deploy Swift applications using sst ion. - -## Build - -Building your application for deployment requires installing Docker. - -When deploying with `sst deploy` your application will be build for Amazon Linux, ensuring its compatible with the AWS Lambda provided runtime. - -## Deploy - -Deploy just like any other sst project: - -```sh -sst deploy --stage production -``` - -### GitHub Actions - -When deploying from your local machine, your application is built with Docker. If you want to deploy from GitHub Actions or another CI service, you'll need to use the swift:5.10-amazonlinux2 image. - -## Multiple Targets - -A simple Swift application might include just one lambda function to act as an http server. But a more complex application will want to take advantage of queues and other event driven architectures. This is trivial to do, simply create more `sst.aws.Function`s and reference the relevant target for that function in `build(someTarget)`. Any `executableTarget` in your `Package.swift` can be passed to build. - -Here's an example of hooking up a target to an SQS Queue: - -```typescript -const queue = new sst.aws.Queue("SwiftQueue", {}); -queue.subscribe({ - runtime: "provided.al2023", - architecture: process.arch === "arm64" ? "arm64" : "x86_64", - bundle: build("queue"), - handler: "bootstrap", -}); -``` diff --git a/examples/aws-t3/.env.example b/examples/aws-t3/.env.example deleted file mode 100644 index 2a7348f326..0000000000 --- a/examples/aws-t3/.env.example +++ /dev/null @@ -1,17 +0,0 @@ -# Since the ".env" file is gitignored, you can use the ".env.example" file to -# build a new ".env" file when you clone the repo. Keep this file up-to-date -# when you add new variables to `.env`. - -# This file will be committed to version control, so make sure not to have any -# secrets in it. If you are cloning this repo, create a copy of this file named -# ".env" and populate it with your secrets. - -# When adding additional environment variables, the schema in "/src/env.js" -# should be updated accordingly. - -# Drizzle -# DATABASE_URL="postgresql://postgres:password@localhost:5432/aws-t3" - -# Example: -# SERVERVAR="foo" -# NEXT_PUBLIC_CLIENTVAR="bar" diff --git a/examples/aws-t3/README.md b/examples/aws-t3/README.md deleted file mode 100644 index 67943c7fb4..0000000000 --- a/examples/aws-t3/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# Create T3 App - -This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`. - -## What's next? How do I make an app with this? - -We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary. - -If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help. - -- [Next.js](https://nextjs.org) -- [NextAuth.js](https://next-auth.js.org) -- [Prisma](https://prisma.io) -- [Drizzle](https://orm.drizzle.team) -- [Tailwind CSS](https://tailwindcss.com) -- [tRPC](https://trpc.io) - -## Learn More - -To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources: - -- [Documentation](https://create.t3.gg/) -- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) β€” Check out these awesome tutorials - -You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) β€” your feedback and contributions are welcome! - -## How do I deploy this? - -Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information. diff --git a/examples/aws-t3/package.json b/examples/aws-t3/package.json index e977796c5f..101bfcffd5 100644 --- a/examples/aws-t3/package.json +++ b/examples/aws-t3/package.json @@ -27,7 +27,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "server-only": "^0.0.1", - "sst": "^3", + "sst": "file:../../sdk/js", "superjson": "^2.2.1", "zod": "^3.23.3" }, diff --git a/examples/aws-t3/sst-env.d.ts b/examples/aws-t3/sst-env.d.ts deleted file mode 100644 index 48bf53031d..0000000000 --- a/examples/aws-t3/sst-env.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyPostgres": { - "database": string - "host": string - "password": string - "port": number - "type": "sst.aws.Postgres" - "username": string - } - "MyVpc": { - "bastion": string - "type": "sst.aws.Vpc" - } - "MyWeb": { - "type": "sst.aws.Nextjs" - "url": string - } - } -} diff --git a/examples/aws-tanstack-start/package.json b/examples/aws-tanstack-start/package.json index 26ccbe38dd..854758a5ea 100644 --- a/examples/aws-tanstack-start/package.json +++ b/examples/aws-tanstack-start/package.json @@ -33,7 +33,7 @@ "nitro": "latest", "react": "^19.2.0", "react-dom": "^19.2.0", - "sst": "^3", + "sst": "file:../../sdk/js", "tailwind-merge": "^3.0.2", "tailwindcss": "^4.0.6", "tw-animate-css": "^1.3.6", diff --git a/examples/aws-tanstack-start/sst-env.d.ts b/examples/aws-tanstack-start/sst-env.d.ts deleted file mode 100644 index 5e42b5492b..0000000000 --- a/examples/aws-tanstack-start/sst-env.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -declare module "sst" { - export interface Resource { - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyWeb": { - "type": "sst.aws.TanstackStart" - "url": string - } - } -} -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/aws-task-cron/package.json b/examples/aws-task-cron/package.json index 2197620ad9..be1205d901 100644 --- a/examples/aws-task-cron/package.json +++ b/examples/aws-task-cron/package.json @@ -10,6 +10,6 @@ "author": "", "license": "ISC", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-task-cron/sst-env.d.ts b/examples/aws-task-cron/sst-env.d.ts deleted file mode 100644 index 8780b79adc..0000000000 --- a/examples/aws-task-cron/sst-env.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyTask": { - "assignPublicIp": boolean - "cluster": string - "containers": any - "securityGroups": any - "subnets": any - "taskDefinition": string - "type": "sst.aws.Task" - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-task/image/index.mjs b/examples/aws-task/image/index.mjs index ecc9a3d8d8..290c4cbafa 100644 --- a/examples/aws-task/image/index.mjs +++ b/examples/aws-task/image/index.mjs @@ -1,6 +1,11 @@ import { Resource } from "sst"; +import { createServer } from "http"; -for (let i = 0; i < 10; i++) { - console.log(`NEW: The bucket name is ${Resource.MyBucket.name}`); - await new Promise((resolve) => setTimeout(resolve, 1000)); -} +const server = createServer((req, res) => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ bucket: Resource.MyBucket.name })); +}); + +server.listen(8080, () => { + console.log(`Listening on :8080, bucket=${Resource.MyBucket.name}`); +}); diff --git a/examples/aws-task/image/package.json b/examples/aws-task/image/package.json index 3cfbb459c5..3101ceb26a 100644 --- a/examples/aws-task/image/package.json +++ b/examples/aws-task/image/package.json @@ -7,6 +7,6 @@ "author": "", "license": "ISC", "dependencies": { - "sst": "^3" + "sst": "file:../../../sdk/js" } } diff --git a/examples/aws-task/image/sst-env.d.ts b/examples/aws-task/image/sst-env.d.ts deleted file mode 100644 index 7ff575d49d..0000000000 --- a/examples/aws-task/image/sst-env.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyApp": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyTask": { - "assignPublicIp": boolean - "cluster": string - "containers": any - "securityGroups": any - "subnets": any - "taskDefinition": string - "type": "sst.aws.Task" - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-task/package.json b/examples/aws-task/package.json index 895ed84a23..659b639af5 100644 --- a/examples/aws-task/package.json +++ b/examples/aws-task/package.json @@ -10,6 +10,6 @@ "author": "", "license": "ISC", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-task/sst-env.d.ts b/examples/aws-task/sst-env.d.ts deleted file mode 100644 index 7ff575d49d..0000000000 --- a/examples/aws-task/sst-env.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - "MyApp": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyTask": { - "assignPublicIp": boolean - "cluster": string - "containers": any - "securityGroups": any - "subnets": any - "taskDefinition": string - "type": "sst.aws.Task" - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} diff --git a/examples/aws-task/sst.config.ts b/examples/aws-task/sst.config.ts index d95d3a6de6..e7286ebaf8 100644 --- a/examples/aws-task/sst.config.ts +++ b/examples/aws-task/sst.config.ts @@ -50,6 +50,7 @@ export default $config({ const task = new sst.aws.Task("MyTask", { cluster, + public: true, link: [bucket], image: { context: "image", diff --git a/examples/aws-topic/package.json b/examples/aws-topic/package.json index 2cc2bbe1cb..dadc45ada8 100644 --- a/examples/aws-topic/package.json +++ b/examples/aws-topic/package.json @@ -14,7 +14,7 @@ "license": "ISC", "devDependencies": { "@types/aws-lambda": "^8.10.149", - "sst": "^3" + "sst": "file:../../sdk/js" }, "dependencies": { "@aws-sdk/client-sns": "^3.515.0" diff --git a/examples/aws-trpc/package.json b/examples/aws-trpc/package.json index 6fb3a031eb..e357a5c396 100644 --- a/examples/aws-trpc/package.json +++ b/examples/aws-trpc/package.json @@ -12,7 +12,7 @@ "dependencies": { "@trpc/client": "^11.0.0-rc.528", "@trpc/server": "^11.0.0-rc.528", - "sst": "^3", + "sst": "file:../../sdk/js", "zod": "^3.22.4" }, "devDependencies": { diff --git a/examples/aws-vector/package.json b/examples/aws-vector/package.json index 187e65a6a5..ef52a9d730 100644 --- a/examples/aws-vector/package.json +++ b/examples/aws-vector/package.json @@ -14,6 +14,6 @@ "license": "ISC", "dependencies": { "openai": "^4.44.0", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/aws-vite/README.md b/examples/aws-vite/README.md deleted file mode 100644 index 0d6babeddb..0000000000 --- a/examples/aws-vite/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - -- Configure the top-level `parserOptions` property like this: - -```js -export default { - // other rules... - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['./tsconfig.json', './tsconfig.node.json'], - tsconfigRootDir: __dirname, - }, -} -``` - -- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` -- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/examples/aws-vite/src/sst-env.d.ts b/examples/aws-vite/src/sst-env.d.ts deleted file mode 100644 index 1fadcdf703..0000000000 --- a/examples/aws-vite/src/sst-env.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/// - interface ImportMetaEnv { - - } - interface ImportMeta { - readonly env: ImportMetaEnv - } \ No newline at end of file diff --git a/examples/aws-vite/sst-env.d.ts b/examples/aws-vite/sst-env.d.ts deleted file mode 100644 index 5357f35179..0000000000 --- a/examples/aws-vite/sst-env.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - "Web": { - "type": "sst.aws.StaticSite" - "url": string - } - } -} -export {} diff --git a/examples/aws-workflow-bus/package.json b/examples/aws-workflow-bus/package.json new file mode 100644 index 0000000000..05c9c3c981 --- /dev/null +++ b/examples/aws-workflow-bus/package.json @@ -0,0 +1,18 @@ +{ + "name": "aws-workflow-bus", + "version": "1.0.0", + "description": "", + "type": "module", + "main": "index.js", + "scripts": { + "dev": "go run ../../cmd/sst dev", + "deploy": "go run ../../cmd/sst deploy", + "remove": "go run ../../cmd/sst remove" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-workflow-bus/src/publisher.ts b/examples/aws-workflow-bus/src/publisher.ts new file mode 100644 index 0000000000..86699fa495 --- /dev/null +++ b/examples/aws-workflow-bus/src/publisher.ts @@ -0,0 +1,12 @@ +import { Resource } from "sst"; +import { bus } from "sst/aws/bus"; + +export async function handler() { + await bus.publish(Resource.Bus, "app.workflow.requested", { + message: "Hello from the bus", + requestId: `workflow-request-${Date.now()}`, + requestedAt: new Date().toISOString(), + }); + + return "Message published. Check the logs to see the workflow start."; +} diff --git a/examples/aws-workflow-bus/src/workflow.ts b/examples/aws-workflow-bus/src/workflow.ts new file mode 100644 index 0000000000..95a6d691c8 --- /dev/null +++ b/examples/aws-workflow-bus/src/workflow.ts @@ -0,0 +1,22 @@ +import { workflow } from "sst/aws/workflow"; + +interface Event { + "detail-type": string; + detail: { + properties: { + message: string; + }; + }; +} + +export const handler = workflow.handler(async (event, ctx) => { + await ctx.step("start", async ({ logger }) => { + logger.info("Workflow invoked by bus."); + }); + + await ctx.wait("ten-seconds", { seconds: 10 }); + + await ctx.step("finish", async ({ logger }) => { + logger.info("Workflow finished."); + }); +}); diff --git a/examples/aws-workflow-bus/sst.config.ts b/examples/aws-workflow-bus/sst.config.ts new file mode 100644 index 0000000000..446674d455 --- /dev/null +++ b/examples/aws-workflow-bus/sst.config.ts @@ -0,0 +1,42 @@ +/// + +/** + * ## AWS Workflow Bus + * + * Creates an [AWS Lambda durable workflow](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html) + * and triggers it with a [`Bus`](/docs/component/aws/bus). + */ +export default $config({ + app(input) { + return { + name: "aws-workflow-bus", + home: "aws", + removal: input?.stage === "production" ? "retain" : "remove", + }; + }, + async run() { + const workflow = new sst.aws.Workflow("MyWorkflow", { + handler: "src/workflow.handler", + }); + + const bus = new sst.aws.Bus("Bus"); + + bus.subscribe("Workflow", workflow, { + pattern: { + detailType: ["app.workflow.requested"], + }, + }); + + const publisher = new sst.aws.Function("Publisher", { + handler: "src/publisher.handler", + url: true, + link: [bus], + }); + + return { + bus: bus.name, + publisher: publisher.url, + workflow: workflow.name, + }; + }, +}); diff --git a/examples/aws-workflow-bus/tsconfig.json b/examples/aws-workflow-bus/tsconfig.json new file mode 100644 index 0000000000..6ec1c39406 --- /dev/null +++ b/examples/aws-workflow-bus/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true + } +} diff --git a/examples/aws-workflow-cron/package.json b/examples/aws-workflow-cron/package.json new file mode 100644 index 0000000000..1ad1e2e59e --- /dev/null +++ b/examples/aws-workflow-cron/package.json @@ -0,0 +1,18 @@ +{ + "name": "aws-workflow-cron", + "version": "1.0.0", + "description": "", + "type": "module", + "main": "index.js", + "scripts": { + "dev": "go run ../../cmd/sst dev", + "deploy": "go run ../../cmd/sst deploy", + "remove": "go run ../../cmd/sst remove" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "sst": "file:../../sdk/js" + } +} diff --git a/examples/aws-workflow-cron/src/workflow.ts b/examples/aws-workflow-cron/src/workflow.ts new file mode 100644 index 0000000000..bac95fa75e --- /dev/null +++ b/examples/aws-workflow-cron/src/workflow.ts @@ -0,0 +1,13 @@ +import { workflow } from "sst/aws/workflow"; + +export const handler = workflow.handler(async (event, ctx) => { + await ctx.step("start", async ({ logger }) => { + logger.info("Workflow invoked by cron."); + }); + + await ctx.wait("ten-seconds", { seconds: 10 }); + + await ctx.step("finish", async ({ logger }) => { + logger.info("Workflow finished."); + }); +}); diff --git a/examples/aws-workflow-cron/sst.config.ts b/examples/aws-workflow-cron/sst.config.ts new file mode 100644 index 0000000000..72aa6411f5 --- /dev/null +++ b/examples/aws-workflow-cron/sst.config.ts @@ -0,0 +1,45 @@ +/// + +/** + * ## AWS Workflow Cron + * + * Creates an [AWS Lambda durable workflow](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html) + * and triggers it on a schedule using [`CronV2`](/docs/component/aws/cron-v2). + * + * Since `CronV2` accepts a `Workflow`, the setup is just: + * + * ```ts title="sst.config.ts" + * const workflow = new sst.aws.Workflow("MyWorkflow", { + * handler: "src/workflow.handler", + * }); + * + * new sst.aws.CronV2("MyCron", { + * schedule: "rate(1 minute)", + * function: workflow, + * }); + * ``` + */ +export default $config({ + app(input) { + return { + name: "aws-workflow-cron", + home: "aws", + removal: input?.stage === "production" ? "retain" : "remove", + }; + }, + async run() { + const workflow = new sst.aws.Workflow("MyWorkflow", { + handler: "src/workflow.handler", + }); + + const cron = new sst.aws.CronV2("MyCron", { + schedule: "rate(1 minute)", + function: workflow, + }); + + return { + schedule: cron.nodes.schedule.name, + workflow: workflow.name, + }; + }, +}); diff --git a/examples/aws-workflow-cron/tsconfig.json b/examples/aws-workflow-cron/tsconfig.json new file mode 100644 index 0000000000..6ec1c39406 --- /dev/null +++ b/examples/aws-workflow-cron/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true + } +} diff --git a/examples/aws-workflow-python/.gitignore b/examples/aws-workflow-python/.gitignore new file mode 100644 index 0000000000..ab8ca92279 --- /dev/null +++ b/examples/aws-workflow-python/.gitignore @@ -0,0 +1,4 @@ +.sst +.venv +node_modules +*/__pycache__/ \ No newline at end of file diff --git a/examples/in-progress/aws-drizzle/src/todo.sql.ts b/examples/aws-workflow-python/invoker/__init__.py similarity index 100% rename from examples/in-progress/aws-drizzle/src/todo.sql.ts rename to examples/aws-workflow-python/invoker/__init__.py diff --git a/examples/aws-workflow-python/invoker/main.py b/examples/aws-workflow-python/invoker/main.py new file mode 100644 index 0000000000..1f480d69c4 --- /dev/null +++ b/examples/aws-workflow-python/invoker/main.py @@ -0,0 +1,18 @@ +import boto3 +import json +from typing import Any, Dict + +from sst import Resource + +client = boto3.client("lambda") + + +def handler(_event: Dict[str, Any], _context: Any) -> str: + client.invoke( + FunctionName=Resource.Workflow.name, + Qualifier=Resource.Workflow.qualifier, + InvocationType="Event", + Payload=json.dumps({"resolverUrl": Resource.Resolver.url}), + ) + + return "Workflow started. Check the logs for the callback URL." diff --git a/examples/aws-workflow-python/invoker/pyproject.toml b/examples/aws-workflow-python/invoker/pyproject.toml new file mode 100644 index 0000000000..a10493453a --- /dev/null +++ b/examples/aws-workflow-python/invoker/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "invoker" +version = "0.1.0" +description = "A SST app" +dependencies = [ + "sst-sdk", + "boto3 (>=1.42.59,<2.0.0)" +] +requires-python = "==3.13.*" + + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["main"] + + +[tool.uv.sources] +sst-sdk = { git = "https://github.com/anomalyco/sst.git", subdirectory = "sdk/python", branch = "dev" } \ No newline at end of file diff --git a/examples/aws-workflow-python/package.json b/examples/aws-workflow-python/package.json new file mode 100644 index 0000000000..3913b4208e --- /dev/null +++ b/examples/aws-workflow-python/package.json @@ -0,0 +1,24 @@ +{ + "name": "aws-workflow-python", + "version": "1.0.0", + "description": "", + "type": "module", + "main": "index.js", + "scripts": { + "dev": "go run ../../cmd/sst dev", + "deploy": "go run ../../cmd/sst deploy", + "remove": "go run ../../cmd/sst remove", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "sst": "file:../../sdk/js", + "@types/aws-lambda": "8.10.145" + }, + "dependencies": { + "@aws/durable-execution-sdk-js": "1.0.2", + "@aws-sdk/client-lambda": "^3.714.0" + } +} diff --git a/examples/aws-workflow-python/pyproject.toml b/examples/aws-workflow-python/pyproject.toml new file mode 100644 index 0000000000..051b3d7ee7 --- /dev/null +++ b/examples/aws-workflow-python/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "app" +version = "0.1.0" +description = "A SST app" +dependencies = [ + "sst-sdk", + "aws-durable-execution-sdk-python (>=1.3.0,<2.0.0)", + "boto3 (>=1.42.59,<2.0.0)" +] +requires-python = "==3.13.*" + + +[tool.uv.workspace] +members = ["workflow", "invoker", "resolver"] + +[tool.uv.sources] +sst-sdk = { git = "https://github.com/anomalyco/sst.git", subdirectory = "sdk/python", branch = "dev" } diff --git a/examples/internal/playground/sites/analog/public/.gitkeep b/examples/aws-workflow-python/resolver/__init__.py similarity index 100% rename from examples/internal/playground/sites/analog/public/.gitkeep rename to examples/aws-workflow-python/resolver/__init__.py diff --git a/examples/aws-workflow-python/resolver/main.py b/examples/aws-workflow-python/resolver/main.py new file mode 100644 index 0000000000..59d279a889 --- /dev/null +++ b/examples/aws-workflow-python/resolver/main.py @@ -0,0 +1,20 @@ +import boto3 +import json +from typing import Any, Dict + + +lambda_client = boto3.client("lambda") + + +def handler(event: Dict[str, Any], _context: Any) -> str: + query_params = event.get("queryStringParameters") or {} + token = query_params.get("token") + + if not token: + return "Missing token in query parameters" + + lambda_client.send_durable_execution_callback_success( + CallbackId=token, Result=json.dumps({"message": "Sent from the resolver."}) + ) + + return "Workflow callback sent. Check the logs to see the workflow succeed." diff --git a/examples/aws-workflow-python/resolver/pyproject.toml b/examples/aws-workflow-python/resolver/pyproject.toml new file mode 100644 index 0000000000..a911fff4eb --- /dev/null +++ b/examples/aws-workflow-python/resolver/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "resolver" +version = "0.1.0" +description = "A SST app" +authors = [{ name = "Nick Wall", email = "mail@walln.dev" }] +dependencies = [ + "sst-sdk", + "boto3 (>=1.42.59,<2.0.0)" +] +requires-python = "==3.13.*" + + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["main"] + + +[tool.uv.sources] +sst-sdk = { git = "https://github.com/anomalyco/sst.git", subdirectory = "sdk/python", branch = "dev" } \ No newline at end of file diff --git a/examples/aws-workflow-python/sst.config.ts b/examples/aws-workflow-python/sst.config.ts new file mode 100644 index 0000000000..8054965151 --- /dev/null +++ b/examples/aws-workflow-python/sst.config.ts @@ -0,0 +1,47 @@ +/// + +/** + * ## AWS Workflow Python + * + * Uses the [`Workflow`](/docs/component/aws/workflow) component to create an + * [AWS Lambda durable workflow](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html) + * using the Python runtime. + * + * Hit the `Invoker` URL to start the workflow. The workflow logs a callback URL + * with a `token` query parameter. Open that URL to resume the waiting step. + */ +export default $config({ + app(input) { + return { + name: "aws-workflow-python", + home: "aws", + removal: input?.stage === "production" ? "retain" : "remove", + }; + }, + async run() { + const workflow = new sst.aws.Workflow("Workflow", { + handler: "workflow/main.handler", + runtime: "python3.13", + }); + + const resolver = new sst.aws.Function("Resolver", { + handler: "resolver/main.handler", + runtime: "python3.13", + url: true, + link: [workflow], + }); + + const invoker = new sst.aws.Function("Invoker", { + handler: "invoker/main.handler", + runtime: "python3.13", + url: true, + link: [workflow, resolver], + }); + + return { + workflow: workflow.name, + invoker: invoker.url, + resolver: resolver.url, + }; + }, +}); diff --git a/platform/functions/worker-wrapper/index.ts b/examples/aws-workflow-python/workflow/__init__.py similarity index 100% rename from platform/functions/worker-wrapper/index.ts rename to examples/aws-workflow-python/workflow/__init__.py diff --git a/examples/aws-workflow-python/workflow/main.py b/examples/aws-workflow-python/workflow/main.py new file mode 100644 index 0000000000..52d49f1cd5 --- /dev/null +++ b/examples/aws-workflow-python/workflow/main.py @@ -0,0 +1,37 @@ +from typing import Any, Dict +from urllib.parse import urlencode + +from aws_durable_execution_sdk_python import ( + DurableContext, + StepContext, + durable_execution, + durable_step, +) +from aws_durable_execution_sdk_python.config import Duration, WaitForCallbackConfig + + +@durable_step +def start(step_context: StepContext) -> None: + step_context.logger.info("Workflow started.") + + +@durable_step +def finish(step_context: StepContext, result: Any) -> None: + step_context.logger.info(result) + + +@durable_execution +def handler(event: Dict[str, Any], context: DurableContext) -> None: + context.step(start()) + + def log_callback_url(token: str, step_context: StepContext) -> None: + callback_url = f"{event['resolverUrl']}?{urlencode({'token': token})}" + step_context.logger.info(callback_url) + + result = context.wait_for_callback( + log_callback_url, + name="callback", + config=WaitForCallbackConfig(timeout=Duration.from_minutes(5)), + ) + + context.step(finish(result)) diff --git a/examples/aws-workflow-python/workflow/pyproject.toml b/examples/aws-workflow-python/workflow/pyproject.toml new file mode 100644 index 0000000000..9f98baee3c --- /dev/null +++ b/examples/aws-workflow-python/workflow/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "workflow" +version = "0.1.0" +description = "A SST app" +dependencies = [ + "sst-sdk", + "aws-durable-execution-sdk-python (>=1.3.0,<2.0.0)", + "boto3 (>=1.42.59,<2.0.0)" +] +requires-python = "==3.13.*" + + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["main"] + +[tool.uv.sources] +sst-sdk = { git = "https://github.com/anomalyco/sst.git", subdirectory = "sdk/python", branch = "dev" } diff --git a/examples/aws-workflow/.gitignore b/examples/aws-workflow/.gitignore new file mode 100644 index 0000000000..67f09953e7 --- /dev/null +++ b/examples/aws-workflow/.gitignore @@ -0,0 +1,2 @@ +.sst +node_modules \ No newline at end of file diff --git a/examples/aws-workflow/package.json b/examples/aws-workflow/package.json new file mode 100644 index 0000000000..cc9f642fc2 --- /dev/null +++ b/examples/aws-workflow/package.json @@ -0,0 +1,20 @@ +{ + "name": "aws-workflow", + "version": "1.0.0", + "description": "", + "type": "module", + "main": "index.js", + "scripts": { + "dev": "go run ../../cmd/sst dev", + "deploy": "go run ../../cmd/sst deploy", + "remove": "go run ../../cmd/sst remove", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "sst": "file:../../sdk/js", + "@types/aws-lambda": "8.10.145" + } +} diff --git a/examples/aws-workflow/src/invoker.ts b/examples/aws-workflow/src/invoker.ts new file mode 100644 index 0000000000..262aad5196 --- /dev/null +++ b/examples/aws-workflow/src/invoker.ts @@ -0,0 +1,13 @@ +import { Resource } from "sst"; +import { workflow } from "sst/aws/workflow"; + +export const handler = async () => { + await workflow.start(Resource.Workflow, { + name: `workflow-example-${Date.now()}`, + payload: { + resolverUrl: Resource.Resolver.url, + }, + }); + + return "Workflow started. Check the logs for the callback URL."; +}; diff --git a/examples/aws-workflow/src/resolver.ts b/examples/aws-workflow/src/resolver.ts new file mode 100644 index 0000000000..3dddf0fd58 --- /dev/null +++ b/examples/aws-workflow/src/resolver.ts @@ -0,0 +1,18 @@ +import { APIGatewayProxyEventV2 } from "aws-lambda"; +import { workflow } from "sst/aws/workflow"; + +export const handler = async (event: APIGatewayProxyEventV2) => { + const token = event.queryStringParameters?.token; + + if (!token) { + return "Missing token in query parameters"; + } + + await workflow.succeed(token, { + payload: { + message: "Sent from the resolver." + }, + }); + + return "Workflow callback sent. Check the logs to see the workflow succeed." ; +}; diff --git a/examples/aws-workflow/src/workflow.ts b/examples/aws-workflow/src/workflow.ts new file mode 100644 index 0000000000..81c1c962b7 --- /dev/null +++ b/examples/aws-workflow/src/workflow.ts @@ -0,0 +1,30 @@ +import { workflow } from "sst/aws/workflow"; + +interface Event { + resolverUrl: string; +} + +export const handler = workflow.handler(async (event, ctx) => { + await ctx.step("start", async ({ logger }) => { + logger.info("Workflow started."); + }); + + const result = await ctx.waitForCallback( + "callback", + async (token, { logger }) => { + const resolverUrl = new URL(event.resolverUrl); + resolverUrl.searchParams.set("token", token); + + logger.info(resolverUrl.toString()); + }, + { + timeout: { + minutes: 5, + }, + }, + ); + + await ctx.step("finish", async ({ logger }) => { + logger.info(result); + }); +}); diff --git a/examples/aws-workflow/sst.config.ts b/examples/aws-workflow/sst.config.ts new file mode 100644 index 0000000000..86ca3e8c7c --- /dev/null +++ b/examples/aws-workflow/sst.config.ts @@ -0,0 +1,43 @@ +/// + +/** + * ## AWS Workflow + * + * Uses the [`Workflow`](/docs/component/aws/workflow) component to create an + * [AWS Lambda durable workflow](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html). + * + * Hit the `Invoker` URL to start the workflow. The workflow logs a callback URL + * with a `token` query parameter. Open that URL to resume the waiting step. + */ +export default $config({ + app(input) { + return { + name: "aws-workflow", + home: "aws", + removal: input?.stage === "production" ? "retain" : "remove", + }; + }, + async run() { + const workflow = new sst.aws.Workflow("Workflow", { + handler: "src/workflow.handler", + }); + + const resolver = new sst.aws.Function("Resolver", { + handler: "src/resolver.handler", + url: true, + link: [workflow], + }); + + const invoker = new sst.aws.Function("Invoker", { + handler: "src/invoker.handler", + url: true, + link: [workflow, resolver], + }); + + return { + workflow: workflow.name, + invoker: invoker.url, + resolver: resolver.url, + }; + }, +}); diff --git a/examples/aws-zero/package.json b/examples/aws-zero/package.json index 539d55a283..d19fc63147 100644 --- a/examples/aws-zero/package.json +++ b/examples/aws-zero/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "dependencies": { "@rocicorp/zero": "^0.7.2024120400", - "sst": "^3" + "sst": "file:../../sdk/js" }, "trustedDependencies": [ "@rocicorp/zero-sqlite3" diff --git a/examples/aws-zero/sst-env.d.ts b/examples/aws-zero/sst-env.d.ts deleted file mode 100644 index e973cf25d9..0000000000 --- a/examples/aws-zero/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - } -} diff --git a/examples/bad-decision/package.json b/examples/bad-decision/package.json index 324a3c2f4e..4680e6a944 100644 --- a/examples/bad-decision/package.json +++ b/examples/bad-decision/package.json @@ -2,6 +2,6 @@ "name": "bad-decision", "version": "0.0.0", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/cloudflare-astro-v5/.gitignore b/examples/cloudflare-astro-v5/.gitignore new file mode 100644 index 0000000000..1ebaf43005 --- /dev/null +++ b/examples/cloudflare-astro-v5/.gitignore @@ -0,0 +1,24 @@ +# build output +dist/ + +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store + +# cloudflare +.wrangler diff --git a/examples/cloudflare-astro-v5/astro.config.mjs b/examples/cloudflare-astro-v5/astro.config.mjs new file mode 100644 index 0000000000..3f3e9df000 --- /dev/null +++ b/examples/cloudflare-astro-v5/astro.config.mjs @@ -0,0 +1,7 @@ +import { defineConfig } from "astro/config"; +import cloudflare from "@astrojs/cloudflare"; + +export default defineConfig({ + output: "server", + adapter: cloudflare(), +}); diff --git a/examples/cloudflare-astro-v5/package.json b/examples/cloudflare-astro-v5/package.json new file mode 100644 index 0000000000..670035a540 --- /dev/null +++ b/examples/cloudflare-astro-v5/package.json @@ -0,0 +1,19 @@ +{ + "name": "cloudflare-astro-v5", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/cloudflare": "^12.5.3", + "astro": "^5.8.1", + "sst": "file:../../sdk/js" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20260414.1" + } +} diff --git a/examples/cloudflare-astro-v5/src/env.d.ts b/examples/cloudflare-astro-v5/src/env.d.ts new file mode 100644 index 0000000000..f964fe0cff --- /dev/null +++ b/examples/cloudflare-astro-v5/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/cloudflare-astro-v5/src/layouts/Layout.astro b/examples/cloudflare-astro-v5/src/layouts/Layout.astro new file mode 100644 index 0000000000..586b0c9e0b --- /dev/null +++ b/examples/cloudflare-astro-v5/src/layouts/Layout.astro @@ -0,0 +1,65 @@ +--- +interface Props { + title: string; +} + +const { title } = Astro.props; +--- + + + + + + + + {title} + + +
+ +
+ + + + diff --git a/examples/cloudflare-astro-v5/src/pages/index.astro b/examples/cloudflare-astro-v5/src/pages/index.astro new file mode 100644 index 0000000000..7d466f9875 --- /dev/null +++ b/examples/cloudflare-astro-v5/src/pages/index.astro @@ -0,0 +1,14 @@ +--- +import Layout from "../layouts/Layout.astro"; +--- + + +

Astro on Cloudflare

+

+ This page is server rendered and reads the linked R2 bucket through + Astro.locals.runtime.env. +

+

+ Open the prerendered page +

+
diff --git a/examples/cloudflare-astro-v5/src/pages/prerendered.astro b/examples/cloudflare-astro-v5/src/pages/prerendered.astro new file mode 100644 index 0000000000..fb0854578a --- /dev/null +++ b/examples/cloudflare-astro-v5/src/pages/prerendered.astro @@ -0,0 +1,16 @@ +--- +import Layout from "../layouts/Layout.astro"; + +export const prerender = true; +--- + + +

Prerendered page

+

+ Astro can mix prerendered routes with server rendered routes in the same + Cloudflare app. +

+

+ Back to the server rendered page +

+
diff --git a/examples/cloudflare-astro-v5/sst.config.ts b/examples/cloudflare-astro-v5/sst.config.ts new file mode 100644 index 0000000000..f553e64b11 --- /dev/null +++ b/examples/cloudflare-astro-v5/sst.config.ts @@ -0,0 +1,22 @@ +/// + +export default $config({ + app(input) { + return { + name: "cloudflare-astro-v5", + removal: input?.stage === "production" ? "retain" : "remove", + home: "cloudflare", + }; + }, + async run() { + const bucket = new sst.cloudflare.Bucket("MyBucket"); + const kv = new sst.cloudflare.Kv("MyKv"); + + new sst.cloudflare.Astro("MyWeb", { + link: [bucket, kv], + environment: { + MESSAGE: "Hello from Astro on Cloudflare", + }, + }); + }, +}); diff --git a/examples/internal/playground/sites/astro5-static/tsconfig.json b/examples/cloudflare-astro-v5/tsconfig.json similarity index 100% rename from examples/internal/playground/sites/astro5-static/tsconfig.json rename to examples/cloudflare-astro-v5/tsconfig.json diff --git a/examples/cloudflare-astro/.gitignore b/examples/cloudflare-astro/.gitignore new file mode 100644 index 0000000000..2ee5819604 --- /dev/null +++ b/examples/cloudflare-astro/.gitignore @@ -0,0 +1,25 @@ +# build output +dist/ + +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store + +# cloudflare +.wrangler +wrangler.jsonc diff --git a/examples/cloudflare-astro/astro.config.mjs b/examples/cloudflare-astro/astro.config.mjs new file mode 100644 index 0000000000..1ffdb32f3e --- /dev/null +++ b/examples/cloudflare-astro/astro.config.mjs @@ -0,0 +1,8 @@ +import { defineConfig } from "astro/config"; +import cloudflare from "@astrojs/cloudflare"; + +export default defineConfig({ + adapter: cloudflare({ + configPath: process.env.SST_WRANGLER_PATH, + }), +}); diff --git a/examples/cloudflare-astro/package.json b/examples/cloudflare-astro/package.json new file mode 100644 index 0000000000..10895142cc --- /dev/null +++ b/examples/cloudflare-astro/package.json @@ -0,0 +1,19 @@ +{ + "name": "cloudflare-astro", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/cloudflare": "^13", + "astro": "^6", + "sst": "file:../../sdk/js" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4" + } +} diff --git a/examples/cloudflare-astro/src/layouts/Layout.astro b/examples/cloudflare-astro/src/layouts/Layout.astro new file mode 100644 index 0000000000..586b0c9e0b --- /dev/null +++ b/examples/cloudflare-astro/src/layouts/Layout.astro @@ -0,0 +1,65 @@ +--- +interface Props { + title: string; +} + +const { title } = Astro.props; +--- + + + + + + + + {title} + + +
+ +
+ + + + diff --git a/examples/cloudflare-astro/src/pages/index.astro b/examples/cloudflare-astro/src/pages/index.astro new file mode 100644 index 0000000000..51bdec8edd --- /dev/null +++ b/examples/cloudflare-astro/src/pages/index.astro @@ -0,0 +1,32 @@ +--- +import Layout from "../layouts/Layout.astro"; +import { Resource } from "sst/resource"; + +const objects = await Resource.MyBucket.list(); +--- + + +

Astro on Cloudflare

+

+ This page is server rendered using Resource.MyBucket from + sst/resource β€” works in both sst dev and + sst deploy. +

+ +

+ Open the prerendered page +

+ +
+

Bucket objects

+ { + objects.objects.length > 0 ? ( +
    + {objects.objects.map((object) =>
  • {object.key}
  • )} +
+ ) : ( +

No objects yet.

+ ) + } +
+
diff --git a/examples/cloudflare-astro/src/pages/prerendered.astro b/examples/cloudflare-astro/src/pages/prerendered.astro new file mode 100644 index 0000000000..fb0854578a --- /dev/null +++ b/examples/cloudflare-astro/src/pages/prerendered.astro @@ -0,0 +1,16 @@ +--- +import Layout from "../layouts/Layout.astro"; + +export const prerender = true; +--- + + +

Prerendered page

+

+ Astro can mix prerendered routes with server rendered routes in the same + Cloudflare app. +

+

+ Back to the server rendered page +

+
diff --git a/examples/cloudflare-astro/sst.config.ts b/examples/cloudflare-astro/sst.config.ts new file mode 100644 index 0000000000..aa4bf2f67b --- /dev/null +++ b/examples/cloudflare-astro/sst.config.ts @@ -0,0 +1,24 @@ +/// + +/** + * ## Cloudflare Astro + * + * Deploy an [Astro](https://astro.build) site to Cloudflare. + */ +export default $config({ + app(input) { + return { + name: "cloudflare-astro", + removal: input?.stage === "production" ? "retain" : "remove", + home: "cloudflare", + }; + }, + async run() { + const bucket = new sst.cloudflare.Bucket("MyBucket"); + const kv = new sst.cloudflare.Kv("MyKv"); + + new sst.cloudflare.Astro("MyWeb", { + link: [bucket, kv], + }); + }, +}); diff --git a/examples/internal/playground/sites/astro5/tsconfig.json b/examples/cloudflare-astro/tsconfig.json similarity index 100% rename from examples/internal/playground/sites/astro5/tsconfig.json rename to examples/cloudflare-astro/tsconfig.json diff --git a/examples/cloudflare-auth/bun.lockb b/examples/cloudflare-auth/bun.lockb deleted file mode 100755 index faed8d72a9..0000000000 Binary files a/examples/cloudflare-auth/bun.lockb and /dev/null differ diff --git a/examples/cloudflare-auth/package.json b/examples/cloudflare-auth/package.json index a6fa930de7..0d16f72f39 100644 --- a/examples/cloudflare-auth/package.json +++ b/examples/cloudflare-auth/package.json @@ -12,6 +12,6 @@ "license": "ISC", "dependencies": { "@cloudflare/workers-types": "^4.20240403.0", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/cloudflare-auth/sst-env.d.ts b/examples/cloudflare-auth/sst-env.d.ts deleted file mode 100644 index ac168d9fdb..0000000000 --- a/examples/cloudflare-auth/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -import "sst" -declare module "sst" { - export interface Resource { - } -} diff --git a/examples/cloudflare-cron/package.json b/examples/cloudflare-cron/package.json index 37457373aa..c9f9240fb9 100644 --- a/examples/cloudflare-cron/package.json +++ b/examples/cloudflare-cron/package.json @@ -2,7 +2,7 @@ "name": "cloudflare-cron", "version": "0.0.0", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@cloudflare/workers-types": "^4.20240405.0" diff --git a/examples/cloudflare-cron/sst-env.d.ts b/examples/cloudflare-cron/sst-env.d.ts deleted file mode 100644 index 37fafe8654..0000000000 --- a/examples/cloudflare-cron/sst-env.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -import "sst" -declare module "sst" { - export interface Resource { - } -} -// cloudflare -import * as cloudflare from "@cloudflare/workers-types"; -declare module "sst" { - export interface Resource { - "CronHandler": cloudflare.Service - } -} diff --git a/examples/cloudflare-d1-static/package.json b/examples/cloudflare-d1-static/package.json index c3be32852f..8e19d043c6 100644 --- a/examples/cloudflare-d1-static/package.json +++ b/examples/cloudflare-d1-static/package.json @@ -2,7 +2,7 @@ "name": "cloudflare-d1-static", "version": "0.0.0", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@cloudflare/workers-types": "^4.20240405.0" diff --git a/examples/cloudflare-d1-static/sst-env.d.ts b/examples/cloudflare-d1-static/sst-env.d.ts deleted file mode 100644 index a4988265c8..0000000000 --- a/examples/cloudflare-d1-static/sst-env.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -import "sst" -declare module "sst" { - export interface Resource { - } -} -// cloudflare -import * as cloudflare from "@cloudflare/workers-types"; -declare module "sst" { - export interface Resource { - "MyDatabase": cloudflare.D1Database - "Worker": cloudflare.Service - } -} - -import "sst" -export {} \ No newline at end of file diff --git a/examples/cloudflare-d1/package.json b/examples/cloudflare-d1/package.json index e8b2f1c939..8cf3d4422d 100644 --- a/examples/cloudflare-d1/package.json +++ b/examples/cloudflare-d1/package.json @@ -2,7 +2,7 @@ "name": "cloudflare-d1", "version": "0.0.0", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@cloudflare/workers-types": "^4.20240405.0" diff --git a/examples/cloudflare-d1/sst-env.d.ts b/examples/cloudflare-d1/sst-env.d.ts deleted file mode 100644 index 32596882a3..0000000000 --- a/examples/cloudflare-d1/sst-env.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import "sst" -declare module "sst" { - export interface Resource { - Worker: { - type: "sst.cloudflare.Worker" - url: string - } - } -} -// cloudflare -declare module "sst" { - export interface Resource { - MyDatabase: import("@cloudflare/workers-types").D1Database - } -} diff --git a/examples/cloudflare-durable-object/package.json b/examples/cloudflare-durable-object/package.json new file mode 100644 index 0000000000..489dfee08d --- /dev/null +++ b/examples/cloudflare-durable-object/package.json @@ -0,0 +1,10 @@ +{ + "name": "cloudflare-durable-object", + "version": "0.0.0", + "dependencies": { + "sst": "file:../../sdk/js" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20240405.0" + } +} diff --git a/examples/cloudflare-durable-object/sst.config.ts b/examples/cloudflare-durable-object/sst.config.ts new file mode 100644 index 0000000000..fee3156dc5 --- /dev/null +++ b/examples/cloudflare-durable-object/sst.config.ts @@ -0,0 +1,40 @@ +/// + +/** + * ## Cloudflare Durable Object + * + * This example creates a Durable Object and links it to a worker. + * + * Send a `GET` request to the `url` output. The worker calls the Durable + * Object, and the Durable Object logs the current count. + */ +export default $config({ + app(input) { + return { + name: "cloudflare-durable-object", + home: "cloudflare", + removal: input?.stage === "production" ? "retain" : "remove", + }; + }, + async run() { + const counter = new sst.cloudflare.DurableObject("Counter", { + className: "Counter", + }); + + const api = new sst.cloudflare.Worker("Api", { + migrations: [ + { + tag: "v1", + newSqliteClasses: [counter.className], + }, + ], + handler: "worker.ts", + link: [counter], + url: true, + }); + + return { + url: api.url, + }; + }, +}); diff --git a/examples/cloudflare-durable-object/tsconfig.json b/examples/cloudflare-durable-object/tsconfig.json new file mode 100644 index 0000000000..071591ba65 --- /dev/null +++ b/examples/cloudflare-durable-object/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["@cloudflare/workers-types"] + } +} diff --git a/examples/cloudflare-durable-object/worker.ts b/examples/cloudflare-durable-object/worker.ts new file mode 100644 index 0000000000..b6c8ef0386 --- /dev/null +++ b/examples/cloudflare-durable-object/worker.ts @@ -0,0 +1,28 @@ +import { Resource } from "sst"; +import { DurableObject } from "cloudflare:workers"; + +export default { + async fetch(request: Request) { + const url = new URL(request.url); + + if (url.pathname === "/favicon.ico") { + return new Response(null, { status: 204 }); + } + + const stub = Resource.Counter.getByName("global"); + return stub.fetch("https://counter/"); + }, +}; + +export class Counter extends DurableObject { + async fetch() { + const current = (await this.ctx.storage.get("count")) ?? 0; + const count = current + 1; + + await this.ctx.storage.put("count", count); + + console.log("durable object hit", { count }); + + return Response.json({ count }); + } +} diff --git a/examples/cloudflare-hono/bun.lockb b/examples/cloudflare-hono/bun.lockb deleted file mode 100755 index d2ab7c8d6a..0000000000 Binary files a/examples/cloudflare-hono/bun.lockb and /dev/null differ diff --git a/examples/cloudflare-hono/index.ts b/examples/cloudflare-hono/index.ts index 03c49c4467..384cdd1c8c 100644 --- a/examples/cloudflare-hono/index.ts +++ b/examples/cloudflare-hono/index.ts @@ -21,6 +21,12 @@ const app = new Hono() const result = await Resource.MyBucket.get(first.key); c.header("content-type", result.httpMetadata.contentType); return c.body(result.body); + }) + .get("/ai", async (c) => { + const result = await Resource.Ai.run("@cf/meta/llama-3-8b-instruct", { + prompt: "What is the origin of the phrase 'Hello, World'", + }); + return c.json(result.response); }); export default app; diff --git a/examples/cloudflare-hono/package.json b/examples/cloudflare-hono/package.json index fcadf581f4..5bf7bf9909 100644 --- a/examples/cloudflare-hono/package.json +++ b/examples/cloudflare-hono/package.json @@ -10,10 +10,10 @@ "author": "", "license": "ISC", "dependencies": { - "hono": "^4.2.3", - "sst": "^3" + "hono": "^4.11.4", + "sst": "../../sdk/js" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20240405.0" + "@cloudflare/workers-types": "^4.20260114.0" } } diff --git a/examples/cloudflare-hono/sst-env.d.ts b/examples/cloudflare-hono/sst-env.d.ts deleted file mode 100644 index 748808368f..0000000000 --- a/examples/cloudflare-hono/sst-env.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -import "sst" -declare module "sst" { - export interface Resource { - } -} -// cloudflare -import * as cloudflare from "@cloudflare/workers-types"; -declare module "sst" { - export interface Resource { - "Hono": cloudflare.Service - "MyBucket": cloudflare.R2Bucket - } -} - -import "sst" -export {} \ No newline at end of file diff --git a/examples/cloudflare-hono/sst.config.ts b/examples/cloudflare-hono/sst.config.ts index 2d9a4a5642..b61e503715 100644 --- a/examples/cloudflare-hono/sst.config.ts +++ b/examples/cloudflare-hono/sst.config.ts @@ -9,10 +9,11 @@ export default $config({ }; }, async run() { + const ai = new sst.cloudflare.Ai("Ai"); const bucket = new sst.cloudflare.Bucket("MyBucket"); const hono = new sst.cloudflare.Worker("Hono", { url: true, - link: [bucket], + link: [bucket, ai], handler: "index.ts", }); diff --git a/examples/cloudflare-hyperdrive-aws/lambda.ts b/examples/cloudflare-hyperdrive-aws/lambda.ts new file mode 100644 index 0000000000..0186385826 --- /dev/null +++ b/examples/cloudflare-hyperdrive-aws/lambda.ts @@ -0,0 +1,23 @@ +import postgres from "postgres"; +import { Resource } from "sst"; + +const sql = postgres( + `postgres://${Resource.Postgres.username}:${Resource.Postgres.password}@${Resource.Postgres.host}:${Resource.Postgres.port}/${Resource.Postgres.database}`, +); + +export async function handler() { + try { + const now = Date.now(); + const result = await sql`select * from pg_tables limit 10`; + const delay = Date.now() - now; + return { + statusCode: 200, + body: JSON.stringify({ result, delay }), + }; + } catch (e: any) { + return { + statusCode: 200, + body: JSON.stringify({ error: e.message }), + }; + } +} diff --git a/examples/cloudflare-hyperdrive-aws/package.json b/examples/cloudflare-hyperdrive-aws/package.json new file mode 100644 index 0000000000..ed1c8eaf51 --- /dev/null +++ b/examples/cloudflare-hyperdrive-aws/package.json @@ -0,0 +1,12 @@ +{ + "name": "cloudflare-hyperdrive-aws", + "version": "0.0.0", + "type": "module", + "dependencies": { + "postgres": "^3.4.5", + "sst": "file:../../sdk/js" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250412.0" + } +} diff --git a/examples/cloudflare-hyperdrive-aws/sst.config.ts b/examples/cloudflare-hyperdrive-aws/sst.config.ts new file mode 100644 index 0000000000..b457f4888e --- /dev/null +++ b/examples/cloudflare-hyperdrive-aws/sst.config.ts @@ -0,0 +1,191 @@ +/// + +/** + * ## Cloudflare Hyperdrive with AWS Postgres + * + * Connect a Cloudflare Worker to an AWS RDS Postgres database through Cloudflare + * Hyperdrive. Since RDS lives in a private VPC, a Cloudflare tunnel runs on + * Fargate to expose the database to Hyperdrive. Cloudflare Access locks the + * tunnel down to Hyperdrive using a service token. + * + * The worker uses the `sst.cloudflare.Hyperdrive` binding. The lambda connects + * directly through the VPC for comparison. + */ +export default $config({ + app(input) { + return { + name: "cloudflare-hyperdrive-aws", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + providers: { cloudflare: "6.13.0", random: "4.18.0" }, + }; + }, + async run() { + const domain = "sst-dev.org"; + + const vpc = new sst.aws.Vpc("Vpc", { nat: "managed" }); + const cluster = new sst.aws.Cluster("Cluster", { vpc }); + const postgres = new sst.aws.Postgres("Postgres", { vpc }); + + const zone = cloudflare.getZoneOutput({ filter: { name: domain } }); + + const tunnelSecret = new random.RandomString("TunnelSecret", { + length: 32, + }); + + const tunnel = new cloudflare.ZeroTrustTunnelCloudflared("Tunnel", { + accountId: sst.cloudflare.DEFAULT_ACCOUNT_ID, + name: `${$app.name}-${$app.stage}-tunnel`, + tunnelSecret: tunnelSecret.result.apply((v) => + Buffer.from(v).toString("base64"), + ), + }); + + const record = new cloudflare.DnsRecord("TunnelRecord", { + name: `hyperdrive-${$app.stage}.${domain}`, + ttl: 1, + type: "CNAME", + zoneId: zone.zoneId, + content: $interpolate`${tunnel.id}.cfargotunnel.com`, + proxied: true, + }); + + const tunnelConfig = new cloudflare.ZeroTrustTunnelCloudflaredConfig( + "TunnelConfig", + { + accountId: sst.cloudflare.DEFAULT_ACCOUNT_ID, + tunnelId: tunnel.id, + config: { + ingresses: [ + { + hostname: record.name, + service: $interpolate`tcp://${postgres.host}:${postgres.port}`, + }, + { service: "http_status:404" }, + ], + }, + }, + ); + + const tunnelToken = cloudflare.getZeroTrustTunnelCloudflaredTokenOutput({ + accountId: sst.cloudflare.DEFAULT_ACCOUNT_ID, + tunnelId: tunnel.id, + }).token; + + const serviceToken = new cloudflare.ZeroTrustAccessServiceToken( + "HyperdriveServiceToken", + { + name: `${$app.name}-${$app.stage}-hyperdrive-token`, + accountId: sst.cloudflare.DEFAULT_ACCOUNT_ID, + }, + ); + + new cloudflare.ZeroTrustAccessApplication("HyperdriveAccess", { + accountId: sst.cloudflare.DEFAULT_ACCOUNT_ID, + type: "self_hosted", + name: `${$app.name}-${$app.stage}-hyperdrive`, + domain: record.name, + destinations: [{ uri: record.name, type: "public" }], + appLauncherVisible: false, + policies: [ + { + decision: "non_identity", + includes: [ + { serviceToken: { tokenId: serviceToken.id } }, + ], + name: `${$app.name}-${$app.stage}-hyperdrive-policy`, + }, + ], + }); + + const cloudflaredService = new sst.aws.Service( + "Cloudflared", + { + wait: true, + capacity: "spot", + cluster, + containers: [ + { + name: "cloudflared", + image: "cloudflare/cloudflared:latest", + command: ["tunnel", "run"], + environment: { + TUNNEL_TOKEN: tunnelToken, + TUNNEL_METRICS: "0.0.0.0:20241", + }, + health: { + command: [ + "CMD", + "cloudflared", + "tunnel", + "--metrics", + "localhost:20241", + "ready", + ], + startPeriod: "60 seconds", + timeout: "5 seconds", + interval: "30 seconds", + retries: 3, + }, + dev: { + autostart: true, + command: $interpolate`docker run \ + --rm \ + -e TUNNEL_LOGLEVEL=info \ + --network ${$app.name} \ + --name ${$app.name}-${$app.stage}-cloudflared \ + cloudflare/cloudflared:latest \ + tunnel run --token ${tunnelToken}`, + }, + }, + ], + }, + // Make sure the tunnel's ingress rules exist before the container starts, + // otherwise cloudflared serves 503s until it re-polls the config. + { dependsOn: [tunnelConfig] }, + ); + + const hyperdrive = new sst.cloudflare.Hyperdrive( + "Database", + { + origin: { + host: record.name, + user: postgres.username, + password: postgres.password, + database: postgres.database, + accessClientId: serviceToken.clientId, + accessClientSecret: serviceToken.clientSecret, + scheme: "postgres", + }, + }, + { + dependsOn: [ + postgres, + tunnel, + record, + tunnelConfig, + cloudflaredService, + cluster, + ], + }, + ); + + const worker = new sst.cloudflare.Worker("Worker", { + handler: "./worker.ts", + link: [hyperdrive], + url: true, + }); + + const lambda = new sst.aws.Function("Lambda", { + handler: "lambda.handler", + link: [postgres], + vpc, + url: true, + }); + + return { + worker: worker.url, + lambda: lambda.url, + }; + }, +}); diff --git a/examples/cloudflare-hyperdrive-aws/tsconfig.json b/examples/cloudflare-hyperdrive-aws/tsconfig.json new file mode 100644 index 0000000000..b6990d00a2 --- /dev/null +++ b/examples/cloudflare-hyperdrive-aws/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "lib": ["esnext"], + "types": ["@cloudflare/workers-types"], + "esModuleInterop": true + } +} diff --git a/examples/cloudflare-hyperdrive-aws/worker.ts b/examples/cloudflare-hyperdrive-aws/worker.ts new file mode 100644 index 0000000000..36a18f7106 --- /dev/null +++ b/examples/cloudflare-hyperdrive-aws/worker.ts @@ -0,0 +1,21 @@ +import postgres from "postgres"; +import { Resource } from "sst"; + +export default { + async fetch(_request: Request, _env: unknown, ctx: ExecutionContext) { + const sql = postgres(Resource.Database.connectionString); + + try { + const now = Date.now(); + const result = await sql`select * from pg_tables limit 10`; + const delay = Date.now() - now; + + ctx.waitUntil(sql.end()); + + return Response.json({ delay, result }); + } catch (e: any) { + console.log(e); + return Response.json({ error: e.message }, { status: 500 }); + } + }, +}; diff --git a/examples/cloudflare-hyperdrive-planetscale/.gitignore b/examples/cloudflare-hyperdrive-planetscale/.gitignore new file mode 100644 index 0000000000..9a902f0c53 --- /dev/null +++ b/examples/cloudflare-hyperdrive-planetscale/.gitignore @@ -0,0 +1,2 @@ +# sst +.sst diff --git a/examples/cloudflare-hyperdrive-planetscale/package.json b/examples/cloudflare-hyperdrive-planetscale/package.json new file mode 100644 index 0000000000..030ce5a8df --- /dev/null +++ b/examples/cloudflare-hyperdrive-planetscale/package.json @@ -0,0 +1,12 @@ +{ + "name": "cloudflare-hyperdrive-planetscale", + "version": "0.0.0", + "dependencies": { + "pg": "^8.20.0", + "sst": "file:../../sdk/js" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20240405.0", + "@types/pg": "^8.20.0" + } +} diff --git a/examples/cloudflare-hyperdrive-planetscale/sst.config.ts b/examples/cloudflare-hyperdrive-planetscale/sst.config.ts new file mode 100644 index 0000000000..932b1bb737 --- /dev/null +++ b/examples/cloudflare-hyperdrive-planetscale/sst.config.ts @@ -0,0 +1,71 @@ +/// + +/** + * ## Cloudflare Hyperdrive PlanetScale + * + * Connect a Cloudflare Worker to a PlanetScale Postgres database through + * Cloudflare Hyperdrive. + * + */ +export default $config({ + app(input) { + return { + name: "cloudflare-hyperdrive-planetscale", + removal: input?.stage === "production" ? "retain" : "remove", + home: "cloudflare", + providers: { + planetscale: "1.0.0", + }, + }; + }, + async run() { + const db = planetscale.getDatabaseVitessOutput({ + id: "mydb", + organization: "myorg", + }); + + const branch = + $app.stage === "production" + ? planetscale.getPostgresBranchOutput({ + id: db.defaultBranch, + database: db.name, + organization: db.organization, + }) + : new planetscale.PostgresBranch("DatabaseBranch", { + database: db.name, + name: $app.stage, + organization: db.organization, + parentBranch: db.defaultBranch, + }); + + const role = new planetscale.PostgresBranchRole("DatabaseRole", { + branch: branch.name, + database: db.name, + inheritedRoles: ["pg_read_all_data", "pg_write_all_data"], + name: `${$app.name}-${$app.stage}`, + organization: db.organization, + }); + + const hyperdrive = new sst.cloudflare.Hyperdrive("Database", { + origin: { + host: role.accessHostUrl, + database: role.databaseName, + user: role.username, + password: role.password, + port: 6432, // Use 5432 for direct connection instead of PgBouncer + scheme: "postgres", + }, + caching: false, + }); + + const worker = new sst.cloudflare.Worker("Worker", { + handler: "./worker.ts", + link: [hyperdrive], + url: true, + }); + + return { + url: worker.url, + }; + }, +}); diff --git a/examples/hyperdrive/tsconfig.json b/examples/cloudflare-hyperdrive-planetscale/tsconfig.json similarity index 100% rename from examples/hyperdrive/tsconfig.json rename to examples/cloudflare-hyperdrive-planetscale/tsconfig.json diff --git a/examples/cloudflare-hyperdrive-planetscale/worker.ts b/examples/cloudflare-hyperdrive-planetscale/worker.ts new file mode 100644 index 0000000000..58bcc69d44 --- /dev/null +++ b/examples/cloudflare-hyperdrive-planetscale/worker.ts @@ -0,0 +1,19 @@ +import { Client } from "pg"; +import { Resource } from "sst"; + +export default { + async fetch() { + const client = new Client({ + connectionString: Resource.Database.connectionString, + }); + + await client.connect(); + + const result = await client.query(`SELECT NOW()`); + + return Response.json({ + success: true, + result: result.rows, + }); + }, +}; diff --git a/examples/cloudflare-kv-static/package.json b/examples/cloudflare-kv-static/package.json index 7590f0dbf7..d2a5a289c4 100644 --- a/examples/cloudflare-kv-static/package.json +++ b/examples/cloudflare-kv-static/package.json @@ -2,7 +2,7 @@ "name": "cloudflare-kv-static", "version": "0.0.0", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@cloudflare/workers-types": "^4.20240405.0" diff --git a/examples/cloudflare-kv-static/sst-env.d.ts b/examples/cloudflare-kv-static/sst-env.d.ts deleted file mode 100644 index ae17f0dc7b..0000000000 --- a/examples/cloudflare-kv-static/sst-env.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ -/* biome-ignore-all lint: auto-generated */ - -import "sst" -declare module "sst" { - export interface Resource { - } -} -// cloudflare -import * as cloudflare from "@cloudflare/workers-types"; -declare module "sst" { - export interface Resource { - "MyStorage": cloudflare.KVNamespace - "Worker": cloudflare.Service - } -} - -import "sst" -export {} \ No newline at end of file diff --git a/examples/cloudflare-kv/package.json b/examples/cloudflare-kv/package.json index c2974d22db..6bcc929e8d 100644 --- a/examples/cloudflare-kv/package.json +++ b/examples/cloudflare-kv/package.json @@ -2,7 +2,7 @@ "name": "cloudflare-kv", "version": "0.0.0", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@cloudflare/workers-types": "^4.20240405.0" diff --git a/examples/cloudflare-kv/sst-env.d.ts b/examples/cloudflare-kv/sst-env.d.ts deleted file mode 100644 index 677c14f4bc..0000000000 --- a/examples/cloudflare-kv/sst-env.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -import "sst" -declare module "sst" { - export interface Resource { - } -} -// cloudflare -import * as cloudflare from "@cloudflare/workers-types"; -declare module "sst" { - export interface Resource { - "MyStorage": cloudflare.KVNamespace - "Worker": cloudflare.Service - } -} diff --git a/examples/cloudflare-queue/consumer.ts b/examples/cloudflare-queue/consumer.ts new file mode 100644 index 0000000000..bb00621d0b --- /dev/null +++ b/examples/cloudflare-queue/consumer.ts @@ -0,0 +1,7 @@ +export default { + async queue(batch, env) { + for (const message of batch.messages) { + console.log("Processing message:", message.body); + } + }, +}; diff --git a/examples/cloudflare-queue/package.json b/examples/cloudflare-queue/package.json new file mode 100644 index 0000000000..164e499420 --- /dev/null +++ b/examples/cloudflare-queue/package.json @@ -0,0 +1,10 @@ +{ + "name": "cloudflare-queue", + "version": "0.0.0", + "dependencies": { + "sst": "file:../../sdk/js" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20240405.0" + } +} diff --git a/examples/cloudflare-queue/producer.ts b/examples/cloudflare-queue/producer.ts new file mode 100644 index 0000000000..4b6f77c843 --- /dev/null +++ b/examples/cloudflare-queue/producer.ts @@ -0,0 +1,8 @@ +import { Resource } from "sst"; + +export default { + async fetch() { + await Resource.MyQueue.send({ hello: "world" }); + return new Response("Message sent!"); + }, +}; diff --git a/examples/cloudflare-queue/sst.config.ts b/examples/cloudflare-queue/sst.config.ts new file mode 100644 index 0000000000..c914322219 --- /dev/null +++ b/examples/cloudflare-queue/sst.config.ts @@ -0,0 +1,32 @@ +/// + +/** + * ## Cloudflare Queue + * + * This example creates a Cloudflare Queue with a producer and consumer worker. + * + */ +export default $config({ + app(input) { + return { + name: "cloudflare-queue", + removal: input?.stage === "production" ? "retain" : "remove", + home: "cloudflare", + }; + }, + async run() { + const queue = new sst.cloudflare.Queue("MyQueue"); + + queue.subscribe("consumer.ts"); + + const producer = new sst.cloudflare.Worker("Producer", { + handler: "producer.ts", + link: [queue], + url: true, + }); + + return { + url: producer.url, + }; + }, +}); diff --git a/examples/cloudflare-queue/tsconfig.json b/examples/cloudflare-queue/tsconfig.json new file mode 100644 index 0000000000..3c43903cfd --- /dev/null +++ b/examples/cloudflare-queue/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.json" +} diff --git a/examples/cloudflare-rate-limit/index.ts b/examples/cloudflare-rate-limit/index.ts new file mode 100644 index 0000000000..f3a815d211 --- /dev/null +++ b/examples/cloudflare-rate-limit/index.ts @@ -0,0 +1,14 @@ +import { Resource } from "sst/resource"; + +export default { + async fetch(req: Request) { + const url = new URL(req.url); + + const outcome = await Resource.MyRateLimit.limit({ key: url.pathname }); + if (!outcome.success) { + return new Response(`Rate limit exceeded for ${url.pathname}`, { status: 429 }); + } + + return new Response("OK", { status: 200 }); + }, +}; diff --git a/examples/cloudflare-rate-limit/package.json b/examples/cloudflare-rate-limit/package.json new file mode 100644 index 0000000000..9c3fc082c8 --- /dev/null +++ b/examples/cloudflare-rate-limit/package.json @@ -0,0 +1,9 @@ +{ + "name": "cloudflare-rate-limit", + "version": "1.0.0", + "type": "module", + "dependencies": { + "@cloudflare/workers-types": "^4.20260426.1", + "sst": "file:../../sdk/js" + } +} diff --git a/examples/cloudflare-rate-limit/sst.config.ts b/examples/cloudflare-rate-limit/sst.config.ts new file mode 100644 index 0000000000..1dfa0a61c2 --- /dev/null +++ b/examples/cloudflare-rate-limit/sst.config.ts @@ -0,0 +1,33 @@ +/// + +/** + * ## Cloudflare Rate Limit + * + * This example creates a Cloudflare Rate Limit and a Worker that applies it. + */ +export default $config({ + app(input) { + return { + name: "cloudflare-rate-limit", + removal: input?.stage === "production" ? "retain" : "remove", + home: "cloudflare", + }; + }, + async run() { + const rateLimit = new sst.cloudflare.RateLimit("MyRateLimit", { + namespaceId: 1001, + limit: 100, + period: "1 minute", + }); + + const worker = new sst.cloudflare.Worker("MyWorker", { + handler: "./index.ts", + url: true, + link: [rateLimit], + }); + + return { + api: worker.url, + }; + }, +}); diff --git a/examples/cloudflare-rate-limit/tsconfig.json b/examples/cloudflare-rate-limit/tsconfig.json new file mode 100644 index 0000000000..4d3c77fe88 --- /dev/null +++ b/examples/cloudflare-rate-limit/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "lib": ["esnext"], + "types": ["@cloudflare/workers-types"] + } +} diff --git a/examples/cloudflare-react-router/.gitignore b/examples/cloudflare-react-router/.gitignore new file mode 100644 index 0000000000..a112f868a3 --- /dev/null +++ b/examples/cloudflare-react-router/.gitignore @@ -0,0 +1,19 @@ +.DS_Store +.env +/node_modules/ +*.tsbuildinfo + +# React Router +/.react-router/ +/build/ + +# Cloudflare +.mf +.wrangler +.dev.vars* + +# SST +.sst + +!.dev.vars.example +!.env.example diff --git a/examples/cloudflare-react-router/app/app.css b/examples/cloudflare-react-router/app/app.css new file mode 100644 index 0000000000..9b4c3ef902 --- /dev/null +++ b/examples/cloudflare-react-router/app/app.css @@ -0,0 +1,15 @@ +@import "tailwindcss" source("."); + +@theme { + --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +} + +html, +body { + @apply bg-white dark:bg-gray-950; + + @media (prefers-color-scheme: dark) { + color-scheme: dark; + } +} diff --git a/examples/cloudflare-react-router/app/entry.server.tsx b/examples/cloudflare-react-router/app/entry.server.tsx new file mode 100644 index 0000000000..0d843dbb1f --- /dev/null +++ b/examples/cloudflare-react-router/app/entry.server.tsx @@ -0,0 +1,43 @@ +import type { AppLoadContext, EntryContext } from "react-router"; +import { ServerRouter } from "react-router"; +import { isbot } from "isbot"; +import { renderToReadableStream } from "react-dom/server"; + +export default async function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + routerContext: EntryContext, + _loadContext: AppLoadContext +) { + let shellRendered = false; + const userAgent = request.headers.get("user-agent"); + + const body = await renderToReadableStream( + , + { + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + } + ); + shellRendered = true; + + // Ensure requests from bots and SPA Mode renders wait for all content to load before responding + // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation + if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) { + await body.allReady; + } + + responseHeaders.set("Content-Type", "text/html"); + return new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }); +} diff --git a/examples/internal/playground/sites/react-router-7-csr/app/root.tsx b/examples/cloudflare-react-router/app/root.tsx similarity index 100% rename from examples/internal/playground/sites/react-router-7-csr/app/root.tsx rename to examples/cloudflare-react-router/app/root.tsx diff --git a/examples/cloudflare-react-router/app/routes.ts b/examples/cloudflare-react-router/app/routes.ts new file mode 100644 index 0000000000..102b402587 --- /dev/null +++ b/examples/cloudflare-react-router/app/routes.ts @@ -0,0 +1,3 @@ +import { type RouteConfig, index } from "@react-router/dev/routes"; + +export default [index("routes/home.tsx")] satisfies RouteConfig; diff --git a/examples/cloudflare-react-router/app/routes/home.tsx b/examples/cloudflare-react-router/app/routes/home.tsx new file mode 100644 index 0000000000..6dd973fd28 --- /dev/null +++ b/examples/cloudflare-react-router/app/routes/home.tsx @@ -0,0 +1,27 @@ +import { Resource } from "sst/resource"; +import type { Route } from "./+types/home"; +import { Welcome } from "../welcome/welcome"; + +export function meta({}: Route.MetaArgs) { + return [ + { title: "React Router on Cloudflare with SST" }, + { name: "description", content: "Deployed with sst.cloudflare.ReactRouter" }, + ]; +} + +export async function loader() { + const count = await Resource.MyKv.get("counter"); + return { count: count ? parseInt(count, 10) : 0 }; +} + +export async function action() { + const current = await Resource.MyKv.get("counter"); + const next = current ? parseInt(current, 10) + 1 : 1; + await Resource.MyKv.put("counter", next.toString()); + return { count: next }; +} + +export default function Home({ loaderData, actionData }: Route.ComponentProps) { + const count = actionData?.count ?? loaderData.count; + return ; +} diff --git a/examples/internal/playground/sites/react-router-7-csr/app/welcome/logo-dark.svg b/examples/cloudflare-react-router/app/welcome/logo-dark.svg similarity index 100% rename from examples/internal/playground/sites/react-router-7-csr/app/welcome/logo-dark.svg rename to examples/cloudflare-react-router/app/welcome/logo-dark.svg diff --git a/examples/internal/playground/sites/react-router-7-csr/app/welcome/logo-light.svg b/examples/cloudflare-react-router/app/welcome/logo-light.svg similarity index 100% rename from examples/internal/playground/sites/react-router-7-csr/app/welcome/logo-light.svg rename to examples/cloudflare-react-router/app/welcome/logo-light.svg diff --git a/examples/cloudflare-react-router/app/welcome/welcome.tsx b/examples/cloudflare-react-router/app/welcome/welcome.tsx new file mode 100644 index 0000000000..580fad14dd --- /dev/null +++ b/examples/cloudflare-react-router/app/welcome/welcome.tsx @@ -0,0 +1,104 @@ +import { Form } from "react-router"; +import logoDark from "./logo-dark.svg"; +import logoLight from "./logo-light.svg"; + +export function Welcome({ count }: { count: number }) { + return ( +
+
+
+
+ React Router + React Router +
+
+
+ +
+
+
+ ); +} + +const resources = [ + { + href: "https://reactrouter.com/docs", + text: "React Router Docs", + icon: ( + + + + ), + }, + { + href: "https://rmx.as/discord", + text: "Join Discord", + icon: ( + + + + ), + }, +]; diff --git a/examples/cloudflare-react-router/package.json b/examples/cloudflare-react-router/package.json new file mode 100644 index 0000000000..33a010539d --- /dev/null +++ b/examples/cloudflare-react-router/package.json @@ -0,0 +1,33 @@ +{ + "name": "cloudflare-react-router", + "private": true, + "type": "module", + "scripts": { + "build": "react-router build", + "cf-typegen": "wrangler types", + "dev": "react-router dev", + "preview": "npm run build && vite preview", + "typecheck": "react-router typegen && tsc -b" + }, + "dependencies": { + "isbot": "^5.1.31", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-router": "^7.10.0", + "sst": "file:../../sdk/js" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "^1.13.5", + "@cloudflare/workers-types": "^4.20251012.0", + "@react-router/dev": "^7.10.0", + "@tailwindcss/vite": "^4.1.13", + "@types/node": "^22.19.17", + "@types/react": "^19.1.13", + "@types/react-dom": "^19.1.9", + "tailwindcss": "^4.1.13", + "typescript": "^5.9.2", + "vite": "^7.1.7", + "vite-tsconfig-paths": "^5.1.4", + "wrangler": "^4.83.0" + } +} diff --git a/examples/internal/playground/sites/react-router-7-csr/public/favicon.ico b/examples/cloudflare-react-router/public/favicon.ico similarity index 100% rename from examples/internal/playground/sites/react-router-7-csr/public/favicon.ico rename to examples/cloudflare-react-router/public/favicon.ico diff --git a/examples/cloudflare-react-router/react-router.config.ts b/examples/cloudflare-react-router/react-router.config.ts new file mode 100644 index 0000000000..c5aecdb73e --- /dev/null +++ b/examples/cloudflare-react-router/react-router.config.ts @@ -0,0 +1,8 @@ +import type { Config } from "@react-router/dev/config"; + +export default { + ssr: true, + future: { + v8_viteEnvironmentApi: true, + }, +} satisfies Config; diff --git a/examples/cloudflare-react-router/sst.config.ts b/examples/cloudflare-react-router/sst.config.ts new file mode 100644 index 0000000000..80241fed9a --- /dev/null +++ b/examples/cloudflare-react-router/sst.config.ts @@ -0,0 +1,23 @@ +/// + +/** + * ## Cloudflare React Router + * + * Deploy a [React Router](https://reactrouter.com) app to Cloudflare. + */ +export default $config({ + app(input) { + return { + name: "cloudflare-react-router", + removal: input?.stage === "production" ? "retain" : "remove", + home: "cloudflare", + }; + }, + async run() { + const kv = new sst.cloudflare.Kv("MyKv"); + + new sst.cloudflare.ReactRouter("MyWeb", { + link: [kv], + }); + }, +}); diff --git a/examples/cloudflare-react-router/tsconfig.cloudflare.json b/examples/cloudflare-react-router/tsconfig.cloudflare.json new file mode 100644 index 0000000000..dfad8b4e82 --- /dev/null +++ b/examples/cloudflare-react-router/tsconfig.cloudflare.json @@ -0,0 +1,27 @@ +{ + "extends": "./tsconfig.json", + "include": [ + ".react-router/types/**/*", + "app/**/*", + "app/**/.server/**/*", + "app/**/.client/**/*", + "workers/**/*" + ], + "compilerOptions": { + "composite": true, + "strict": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["vite/client", "@cloudflare/workers-types"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "baseUrl": ".", + "rootDirs": [".", "./.react-router/types"], + "paths": { + "~/*": ["./app/*"] + }, + "esModuleInterop": true, + "resolveJsonModule": true + } +} diff --git a/examples/cloudflare-react-router/tsconfig.json b/examples/cloudflare-react-router/tsconfig.json new file mode 100644 index 0000000000..d7ce9e49b0 --- /dev/null +++ b/examples/cloudflare-react-router/tsconfig.json @@ -0,0 +1,14 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.cloudflare.json" } + ], + "compilerOptions": { + "checkJs": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true + } +} diff --git a/examples/cloudflare-react-router/tsconfig.node.json b/examples/cloudflare-react-router/tsconfig.node.json new file mode 100644 index 0000000000..bf6283b0c1 --- /dev/null +++ b/examples/cloudflare-react-router/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "include": ["vite.config.ts"], + "compilerOptions": { + "composite": true, + "strict": true, + "types": ["node"], + "lib": ["ES2022"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler" + } +} diff --git a/examples/cloudflare-react-router/vite.config.ts b/examples/cloudflare-react-router/vite.config.ts new file mode 100644 index 0000000000..5433c4bf18 --- /dev/null +++ b/examples/cloudflare-react-router/vite.config.ts @@ -0,0 +1,17 @@ +import { reactRouter } from "@react-router/dev/vite"; +import { cloudflare } from "@cloudflare/vite-plugin"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ + cloudflare({ + viteEnvironment: { name: "ssr" }, + configPath: process.env.SST_WRANGLER_PATH, + }), + tailwindcss(), + reactRouter(), + tsconfigPaths({ projects: ["./tsconfig.json"] }), + ], +}); diff --git a/examples/cloudflare-react-router/workers/app.ts b/examples/cloudflare-react-router/workers/app.ts new file mode 100644 index 0000000000..c737298ebe --- /dev/null +++ b/examples/cloudflare-react-router/workers/app.ts @@ -0,0 +1,29 @@ +import { createRequestHandler } from "react-router"; + +declare module "react-router" { + export interface AppLoadContext { + cloudflare: { + env: Env; + ctx: ExecutionContext; + }; + } +} + +// Minimal Env type; use `sst/resource` for linked resources instead of +// reading bindings off `context.cloudflare.env`. +interface Env { + [key: string]: unknown; +} + +const requestHandler = createRequestHandler( + () => import("virtual:react-router/server-build"), + import.meta.env.MODE, +); + +export default { + async fetch(request, env, ctx) { + return requestHandler(request, { + cloudflare: { env, ctx }, + }); + }, +} satisfies ExportedHandler; diff --git a/examples/cloudflare-solid/.gitignore b/examples/cloudflare-solid/.gitignore new file mode 100644 index 0000000000..415187d94e --- /dev/null +++ b/examples/cloudflare-solid/.gitignore @@ -0,0 +1,31 @@ +dist +.solid +.output +.vercel +.netlify +.vinxi +app.config.timestamp_*.js + +# Environment +.env +.env*.local + +# dependencies +/node_modules + +# IDEs and editors +/.idea +.project +.classpath +*.launch +.settings/ + +# Temp +gitignore + +# System Files +.DS_Store +Thumbs.db + +# sst +.sst diff --git a/examples/internal/playground/infra-cf/sites/solid-start/app.config.ts b/examples/cloudflare-solid/app.config.ts similarity index 100% rename from examples/internal/playground/infra-cf/sites/solid-start/app.config.ts rename to examples/cloudflare-solid/app.config.ts diff --git a/examples/cloudflare-solid/package.json b/examples/cloudflare-solid/package.json new file mode 100644 index 0000000000..f7c0de06ed --- /dev/null +++ b/examples/cloudflare-solid/package.json @@ -0,0 +1,21 @@ +{ + "name": "cloudflare-solid", + "type": "module", + "scripts": { + "build": "vinxi build", + "dev": "vinxi dev", + "start": "vinxi start", + "version": "vinxi version" + }, + "dependencies": { + "@solidjs/meta": "^0.29.4", + "@solidjs/router": "^0.15.0", + "@solidjs/start": "^1.1.0", + "solid-js": "^1.9.5", + "sst": "file:../../sdk/js", + "vinxi": "^0.5.7" + }, + "engines": { + "node": ">=22" + } +} diff --git a/examples/cloudflare-solid/src/app.css b/examples/cloudflare-solid/src/app.css new file mode 100644 index 0000000000..203812cd41 --- /dev/null +++ b/examples/cloudflare-solid/src/app.css @@ -0,0 +1,39 @@ +body { + font-family: Gordita, Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; +} + +a { + margin-right: 1rem; +} + +main { + text-align: center; + padding: 1em; + margin: 0 auto; +} + +h1 { + color: #f48120; + text-transform: uppercase; + font-size: 4rem; + font-weight: 100; + line-height: 1.1; + margin: 4rem auto; + max-width: 14rem; +} + +p { + max-width: 18rem; + margin: 2rem auto; + line-height: 1.35; +} + +@media (min-width: 480px) { + h1 { + max-width: none; + } + + p { + max-width: none; + } +} diff --git a/examples/cloudflare-solid/src/app.tsx b/examples/cloudflare-solid/src/app.tsx new file mode 100644 index 0000000000..d83f71ecfd --- /dev/null +++ b/examples/cloudflare-solid/src/app.tsx @@ -0,0 +1,23 @@ +import { MetaProvider, Title } from "@solidjs/meta"; +import { A, Router } from "@solidjs/router"; +import { FileRoutes } from "@solidjs/start/router"; +import { Suspense } from "solid-js"; +import "./app.css"; + +export default function App() { + return ( + ( + + SolidStart - Cloudflare + Index + About + {props.children} + + )} + > + + + ); +} diff --git a/examples/internal/playground/infra-cf/sites/solid-start/src/entry-client.tsx b/examples/cloudflare-solid/src/entry-client.tsx similarity index 100% rename from examples/internal/playground/infra-cf/sites/solid-start/src/entry-client.tsx rename to examples/cloudflare-solid/src/entry-client.tsx diff --git a/examples/cloudflare-solid/src/entry-server.tsx b/examples/cloudflare-solid/src/entry-server.tsx new file mode 100644 index 0000000000..f28c4d8f98 --- /dev/null +++ b/examples/cloudflare-solid/src/entry-server.tsx @@ -0,0 +1,20 @@ +// @refresh reload +import { createHandler, StartServer } from "@solidjs/start/server"; + +export default createHandler(() => ( + ( + + + + + {assets} + + +
{children}
+ {scripts} + + + )} + /> +)); diff --git a/examples/internal/playground/sites/solid-start/src/global.d.ts b/examples/cloudflare-solid/src/global.d.ts similarity index 100% rename from examples/internal/playground/sites/solid-start/src/global.d.ts rename to examples/cloudflare-solid/src/global.d.ts diff --git a/examples/internal/playground/infra-cf/sites/solid-start/src/routes/[...404].tsx b/examples/cloudflare-solid/src/routes/[...404].tsx similarity index 100% rename from examples/internal/playground/infra-cf/sites/solid-start/src/routes/[...404].tsx rename to examples/cloudflare-solid/src/routes/[...404].tsx diff --git a/examples/cloudflare-solid/src/routes/about.tsx b/examples/cloudflare-solid/src/routes/about.tsx new file mode 100644 index 0000000000..2121089c06 --- /dev/null +++ b/examples/cloudflare-solid/src/routes/about.tsx @@ -0,0 +1,11 @@ +import { Title } from "@solidjs/meta"; + +export default function Home() { + return ( +
+ About +

About

+

SolidStart running on Cloudflare with SST.

+
+ ); +} diff --git a/examples/cloudflare-solid/src/routes/index.tsx b/examples/cloudflare-solid/src/routes/index.tsx new file mode 100644 index 0000000000..960b8f3142 --- /dev/null +++ b/examples/cloudflare-solid/src/routes/index.tsx @@ -0,0 +1,24 @@ +import { createAsync } from "@solidjs/router"; + +async function loadMessage() { + "use server"; + + return { + message: process.env.MESSAGE ?? "Hello from SolidStart", + }; +} + +export const route = { + load: () => loadMessage(), +}; + +export default function Home() { + const data = createAsync(() => loadMessage()); + + return ( +
+

Hello world!

+

{data()?.message}

+
+ ); +} diff --git a/examples/cloudflare-solid/sst.config.ts b/examples/cloudflare-solid/sst.config.ts new file mode 100644 index 0000000000..f7afd1015a --- /dev/null +++ b/examples/cloudflare-solid/sst.config.ts @@ -0,0 +1,25 @@ +/// + +export default $config({ + app(input) { + return { + name: "cloudflare-solid", + removal: input?.stage === "production" ? "retain" : "remove", + home: "cloudflare", + }; + }, + async run() { + new sst.cloudflare.x.SolidStart("MyWeb", { + environment: { + MESSAGE: "Hello from SST on Cloudflare", + }, + transform: { + server: { + placement: { + region: "aws:eu-west-1", + }, + }, + }, + }); + }, +}); diff --git a/examples/internal/playground/infra-cf/sites/solid-start/tsconfig.json b/examples/cloudflare-solid/tsconfig.json similarity index 100% rename from examples/internal/playground/infra-cf/sites/solid-start/tsconfig.json rename to examples/cloudflare-solid/tsconfig.json diff --git a/examples/cloudflare-tanstack-start/.cta.json b/examples/cloudflare-tanstack-start/.cta.json new file mode 100644 index 0000000000..cf8d8b0bd6 --- /dev/null +++ b/examples/cloudflare-tanstack-start/.cta.json @@ -0,0 +1,19 @@ +{ + "projectName": "cloudflare-tanstack-start", + "mode": "file-router", + "typescript": true, + "tailwind": true, + "packageManager": "bun", + "git": false, + "install": true, + "addOnOptions": {}, + "includeExamples": false, + "envVarValues": {}, + "routerOnly": false, + "version": 1, + "framework": "react", + "chosenAddOns": [ + "eslint", + "cloudflare" + ] +} \ No newline at end of file diff --git a/examples/cloudflare-tanstack-start/.gitignore b/examples/cloudflare-tanstack-start/.gitignore new file mode 100644 index 0000000000..8b25bb54ef --- /dev/null +++ b/examples/cloudflare-tanstack-start/.gitignore @@ -0,0 +1,13 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +.env +.nitro +.tanstack +.wrangler +.output +.vinxi +__unconfig* +todos.json diff --git a/examples/cloudflare-tanstack-start/.prettierignore b/examples/cloudflare-tanstack-start/.prettierignore new file mode 100644 index 0000000000..5322d7feea --- /dev/null +++ b/examples/cloudflare-tanstack-start/.prettierignore @@ -0,0 +1,3 @@ +package-lock.json +pnpm-lock.yaml +yarn.lock \ No newline at end of file diff --git a/examples/internal/playground/sites/tanstack-start/.vscode/settings.json b/examples/cloudflare-tanstack-start/.vscode/settings.json similarity index 100% rename from examples/internal/playground/sites/tanstack-start/.vscode/settings.json rename to examples/cloudflare-tanstack-start/.vscode/settings.json diff --git a/examples/cloudflare-tanstack-start/README.md b/examples/cloudflare-tanstack-start/README.md new file mode 100644 index 0000000000..3db06b6da5 --- /dev/null +++ b/examples/cloudflare-tanstack-start/README.md @@ -0,0 +1,204 @@ +Welcome to your new TanStack Start app! + +# Getting Started + +To run this application: + +```bash +bun install +bun --bun run dev +``` + +# Building For Production + +To build this application for production: + +```bash +bun --bun run build +``` + +## Testing + +This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with: + +```bash +bun --bun run test +``` + +## Styling + +This project uses [Tailwind CSS](https://tailwindcss.com/) for styling. + +### Removing Tailwind CSS + +If you prefer not to use Tailwind CSS: + +1. Remove the demo pages in `src/routes/demo/` +2. Replace the Tailwind import in `src/styles.css` with your own styles +3. Remove `tailwindcss()` from the plugins array in `vite.config.ts` +4. Uninstall the packages: `bun install @tailwindcss/vite tailwindcss -D` + +## Linting & Formatting + + +This project uses [eslint](https://eslint.org/) and [prettier](https://prettier.io/) for linting and formatting. Eslint is configured using [tanstack/eslint-config](https://tanstack.com/config/latest/docs/eslint). The following scripts are available: + +```bash +bun --bun run lint +bun --bun run format +bun --bun run check +``` + + + +## Routing + +This project uses [TanStack Router](https://tanstack.com/router) with file-based routing. Routes are managed as files in `src/routes`. + +### Adding A Route + +To add a new route to your application just add a new file in the `./src/routes` directory. + +TanStack will automatically generate the content of the route file for you. + +Now that you have two routes you can use a `Link` component to navigate between them. + +### Adding Links + +To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`. + +```tsx +import { Link } from "@tanstack/react-router"; +``` + +Then anywhere in your JSX you can use it like so: + +```tsx +About +``` + +This will create a link that will navigate to the `/about` route. + +More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent). + +### Using A Layout + +In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you render `{children}` in the `shellComponent`. + +Here is an example layout that includes a header: + +```tsx +import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { title: 'My App' }, + ], + }), + shellComponent: ({ children }) => ( + + + + + +
+ +
+ {children} + + + + ), +}) +``` + +More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts). + +## Server Functions + +TanStack Start provides server functions that allow you to write server-side code that seamlessly integrates with your client components. + +```tsx +import { createServerFn } from '@tanstack/react-start' + +const getServerTime = createServerFn({ + method: 'GET', +}).handler(async () => { + return new Date().toISOString() +}) + +// Use in a component +function MyComponent() { + const [time, setTime] = useState('') + + useEffect(() => { + getServerTime().then(setTime) + }, []) + + return
Server time: {time}
+} +``` + +## API Routes + +You can create API routes by using the `server` property in your route definitions: + +```tsx +import { createFileRoute } from '@tanstack/react-router' +import { json } from '@tanstack/react-start' + +export const Route = createFileRoute('/api/hello')({ + server: { + handlers: { + GET: () => json({ message: 'Hello, World!' }), + }, + }, +}) +``` + +## Data Fetching + +There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered. + +For example: + +```tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/people')({ + loader: async () => { + const response = await fetch('https://swapi.dev/api/people') + return response.json() + }, + component: PeopleComponent, +}) + +function PeopleComponent() { + const data = Route.useLoaderData() + return ( +
    + {data.results.map((person) => ( +
  • {person.name}
  • + ))} +
+ ) +} +``` + +Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters). + +# Demo files + +Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed. + +# Learn More + +You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com). + +For TanStack Start specific documentation, visit [TanStack Start](https://tanstack.com/start). diff --git a/examples/cloudflare-tanstack-start/eslint.config.js b/examples/cloudflare-tanstack-start/eslint.config.js new file mode 100644 index 0000000000..3f272c0398 --- /dev/null +++ b/examples/cloudflare-tanstack-start/eslint.config.js @@ -0,0 +1,20 @@ +// @ts-check + +import { tanstackConfig } from '@tanstack/eslint-config' + +export default [ + ...tanstackConfig, + { + rules: { + 'import/no-cycle': 'off', + 'import/order': 'off', + 'sort-imports': 'off', + '@typescript-eslint/array-type': 'off', + '@typescript-eslint/require-await': 'off', + 'pnpm/json-enforce-catalog': 'off', + }, + }, + { + ignores: ['eslint.config.js', 'prettier.config.js'], + }, +] diff --git a/examples/cloudflare-tanstack-start/package.json b/examples/cloudflare-tanstack-start/package.json new file mode 100644 index 0000000000..10f9bab6be --- /dev/null +++ b/examples/cloudflare-tanstack-start/package.json @@ -0,0 +1,56 @@ +{ + "name": "cloudflare-tanstack-start", + "private": true, + "type": "module", + "imports": { + "#/*": "./src/*" + }, + "scripts": { + "dev": "vite dev --port 3000", + "build": "vite build", + "preview": "vite preview", + "test": "vitest run", + "lint": "eslint", + "format": "prettier --check .", + "check": "prettier --write . && eslint --fix", + "deploy": "bun run build && wrangler deploy" + }, + "dependencies": { + "@cloudflare/vite-plugin": "^1.26.0", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/react-devtools": "latest", + "@tanstack/react-router": "latest", + "@tanstack/react-router-devtools": "latest", + "@tanstack/react-router-ssr-query": "latest", + "@tanstack/react-start": "latest", + "@tanstack/router-plugin": "^1.132.0", + "lucide-react": "^0.545.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "sst": "file:../../sdk/js", + "tailwindcss": "^4.1.18" + }, + "devDependencies": { + "@tailwindcss/typography": "^0.5.16", + "@tanstack/devtools-vite": "latest", + "@tanstack/eslint-config": "latest", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.0", + "@types/node": "^22.10.2", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^6.0.1", + "jsdom": "^28.1.0", + "prettier": "^3.8.1", + "typescript": "^5.7.2", + "vite": "^8.0.0", + "vitest": "^3.0.5", + "wrangler": "^4.70.0" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "esbuild", + "lightningcss" + ] + } +} diff --git a/examples/cloudflare-tanstack-start/prettier.config.js b/examples/cloudflare-tanstack-start/prettier.config.js new file mode 100644 index 0000000000..aea1c4804b --- /dev/null +++ b/examples/cloudflare-tanstack-start/prettier.config.js @@ -0,0 +1,10 @@ +// @ts-check + +/** @type {import('prettier').Config} */ +const config = { + semi: false, + singleQuote: true, + trailingComma: "all", +}; + +export default config; diff --git a/examples/cloudflare-tanstack-start/public/favicon.ico b/examples/cloudflare-tanstack-start/public/favicon.ico new file mode 100644 index 0000000000..a11777cc47 Binary files /dev/null and b/examples/cloudflare-tanstack-start/public/favicon.ico differ diff --git a/examples/cloudflare-tanstack-start/public/logo192.png b/examples/cloudflare-tanstack-start/public/logo192.png new file mode 100644 index 0000000000..fc44b0a379 Binary files /dev/null and b/examples/cloudflare-tanstack-start/public/logo192.png differ diff --git a/examples/cloudflare-tanstack-start/public/logo512.png b/examples/cloudflare-tanstack-start/public/logo512.png new file mode 100644 index 0000000000..a4e47a6545 Binary files /dev/null and b/examples/cloudflare-tanstack-start/public/logo512.png differ diff --git a/examples/cloudflare-tanstack-start/public/manifest.json b/examples/cloudflare-tanstack-start/public/manifest.json new file mode 100644 index 0000000000..078ef50116 --- /dev/null +++ b/examples/cloudflare-tanstack-start/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "TanStack App", + "name": "Create TanStack App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/cloudflare-tanstack-start/public/robots.txt b/examples/cloudflare-tanstack-start/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/examples/cloudflare-tanstack-start/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/cloudflare-tanstack-start/src/components/Footer.tsx b/examples/cloudflare-tanstack-start/src/components/Footer.tsx new file mode 100644 index 0000000000..c8bfd17b5c --- /dev/null +++ b/examples/cloudflare-tanstack-start/src/components/Footer.tsx @@ -0,0 +1,44 @@ +export default function Footer() { + const year = new Date().getFullYear() + + return ( + + ) +} diff --git a/examples/cloudflare-tanstack-start/src/components/Header.tsx b/examples/cloudflare-tanstack-start/src/components/Header.tsx new file mode 100644 index 0000000000..fa196d6047 --- /dev/null +++ b/examples/cloudflare-tanstack-start/src/components/Header.tsx @@ -0,0 +1,78 @@ +import { Link } from '@tanstack/react-router' +import ThemeToggle from './ThemeToggle' + +export default function Header() { + return ( +
+ +
+ ) +} diff --git a/examples/cloudflare-tanstack-start/src/components/ThemeToggle.tsx b/examples/cloudflare-tanstack-start/src/components/ThemeToggle.tsx new file mode 100644 index 0000000000..081ebe2b49 --- /dev/null +++ b/examples/cloudflare-tanstack-start/src/components/ThemeToggle.tsx @@ -0,0 +1,81 @@ +import { useEffect, useState } from 'react' + +type ThemeMode = 'light' | 'dark' | 'auto' + +function getInitialMode(): ThemeMode { + if (typeof window === 'undefined') { + return 'auto' + } + + const stored = window.localStorage.getItem('theme') + if (stored === 'light' || stored === 'dark' || stored === 'auto') { + return stored + } + + return 'auto' +} + +function applyThemeMode(mode: ThemeMode) { + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches + const resolved = mode === 'auto' ? (prefersDark ? 'dark' : 'light') : mode + + document.documentElement.classList.remove('light', 'dark') + document.documentElement.classList.add(resolved) + + if (mode === 'auto') { + document.documentElement.removeAttribute('data-theme') + } else { + document.documentElement.setAttribute('data-theme', mode) + } + + document.documentElement.style.colorScheme = resolved +} + +export default function ThemeToggle() { + const [mode, setMode] = useState('auto') + + useEffect(() => { + const initialMode = getInitialMode() + setMode(initialMode) + applyThemeMode(initialMode) + }, []) + + useEffect(() => { + if (mode !== 'auto') { + return + } + + const media = window.matchMedia('(prefers-color-scheme: dark)') + const onChange = () => applyThemeMode('auto') + + media.addEventListener('change', onChange) + return () => { + media.removeEventListener('change', onChange) + } + }, [mode]) + + function toggleMode() { + const nextMode: ThemeMode = + mode === 'light' ? 'dark' : mode === 'dark' ? 'auto' : 'light' + setMode(nextMode) + applyThemeMode(nextMode) + window.localStorage.setItem('theme', nextMode) + } + + const label = + mode === 'auto' + ? 'Theme mode: auto (system). Click to switch to light mode.' + : `Theme mode: ${mode}. Click to switch mode.` + + return ( + + ) +} diff --git a/examples/cloudflare-tanstack-start/src/routeTree.gen.ts b/examples/cloudflare-tanstack-start/src/routeTree.gen.ts new file mode 100644 index 0000000000..421daf2790 --- /dev/null +++ b/examples/cloudflare-tanstack-start/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as AboutRouteImport } from './routes/about' +import { Route as IndexRouteImport } from './routes/index' + +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/about' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/about' + id: '__root__' | '/' | '/about' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/examples/cloudflare-tanstack-start/src/router.tsx b/examples/cloudflare-tanstack-start/src/router.tsx new file mode 100644 index 0000000000..e7b1c4d2aa --- /dev/null +++ b/examples/cloudflare-tanstack-start/src/router.tsx @@ -0,0 +1,19 @@ +import { createRouter as createTanStackRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + const router = createTanStackRouter({ + routeTree, + scrollRestoration: true, + defaultPreload: 'intent', + defaultPreloadStaleTime: 0, + }) + + return router +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/examples/cloudflare-tanstack-start/src/routes/__root.tsx b/examples/cloudflare-tanstack-start/src/routes/__root.tsx new file mode 100644 index 0000000000..3f7f8c21e3 --- /dev/null +++ b/examples/cloudflare-tanstack-start/src/routes/__root.tsx @@ -0,0 +1,61 @@ +import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router' +import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' +import { TanStackDevtools } from '@tanstack/react-devtools' +import Footer from '../components/Footer' +import Header from '../components/Header' + +import appCss from '../styles.css?url' + +const THEME_INIT_SCRIPT = `(function(){try{var stored=window.localStorage.getItem('theme');var mode=(stored==='light'||stored==='dark'||stored==='auto')?stored:'auto';var prefersDark=window.matchMedia('(prefers-color-scheme: dark)').matches;var resolved=mode==='auto'?(prefersDark?'dark':'light'):mode;var root=document.documentElement;root.classList.remove('light','dark');root.classList.add(resolved);if(mode==='auto'){root.removeAttribute('data-theme')}else{root.setAttribute('data-theme',mode)}root.style.colorScheme=resolved;}catch(e){}})();` + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { + charSet: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + { + title: 'TanStack Start Starter', + }, + ], + links: [ + { + rel: 'stylesheet', + href: appCss, + }, + ], + }), + shellComponent: RootDocument, +}) + +function RootDocument({ children }: { children: React.ReactNode }) { + return ( + + + + + diff --git a/examples/cloudflare-vite/package.json b/examples/cloudflare-vite/package.json new file mode 100644 index 0000000000..c9b4d94e05 --- /dev/null +++ b/examples/cloudflare-vite/package.json @@ -0,0 +1,23 @@ +{ + "name": "cloudflare-vite", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.4", + "typescript": "^5.2.2", + "vite": "^7.3.1" + } +} diff --git a/examples/cloudflare-vite/public/vite.svg b/examples/cloudflare-vite/public/vite.svg new file mode 100644 index 0000000000..0906f9ca5e --- /dev/null +++ b/examples/cloudflare-vite/public/vite.svg @@ -0,0 +1 @@ + diff --git a/examples/cloudflare-vite/src/App.css b/examples/cloudflare-vite/src/App.css new file mode 100644 index 0000000000..861d6f7911 --- /dev/null +++ b/examples/cloudflare-vite/src/App.css @@ -0,0 +1,21 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} + +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} + +.card { + padding: 2em; +} diff --git a/examples/cloudflare-vite/src/App.tsx b/examples/cloudflare-vite/src/App.tsx new file mode 100644 index 0000000000..6f4a5236ec --- /dev/null +++ b/examples/cloudflare-vite/src/App.tsx @@ -0,0 +1,23 @@ +import { useState } from "react"; +import "./App.css"; + +function App() { + const [count, setCount] = useState(0); + + return ( +
+ {" "} +

Cloudflare + Vite + React

+
+ +

+ Edit src/App.tsx and save to test HMR +

+
+
+ ); +} + +export default App; diff --git a/examples/cloudflare-vite/src/index.css b/examples/cloudflare-vite/src/index.css new file mode 100644 index 0000000000..18aa9a2239 --- /dev/null +++ b/examples/cloudflare-vite/src/index.css @@ -0,0 +1,73 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} + +button:hover { + border-color: #646cff; +} + +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } +} diff --git a/examples/internal/cloudflare-vite/src/main.tsx b/examples/cloudflare-vite/src/main.tsx similarity index 100% rename from examples/internal/cloudflare-vite/src/main.tsx rename to examples/cloudflare-vite/src/main.tsx diff --git a/examples/cloudflare-vite/sst.config.ts b/examples/cloudflare-vite/sst.config.ts new file mode 100644 index 0000000000..42c696d65b --- /dev/null +++ b/examples/cloudflare-vite/sst.config.ts @@ -0,0 +1,25 @@ +/// + +/** + * ## Cloudflare SPA with Vite + * + * Deploy a single-page app (SPA) with Vite to Cloudflare. + */ +export default $config({ + app(input) { + return { + name: "cloudflare-vite", + home: "cloudflare", + removal: input?.stage === "production" ? "retain" : "remove", + }; + }, + async run() { + new sst.cloudflare.StaticSiteV2("Vite", { + notFound: "single-page-application", + build: { + command: "pnpm run build", + output: "dist", + }, + }); + }, +}); diff --git a/examples/internal/cloudflare-vite/tsconfig.json b/examples/cloudflare-vite/tsconfig.json similarity index 100% rename from examples/internal/cloudflare-vite/tsconfig.json rename to examples/cloudflare-vite/tsconfig.json diff --git a/examples/internal/cloudflare-vite/tsconfig.node.json b/examples/cloudflare-vite/tsconfig.node.json similarity index 100% rename from examples/internal/cloudflare-vite/tsconfig.node.json rename to examples/cloudflare-vite/tsconfig.node.json diff --git a/examples/internal/cloudflare-vite/vite.config.ts b/examples/cloudflare-vite/vite.config.ts similarity index 100% rename from examples/internal/cloudflare-vite/vite.config.ts rename to examples/cloudflare-vite/vite.config.ts diff --git a/examples/cloudflare-worker/package.json b/examples/cloudflare-worker/package.json index d26b305ab2..47afa10e39 100644 --- a/examples/cloudflare-worker/package.json +++ b/examples/cloudflare-worker/package.json @@ -12,6 +12,6 @@ "license": "ISC", "dependencies": { "@cloudflare/workers-types": "^4.20240403.0", - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/cloudflare-worker/sst-env.d.ts b/examples/cloudflare-worker/sst-env.d.ts deleted file mode 100644 index dddc29e972..0000000000 --- a/examples/cloudflare-worker/sst-env.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - MyWorker: { - type: "sst.cloudflare.Worker" - url: string - } - } -} -// cloudflare -declare module "sst" { - export interface Resource { - MyBucket: import("@cloudflare/workers-types").R2Bucket - } -} diff --git a/examples/cloudflare-worker/sst.config.ts b/examples/cloudflare-worker/sst.config.ts index acd4c2a51e..33e0b24a9c 100644 --- a/examples/cloudflare-worker/sst.config.ts +++ b/examples/cloudflare-worker/sst.config.ts @@ -14,6 +14,10 @@ export default $config({ handler: "./index.ts", link: [bucket], url: true, + compatibility: { + date: "2025-01-01", + flags: ["nodejs_compat"], + }, }); return { diff --git a/examples/cloudflare-workflow/api.ts b/examples/cloudflare-workflow/api.ts new file mode 100644 index 0000000000..fdea03831e --- /dev/null +++ b/examples/cloudflare-workflow/api.ts @@ -0,0 +1,33 @@ +import { Resource } from "sst"; + +export default { + async fetch(req: Request) { + const url = new URL(req.url); + + if (req.method === "POST" && url.pathname === "/orders") { + const orderId = crypto.randomUUID(); + const instance = await Resource.OrderProcessor.create({ + params: { orderId }, + }); + return Response.json({ + orderId, + instanceId: instance.id, + status: await instance.status(), + }); + } + + if (req.method === "GET" && url.pathname.startsWith("/orders/")) { + const id = url.pathname.slice("/orders/".length); + const instance = await Resource.OrderProcessor.get(id); + return Response.json({ + instanceId: instance.id, + status: await instance.status(), + }); + } + + return new Response( + "POST /orders to trigger a workflow, GET /orders/:id to check status", + { status: 200 }, + ); + }, +}; diff --git a/examples/cloudflare-workflow/bun.lock b/examples/cloudflare-workflow/bun.lock new file mode 100644 index 0000000000..f39ca7b7e2 --- /dev/null +++ b/examples/cloudflare-workflow/bun.lock @@ -0,0 +1,240 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "cloudflare-workflow", + "dependencies": { + "@cloudflare/workers-types": "^4.20240403.0", + "sst": "file:../../sdk/js", + }, + }, + }, + "packages": { + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], + + "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], + + "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], + + "@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="], + + "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + + "@aws-sdk/client-lambda": ["@aws-sdk/client-lambda@3.1032.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.1", "@aws-sdk/credential-provider-node": "^3.972.32", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", "@aws-sdk/middleware-user-agent": "^3.972.31", "@aws-sdk/region-config-resolver": "^3.972.12", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.7", "@aws-sdk/util-user-agent-browser": "^3.972.10", "@aws-sdk/util-user-agent-node": "^3.973.17", "@smithy/config-resolver": "^4.4.16", "@smithy/core": "^3.23.15", "@smithy/eventstream-serde-browser": "^4.2.14", "@smithy/eventstream-serde-config-resolver": "^4.3.14", "@smithy/eventstream-serde-node": "^4.2.14", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/hash-node": "^4.2.14", "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.30", "@smithy/middleware-retry": "^4.5.3", "@smithy/middleware-serde": "^4.2.18", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/node-http-handler": "^4.5.3", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.47", "@smithy/util-defaults-mode-node": "^4.2.52", "@smithy/util-endpoints": "^3.4.1", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.2", "@smithy/util-stream": "^4.5.23", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.16", "tslib": "^2.6.2" } }, "sha512-HLNMYSus976SNtEl9w9HKDmW5rY61FlnBZdux63tUVDuIk82ycF31ZktigEBz0D0rtc7wi1WulpqJHyT8s6jxg=="], + + "@aws-sdk/core": ["@aws-sdk/core@3.974.1", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.18", "@smithy/core": "^3.23.15", "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-gy/gffKz0zaHDaqRiLCdIvgHmaAL/HXuAtMcBP7euYSFx4BsbsdlfmUBJag+Gqe62z6/XuloKyQyaiH+kS3Vrg=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.27", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-xfUt2CUZDC+Tf16A6roD1b4pk/nrXdkoLY3TEhv198AXDtBo5xUJP1zd0e8SmuKLN4PpIBX96OizZbmMlcI6oQ=="], + + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/types": "^3.973.8", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/node-http-handler": "^4.5.3", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/util-stream": "^4.5.23", "tslib": "^2.6.2" } }, "sha512-hjNeYb6oLyHgMihra83ie0J/T2y9om3cy1qC90h9DRgvYXEoN4BCFf8bHguZjKhXunnv7YkmZRuYL5Mkk77eCA=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/credential-provider-env": "^3.972.27", "@aws-sdk/credential-provider-http": "^3.972.29", "@aws-sdk/credential-provider-login": "^3.972.31", "@aws-sdk/credential-provider-process": "^3.972.27", "@aws-sdk/credential-provider-sso": "^3.972.31", "@aws-sdk/credential-provider-web-identity": "^3.972.31", "@aws-sdk/nested-clients": "^3.996.21", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-PuQ7e8WYzAPpzvFcajxf8c0LqSzakVHVlKw8M0oubk8Kf347YOCCqT1seQrHs5AdZuIh2RD9LX4O+Xa5ImEBfQ=="], + + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/nested-clients": "^3.996.21", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-bBmWDmtSpmLOZR6a0kmowBcVL1hiL8Vlap/RXeMpFd7JbWl87YcwqL6T9LH/0oBVEZXu1dUZAtojgSuZgMO5xw=="], + + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.32", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.27", "@aws-sdk/credential-provider-http": "^3.972.29", "@aws-sdk/credential-provider-ini": "^3.972.31", "@aws-sdk/credential-provider-process": "^3.972.27", "@aws-sdk/credential-provider-sso": "^3.972.31", "@aws-sdk/credential-provider-web-identity": "^3.972.31", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-9aj0x9hGYUondBZSD0XkksAdHhOKttFw4BWpLCeggeg40qSJxGrAP++g0GCm0VqWc1WtC/NRFiAVzPCy56vmog=="], + + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.27", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-1CZvfb1WzudWWIFAVQkd1OI/T1RxPcSvNWzNsb2BMBVsBJzBtB8dV5f2nymHVU4UqwxipdVt/DAbgdDRf33JDg=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/nested-clients": "^3.996.21", "@aws-sdk/token-providers": "3.1032.0", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-x8Mx18S48XMl9bEEpYwmXDTvjWGPIfDadReN37Lc099/DUrlL4Zs9T9rwwggo6DkKS1aev6v+MTUx7JTa87TZQ=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/nested-clients": "^3.996.21", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-zfuNMIkGfjYsHis9qytYf74Bcmq6Ji9Xwf4w53baRCI/b2otTwZv3SW1uRiJ5Di7999QzRGhHZ96+eUeo3gSOA=="], + + "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg=="], + + "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ=="], + + "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ=="], + + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.7", "@smithy/core": "^3.23.15", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-retry": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-L+hXN2HDomlIsWSHW5DVD7ppccCeRnlHXZ5uHG34ePTjF5bm0I1fmrJLbUGiW97xRXWryit5cjdP4Sx2FwiGog=="], + + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.21", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.1", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", "@aws-sdk/middleware-user-agent": "^3.972.31", "@aws-sdk/region-config-resolver": "^3.972.12", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.7", "@aws-sdk/util-user-agent-browser": "^3.972.10", "@aws-sdk/util-user-agent-node": "^3.973.17", "@smithy/config-resolver": "^4.4.16", "@smithy/core": "^3.23.15", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/hash-node": "^4.2.14", "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.30", "@smithy/middleware-retry": "^4.5.3", "@smithy/middleware-serde": "^4.2.18", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/node-http-handler": "^4.5.3", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.47", "@smithy/util-defaults-mode-node": "^4.2.52", "@smithy/util-endpoints": "^3.4.1", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Me3d/ua2lb2G0bQfFmvCeQQp3+nN6GSPqMxDmi/IQlQ8CrlpQ5C0JJHpz2AnOUkEFI0lBNrAL3Vnt29l44ndkA=="], + + "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.12", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/config-resolver": "^4.4.16", "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-QQI43Mxd53nBij0pm8HXC+t4IOC6gnhhZfzxE0OATQyO6QfPV4e+aTIRRuAJKA6Nig/cR8eLwPryqYTX9ZrjAQ=="], + + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1032.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/nested-clients": "^3.996.21", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-n+PU8Z+gll7p3wDrH+Wo6fkt8sPrVnq30YYM6Ryga95oJlEneNMEbDHj0iqjMX3V7gaGdJo/hJWyPo4lscP+mA=="], + + "@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + + "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-endpoints": "^3.4.1", "tslib": "^2.6.2" } }, "sha512-ty4LQxN1QC+YhUP28NfEgZDEGXkyqOQy+BDriBozqHsrYO4JMgiPhfizqOGF7P+euBTZ5Ez6SKlLAMCLo8tzmw=="], + + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.5", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ=="], + + "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g=="], + + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.17", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.31", "@aws-sdk/types": "^3.973.8", "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-utF5qjjbuJQuU9VdCkWl7L87sr93cApsrD+uxGfUnlafX8iyEzJrb7EZnufjThURZVTOtelRMXrblWxpefElUg=="], + + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.18", "", { "dependencies": { "@smithy/types": "^4.14.1", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-BMDNVG1ETXRhl1tnisQiYBef3RShJ1kfZA7x7afivTFMLirfHNTb6U71K569HNXhSXbQZsweHvSDZ6euBw8hPA=="], + + "@aws/durable-execution-sdk-js": ["@aws/durable-execution-sdk-js@1.0.2", "", { "dependencies": { "@aws-sdk/client-lambda": "^3.943.0" } }, "sha512-KIYBVqV9gArkWdPEDYOjJqLlO+NecmCXOXadNBlOspJTmqztwuiyb6aZc468bYWSGuL4Me/gyTIK97ubkwFNCw=="], + + "@aws/durable-execution-sdk-js-testing": ["@aws/durable-execution-sdk-js-testing@1.1.1", "", { "dependencies": { "@aws-sdk/client-lambda": "^3.943.0", "@sinonjs/fake-timers": "^13.0.5", "commander": "^14.0.2" }, "peerDependencies": { "@aws/durable-execution-sdk-js": "^1.0.1" }, "bin": { "run-durable": "dist/cli/run-durable.mjs" } }, "sha512-4zjEDKBKmKl2Nu9FFF+RgnmOp2yKuqHegxRfGQXI+sinCQsz6FX3ESlzAFVOWXkt0KgdhoQvwVTWRrXQVFgegA=="], + + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], + + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260420.1", "", {}, "sha512-DHT9JnSn9cIiCSdL76OxW+Xvc1+ml1CWzWvgVwreoHQ+E604aeFxPPHp9X7nE+XRWm2NH4l0OgtxUI5T/nuI3g=="], + + "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], + + "@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], + + "@smithy/config-resolver": ["@smithy/config-resolver@4.4.16", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.4.1", "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" } }, "sha512-GFlGPNLZKrGfqWpqVb31z7hvYCA9ZscfX1buYnvvMGcRYsQQnhH+4uN6mWWflcD5jB4OXP/LBrdpukEdjl41tg=="], + + "@smithy/core": ["@smithy/core@3.23.15", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-stream": "^4.5.23", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-E7GVCgsQttzfujEZb6Qep005wWf4xiL4x06apFEtzQMWYBPggZh/0cnOxPficw5cuK/YjjkehKoIN4YUaSh0UQ=="], + + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.14", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "tslib": "^2.6.2" } }, "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg=="], + + "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="], + + "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.14", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ=="], + + "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA=="], + + "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.14", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw=="], + + "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.14", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg=="], + + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.17", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/querystring-builder": "^4.2.14", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw=="], + + "@smithy/hash-node": ["@smithy/hash-node@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g=="], + + "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw=="], + + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow=="], + + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.14", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw=="], + + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.30", "", { "dependencies": { "@smithy/core": "^3.23.15", "@smithy/middleware-serde": "^4.2.18", "@smithy/node-config-provider": "^4.3.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" } }, "sha512-qS2XqhKeXmdZ4nEQ4cOxIczSP/Y91wPAHYuRwmWDCh975B7/57uxsm5d6sisnUThn2u2FwzMdJNM7AbO1YPsPg=="], + + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.5.3", "", { "dependencies": { "@smithy/core": "^3.23.15", "@smithy/node-config-provider": "^4.3.14", "@smithy/protocol-http": "^5.3.14", "@smithy/service-error-classification": "^4.2.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-TE8dJNi6JuxzGSxMCVd3i9IEWDndCl3bmluLsBNDWok8olgj65OfkndMhl9SZ7m14c+C5SQn/PcUmrDl57rSFw=="], + + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.18", "", { "dependencies": { "@smithy/core": "^3.23.15", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-M6CSgnp3v4tYz9ynj2JHbA60woBZcGqEwNjTKjBsNHPV26R1ZX52+0wW8WsZU18q45jD0tw2wL22S17Ze9LpEw=="], + + "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA=="], + + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.14", "", { "dependencies": { "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg=="], + + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.3", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/querystring-builder": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-lc5jFL++x17sPhIwMWJ3YOnqmSjw/2Po6VLDlUIXvxVWRuJwRXnJ4jOBBLB0cfI5BB5ehIl02Fxr1PDvk/kxDw=="], + + "@smithy/property-provider": ["@smithy/property-provider@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ=="], + + "@smithy/protocol-http": ["@smithy/protocol-http@5.3.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ=="], + + "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A=="], + + "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw=="], + + "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1" } }, "sha512-vVimoUnGxlx4eLLQbZImdOZFOe+Zh+5ACntv8VxZuGP72LdWu5GV3oEmCahSEReBgRJoWjypFkrehSj7BWx1HQ=="], + + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.9", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ=="], + + "@smithy/signature-v4": ["@smithy/signature-v4@5.3.14", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA=="], + + "@smithy/smithy-client": ["@smithy/smithy-client@4.12.11", "", { "dependencies": { "@smithy/core": "^3.23.15", "@smithy/middleware-endpoint": "^4.4.30", "@smithy/middleware-stack": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-stream": "^4.5.23", "tslib": "^2.6.2" } }, "sha512-wzz/Wa1CH/Tlhxh0s4DQPEcXSxSVfJ59AZcUh9Gu0c6JTlKuwGf4o/3P2TExv0VbtPFt8odIBG+eQGK2+vTECg=="], + + "@smithy/types": ["@smithy/types@4.14.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg=="], + + "@smithy/url-parser": ["@smithy/url-parser@4.2.14", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ=="], + + "@smithy/util-base64": ["@smithy/util-base64@4.3.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ=="], + + "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ=="], + + "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g=="], + + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="], + + "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ=="], + + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.47", "", { "dependencies": { "@smithy/property-provider": "^4.2.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-zlIuXai3/SHjQUQ8y3g/woLvrH573SK2wNjcDaHu5e9VOcC0JwM1MI0Sq0GZJyN3BwSUneIhpjZ18nsiz5AtQw=="], + + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.52", "", { "dependencies": { "@smithy/config-resolver": "^4.4.16", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-cQBz8g68Vnw1W2meXlkb3D/hXJU+Taiyj9P8qLJtjREEV9/Td65xi4A/H1sRQ8EIgX5qbZbvdYPKygKLholZ3w=="], + + "@smithy/util-endpoints": ["@smithy/util-endpoints@3.4.1", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-wMxNDZJrgS5mQV9oxCs4TWl5767VMgOfqfZ3JHyCkMtGC2ykW9iPqMvFur695Otcc5yxLG8OKO/80tsQBxrhXg=="], + + "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg=="], + + "@smithy/util-middleware": ["@smithy/util-middleware@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw=="], + + "@smithy/util-retry": ["@smithy/util-retry@4.3.2", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-2+KTsJEwTi63NUv4uR9IQ+IFT1yu6Rf6JuoBK2WKaaJ/TRvOiOVGcXAsEqX/TQN2thR9yII21kPUJq1UV/WI2A=="], + + "@smithy/util-stream": ["@smithy/util-stream@4.5.23", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.17", "@smithy/node-http-handler": "^4.5.3", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-N6on1+ngJ3RznZOnDWNveIwnTSlqxNnXuNAh7ez889ZZaRdXoNRTXKgmYOLe6dB0gCmAVtuRScE1hymQFl4hpg=="], + + "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw=="], + + "@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@smithy/util-waiter": ["@smithy/util-waiter@4.2.16", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-GtclrKoZ3Lt7jPQ7aTIYKfjY92OgceScftVnkTsG8e1KV8rkvZgN+ny6YSRhd9hxB8rZtwVbmln7NTvE5O3GmQ=="], + + "@smithy/uuid": ["@smithy/uuid@1.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g=="], + + "@tsconfig/node20": ["@tsconfig/node20@20.1.4", "", {}, "sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg=="], + + "@types/aws-lambda": ["@types/aws-lambda@8.10.161", "", {}, "sha512-rUYdp+MQwSFocxIOcSsYSF3YYYC/uUpMbCY/mbO21vGqfrEYvNSoPyKYDj6RhXXpPfS0KstW9RwG3qXh9sL7FQ=="], + + "@types/node": ["@types/node@22.10.0", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA=="], + + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + + "aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="], + + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], + + "bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], + + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "fast-xml-builder": ["fast-xml-builder@1.1.5", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA=="], + + "fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="], + + "hono": ["hono@4.3.9", "", {}, "sha512-6c5LVE23HnIS8iBhY+XPmYJlPeeClznOi7mBNsAsJCgxo8Ciz75LTjqRUf5wv4RYq8kL+1KPLUZHCtKmbZssNg=="], + + "jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="], + + "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], + + "oidc-token-hash": ["oidc-token-hash@5.2.0", "", {}, "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw=="], + + "openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="], + + "path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="], + + "sst": ["sst@file:../../sdk/js", { "dependencies": { "@aws/durable-execution-sdk-js": "1.0.2", "aws4fetch": "1.0.18", "jose": "5.2.3", "openid-client": "5.6.4" }, "devDependencies": { "@aws/durable-execution-sdk-js-testing": "^1.1.1", "@tsconfig/node20": "20.1.4", "@types/aws-lambda": "^8.10.155", "@types/node": "22.10.0", "bun-types": "1.3.0", "hono": "4.3.9", "typescript": "5.7.2", "valibot": "^1.0.0-rc.3", "zod": "^3.24.2" }, "bin": { "sst": "./bin/sst.mjs" } }], + + "strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], + + "typescript": ["typescript@5.7.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg=="], + + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "valibot": ["valibot@1.3.1", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg=="], + + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + } +} diff --git a/examples/cloudflare-workflow/package.json b/examples/cloudflare-workflow/package.json new file mode 100644 index 0000000000..47c109e863 --- /dev/null +++ b/examples/cloudflare-workflow/package.json @@ -0,0 +1,9 @@ +{ + "name": "cloudflare-workflow", + "version": "1.0.0", + "type": "module", + "dependencies": { + "@cloudflare/workers-types": "^4.20240403.0", + "sst": "file:../../sdk/js" + } +} diff --git a/examples/cloudflare-workflow/sst.config.ts b/examples/cloudflare-workflow/sst.config.ts new file mode 100644 index 0000000000..d8c7454ce0 --- /dev/null +++ b/examples/cloudflare-workflow/sst.config.ts @@ -0,0 +1,32 @@ +/// + +/** + * ## Cloudflare Workflow + * + * This example creates a Cloudflare Workflow and a Worker that triggers it. + */ +export default $config({ + app(input) { + return { + name: "cloudflare-workflow", + removal: input?.stage === "production" ? "retain" : "remove", + home: "cloudflare", + }; + }, + async run() { + const processor = new sst.cloudflare.Workflow("OrderProcessor", { + handler: "./workflow.ts", + className: "OrderProcessor", + }); + + const api = new sst.cloudflare.Worker("Api", { + handler: "./api.ts", + link: [processor], + url: true, + }); + + return { + api: api.url, + }; + }, +}); diff --git a/examples/cloudflare-workflow/tsconfig.json b/examples/cloudflare-workflow/tsconfig.json new file mode 100644 index 0000000000..4d3c77fe88 --- /dev/null +++ b/examples/cloudflare-workflow/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "lib": ["esnext"], + "types": ["@cloudflare/workers-types"] + } +} diff --git a/examples/cloudflare-workflow/workflow.ts b/examples/cloudflare-workflow/workflow.ts new file mode 100644 index 0000000000..fb87d75cdd --- /dev/null +++ b/examples/cloudflare-workflow/workflow.ts @@ -0,0 +1,25 @@ +import { + WorkflowEntrypoint, + WorkflowEvent, + WorkflowStep, +} from "cloudflare:workers"; + +type Params = { + orderId: string; +}; + +export class OrderProcessor extends WorkflowEntrypoint<{}, Params> { + async run(event: WorkflowEvent, step: WorkflowStep) { + const validated = await step.do("validate", async () => { + return { orderId: event.payload.orderId, valid: true }; + }); + + await step.sleep("cooldown", "5 seconds"); + + const charged = await step.do("charge", async () => { + return { orderId: validated.orderId, chargedAt: Date.now() }; + }); + + return charged; + } +} diff --git a/examples/hetzner-minecraft/package.json b/examples/hetzner-minecraft/package.json new file mode 100644 index 0000000000..ae17c043b6 --- /dev/null +++ b/examples/hetzner-minecraft/package.json @@ -0,0 +1,7 @@ +{ + "name": "hetzner-minecraft", + "version": "0.0.0", + "dependencies": { + "sst": "file:../../sdk/js" + } +} diff --git a/examples/hetzner/bun.lockb b/examples/hetzner/bun.lockb deleted file mode 100755 index 99d6e605f9..0000000000 Binary files a/examples/hetzner/bun.lockb and /dev/null differ diff --git a/examples/hetzner/package.json b/examples/hetzner/package.json index 33d39b231d..c5e263ed9c 100644 --- a/examples/hetzner/package.json +++ b/examples/hetzner/package.json @@ -2,6 +2,6 @@ "name": "hetzner", "version": "0.0.0", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/hetzner/sst-env.d.ts b/examples/hetzner/sst-env.d.ts deleted file mode 100644 index f110f33043..0000000000 --- a/examples/hetzner/sst-env.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - } -} -export {} diff --git a/examples/hyperdrive/.dockerignore b/examples/hyperdrive/.dockerignore deleted file mode 100644 index 7af11bfb01..0000000000 --- a/examples/hyperdrive/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ - - -# sst -.sst \ No newline at end of file diff --git a/examples/hyperdrive/Dockerfile b/examples/hyperdrive/Dockerfile deleted file mode 100644 index a79c13b4a3..0000000000 --- a/examples/hyperdrive/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM cloudflare/cloudflared:latest - -CMD ["tunnel", "run"] diff --git a/examples/hyperdrive/bun.lockb b/examples/hyperdrive/bun.lockb deleted file mode 100755 index eece810081..0000000000 Binary files a/examples/hyperdrive/bun.lockb and /dev/null differ diff --git a/examples/hyperdrive/lambda.ts b/examples/hyperdrive/lambda.ts deleted file mode 100644 index 378e591730..0000000000 --- a/examples/hyperdrive/lambda.ts +++ /dev/null @@ -1,29 +0,0 @@ -import postgres from "postgres"; -import { Resource } from "sst"; - -const sql = postgres( - `postgres://${Resource.Postgres.username}:${Resource.Postgres.password}@${Resource.Postgres.host}:${Resource.Postgres.port}/${Resource.Postgres.database}`, -); - -export async function handler() { - try { - // A very simple test query - const now = Date.now(); - const result = await sql`select * from pg_tables limit 10`; - const delay = Date.now() - now; - return { - statusCode: 200, - body: JSON.stringify({ - result, - delay, - }), - }; - } catch (e) { - return { - statusCode: 200, - body: JSON.stringify({ - error: e.message, - }), - }; - } -} diff --git a/examples/hyperdrive/package.json b/examples/hyperdrive/package.json deleted file mode 100644 index 974615e071..0000000000 --- a/examples/hyperdrive/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "hyperdrive", - "version": "0.0.0", - "type": "module", - "dependencies": { - "@cloudflare/workers-types": "^4.20240919.0", - "postgres": "^3.4.4", - "sst": "^3" - } -} diff --git a/examples/hyperdrive/sst-env.d.ts b/examples/hyperdrive/sst-env.d.ts deleted file mode 100644 index 1c021d2dca..0000000000 --- a/examples/hyperdrive/sst-env.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -import "sst" -declare module "sst" { - export interface Resource { - "Cloudflared": { - "service": string - "type": "sst.aws.Service" - } - "Lambda": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "Postgres": { - "clusterArn": string - "database": string - "host": string - "password": string - "port": number - "secretArn": string - "type": "sst.aws.Postgres" - "username": string - } - "Vpc": { - "type": "sst.aws.Vpc" - } - } -} -// cloudflare -import * as cloudflare from "@cloudflare/workers-types"; -declare module "sst" { - export interface Resource { - "Worker": cloudflare.Service - } -} diff --git a/examples/hyperdrive/sst.config.ts b/examples/hyperdrive/sst.config.ts deleted file mode 100644 index 4737a21de5..0000000000 --- a/examples/hyperdrive/sst.config.ts +++ /dev/null @@ -1,104 +0,0 @@ -/// -export default $config({ - app(input) { - return { - name: "hyperdrive", - removal: input?.stage === "production" ? "retain" : "remove", - home: "aws", - providers: { cloudflare: "5.39.0", random: "4.16.5" }, - }; - }, - async run() { - // THIS EXAMPLE IS NOT READY YET - - const vpc = new sst.aws.Vpc("Vpc", { - nat: "managed", - }); - const cluster = new sst.aws.Cluster("Cluster", { vpc }); - const postgres = new sst.aws.Postgres("Postgres", { vpc }); - const domain = "sst.cheap"; - const zone = cloudflare.getZoneOutput({ name: domain }); - - const tunnelSecret = new random.RandomString("TunnelSecret", { - length: 32, - }); - const tunnel = new cloudflare.Tunnel("Tunnel", { - name: `${$app.name}-${$app.stage}-tunnel`, - secret: tunnelSecret.result.apply((v) => - Buffer.from(v).toString("base64") - ), - accountId: sst.cloudflare.DEFAULT_ACCOUNT_ID, - }); - new cloudflare.TunnelConfig("TunnelConfig", { - accountId: sst.cloudflare.DEFAULT_ACCOUNT_ID, - tunnelId: tunnel.id, - config: { - ingressRules: [ - { - service: $interpolate`tcp://${postgres.host}:${postgres.port}`, - }, - ], - }, - }); - const record = new cloudflare.Record("TunnelRecord", { - name: $interpolate`hypedrive.${domain}`, - zoneId: zone.id, - type: "CNAME", - value: $interpolate`${tunnel.id}.cfargotunnel.com`, - proxied: true, - }); - const hyperdriveConfig = new cloudflare.HyperdriveConfig( - "HyperdriveConfig", - { - name: `${$app.name}-${$app.stage}-config`, - accountId: sst.cloudflare.DEFAULT_ACCOUNT_ID, - origin: { - host: record.name, - user: postgres.username, - password: postgres.password, - database: postgres.database, - accessClientId: "dummy", - accessClientSecret: "dummy", - scheme: "postgres", - }, - } - ); - new sst.aws.Service("Cloudflared", { - cluster, - environment: { - TUNNEL_TOKEN: tunnel.tunnelToken, - }, - }); - const worker = new sst.cloudflare.Worker("Worker", { - handler: "worker.ts", - url: true, - transform: { - worker: { - placements: [ - { - mode: "smart", - }, - ], - hyperdriveConfigBindings: [ - { - binding: "HYPERDRIVE", - id: "7c064ebd005348329a38106b076d579d", - }, - ], - }, - }, - }); - - const lambda = new sst.aws.Function("Lambda", { - handler: "lambda.handler", - link: [postgres], - vpc, - url: true, - }); - - return { - worker: worker.url, - lambda: lambda.url, - }; - }, -}); diff --git a/examples/hyperdrive/worker.ts b/examples/hyperdrive/worker.ts deleted file mode 100644 index 22408bfcc7..0000000000 --- a/examples/hyperdrive/worker.ts +++ /dev/null @@ -1,33 +0,0 @@ -import postgres from "postgres"; -import { Hyperdrive, ExecutionContext } from "@cloudflare/workers-types"; - -export interface Env { - // If you set another name in wrangler.toml as the value for 'binding', - // replace "HYPERDRIVE" with the variable name you defined. - HYPERDRIVE: Hyperdrive; -} - -export default { - async fetch(request: Request, env: Env, ctx: ExecutionContext) { - // NOTE: if `prepare: false` is passed when connecting, performance will - // be slower but still correctly supported. - const sql = postgres(env.HYPERDRIVE.connectionString); - - try { - // A very simple test query - const now = Date.now(); - const result = await sql`select * from pg_tables limit 10`; - const delay = Date.now() - now; - - // Clean up the client, ensuring we don't kill the worker before that is - // completed. - ctx.waitUntil(sql.end()); - - // Return result rows as JSON - return Response.json({ delay, result }); - } catch (e) { - console.log(e); - return Response.json({ error: e.message }, { status: 500 }); - } - }, -}; diff --git a/examples/in-progress/aws-drizzle/drizzle.config.ts b/examples/in-progress/aws-drizzle/drizzle.config.ts deleted file mode 100644 index 2901764f06..0000000000 --- a/examples/in-progress/aws-drizzle/drizzle.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from "drizzle-kit"; - -export default defineConfig({ - strict: true, - verbose: true, - out: "./migrations/", - schema: "./src/**/*.sql.ts", - driver: "pg", -}); diff --git a/examples/in-progress/aws-drizzle/package.json b/examples/in-progress/aws-drizzle/package.json deleted file mode 100644 index 16ef591ff9..0000000000 --- a/examples/in-progress/aws-drizzle/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "base-ts", - "version": "0.0.0", - "dependencies": { - "@aws-sdk/client-rds-data": "^3.549.0", - "@types/aws-lambda": "^8.10.137", - "drizzle-orm": "^0.30.7", - "sst": "^3.0.4" - }, - "devDependencies": { - "drizzle-kit": "^0.20.14" - } -} diff --git a/examples/in-progress/aws-drizzle/src/index.ts b/examples/in-progress/aws-drizzle/src/index.ts deleted file mode 100644 index 0f21d2bbe7..0000000000 --- a/examples/in-progress/aws-drizzle/src/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { drizzle } from "drizzle-orm/aws-data-api/pg"; -import { RDSDataClient } from "@aws-sdk/client-rds-data"; -import { Resource } from "sst"; -import { APIGatewayProxyHandlerV2 } from "aws-lambda"; - -const client = new RDSDataClient(); -const db = drizzle(client, { - database: Resource.Postgres.database, - secretArn: Resource.Postgres.secretArn, - resourceArn: Resource.Postgres.clusterArn, -}); - -export const handler: APIGatewayProxyHandlerV2 = async (event) => { - return { - statusCode: 200, - body: "ok", - }; -}; diff --git a/examples/in-progress/aws-drizzle/sst.config.ts b/examples/in-progress/aws-drizzle/sst.config.ts deleted file mode 100644 index 0f1222a99a..0000000000 --- a/examples/in-progress/aws-drizzle/sst.config.ts +++ /dev/null @@ -1,23 +0,0 @@ -/// - -export default $config({ - app(input) { - return { - name: "base-ts", - removal: input?.stage === "production" ? "retain" : "remove", - home: "aws", - }; - }, - async run() { - // on first deploy this can take a while ~ 10 minutes - const pg = new sst.aws.Postgres("Postgres"); - const fn = new sst.aws.Function("Function", { - link: [pg], - url: true, - handler: "./src/index.handler", - }); - return { - url: fn.url, - }; - }, -}); diff --git a/examples/in-progress/cloudflare-drizzle/drizzle.config.ts b/examples/in-progress/cloudflare-drizzle/drizzle.config.ts deleted file mode 100644 index deb2ec1cc5..0000000000 --- a/examples/in-progress/cloudflare-drizzle/drizzle.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from "drizzle-kit"; - -export default defineConfig({ - strict: true, - verbose: true, - out: "./migrations/", - schema: "./src/**/*.sql.ts", - driver: "d1", -}); diff --git a/examples/in-progress/cloudflare-drizzle/package.json b/examples/in-progress/cloudflare-drizzle/package.json deleted file mode 100644 index 0e0bb7f26d..0000000000 --- a/examples/in-progress/cloudflare-drizzle/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "base-ts", - "version": "0.0.0", - "dependencies": { - "drizzle-orm": "^0.30.8", - "sst": "^3.0.4" - }, - "devDependencies": { - "drizzle-kit": "^0.20.14" - } -} diff --git a/examples/in-progress/cloudflare-drizzle/sst.config.ts b/examples/in-progress/cloudflare-drizzle/sst.config.ts deleted file mode 100644 index 74472826ac..0000000000 --- a/examples/in-progress/cloudflare-drizzle/sst.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -/// - -export default $config({ - app(input) { - return { - name: "cloudflare-drizzle", - removal: input?.stage === "production" ? "retain" : "remove", - home: "cloudflare", - }; - }, - async run() {}, -}); diff --git a/examples/internal/aws-ai-video-generation/api_server.py b/examples/internal/aws-ai-video-generation/api_server.py deleted file mode 100644 index ebc456cb17..0000000000 --- a/examples/internal/aws-ai-video-generation/api_server.py +++ /dev/null @@ -1,76 +0,0 @@ -import os -import uuid -from flask import Flask, request, jsonify -import torch -from PIL import Image -import boto3 -from sgm.inference.helpers import embed_watermark -from sgm.inference.api import ( - SamplingParams, - SamplingPipeline, - ModelArchitecture, - get_sampler, - get_model, -) - -app = Flask(__name__) - -# Initialize the model -device = torch.device("cuda" if torch.cuda.is_available() else "cpu") -model = get_model(ModelArchitecture.SVD, device=device) -sampler = get_sampler(device=device) -pipeline = SamplingPipeline( - model=model, - sampler=sampler, - scheduler=model.scheduler, -) - -# Initialize S3 client -s3_client = boto3.client('s3') - -@app.route('/generate_video', methods=['POST']) -def generate_video(): - if 'image' not in request.files: - return jsonify({'error': 'No image file provided'}), 400 - - image_file = request.files['image'] - image = Image.open(image_file).convert("RGB") - - # Set sampling parameters - params = SamplingParams( - batch_size=1, - height=576, - width=1024, - num_frames=14, - motion_bucket_id=127, - fps=6, - ) - - # Generate video - output = pipeline( - image, - params, - ) - - # Save the output video locally - output_filename = f"{uuid.uuid4()}.mp4" - output_path = f"/tmp/{output_filename}" - embed_watermark(output[0]).save(output_path) - - # Upload to S3 - bucket_name = 'aws-ai-video-generation-frank-bucket-uhfextfm' - s3_client.upload_file(output_path, bucket_name, output_filename) - - # Generate a presigned URL - s3_url = s3_client.generate_presigned_url('get_object', - Params={'Bucket': bucket_name, - 'Key': output_filename}, - ExpiresIn=3600) - - # Clean up local file - os.remove(output_path) - - return jsonify({'video_url': s3_url}) - -if __name__ == '__main__': - app.run(host='0.0.0.0', port=80) \ No newline at end of file diff --git a/examples/internal/aws-ai-video-generation/requirements.txt b/examples/internal/aws-ai-video-generation/requirements.txt deleted file mode 100644 index fb6a754cbd..0000000000 --- a/examples/internal/aws-ai-video-generation/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -flask==3.0.3 -pillow==10.4.0 -numpy==2.0.2 \ No newline at end of file diff --git a/examples/internal/aws-ai-video-generation/script.py b/examples/internal/aws-ai-video-generation/script.py deleted file mode 100644 index e8b063f803..0000000000 --- a/examples/internal/aws-ai-video-generation/script.py +++ /dev/null @@ -1,45 +0,0 @@ -import torch -from PIL import Image -from pathlib import Path -from sgm.inference.helpers import embed_watermark -from sgm.inference.api import ( - SamplingPipeline, - ModelArchitecture, - SamplingParams, -) -from sgm.inference.helpers import init_model - -def main(): - # Set up device - device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - print(f"Using device: {device}") - - # Initialize the model - model = init_model(ModelArchitecture.SVD, device=device) - pipeline = SamplingPipeline(model=model) - - # Load the input image - input_image_path = "path/to/your/input/image.jpg" - image = Image.open(input_image_path).convert("RGB") - - # Set sampling parameters - params = SamplingParams( - batch_size=1, - height=576, - width=1024, - num_frames=14, - motion_bucket_id=127, - fps=6, - ) - - # Generate video - print("Generating video...") - output = pipeline(image, params) - - # Save the output video - output_path = "output_video.mp4" - embed_watermark(output[0]).save(output_path) - print(f"Video saved to {output_path}") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/examples/internal/aws-ai-video-generation/sst.config.ts b/examples/internal/aws-ai-video-generation/sst.config.ts deleted file mode 100644 index 81c10f1917..0000000000 --- a/examples/internal/aws-ai-video-generation/sst.config.ts +++ /dev/null @@ -1,139 +0,0 @@ -/// - -export default $config({ - app(input) { - return { - name: "aws-ai-video-generation", - removal: input?.stage === "production" ? "retain" : "remove", - home: "aws", - }; - }, - async run() { - const bucket = new sst.aws.Bucket(`Bucket`); - const apiServer = new aws.s3.BucketObject(`ApiServer`, { - bucket: bucket.name, - key: "api_server.py", - source: $asset("api_server.py"), - }); - - const vpc = new sst.aws.Vpc(`Vpc`); - const sg = new aws.ec2.SecurityGroup(`SecurityGroup`, { - vpcId: vpc.id, - ingress: [ - { - protocol: "tcp", - fromPort: 22, - toPort: 22, - cidrBlocks: ["0.0.0.0/0"], - }, - { - protocol: "tcp", - fromPort: 80, - toPort: 80, - cidrBlocks: ["0.0.0.0/0"], - }, - ], - egress: [ - { - protocol: "-1", - fromPort: 0, - toPort: 0, - cidrBlocks: ["0.0.0.0/0"], - }, - ], - }); - - const role = new aws.iam.Role(`Role`, { - assumeRolePolicy: aws.iam.getPolicyDocumentOutput({ - statements: [ - { - actions: ["sts:AssumeRole"], - principals: [ - { - type: "Service", - identifiers: ["ec2.amazonaws.com"], - }, - ], - }, - ], - }).json, - managedPolicyArns: [ - "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore", - ], - inlinePolicies: [ - { - name: "inline", - policy: aws.iam.getPolicyDocumentOutput({ - statements: [ - { - actions: ["s3:*"], - resources: [bucket.arn, $interpolate`${bucket.arn}/*`], - }, - ], - }).json, - }, - ], - }); - const instanceProfile = new aws.iam.InstanceProfile(`InstanceProfile`, { - role: role.name, - }); - - // Select DLAMIs https://docs.aws.amazon.com/dlami/latest/devguide/appendix-ami-release-notes.html - const instance = new aws.ec2.Instance(`InstanceB`, { - instanceType: "g4dn.xlarge", - ami: "ami-00ca14132c418aba6", // DLAMI x86 PyTorch 2.2 w/ Python 3.10 - vpcSecurityGroupIds: [sg.id], - subnetId: vpc.publicSubnets.apply((subnets) => subnets[0]), - iamInstanceProfile: instanceProfile.name, - - /* -sudo -i -u ubuntu -cd /opt/dlami/nvme -sudo apt-get install git-lfs -screen -*/ - userData: $interpolate` -#!/bin/bash -xe - -# Create a directory for the project -mkdir -p svd_project -cd svd_project - -# Clone the repository -git clone https://github.com/Stability-AI/generative-models.git -cd generative-models -pip install -r requirements/pt2.txt -pip install . -pip install -e git+https://github.com/Stability-AI/datapipelines.git@main#egg=sdata - -huggingface-cli login --token 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 -huggingface-cli download stabilityai/sv4d --include sv4d.safetensors --cache-dir cache -huggingface-cli download stabilityai/sv3d --include sv3d_u.safetensors --cache-dir cache - -mkdir -p checkpoints -mv cache/models--stabilityai--sv4d/blobs/bdfe5bb33dfc771fc102891883befcf061873f4a96fa602037a964beca83cb44 checkpoints/sv4d.safetensors -mv cache/models--stabilityai--sv3d/blobs/d2c281b817232c492f6db27c9ce597b543187c52229cbad2a3c78e238b06c809 checkpoints/sv3d_u.safetensors - -pip install --force-reinstall -v "numpy==1.25.2" -pip install imageio-ffmpeg -python scripts/sampling/simple_video_sample_4d.py --input_path assets/sv4d_videos/test_video1.mp4 --output_folder outputs/sv4d - -# export PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128 -# change: reduce decode_t in scripts/sampling/simple_video_sample_4d.py -# change: set lowvram_mode = True in scripts/demo/streamlit_helpers.py -`, - // Questions - // - how to know if GPU is being used? - // - what are safetensors? - // - what is the datapipelines package? - // - what are the relationships between the generative-models repo, the datapipelines package, and the safetensors files? - // - what are checkpoints? - }); - - return { - bucket: bucket.name, - instance: instance.id, - url: $interpolate`http://${instance.publicIp}:80`, - }; - }, -}); diff --git a/examples/internal/aws-bun/.dockerignore b/examples/internal/aws-bun/.dockerignore deleted file mode 100644 index 8c3bacfca9..0000000000 --- a/examples/internal/aws-bun/.dockerignore +++ /dev/null @@ -1,9 +0,0 @@ -node_modules -.git -.gitignore -README.md -Dockerfile* - - -# sst -.sst \ No newline at end of file diff --git a/examples/internal/aws-bun/.gitignore b/examples/internal/aws-bun/.gitignore deleted file mode 100644 index 1426860d41..0000000000 --- a/examples/internal/aws-bun/.gitignore +++ /dev/null @@ -1,178 +0,0 @@ -# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore - -# Logs - -logs -_.log -npm-debug.log_ -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Caches - -.cache - -# Diagnostic reports (https://nodejs.org/api/report.html) - -report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json - -# Runtime data - -pids -_.pid -_.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover - -lib-cov - -# Coverage directory used by tools like istanbul - -coverage -*.lcov - -# nyc test coverage - -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) - -.grunt - -# Bower dependency directory (https://bower.io/) - -bower_components - -# node-waf configuration - -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) - -build/Release - -# Dependency directories - -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) - -web_modules/ - -# TypeScript cache - -*.tsbuildinfo - -# Optional npm cache directory - -.npm - -# Optional eslint cache - -.eslintcache - -# Optional stylelint cache - -.stylelintcache - -# Microbundle cache - -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history - -.node_repl_history - -# Output of 'npm pack' - -*.tgz - -# Yarn Integrity file - -.yarn-integrity - -# dotenv environment variable files - -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) - -.parcel-cache - -# Next.js build output - -.next -out - -# Nuxt.js build / generate output - -.nuxt -dist - -# Gatsby files - -# Comment in the public line in if your project uses Gatsby and not Next.js - -# https://nextjs.org/blog/next-9-1#public-directory-support - -# public - -# vuepress build output - -.vuepress/dist - -# vuepress v2.x temp and cache directory - -.temp - -# Docusaurus cache and generated files - -.docusaurus - -# Serverless directories - -.serverless/ - -# FuseBox cache - -.fusebox/ - -# DynamoDB Local files - -.dynamodb/ - -# TernJS port file - -.tern-port - -# Stores VSCode versions used for testing VSCode extensions - -.vscode-test - -# yarn v2 - -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - -# IntelliJ based IDEs -.idea - -# Finder (MacOS) folder config -.DS_Store - -# sst -.sst diff --git a/examples/internal/aws-bun/Dockerfile b/examples/internal/aws-bun/Dockerfile deleted file mode 100644 index a50d40e263..0000000000 --- a/examples/internal/aws-bun/Dockerfile +++ /dev/null @@ -1,38 +0,0 @@ -# use the official Bun image -# see all versions at https://hub.docker.com/r/oven/bun/tags -FROM oven/bun:1 AS base -WORKDIR /usr/src/app - -# install dependencies into temp directory -# this will cache them and speed up future builds -FROM base AS install -RUN mkdir -p /temp/dev -COPY package.json bun.lockb /temp/dev/ -RUN cd /temp/dev && bun install --frozen-lockfile - -# install with --production (exclude devDependencies) -RUN mkdir -p /temp/prod -COPY package.json bun.lockb /temp/prod/ -RUN cd /temp/prod && bun install --frozen-lockfile --production - -# copy node_modules from temp directory -# then copy all (non-ignored) project files into the image -FROM base AS prerelease -COPY --from=install /temp/dev/node_modules node_modules -COPY . . - -# [optional] tests & build -ENV NODE_ENV=production -# RUN bun test -RUN bun run build - -# copy production dependencies and source code into final image -FROM base AS release -COPY --from=install /temp/prod/node_modules node_modules -COPY --from=prerelease /usr/src/app/index.ts . -COPY --from=prerelease /usr/src/app/package.json . - -# run the app -USER bun -EXPOSE 3000/tcp -ENTRYPOINT [ "bun", "run", "index.ts" ] diff --git a/examples/internal/aws-bun/README.md b/examples/internal/aws-bun/README.md deleted file mode 100644 index 41958527f8..0000000000 --- a/examples/internal/aws-bun/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# aws-bun - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run index.ts -``` - -This project was created using `bun init` in bun v1.1.29. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/examples/internal/aws-bun/bun.lockb b/examples/internal/aws-bun/bun.lockb deleted file mode 100755 index 1eeb8e87cc..0000000000 Binary files a/examples/internal/aws-bun/bun.lockb and /dev/null differ diff --git a/examples/internal/aws-bun/index.ts b/examples/internal/aws-bun/index.ts deleted file mode 100644 index 0168d45220..0000000000 --- a/examples/internal/aws-bun/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Resource } from "sst"; -import { Cluster } from "ioredis"; - -const redis = new Cluster( - [{ host: Resource.MyRedis.host, port: Resource.MyRedis.port }], - { - dnsLookup: (address, callback) => callback(null, address), - redisOptions: { - tls: {}, - username: Resource.MyRedis.username, - password: Resource.MyRedis.password, - }, - } -); - -const server = Bun.serve({ - async fetch(req) { - const url = new URL(req.url); - - if (url.pathname === "/" && req.method === "GET") { - const counter = await redis.incr("counter"); - return new Response(`Hit counter: ${counter}`); - } - - return new Response("404!"); - }, -}); - -console.log(`Listening on ${server.url}`); diff --git a/examples/internal/aws-bun/package.json b/examples/internal/aws-bun/package.json deleted file mode 100644 index d5e3569ad6..0000000000 --- a/examples/internal/aws-bun/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "aws-bun", - "module": "index.ts", - "type": "module", - "scripts": { - "build": "bun build --target bun index.ts", - "dev": "bun run --watch index.ts" - }, - "devDependencies": { - "@types/aws-lambda": "8.10.145", - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "dependencies": { - "ioredis": "^5.4.1", - "sst": "3.1.51" - } -} diff --git a/examples/internal/aws-bun/sst-env.d.ts b/examples/internal/aws-bun/sst-env.d.ts deleted file mode 100644 index 5213abcbfa..0000000000 --- a/examples/internal/aws-bun/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - } -} diff --git a/examples/internal/aws-bun/sst.config.ts b/examples/internal/aws-bun/sst.config.ts deleted file mode 100644 index dad1e0737b..0000000000 --- a/examples/internal/aws-bun/sst.config.ts +++ /dev/null @@ -1,27 +0,0 @@ -/// - -export default $config({ - app(input) { - return { - name: "aws-bun", - removal: input?.stage === "production" ? "retain" : "remove", - home: "aws", - }; - }, - async run() { - const vpc = new sst.aws.Vpc("MyVpc"); - const redis = new sst.aws.Redis("MyRedis", { vpc }); - const cluster = new sst.aws.Cluster("MyCluster", { vpc }); - - new sst.aws.Service("MyService", { - cluster, - link: [redis], - loadBalancer: { - ports: [{ listen: "80/http", forward: "3000/http" }], - }, - dev: { - command: "bun dev", - }, - }); - }, -}); diff --git a/examples/internal/aws-bun/tsconfig.json b/examples/internal/aws-bun/tsconfig.json deleted file mode 100644 index 238655f2ce..0000000000 --- a/examples/internal/aws-bun/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - // Enable latest features - "lib": ["ESNext", "DOM"], - "target": "ESNext", - "module": "ESNext", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } -} diff --git a/examples/internal/aws-nitro/.nitro/dev/index.mjs b/examples/internal/aws-nitro/.nitro/dev/index.mjs deleted file mode 100644 index e3a12b394f..0000000000 --- a/examples/internal/aws-nitro/.nitro/dev/index.mjs +++ /dev/null @@ -1,896 +0,0 @@ -import process from 'node:process';globalThis._importMeta_={url:import.meta.url,env:process.env};import destr from 'file:///home/thdxr/dev/projects/sst/ion/examples/aws-nitro/node_modules/destr/dist/index.mjs'; -import { getRequestHeader, splitCookiesString, setResponseStatus, setResponseHeader, send, defineEventHandler, handleCacheHeaders, createEvent, fetchWithEvent, isEvent, eventHandler, setHeaders, sendRedirect, proxyRequest, createApp, createRouter as createRouter$1, toNodeListener, lazyEventHandler, createError, getRouterParam, getQuery as getQuery$1, readBody } from 'file:///home/thdxr/dev/projects/sst/ion/examples/aws-nitro/node_modules/h3/dist/index.mjs'; -import { createHooks } from 'file:///home/thdxr/dev/projects/sst/ion/examples/aws-nitro/node_modules/hookable/dist/index.mjs'; -import { createFetch as createFetch$1, Headers as Headers$1 } from 'file:///home/thdxr/dev/projects/sst/ion/examples/aws-nitro/node_modules/ofetch/dist/node.mjs'; -import { createCall, createFetch } from 'file:///home/thdxr/dev/projects/sst/ion/node_modules/unenv/runtime/fetch/index.mjs'; -import { hash } from 'file:///home/thdxr/dev/projects/sst/ion/examples/aws-nitro/node_modules/ohash/dist/index.mjs'; -import { parseURL, withoutBase, joinURL, getQuery, withQuery } from 'file:///home/thdxr/dev/projects/sst/ion/node_modules/ufo/dist/index.mjs'; -import { createStorage, prefixStorage } from 'file:///home/thdxr/dev/projects/sst/ion/examples/aws-nitro/node_modules/unstorage/dist/index.mjs'; -import fsDriver from 'file:///home/thdxr/dev/projects/sst/ion/examples/aws-nitro/node_modules/unstorage/drivers/fs.mjs'; -import { klona } from 'file:///home/thdxr/dev/projects/sst/ion/examples/aws-nitro/node_modules/klona/dist/index.mjs'; -import defu, { defuFn } from 'file:///home/thdxr/dev/projects/sst/ion/node_modules/defu/dist/defu.mjs'; -import { snakeCase } from 'file:///home/thdxr/dev/projects/sst/ion/examples/aws-nitro/node_modules/scule/dist/index.mjs'; -import { getContext } from 'file:///home/thdxr/dev/projects/sst/ion/examples/aws-nitro/node_modules/unctx/dist/index.mjs'; -import { toRouteMatcher, createRouter } from 'file:///home/thdxr/dev/projects/sst/ion/examples/aws-nitro/node_modules/radix3/dist/index.mjs'; -import { Server } from 'node:http'; -import { mkdirSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { parentPort, threadId } from 'node:worker_threads'; -import { provider, isWindows } from 'file:///home/thdxr/dev/projects/sst/ion/node_modules/std-env/dist/index.mjs'; - -function hasReqHeader(event, name, includes) { - const value = getRequestHeader(event, name); - return value && typeof value === "string" && value.toLowerCase().includes(includes); -} -function isJsonRequest(event) { - if (hasReqHeader(event, "accept", "text/html")) { - return false; - } - return hasReqHeader(event, "accept", "application/json") || hasReqHeader(event, "user-agent", "curl/") || hasReqHeader(event, "user-agent", "httpie/") || hasReqHeader(event, "sec-fetch-mode", "cors") || event.path.startsWith("/api/") || event.path.endsWith(".json"); -} -function normalizeError(error, isDev) { - const cwd = typeof process.cwd === "function" ? process.cwd() : "/"; - const stack = (error.stack || "").split("\n").splice(1).filter((line) => line.includes("at ")).map((line) => { - const text = line.replace(cwd + "/", "./").replace("webpack:/", "").replace("file://", "").trim(); - return { - text, - internal: line.includes("node_modules") && !line.includes(".cache") || line.includes("internal") || line.includes("new Promise") - }; - }); - const statusCode = error.statusCode || 500; - const statusMessage = error.statusMessage ?? (statusCode === 404 ? "Not Found" : ""); - const message = error.message || error.toString(); - return { - stack, - statusCode, - statusMessage, - message - }; -} -function _captureError(error, type) { - console.error(`[nitro] [${type}]`, error); - useNitroApp().captureError(error, { tags: [type] }); -} -function trapUnhandledNodeErrors() { - process.on( - "unhandledRejection", - (error) => _captureError(error, "unhandledRejection") - ); - process.on( - "uncaughtException", - (error) => _captureError(error, "uncaughtException") - ); -} -function joinHeaders(value) { - return Array.isArray(value) ? value.join(", ") : String(value); -} -function normalizeFetchResponse(response) { - if (!response.headers.has("set-cookie")) { - return response; - } - return new Response(response.body, { - status: response.status, - statusText: response.statusText, - headers: normalizeCookieHeaders(response.headers) - }); -} -function normalizeCookieHeader(header = "") { - return splitCookiesString(joinHeaders(header)); -} -function normalizeCookieHeaders(headers) { - const outgoingHeaders = new Headers(); - for (const [name, header] of headers) { - if (name === "set-cookie") { - for (const cookie of normalizeCookieHeader(header)) { - outgoingHeaders.append("set-cookie", cookie); - } - } else { - outgoingHeaders.set(name, joinHeaders(header)); - } - } - return outgoingHeaders; -} - -function defineNitroErrorHandler(handler) { - return handler; -} -const errorHandler = defineNitroErrorHandler( - function defaultNitroErrorHandler(error, event) { - const { stack, statusCode, statusMessage, message } = normalizeError( - error); - const showDetails = statusCode !== 404; - const errorObject = { - url: event.path || "", - statusCode, - statusMessage, - message, - stack: showDetails ? stack.map((i) => i.text) : void 0 - }; - if (error.unhandled || error.fatal) { - const tags = [ - "[nitro]", - "[request error]", - error.unhandled && "[unhandled]", - error.fatal && "[fatal]" - ].filter(Boolean).join(" "); - console.error( - tags, - error.message + "\n" + stack.map((l) => " " + l.text).join(" \n") - ); - } - setResponseStatus(event, statusCode, statusMessage); - if (isJsonRequest(event)) { - setResponseHeader(event, "Content-Type", "application/json"); - return send(event, JSON.stringify(errorObject)); - } - setResponseHeader(event, "Content-Type", "text/html"); - return send(event, renderHTMLError(errorObject)); - } -); -function renderHTMLError(error) { - const statusCode = error.statusCode || 500; - const statusMessage = error.statusMessage || "Request Error"; - return ` - - - - - ${statusCode} ${statusMessage} - - - -
- -
-
-

${statusCode} ${statusMessage}

-
- - ${error.message}

- ${"\n" + (error.stack || []).map((i) => `  ${i}`).join("
")} -
- -
-
-
- - -`; -} - -const plugins = [ - -]; - -const _lazy_ZP4wpu = () => Promise.resolve().then(function () { return index$1; }); - -const handlers = [ - { route: '/', handler: _lazy_ZP4wpu, lazy: true, middleware: false, method: undefined } -]; - -const serverAssets = [{"baseName":"server","dir":"/home/thdxr/dev/projects/sst/ion/examples/aws-nitro/server/assets"}]; - -const assets = createStorage(); - -for (const asset of serverAssets) { - assets.mount(asset.baseName, fsDriver({ base: asset.dir, ignore: (asset?.ignore || []) })); -} - -const storage = createStorage({}); - -storage.mount('/assets', assets); - -function useStorage(base = "") { - return base ? prefixStorage(storage, base) : storage; -} - -function defaultCacheOptions() { - return { - name: "_", - base: "/cache", - swr: true, - maxAge: 1 - }; -} -function defineCachedFunction(fn, opts = {}) { - opts = { ...defaultCacheOptions(), ...opts }; - const pending = {}; - const group = opts.group || "nitro/functions"; - const name = opts.name || fn.name || "_"; - const integrity = opts.integrity || hash([fn, opts]); - const validate = opts.validate || ((entry) => entry.value !== void 0); - async function get(key, resolver, shouldInvalidateCache, event) { - const cacheKey = [opts.base, group, name, key + ".json"].filter(Boolean).join(":").replace(/:\/$/, ":index"); - let entry = await useStorage().getItem(cacheKey) || {}; - if (typeof entry !== "object") { - entry = {}; - const error = new Error("Malformed data read from cache."); - console.error("[nitro] [cache]", error); - useNitroApp().captureError(error, { event, tags: ["cache"] }); - } - const ttl = (opts.maxAge ?? 0) * 1e3; - if (ttl) { - entry.expires = Date.now() + ttl; - } - const expired = shouldInvalidateCache || entry.integrity !== integrity || ttl && Date.now() - (entry.mtime || 0) > ttl || validate(entry) === false; - const _resolve = async () => { - const isPending = pending[key]; - if (!isPending) { - if (entry.value !== void 0 && (opts.staleMaxAge || 0) >= 0 && opts.swr === false) { - entry.value = void 0; - entry.integrity = void 0; - entry.mtime = void 0; - entry.expires = void 0; - } - pending[key] = Promise.resolve(resolver()); - } - try { - entry.value = await pending[key]; - } catch (error) { - if (!isPending) { - delete pending[key]; - } - throw error; - } - if (!isPending) { - entry.mtime = Date.now(); - entry.integrity = integrity; - delete pending[key]; - if (validate(entry) !== false) { - const promise = useStorage().setItem(cacheKey, entry).catch((error) => { - console.error(`[nitro] [cache] Cache write error.`, error); - useNitroApp().captureError(error, { event, tags: ["cache"] }); - }); - if (event?.waitUntil) { - event.waitUntil(promise); - } - } - } - }; - const _resolvePromise = expired ? _resolve() : Promise.resolve(); - if (entry.value === void 0) { - await _resolvePromise; - } else if (expired && event && event.waitUntil) { - event.waitUntil(_resolvePromise); - } - if (opts.swr && validate(entry) !== false) { - _resolvePromise.catch((error) => { - console.error(`[nitro] [cache] SWR handler error.`, error); - useNitroApp().captureError(error, { event, tags: ["cache"] }); - }); - return entry; - } - return _resolvePromise.then(() => entry); - } - return async (...args) => { - const shouldBypassCache = await opts.shouldBypassCache?.(...args); - if (shouldBypassCache) { - return fn(...args); - } - const key = await (opts.getKey || getKey)(...args); - const shouldInvalidateCache = await opts.shouldInvalidateCache?.(...args); - const entry = await get( - key, - () => fn(...args), - shouldInvalidateCache, - args[0] && isEvent(args[0]) ? args[0] : void 0 - ); - let value = entry.value; - if (opts.transform) { - value = await opts.transform(entry, ...args) || value; - } - return value; - }; -} -function cachedFunction(fn, opts = {}) { - return defineCachedFunction(fn, opts); -} -function getKey(...args) { - return args.length > 0 ? hash(args, {}) : ""; -} -function escapeKey(key) { - return String(key).replace(/\W/g, ""); -} -function defineCachedEventHandler(handler, opts = defaultCacheOptions()) { - const variableHeaderNames = (opts.varies || []).filter(Boolean).map((h) => h.toLowerCase()).sort(); - const _opts = { - ...opts, - getKey: async (event) => { - const customKey = await opts.getKey?.(event); - if (customKey) { - return escapeKey(customKey); - } - const _path = event.node.req.originalUrl || event.node.req.url || event.path; - const _pathname = escapeKey(decodeURI(parseURL(_path).pathname)).slice(0, 16) || "index"; - const _hashedPath = `${_pathname}.${hash(_path)}`; - const _headers = variableHeaderNames.map((header) => [header, event.node.req.headers[header]]).map(([name, value]) => `${escapeKey(name)}.${hash(value)}`); - return [_hashedPath, ..._headers].join(":"); - }, - validate: (entry) => { - if (!entry.value) { - return false; - } - if (entry.value.code >= 400) { - return false; - } - if (entry.value.body === void 0) { - return false; - } - if (entry.value.headers.etag === "undefined" || entry.value.headers["last-modified"] === "undefined") { - return false; - } - return true; - }, - group: opts.group || "nitro/handlers", - integrity: opts.integrity || hash([handler, opts]) - }; - const _cachedHandler = cachedFunction( - async (incomingEvent) => { - const variableHeaders = {}; - for (const header of variableHeaderNames) { - const value = incomingEvent.node.req.headers[header]; - if (value !== void 0) { - variableHeaders[header] = value; - } - } - const reqProxy = cloneWithProxy(incomingEvent.node.req, { - headers: variableHeaders - }); - const resHeaders = {}; - let _resSendBody; - const resProxy = cloneWithProxy(incomingEvent.node.res, { - statusCode: 200, - writableEnded: false, - writableFinished: false, - headersSent: false, - closed: false, - getHeader(name) { - return resHeaders[name]; - }, - setHeader(name, value) { - resHeaders[name] = value; - return this; - }, - getHeaderNames() { - return Object.keys(resHeaders); - }, - hasHeader(name) { - return name in resHeaders; - }, - removeHeader(name) { - delete resHeaders[name]; - }, - getHeaders() { - return resHeaders; - }, - end(chunk, arg2, arg3) { - if (typeof chunk === "string") { - _resSendBody = chunk; - } - if (typeof arg2 === "function") { - arg2(); - } - if (typeof arg3 === "function") { - arg3(); - } - return this; - }, - write(chunk, arg2, arg3) { - if (typeof chunk === "string") { - _resSendBody = chunk; - } - if (typeof arg2 === "function") { - arg2(void 0); - } - if (typeof arg3 === "function") { - arg3(); - } - return true; - }, - writeHead(statusCode, headers2) { - this.statusCode = statusCode; - if (headers2) { - if (Array.isArray(headers2) || typeof headers2 === "string") { - throw new TypeError("Raw headers is not supported."); - } - for (const header in headers2) { - const value = headers2[header]; - if (value !== void 0) { - this.setHeader( - header, - value - ); - } - } - } - return this; - } - }); - const event = createEvent(reqProxy, resProxy); - event.fetch = (url, fetchOptions) => fetchWithEvent(event, url, fetchOptions, { - fetch: useNitroApp().localFetch - }); - event.$fetch = (url, fetchOptions) => fetchWithEvent(event, url, fetchOptions, { - fetch: globalThis.$fetch - }); - event.context = incomingEvent.context; - event.context.cache = { - options: _opts - }; - const body = await handler(event) || _resSendBody; - const headers = event.node.res.getHeaders(); - headers.etag = String( - headers.Etag || headers.etag || `W/"${hash(body)}"` - ); - headers["last-modified"] = String( - headers["Last-Modified"] || headers["last-modified"] || (/* @__PURE__ */ new Date()).toUTCString() - ); - const cacheControl = []; - if (opts.swr) { - if (opts.maxAge) { - cacheControl.push(`s-maxage=${opts.maxAge}`); - } - if (opts.staleMaxAge) { - cacheControl.push(`stale-while-revalidate=${opts.staleMaxAge}`); - } else { - cacheControl.push("stale-while-revalidate"); - } - } else if (opts.maxAge) { - cacheControl.push(`max-age=${opts.maxAge}`); - } - if (cacheControl.length > 0) { - headers["cache-control"] = cacheControl.join(", "); - } - const cacheEntry = { - code: event.node.res.statusCode, - headers, - body - }; - return cacheEntry; - }, - _opts - ); - return defineEventHandler(async (event) => { - if (opts.headersOnly) { - if (handleCacheHeaders(event, { maxAge: opts.maxAge })) { - return; - } - return handler(event); - } - const response = await _cachedHandler( - event - ); - if (event.node.res.headersSent || event.node.res.writableEnded) { - return response.body; - } - if (handleCacheHeaders(event, { - modifiedTime: new Date(response.headers["last-modified"]), - etag: response.headers.etag, - maxAge: opts.maxAge - })) { - return; - } - event.node.res.statusCode = response.code; - for (const name in response.headers) { - const value = response.headers[name]; - if (name === "set-cookie") { - event.node.res.appendHeader( - name, - splitCookiesString(value) - ); - } else { - if (value !== void 0) { - event.node.res.setHeader(name, value); - } - } - } - return response.body; - }); -} -function cloneWithProxy(obj, overrides) { - return new Proxy(obj, { - get(target, property, receiver) { - if (property in overrides) { - return overrides[property]; - } - return Reflect.get(target, property, receiver); - }, - set(target, property, value, receiver) { - if (property in overrides) { - overrides[property] = value; - return true; - } - return Reflect.set(target, property, value, receiver); - } - }); -} -const cachedEventHandler = defineCachedEventHandler; - -const inlineAppConfig = {}; - - - -const appConfig = defuFn(inlineAppConfig); - -function getEnv(key, opts) { - const envKey = snakeCase(key).toUpperCase(); - return destr( - process.env[opts.prefix + envKey] ?? process.env[opts.altPrefix + envKey] - ); -} -function _isObject(input) { - return typeof input === "object" && !Array.isArray(input); -} -function applyEnv(obj, opts, parentKey = "") { - for (const key in obj) { - const subKey = parentKey ? `${parentKey}_${key}` : key; - const envValue = getEnv(subKey, opts); - if (_isObject(obj[key])) { - if (_isObject(envValue)) { - obj[key] = { ...obj[key], ...envValue }; - applyEnv(obj[key], opts, subKey); - } else if (envValue === void 0) { - applyEnv(obj[key], opts, subKey); - } else { - obj[key] = envValue ?? obj[key]; - } - } else { - obj[key] = envValue ?? obj[key]; - } - if (opts.envExpansion && typeof obj[key] === "string") { - obj[key] = _expandFromEnv(obj[key]); - } - } - return obj; -} -const envExpandRx = /{{(.*?)}}/g; -function _expandFromEnv(value) { - return value.replace(envExpandRx, (match, key) => { - return process.env[key] || match; - }); -} - -const _inlineRuntimeConfig = { - "app": { - "baseURL": "/" - }, - "nitro": { - "routeRules": {} - }, - "streaming": true -}; -const envOptions = { - prefix: "NITRO_", - altPrefix: _inlineRuntimeConfig.nitro.envPrefix ?? process.env.NITRO_ENV_PREFIX ?? "_", - envExpansion: _inlineRuntimeConfig.nitro.envExpansion ?? process.env.NITRO_ENV_EXPANSION ?? false -}; -const _sharedRuntimeConfig = _deepFreeze( - applyEnv(klona(_inlineRuntimeConfig), envOptions) -); -function useRuntimeConfig(event) { - { - return _sharedRuntimeConfig; - } -} -_deepFreeze(klona(appConfig)); -function _deepFreeze(object) { - const propNames = Object.getOwnPropertyNames(object); - for (const name of propNames) { - const value = object[name]; - if (value && typeof value === "object") { - _deepFreeze(value); - } - } - return Object.freeze(object); -} -new Proxy(/* @__PURE__ */ Object.create(null), { - get: (_, prop) => { - console.warn( - "Please use `useRuntimeConfig()` instead of accessing config directly." - ); - const runtimeConfig = useRuntimeConfig(); - if (prop in runtimeConfig) { - return runtimeConfig[prop]; - } - return void 0; - } -}); - -getContext("nitro-app", { - asyncContext: undefined, - AsyncLocalStorage: void 0 -}); - -const config = useRuntimeConfig(); -const _routeRulesMatcher = toRouteMatcher( - createRouter({ routes: config.nitro.routeRules }) -); -function createRouteRulesHandler(ctx) { - return eventHandler((event) => { - const routeRules = getRouteRules(event); - if (routeRules.headers) { - setHeaders(event, routeRules.headers); - } - if (routeRules.redirect) { - let target = routeRules.redirect.to; - if (target.endsWith("/**")) { - let targetPath = event.path; - const strpBase = routeRules.redirect._redirectStripBase; - if (strpBase) { - targetPath = withoutBase(targetPath, strpBase); - } - target = joinURL(target.slice(0, -3), targetPath); - } else if (event.path.includes("?")) { - const query = getQuery(event.path); - target = withQuery(target, query); - } - return sendRedirect(event, target, routeRules.redirect.statusCode); - } - if (routeRules.proxy) { - let target = routeRules.proxy.to; - if (target.endsWith("/**")) { - let targetPath = event.path; - const strpBase = routeRules.proxy._proxyStripBase; - if (strpBase) { - targetPath = withoutBase(targetPath, strpBase); - } - target = joinURL(target.slice(0, -3), targetPath); - } else if (event.path.includes("?")) { - const query = getQuery(event.path); - target = withQuery(target, query); - } - return proxyRequest(event, target, { - fetch: ctx.localFetch, - ...routeRules.proxy - }); - } - }); -} -function getRouteRules(event) { - event.context._nitro = event.context._nitro || {}; - if (!event.context._nitro.routeRules) { - event.context._nitro.routeRules = getRouteRulesForPath( - withoutBase(event.path.split("?")[0], useRuntimeConfig().app.baseURL) - ); - } - return event.context._nitro.routeRules; -} -function getRouteRulesForPath(path) { - return defu({}, ..._routeRulesMatcher.matchAll(path).reverse()); -} - -function createNitroApp() { - const config = useRuntimeConfig(); - const hooks = createHooks(); - const captureError = (error, context = {}) => { - const promise = hooks.callHookParallel("error", error, context).catch((error_) => { - console.error("Error while capturing another error", error_); - }); - if (context.event && isEvent(context.event)) { - const errors = context.event.context.nitro?.errors; - if (errors) { - errors.push({ error, context }); - } - if (context.event.waitUntil) { - context.event.waitUntil(promise); - } - } - }; - const h3App = createApp({ - debug: destr(true), - onError: (error, event) => { - captureError(error, { event, tags: ["request"] }); - return errorHandler(error, event); - }, - onRequest: async (event) => { - await nitroApp$1.hooks.callHook("request", event).catch((error) => { - captureError(error, { event, tags: ["request"] }); - }); - }, - onBeforeResponse: async (event, response) => { - await nitroApp$1.hooks.callHook("beforeResponse", event, response).catch((error) => { - captureError(error, { event, tags: ["request", "response"] }); - }); - }, - onAfterResponse: async (event, response) => { - await nitroApp$1.hooks.callHook("afterResponse", event, response).catch((error) => { - captureError(error, { event, tags: ["request", "response"] }); - }); - } - }); - const router = createRouter$1({ - preemptive: true - }); - const localCall = createCall(toNodeListener(h3App)); - const _localFetch = createFetch(localCall, globalThis.fetch); - const localFetch = (input, init) => _localFetch(input, init).then( - (response) => normalizeFetchResponse(response) - ); - const $fetch = createFetch$1({ - fetch: localFetch, - Headers: Headers$1, - defaults: { baseURL: config.app.baseURL } - }); - globalThis.$fetch = $fetch; - h3App.use(createRouteRulesHandler({ localFetch })); - h3App.use( - eventHandler((event) => { - event.context.nitro = event.context.nitro || { errors: [] }; - const envContext = event.node.req?.__unenv__; - if (envContext) { - Object.assign(event.context, envContext); - } - event.fetch = (req, init) => fetchWithEvent(event, req, init, { fetch: localFetch }); - event.$fetch = (req, init) => fetchWithEvent(event, req, init, { - fetch: $fetch - }); - event.waitUntil = (promise) => { - if (!event.context.nitro._waitUntilPromises) { - event.context.nitro._waitUntilPromises = []; - } - event.context.nitro._waitUntilPromises.push(promise); - if (envContext?.waitUntil) { - envContext.waitUntil(promise); - } - }; - event.captureError = (error, context) => { - captureError(error, { event, ...context }); - }; - }) - ); - for (const h of handlers) { - let handler = h.lazy ? lazyEventHandler(h.handler) : h.handler; - if (h.middleware || !h.route) { - const middlewareBase = (config.app.baseURL + (h.route || "/")).replace( - /\/+/g, - "/" - ); - h3App.use(middlewareBase, handler); - } else { - const routeRules = getRouteRulesForPath( - h.route.replace(/:\w+|\*\*/g, "_") - ); - if (routeRules.cache) { - handler = cachedEventHandler(handler, { - group: "nitro/routes", - ...routeRules.cache - }); - } - router.use(h.route, handler, h.method); - } - } - h3App.use(config.app.baseURL, router.handler); - const app = { - hooks, - h3App, - router, - localCall, - localFetch, - captureError - }; - return app; -} -function runNitroPlugins(nitroApp2) { - for (const plugin of plugins) { - try { - plugin(nitroApp2); - } catch (error) { - nitroApp2.captureError(error, { tags: ["plugin"] }); - throw error; - } - } -} -const nitroApp$1 = createNitroApp(); -function useNitroApp() { - return nitroApp$1; -} -runNitroPlugins(nitroApp$1); - -const scheduledTasks = false; - -const tasks = { - -}; - -const __runningTasks__ = {}; -async function runTask(name, { - payload = {}, - context = {} -} = {}) { - if (__runningTasks__[name]) { - return __runningTasks__[name]; - } - if (!(name in tasks)) { - throw createError({ - message: `Task \`${name}\` is not available!`, - statusCode: 404 - }); - } - if (!tasks[name].resolve) { - throw createError({ - message: `Task \`${name}\` is not implemented!`, - statusCode: 501 - }); - } - const handler = await tasks[name].resolve(); - const taskEvent = { name, payload, context }; - __runningTasks__[name] = handler.run(taskEvent); - try { - const res = await __runningTasks__[name]; - return res; - } finally { - delete __runningTasks__[name]; - } -} - -const nitroApp = useNitroApp(); -const server = new Server(toNodeListener(nitroApp.h3App)); -function getAddress() { - if (provider === "stackblitz" || process.env.NITRO_NO_UNIX_SOCKET || process.versions.bun) { - return 0; - } - const socketName = `worker-${process.pid}-${threadId}.sock`; - if (isWindows) { - return join(String.raw`\\.\pipe\nitro`, socketName); - } - const socketDir = join(tmpdir(), "nitro"); - mkdirSync(socketDir, { recursive: true }); - return join(socketDir, socketName); -} -const listenAddress = getAddress(); -server.listen(listenAddress, () => { - const _address = server.address(); - parentPort?.postMessage({ - event: "listen", - address: typeof _address === "string" ? { socketPath: _address } : { host: "localhost", port: _address?.port } - }); -}); -nitroApp.router.get( - "/_nitro/tasks", - defineEventHandler(async (event) => { - const _tasks = await Promise.all( - Object.entries(tasks).map(async ([name, task]) => { - const _task = await task.resolve?.(); - return [name, { description: _task?.meta?.description }]; - }) - ); - return { - tasks: Object.fromEntries(_tasks), - scheduledTasks - }; - }) -); -nitroApp.router.use( - "/_nitro/tasks/:name", - defineEventHandler(async (event) => { - const name = getRouterParam(event, "name"); - const payload = { - ...getQuery$1(event), - ...await readBody(event).then((r) => r?.payload).catch(() => ({})) - }; - return await runTask(name, { payload }); - }) -); -trapUnhandledNodeErrors(); -async function onShutdown(signal) { - await nitroApp.hooks.callHook("close"); -} -parentPort?.on("message", async (msg) => { - if (msg && msg.event === "shutdown") { - await onShutdown(); - parentPort?.postMessage({ event: "exit" }); - } -}); - -const index = eventHandler((event) => { - return "Start by editing server/routes/index.ts."; -}); - -const index$1 = /*#__PURE__*/Object.freeze({ - __proto__: null, - default: index -}); -//# sourceMappingURL=index.mjs.map diff --git a/examples/internal/aws-nitro/.nitro/dev/index.mjs.map b/examples/internal/aws-nitro/.nitro/dev/index.mjs.map deleted file mode 100644 index d78b17a85a..0000000000 --- a/examples/internal/aws-nitro/.nitro/dev/index.mjs.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.mjs","sources":["../../../../../../nitro/dist/runtime/internal/utils.mjs","../../../../../../nitro/dist/runtime/internal/error.mjs","../../../../../../nitro/dist/runtime/internal/storage.mjs","../../../../../../nitro/dist/runtime/internal/cache.mjs","../../../../../../nitro/dist/runtime/internal/utils.env.mjs","../../../../../../nitro/dist/runtime/internal/config.mjs","../../../../../../nitro/dist/runtime/internal/context.mjs","../../../../../../nitro/dist/runtime/internal/route-rules.mjs","../../../../../../nitro/dist/runtime/internal/app.mjs","../../../../../../nitro/dist/runtime/internal/task.mjs","../../../../../../nitro/dist/presets/_nitro/runtime/nitro-dev.mjs","../../server/routes/index.ts"],"sourcesContent":null,"names":["_inlineAppConfig","createRadixRouter","nitroApp","createRouter","createLocalFetch","createFetch","Headers","getQuery"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA2BO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,YAAY,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,EAAE,CAAI,CAAA,CAAA,CAAA,CAAA,CAAE,QAAQ,CAAE,CAAA,CAAA;AACpD,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAgB,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAC,CAAC;AAC9C,CAAA,CAAE,OAAO,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,KAAK,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,WAAW,CAAE,CAAA,CAAC,QAAQ,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAC;AACtF,CAAC;AACM,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAa,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA;AACrC,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,YAAY,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,EAAE,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAE,CAAA,CAAA;AAClD,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,KAAK,CAAC;AACjB,CAAG,CAAA,CAAA;AACH,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,QAAQ,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAkB,CAAC,CAAA,CAAA,CAAA,CAAI,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAI,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,IAAI,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAgB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,MAAM,CAAC,CAAA,CAAA,CAAA,CAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,UAAU,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AAC5Q,CAAC;AACM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAc,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,KAAK,CAAE,CAAA,CAAA;AAC7C,CAAA,CAAE,MAAM,CAAG,CAAA,CAAA,CAAA,CAAA,CAAG,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,OAAO,CAAC,CAAA,CAAA,CAAG,CAAE,CAAA,CAAA,CAAA,CAAG,GAAG,CAAC;AACtE,CAAA,CAAE,MAAM,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAA+E,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAE,CAAA,CAAA,CAAE,KAAK,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,IAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AAC3L,CAAA,CAAA,CAAA,CAAI,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAG,CAAG,CAAA,CAAA,CAAA,CAAA,CAAG,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAE,CAAA,CAAA,CAAE,CAAC,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAE,CAAA,CAAC,CAAC,CAAA,CAAA,CAAA,CAAI,EAAE,CAAC;AACtG,CAAA,CAAA,CAAA,CAAI,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACX,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAI,CAAA,CAAA,CAAA,CAAA;AACV,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAc,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,aAAa,CAAC;AACtI,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC;AACN,CAAA,CAAA,CAAG,CAAC,CAAC;AACL,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,UAAU,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,GAAG,CAAC;AAC7C,CAAA,CAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAa,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAa,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,GAAG,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAG,CAAA,CAAA,CAAA,CAAE,CAAC,CAAC;AACvF,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAyD,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC;AAC1G,CAAA,CAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAA,CAAA,CAAA,CAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAA,CAAA,CAAA,CAAI,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACd,CAAA,CAAA,CAAA,CAAI,CAAa,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACjB,CAAA,CAAA,CAAA,CAAI,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACX,CAAA,CAAA,CAAG,CAAC;AACJ,CAAC;AACD,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAa,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,IAAI,CAAE,CAAA,CAAA;AACpC,CAAA,CAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAC,CAAC,CAAC,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC;AAC5C,CAAA,CAAE,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,EAAE,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAE,CAAC,CAAC;AACtD,CAAC;AACM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,uBAAuB,CAAG,CAAA,CAAA,CAAA;AAC1C,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAE,CAAA,CAAA;AACZ,CAAA,CAAA,CAAA,CAAI,CAAoB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACxB,CAAI,CAAA,CAAA,CAAA,CAAC,KAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAa,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,oBAAoB,CAAC;AACzD,CAAA,CAAA,CAAG,CAAC;AACJ,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAE,CAAA,CAAA;AACZ,CAAA,CAAA,CAAA,CAAI,CAAmB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACvB,CAAI,CAAA,CAAA,CAAA,CAAC,KAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAa,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,mBAAmB,CAAC;AACxD,CAAA,CAAA,CAAG,CAAC;AACJ,CAAC;AACM,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA;AACnC,CAAA,CAAE,OAAO,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,OAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,IAAI,CAAC,CAAA,CAAA,CAAG,MAAM,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC;AACjE,CAAC;AACM,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAsB,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAE,CAAA,CAAA;AACjD,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAG,CAAA,CAAA,CAAC,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAE,CAAA,CAAA;AAC3C,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,QAAQ,CAAC;AACpB,CAAG,CAAA,CAAA;AACH,CAAA,CAAE,OAAO,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,IAAI,CAAE,CAAA,CAAA;AACrC,CAAA,CAAA,CAAA,CAAI,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AAC3B,CAAA,CAAA,CAAA,CAAI,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACnC,CAAA,CAAA,CAAA,CAAI,OAAO,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAsB,CAAC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,OAAO,CAAC;AACrD,CAAA,CAAA,CAAG,CAAC,CAAC;AACL,CAAC;AACM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAqB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,EAAE,CAAE,CAAA,CAAA;AACnD,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,kBAAkB,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC,CAAC;AACjD,CAAC;AACM,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAsB,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAE,CAAA,CAAA;AAChD,CAAA,CAAE,MAAM,CAAe,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,EAAE,CAAC;AACxC,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,MAAM,CAAC,CAAA,CAAA,CAAA,CAAI,EAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAE,CAAA,CAAA;AACxC,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAE,CAAA,CAAA;AAC/B,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,MAAM,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,qBAAqB,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAE,CAAA,CAAA;AAC1D,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAe,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,YAAY,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAC;AACrD,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAA,CAAA,CAAA,CAAA,CAAK,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACX,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAe,CAAC,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAA,CAAI,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAC,CAAC;AACrD,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAG,CAAA,CAAA;AACH,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,eAAe,CAAC;AACzB,CAAA;;AChGO,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAuB,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAE,CAAA,CAAA;AACjD,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,OAAO,CAAC;AACjB,CAAC;AAED,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAe,CAAuB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACtC,CAAA,CAAE,SAAS,CAAwB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,KAAK,CAAE,CAAA,CAAA;AAClD,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAa,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAE,CAAA,CAAA,CAAA,CAAG,CAAc,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACxE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAEF,CAAC,CAAC;AACN,CAAA,CAAA,CAAA,CAAI,MAAM,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,GAAG,CAAC;AACpD,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,WAAW,CAAG,CAAA,CAAA,CAAA;AACxB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,GAAG,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA,CAAA,CAAA,CAAI,IAAI,CAAE,CAAA,CAAA;AAC3B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AAChB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAa,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACnB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACb,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAG,CAAA,CAAA,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAA,CAAA,CAAG,KAAK,CAAC;AAC5D,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC;AACN,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,IAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAE,CAAA,CAAA;AACxC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,IAAI,CAAG,CAAA,CAAA,CAAA;AACnB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACjB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAiB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACzB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAa,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACxC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AAChC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAC,CAAC;AAClC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACnB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAI,CAAA,CAAA,CAAA,CAAA;AACZ,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,IAAI,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA,CAAA,CAAG,CAAC,CAAC,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAG,CAAA,CAAA,CAAC,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC;AAC3E,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC;AACR,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAiB,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,UAAU,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAa,CAAC,CAAC;AACxD,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAa,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAE,CAAA,CAAA;AAC9B,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAiB,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,cAAc,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAkB,CAAC,CAAC;AACnE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAC,CAAC,CAAC;AACtD,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAiB,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,cAAc,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAC,CAAC;AAC1D,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAI,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,eAAe,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAC,CAAC,CAAC;AACrD,CAAG,CAAA,CAAA;AACH,CAAC,CAAC;AACF,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAe,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA;AAChC,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,UAAU,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,GAAG,CAAC;AAC7C,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,aAAa,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAa,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,eAAe,CAAC;AAC/D,CAAA,CAAE,OAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACV,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,EAAE,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC,CAAA,CAAE,aAAa,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACzC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAgB,EAAE,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC,CAAA,CAAE,aAAa,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA;AAC9C,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AAC5B,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAI,CAAA,CAAA,CAAA,CAAA,CAAE,CAAE,CAAA,CAAA,CAAA,CAAG,CAAC,CAAC,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAE,CAAA,CAAC,CAAC,CAAC,CAAC,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AACrF,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAC,CAAC;AACF,CAAA;;;;;;;;;;;;;;;;;;;;;;;;ACtEO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,EAAE,CAAE,CAAA,CAAA;AACtC,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAa,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC;AACvD,CAAA;;ACQA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,mBAAmB,CAAG,CAAA,CAAA,CAAA;AAC/B,CAAA,CAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,EAAE,CAAG,CAAA,CAAA,CAAA;AACb,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,EAAE,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AAClB,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,EAAE,CAAI,CAAA,CAAA,CAAA,CAAA;AACb,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,EAAE,CAAC;AACb,CAAA,CAAA,CAAG,CAAC;AACJ,CAAC;AACM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,oBAAoB,CAAC,CAAA,CAAE,EAAE,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,EAAE,CAAE,CAAA,CAAA;AACpD,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAG,CAAA,CAAA,CAAA,CAAE,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAmB,EAAE,CAAE,CAAA,CAAA,CAAA,CAAG,CAAI,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC;AAC/C,CAAA,CAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAG,CAAA,CAAA,CAAA,CAAE,CAAC;AACrB,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,iBAAiB,CAAC;AAChD,CAAA,CAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAC;AAC3C,CAAA,CAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAC,CAAC,CAAE,CAAA,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAC,CAAC,CAAC;AACvD,CAAA,CAAE,MAAM,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAI,CAAA,CAAA,CAAA,CAAC,QAAQ,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,KAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC,CAAC;AACxE,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAe,CAAG,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAA,CAAE,QAAQ,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAqB,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA;AAClE,CAAA,CAAA,CAAA,CAAI,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAG,CAAA,CAAA,CAAC,IAAI,CAAC,CAAA,CAAA,CAAA,CAAI,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAE,CAAA,CAAA,CAAA,CAAG,GAAG,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAC,IAAI,CAAC,CAAA,CAAA,CAAG,CAAC,CAAC,OAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAC;AACjH,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAA,CAAA,CAAA,CAAI,EAAE,CAAC;AAC3D,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,QAAQ,CAAE,CAAA,CAAA;AACnC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAG,CAAA,CAAA,CAAA,CAAE,CAAC;AACjB,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAiC,CAAC,CAAC;AACjE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,iBAAiB,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC;AAC9C,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,WAAW,CAAE,CAAA,CAAC,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAE,CAAA,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,IAAI,CAAE,CAAA,CAAC,OAAO,CAAC,CAAA,CAAE,CAAC,CAAC;AACpE,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAG,CAAA,CAAA,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAI,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAC;AACzC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,GAAG,CAAE,CAAA,CAAA;AACb,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAG,CAAA,CAAA,CAAA,CAAE,CAAG,CAAA,CAAA,CAAA,CAAA,CAAG,CAAC;AACvC,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAA,CAAA,CAAA,CAAI,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAqB,IAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,IAAI,CAAC,CAAA,CAAA,CAAG,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAC,CAAA,CAAA,CAAG,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,QAAQ,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,KAAK,CAAC;AACxJ,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACjC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,MAAM,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,OAAO,CAAC,CAAA,CAAA,CAAG,CAAC,CAAC;AACrC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAE,CAAA,CAAA;AACtB,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,KAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,WAAW,CAAI,CAAA,CAAA,CAAA,CAAC,KAAK,CAAC,CAAA,CAAA,CAAA,CAAI,IAAI,CAAC,CAAA,CAAA,CAAG,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA;AAC1F,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC;AAC/B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC;AACnC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC;AAC/B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC;AACjC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAE,CAAA,CAAC,CAAC;AACnD,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAI,CAAA,CAAA,CAAA,CAAA;AACV,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,MAAM,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAC,CAAC;AACzC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA;AACtB,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAE,CAAA,CAAA;AACxB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAG,CAAC,CAAC;AAC9B,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAC;AACpB,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAE,CAAA,CAAA;AACtB,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,IAAI,CAAC,CAAA,CAAA,CAAG,EAAE,CAAC;AACjC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,SAAS,CAAC;AACpC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAG,CAAC,CAAC;AAC5B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,IAAI,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,KAAK,CAAE,CAAA,CAAA;AACvC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,MAAM,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACjF,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC,kCAAkC,CAAC,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AACvE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,WAAW,CAAE,CAAA,CAAC,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAE,CAAA,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,IAAI,CAAE,CAAA,CAAC,OAAO,CAAC,CAAA,CAAE,CAAC,CAAC;AAC1E,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAC,CAAC;AACb,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAE,CAAA,CAAA;AAChC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAC;AACrC,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACX,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC;AACN,CAAA,CAAA,CAAA,CAAI,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAe,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAE,CAAA,CAAA,CAAA,CAAG,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC;AACrE,CAAA,CAAA,CAAA,CAAI,IAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAE,CAAA,CAAA;AAChC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,eAAe,CAAC;AAC5B,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,IAAI,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,KAAK,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAE,CAAA,CAAA;AACpD,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAe,CAAC,CAAC;AACvC,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA;AAC/C,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,eAAe,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC,KAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACvC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC,kCAAkC,CAAC,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AACnE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,WAAW,CAAE,CAAA,CAAC,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAE,CAAA,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,IAAI,CAAE,CAAA,CAAC,OAAO,CAAC,CAAA,CAAE,CAAC,CAAC;AACtE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAC;AACT,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,KAAK,CAAC;AACnB,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,eAAe,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC;AAC7C,CAAG,CAAA,CAAA;AACH,CAAA,CAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AAC5B,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAiB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAiB,CAAG,CAAA,CAAA,CAAA,CAAA,CAAG,CAAI,CAAA,CAAA,CAAA,CAAC,CAAC;AACtE,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,iBAAiB,CAAE,CAAA,CAAA;AAC3B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,OAAO,CAAE,CAAA,CAAC,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAC;AACzB,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAA,CAAA,CAAA,CAAI,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAC;AACvD,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAqB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAqB,CAAG,CAAA,CAAA,CAAA,CAAA,CAAG,CAAI,CAAA,CAAA,CAAA,CAAC,CAAC;AAC9E,CAAA,CAAA,CAAA,CAAI,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAG,CAAA,CAAA,CAAA;AAC3B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAG,CAAA,CAAA,CAAA;AACT,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC,CAAA,CAAA,CAAG,IAAI,CAAC,CAAA;AACvB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAqB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AAC3B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,IAAI,CAAC,CAAC,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAC,CAAC,CAAC,GAAG,CAAI,CAAA,CAAA,CAAA,CAAC,CAAC,CAAC,CAAA,CAAA,CAAG,KAAK,CAAC;AACpD,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC;AACN,CAAA,CAAA,CAAA,CAAI,IAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAC;AAC5B,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAE,CAAA,CAAA;AACxB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAI,KAAK,CAAC;AAC5D,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,KAAK,CAAC;AACjB,CAAA,CAAA,CAAG,CAAC;AACJ,CAAC;AACM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,cAAc,CAAC,CAAA,CAAE,EAAE,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,EAAE,CAAE,CAAA,CAAA;AAC9C,CAAA,CAAE,OAAO,CAAoB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,EAAE,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAC;AACxC,CAAC;AACD,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAE,CAAA,CAAA;AACzB,CAAA,CAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,GAAG,CAAC,CAAA,CAAA,CAAG,CAAI,CAAA,CAAA,CAAA,CAAC,IAAI,CAAE,CAAA,CAAA,CAAE,CAAC,CAAA,CAAA,CAAG,EAAE,CAAC;AAC/C,CAAC;AACD,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC,CAAA,CAAA,CAAG,CAAE,CAAA,CAAA;AACxB,CAAA,CAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAA,CAAA,CAAG,CAAC,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAE,CAAA,CAAC,CAAC;AACxC,CAAC;AACM,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAwB,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,IAAI,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAmB,EAAE,CAAE,CAAA,CAAA;AAChF,CAAA,CAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAmB,CAAG,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAE,CAAA,CAAA,CAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC,CAAA,CAAA,CAAG,CAAC,CAAC,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC;AACrG,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAG,CAAA,CAAA,CAAA;AAChB,CAAA,CAAA,CAAA,CAAI,GAAG,CAAI,CAAA,CAAA,CAAA,CAAA;AACX,CAAA,CAAA,CAAA,CAAI,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AAC7B,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AACnD,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,SAAS,CAAE,CAAA,CAAA;AACrB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC,CAAC;AACpC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAC,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,KAAK,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAG,CAAA,CAAA,CAAC,GAAG,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC;AACnF,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,SAAS,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC,EAAE,CAAE,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC;AAC/F,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAG,CAAA,CAAA,CAAC,EAAE,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC,CAAA,CAAE,IAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC,CAAC,CAAC;AACxD,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,QAAQ,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAmB,CAAC,CAAG,CAAA,CAAA,CAAC,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,MAAM,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAG,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC,CAAC,CAAC,CAAG,CAAA,CAAA,CAAC,CAAC,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAE,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA,CAAE,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AACjK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAC,CAAC;AAClD,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACL,CAAA,CAAA,CAAA,CAAI,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACzB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAI,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAE,CAAA,CAAA;AACxB,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,KAAK,CAAC;AACrB,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,GAAG,CAAE,CAAA,CAAA;AACnC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,KAAK,CAAC;AACrB,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAE,CAAA,CAAA;AACvC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,KAAK,CAAC;AACrB,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,IAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,IAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAe,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAE,CAAA,CAAA;AAC5G,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,KAAK,CAAC;AACrB,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,IAAI,CAAC;AAClB,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACL,CAAA,CAAA,CAAA,CAAI,KAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,IAAI,CAAgB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACzC,CAAA,CAAA,CAAA,CAAI,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAC,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAC;AACtD,CAAA,CAAA,CAAG,CAAC;AACJ,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAc,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAc,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACvC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,aAAa,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AAC7B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAe,CAAG,CAAA,CAAA,CAAA,CAAE,CAAC;AACjC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,mBAAmB,CAAE,CAAA,CAAA;AAChD,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAa,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAC;AAC7D,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAE,CAAA,CAAA;AAC9B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,eAAe,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAA,CAAA,CAAG,KAAK,CAAC;AAC1C,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAc,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,aAAa,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAG,CAAE,CAAA,CAAA;AAC9D,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,EAAE,CAAe,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AAChC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAC;AACT,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAG,CAAA,CAAA,CAAA,CAAE,CAAC;AAC5B,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,YAAY,CAAC;AACvB,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAc,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,aAAa,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAG,CAAE,CAAA,CAAA;AAC9D,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,EAAE,CAAG,CAAA,CAAA,CAAA;AACvB,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAa,EAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AAC5B,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAgB,EAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AAC/B,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,EAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AAC1B,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,EAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACrB,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC,CAAA,CAAA,CAAA,CAAI,CAAE,CAAA,CAAA;AACxB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAC;AAClC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAE,KAAK,CAAE,CAAA,CAAA;AAC/B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,UAAU,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAG,KAAK,CAAC;AACnC,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,IAAI,CAAC;AACtB,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,cAAc,CAAG,CAAA,CAAA,CAAA;AACzB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,OAAO,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,IAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAC;AACzC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC,CAAA,CAAA,CAAA,CAAI,CAAE,CAAA,CAAA;AACxB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC;AACpC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAC,CAAA,CAAA,CAAA,CAAI,CAAE,CAAA,CAAA;AAC3B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAC;AAClC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,UAAU,CAAG,CAAA,CAAA,CAAA;AACrB,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,UAAU,CAAC;AAC5B,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,GAAG,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,EAAE,CAAI,CAAA,CAAA,CAAA,CAAA,CAAE,IAAI,CAAE,CAAA,CAAA;AAC/B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,QAAQ,CAAE,CAAA,CAAA;AACzC,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC;AACjC,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACX,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,UAAU,CAAE,CAAA,CAAA;AAC1C,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,EAAE,CAAC;AACnB,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACX,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,UAAU,CAAE,CAAA,CAAA;AAC1C,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,EAAE,CAAC;AACnB,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACX,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,IAAI,CAAC;AACtB,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,EAAE,CAAI,CAAA,CAAA,CAAA,CAAA,CAAE,IAAI,CAAE,CAAA,CAAA;AACjC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,QAAQ,CAAE,CAAA,CAAA;AACzC,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC;AACjC,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACX,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,UAAU,CAAE,CAAA,CAAA;AAC1C,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAI,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC,CAAC;AACzB,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACX,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,UAAU,CAAE,CAAA,CAAA;AAC1C,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,EAAE,CAAC;AACnB,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACX,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,IAAI,CAAC;AACtB,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,QAAQ,CAAE,CAAA,CAAA;AACxC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAI,CAAA,CAAA,CAAA,CAAC,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,UAAU,CAAC;AACvC,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,QAAQ,CAAE,CAAA,CAAA;AACxB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAA,CAAA,CAAA,CAAI,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAE,CAAA,CAAA;AACzE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAc,MAAM,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAgC,CAAC,CAAC;AACpE,CAAa,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACb,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,QAAQ,CAAE,CAAA,CAAA;AAC3C,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAc,MAAM,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,QAAQ,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAC;AAC7C,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAc,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAE,CAAA,CAAA;AACpC,CAAgB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AAC9B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAkB,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACxB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAkB,CAAK,CAAA,CAAA,CAAA,CAAA;AACvB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAiB,CAAC;AAClB,CAAe,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACf,CAAa,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACb,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACX,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,IAAI,CAAC;AACtB,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAC;AACT,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AACpD,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAG,CAAA,CAAA,CAAC,GAAG,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAc,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAG,CAAA,CAAA,CAAA,CAAE,YAAY,CAAE,CAAA,CAAA;AACpF,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACvC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAC;AACT,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAG,CAAA,CAAA,CAAC,GAAG,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAc,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAG,CAAA,CAAA,CAAA,CAAE,YAAY,CAAE,CAAA,CAAA;AACrF,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA;AAChC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAC;AACT,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,GAAG,CAAa,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,OAAO,CAAC;AAC5C,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAG,CAAA,CAAA,CAAA;AAC5B,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,EAAE,CAAK,CAAA,CAAA,CAAA,CAAA;AACtB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC;AACR,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAC;AACxD,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAC,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC;AAClD,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AAC3B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,OAAO,CAAC,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAG,EAAE,CAAI,CAAA,CAAA,CAAA,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC3D,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC;AACR,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAe,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,GAAG,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACvC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,OAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAe,CAAC,CAAA,CAAA,CAAA,CAAI,OAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAe,CAAC,CAAA,CAAA,CAAA,CAAI,iBAAiB,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAE,CAAA,CAAA,CAAE,WAAW,CAAE,CAAA;AAC1G,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC;AACR,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAG,CAAA,CAAA,CAAA,CAAE,CAAC;AAC9B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAG,CAAE,CAAA,CAAA;AACpB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAE,CAAA,CAAA;AACzB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAC,CAAC,CAAC;AACvD,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAE,CAAA,CAAA;AAC9B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAuB,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAC,CAAC,CAAC,CAAC;AAC1E,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACf,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,YAAY,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAwB,CAAC,CAAC;AACtD,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAC,MAAM,CAAE,CAAA,CAAA;AAC9B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAC,CAAC,CAAC;AACpD,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,IAAI,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAC,CAAE,CAAA,CAAA;AACnC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAe,CAAC,CAAA,CAAA,CAAG,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAC;AAC3D,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,UAAU,CAAG,CAAA,CAAA,CAAA;AACzB,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,EAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,IAAI,CAAC,CAAA,CAAA,CAAG,CAAC,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACvC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACf,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAI,CAAA,CAAA,CAAA;AACZ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC;AACR,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,UAAU,CAAC;AACxB,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACL,CAAA,CAAA,CAAA,CAAI,CAAK,CAAA,CAAA,CAAA,CAAA;AACT,CAAA,CAAA,CAAG,CAAC;AACJ,CAAA,CAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAkB,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,KAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AAC7C,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAE,CAAA,CAAA;AAC1B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAkB,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA,CAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC,CAAE,CAAA,CAAA;AAC9D,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACf,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC;AAC5B,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAA,CAAA,CAAA,CAAI,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAc,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACzC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAK,CAAA,CAAA,CAAA,CAAA;AACX,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC;AACN,CAAA,CAAA,CAAA,CAAI,IAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,GAAG,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAC,aAAa,CAAE,CAAA,CAAA;AACpE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC;AAC3B,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAkB,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA;AAClC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAe,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC,CAAA;AAC/D,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,IAAI,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA;AACjC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA;AACzB,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAE,CAAA,CAAA;AACR,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACb,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC;AAC9C,CAAA,CAAA,CAAA,CAAI,KAAK,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,IAAI,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,OAAO,CAAE,CAAA,CAAA;AACzC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAC;AAC3C,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAE,CAAA,CAAA;AACjC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,KAAK,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAG,CAAC,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACnC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAI,CAAA,CAAA,CAAA,CAAA;AACd,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAkB,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC;AACnC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC;AACV,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACb,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAE,CAAA,CAAA;AAC9B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AAChD,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAA,CAAA,CAAA,CAAI,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC;AACzB,CAAA,CAAA,CAAG,CAAC,CAAC;AACL,CAAC;AACD,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAc,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAA,CAAE,SAAS,CAAE,CAAA,CAAA;AACxC,CAAA,CAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,GAAG,CAAE,CAAA,CAAA;AACxB,CAAA,CAAA,CAAA,CAAI,GAAG,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,EAAE,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,QAAQ,CAAE,CAAA,CAAA;AACpC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAE,CAAA,CAAA;AACjC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAC;AACnC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AACrD,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACL,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,QAAQ,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAE,CAAA,CAAA;AAC3C,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAE,CAAA,CAAA;AACjC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,SAAS,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAA,CAAA,CAAG,KAAK,CAAC;AACpC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,IAAI,CAAC;AACpB,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAC;AAC5D,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAA,CAAA,CAAG,CAAC,CAAC;AACL,CAAC;AACM,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAkB,GAAG,CAAwB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;;;;;;;;AC1UnD,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAA,CAAE,IAAI,CAAE,CAAA,CAAA;AAClC,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,GAAG,CAAC,CAAC,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC;AAC9C,CAAA,CAAE,OAAO,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACd,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAG,CAAA,CAAA,CAAC,IAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,GAAG,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,IAAI,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,GAAG,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,MAAM,CAAC;AAC7E,CAAA,CAAA,CAAG,CAAC;AACJ,CAAC;AACD,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA;AAC1B,CAAA,CAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC;AAC5D,CAAC;AACM,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAG,CAAA,CAAA,CAAA,CAAE,IAAI,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAG,CAAA,CAAA,CAAA,CAAE,CAAE,CAAA,CAAA;AACpD,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,GAAG,CAAE,CAAA,CAAA;AACzB,CAAA,CAAA,CAAA,CAAI,MAAM,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAC,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC,CAAC,EAAE,CAAG,CAAA,CAAA,CAAC,CAAC,CAAA,CAAA,CAAG,GAAG,CAAC;AAC3D,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,QAAQ,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAC,CAAC;AAC1C,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,SAAS,CAAC,CAAA,CAAA,CAAG,CAAC,CAAG,CAAA,CAAA,CAAC,CAAC,CAAE,CAAA,CAAA;AAC7B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAE,CAAA,CAAA;AAC/B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAG,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAC,GAAG,CAAE,CAAA,CAAA,CAAA,CAAG,CAAG,CAAA,CAAA,CAAC,GAAG,CAAC,CAAA,CAAE,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,EAAE,CAAC;AAChD,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAG,CAAC,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAA,CAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AACzC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,MAAM,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAE,CAAA,CAAA;AACtC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAG,CAAC,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAA,CAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AACzC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACb,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAG,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAG,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAC,CAAC;AACxC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAA,CAAA,CAAA,CAAA,CAAK,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACX,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAG,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAG,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAC,CAAC;AACtC,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAG,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAE,CAAA,CAAA;AAC3D,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAG,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAc,CAAC,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAG,CAAC,CAAC,CAAC;AAC1C,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAG,CAAA,CAAA;AACH,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,GAAG,CAAC;AACb,CAAC;AACD,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAC;AACjC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAc,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA;AAC/B,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA,CAAA,CAAG,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACpD,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,OAAO,CAAC,CAAA,CAAA,CAAG,CAAC,CAAG,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC;AACrC,CAAA,CAAA,CAAG,CAAC,CAAC;AACL,CAAA;;ACnCA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,oBAAoB,CAAG,CAAA,CAAA,CAAA;AAAA,CAAA,CAAA,KAAA,CAAA,CAAA,CAAA;AAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,CAAA,CAAA;AAAA,CAAA,CAAA,CAAA,CAAA;AAAA,CAAA,CAAA,OAAA,CAAA,CAAA,CAAA;AAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,CAAA;AAAA,CAAA,CAAA,CAAA,CAAA;AAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA;AAAA,CAA0B,CAAC;AACxD,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,UAAU,CAAG,CAAA,CAAA,CAAA;AACnB,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,EAAE,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AAClB,CAAA,CAAE,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAoB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAgB,IAAI,CAAG,CAAA,CAAA,CAAA;AACxF,CAAA,CAAE,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAoB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAmB,IAAI,CAAK,CAAA,CAAA,CAAA,CAAA;AACnG,CAAC,CAAC;AACF,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAoB,GAAG,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACxC,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,oBAAoB,CAAC,CAAA,CAAE,UAAU,CAAC;AACnD,CAAC,CAAC;AACK,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAgB,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA;AACxC,CAAc,CAAA,CAAA;AACd,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,oBAAoB,CAAC;AAChC,CAAG,CAAA,CAAA;AAQH,CAAC;AACwB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAACA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAgB,CAAC,CAAE,CAAA;AAY9D,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAE,CAAA,CAAA;AAC7B,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,SAAS,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAmB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AACvD,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,SAAS,CAAE,CAAA,CAAA;AAChC,CAAA,CAAA,CAAA,CAAI,MAAM,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,MAAM,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAC;AAC/B,CAAA,CAAA,CAAA,CAAI,IAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,OAAO,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,QAAQ,CAAE,CAAA,CAAA;AAC5C,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AACzB,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAG,CAAA,CAAA;AACH,CAAA,CAAE,OAAO,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,MAAM,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAC;AAC/B,CAAC;AACc,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,iBAAiB,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,MAAM,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAE,CAAA,CAAA;AAC9D,CAAA,CAAE,GAAG,CAAE,CAAA,CAAC,CAAC,CAAA,CAAE,IAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACpB,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA;AAChB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAuE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AAC7E,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC;AACN,CAAA,CAAA,CAAA,CAAI,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAa,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAgB,EAAE,CAAC;AAC7C,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAa,CAAE,CAAA,CAAA;AAC/B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAa,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAC;AACjC,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AAClB,CAAG,CAAA,CAAA;AACH,CAAC,CAAC,CAAA;;ACtD+B,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,WAAW,CAAE,CAAA,CAAA;AACzD,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,EAAE,CAAyB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACzC,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAiB,CAAkD,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC;AAC3E,CAAC,CAAA,CAAA;;ACID,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAgB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC;AAClC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAkB,GAAG,CAAc,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACzC,CAAEC,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAiB,CAAC,CAAA,CAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC;AACxD,CAAC,CAAC;AACK,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAuB,CAAC,CAAA,CAAA,CAAG,CAAE,CAAA,CAAA;AAC7C,CAAA,CAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAC,CAAC,KAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACjC,CAAA,CAAA,CAAA,CAAI,MAAM,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,aAAa,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC;AAC5C,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAE,CAAA,CAAA;AAC5B,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,UAAU,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAC;AAC5C,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAE,CAAA,CAAA;AAC7B,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,MAAM,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,EAAE,CAAC;AAC1C,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,IAAI,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,QAAQ,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAE,CAAA,CAAA;AAClC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,IAAI,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,IAAI,CAAC;AACpC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,QAAQ,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,kBAAkB,CAAC;AAChE,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,QAAQ,CAAE,CAAA,CAAA;AACtB,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,GAAG,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,UAAU,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAC;AACzD,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,MAAM,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC,CAAA,CAAE,CAAC,CAAC,CAAC,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAC;AAC1D,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAC,CAAE,CAAA,CAAA;AAC3C,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAC;AAC3C,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,GAAG,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,MAAM,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC;AAC1C,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAC;AACzE,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA;AAC1B,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,MAAM,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,EAAE,CAAC;AACvC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,IAAI,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,QAAQ,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAE,CAAA,CAAA;AAClC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,IAAI,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,IAAI,CAAC;AACpC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,QAAQ,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,eAAe,CAAC;AAC1D,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,QAAQ,CAAE,CAAA,CAAA;AACtB,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,GAAG,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,UAAU,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAC;AACzD,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,MAAM,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC,CAAA,CAAE,CAAC,CAAC,CAAC,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAC;AAC1D,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAC,CAAE,CAAA,CAAA;AAC3C,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAC;AAC3C,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,GAAG,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,MAAM,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC;AAC1C,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,OAAO,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,MAAM,CAAE,CAAA,CAAA;AACzC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAG,CAAA,CAAA,CAAC,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AAC7B,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA;AAC3B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAC;AACT,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAA,CAAA,CAAG,CAAC,CAAC;AACL,CAAC;AACM,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAa,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA;AACrC,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,EAAE,CAAC;AACpD,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,OAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAE,CAAA,CAAA;AACxC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,MAAM,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,GAAG,CAAoB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AAC1D,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,IAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAG,CAAA,CAAA,CAAC,CAAC,CAAC,CAAC,EAAE,CAAgB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC,CAAG,CAAA,CAAA,CAAC,OAAO,CAAC;AAC3E,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC;AACN,CAAG,CAAA,CAAA;AACH,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,UAAU,CAAC;AACzC,CAAC;AACM,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAoB,CAAC,CAAA,CAAA,CAAA,CAAI,CAAE,CAAA,CAAA;AAC3C,CAAA,CAAE,OAAO,CAAI,CAAA,CAAA,CAAA,CAAC,CAAE,CAAA,CAAA,CAAE,GAAG,CAAkB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,IAAI,CAAC,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC,CAAC;AAClE,CAAA;;AC1CA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,cAAc,CAAG,CAAA,CAAA,CAAA;AAC1B,CAAA,CAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAgB,EAAE,CAAC;AACpC,CAAA,CAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,EAAE,CAAC;AAC9B,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,YAAY,CAAG,CAAA,CAAA,CAAC,KAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAG,CAAA,CAAA,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AAChD,CAAA,CAAA,CAAA,CAAI,MAAM,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAgB,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACtF,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,qCAAqC,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAC;AACnE,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC;AACP,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAE,CAAA,CAAA;AACjD,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC;AACzD,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,MAAM,CAAE,CAAA,CAAA;AAClB,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAE,KAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAE,CAAA,CAAC,CAAC;AACxC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,IAAI,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,SAAS,CAAE,CAAA,CAAA;AACnC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,SAAS,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAC;AACzC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAA,CAAA,CAAG,CAAC;AACJ,CAAA,CAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC,CAAA;AAC1B,CAAA,CAAA,CAAA,CAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,IAAiB,CAAC,CAAA;AACnC,CAAA,CAAA,CAAA,CAAI,OAAO,CAAE,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,KAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AAC/B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,EAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC,CAAA,CAAE,CAAC,CAAC;AACxD,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,OAAO,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC;AACxC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACL,CAAA,CAAA,CAAA,CAAI,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AAChC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,MAAMC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,QAAQ,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC,KAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACvE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,EAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC,CAAA,CAAE,CAAC,CAAC;AAC1D,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAC;AACT,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACL,CAAA,CAAA,CAAA,CAAI,gBAAgB,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,QAAQ,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACjD,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,MAAMA,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAgB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACxF,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,YAAY,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA,CAAE,KAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAE,CAAA,CAAC,SAAS,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAA,CAAE,CAAC,CAAC;AACtE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAC;AACT,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACL,CAAA,CAAA,CAAA,CAAI,eAAe,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,QAAQ,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AAChD,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,MAAMA,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAe,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACvF,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,YAAY,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA,CAAE,KAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAE,CAAA,CAAC,SAAS,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAA,CAAE,CAAC,CAAC;AACtE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAC;AACT,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAA,CAAA,CAAG,CAAC,CAAC;AACL,CAAA,CAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAGC,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAY,CAAC,CAAA;AAC9B,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,EAAE,CAAI,CAAA,CAAA,CAAA;AACpB,CAAA,CAAA,CAAG,CAAC,CAAC;AACL,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,cAAc,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC,CAAC;AACtD,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAGC,CAAgB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AACpE,CAAA,CAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAG,CAAA,CAAA,CAAC,KAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAC,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA;AACnE,CAAA,CAAA,CAAA,CAAI,CAAC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAsB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,QAAQ,CAAC;AAClD,CAAA,CAAA,CAAG,CAAC;AACJ,CAAA,CAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAGC,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAW,CAAC,CAAA;AAC7B,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,EAAE,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACrB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAIC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACX,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAE,CAAA,CAAA,CAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,MAAM,CAAC,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAE,CAAA;AAC7C,CAAA,CAAA,CAAG,CAAC,CAAC;AACL,CAAA,CAAE,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,MAAM,CAAC;AAC7B,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAuB,CAAC,CAAA,CAAE,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC,CAAC,CAAC;AACrD,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAG,CAAA,CAAA,CAAA;AACX,CAAA,CAAA,CAAA,CAAI,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AAC5B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,GAAG,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAI,CAAA,CAAA,CAAA,CAAA,CAAE,MAAM,CAAE,CAAA,CAAA,CAAE,EAAE,CAAC;AAClE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,IAAI,CAAC,CAAA,CAAA,CAAG,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC;AACnD,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,UAAU,CAAE,CAAA,CAAA;AACtB,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AACjD,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAC,CAAG,CAAA,CAAA,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAc,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAE,CAAA,CAAA,CAAA,CAAG,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAE,CAAA,CAAA,CAAE,KAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAE,CAAA,CAAC,CAAC;AAC3F,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAG,CAAA,CAAA,CAAC,GAAG,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAc,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAG,CAAA,CAAA,CAAA,CAAE,IAAI,CAAE,CAAA,CAAA;AACrE,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,EAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA;AACrB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAC;AACT,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAG,CAAA,CAAA,CAAC,OAAO,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACrC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,OAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAkB,CAAE,CAAA,CAAA;AACrD,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAkB,CAAG,CAAA,CAAA,CAAA,CAAE,CAAC;AACtD,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAkB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AAC7D,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAE,CAAA,CAAA;AACnC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,UAAU,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAC;AACxC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC;AACR,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,OAAO,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AAC/C,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAE,CAAA,CAAC,CAAC;AACnD,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC;AACR,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC;AACN,CAAA,CAAA,CAAG,CAAC;AACJ,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAA,CAAA,CAAA,CAAI,QAAQ,CAAE,CAAA,CAAA;AAC5B,CAAA,CAAA,CAAA,CAAI,IAAI,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAC,CAAC,IAAI,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAgB,CAAC,CAAC,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC;AACnE,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,IAAI,CAAC,CAAC,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA;AAClC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,MAAM,CAAc,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAG,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,IAAI,CAAG,CAAA,CAAA,CAAC,EAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AAC5E,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACd,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAG,CAAA,CAAA;AACX,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC;AACR,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAG,CAAA,CAAA,CAAC,cAAc,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAC;AACzC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACX,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAoB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AAC7C,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,OAAO,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAY,CAAE,CAAA,CAAA,CAAA,CAAG,CAAC;AAC1C,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC;AACR,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA;AAC5B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAkB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,OAAO,CAAE,CAAA,CAAA;AAC9C,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,EAAE,CAAc,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AAC/B,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA;AAC7B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC,CAAC;AACX,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACP,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAC,CAAC,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAE,CAAA,CAAC,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAC;AAC7C,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAG,CAAA,CAAA;AACH,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAC,MAAM,CAAC,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAC;AAQhD,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,GAAG,CAAG,CAAA,CAAA,CAAA;AACd,CAAA,CAAA,CAAA,CAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAA,CAAA,CAAA,CAAI,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACT,CAAA,CAAA,CAAA,CAAI,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACV,CAAA,CAAA,CAAA,CAAI,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACb,CAAA,CAAA,CAAA,CAAI,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACd,CAAA,CAAA,CAAA,CAAI,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AAChB,CAAA,CAAA,CAAG,CAAC;AACJ,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,GAAG,CAAC;AACb,CAAC;AACD,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAe,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAE,CAAA,CAAA;AACpC,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,OAAO,CAAE,CAAA,CAAA;AAChC,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAA;AACR,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AACxB,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAE,CAAA,CAAA;AACpB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAE,CAAA,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAA,CAAE,CAAC,CAAC;AAC1D,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAC;AAClB,CAAK,CAAA,CAAA,CAAA,CAAA;AACL,CAAG,CAAA,CAAA;AACH,CAAC;AACM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAMJ,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAc,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC;AAClC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,WAAW,CAAG,CAAA,CAAA,CAAA;AAC9B,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAOA,UAAQ,CAAC;AAClB,CAAC;AACD,CAAe,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAACA,UAAQ,CAAC,CAAA;;;;;;;;AChJzB,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAgB,CAAG,CAAA,CAAA,CAAA,CAAE,CAAC;AACrB,CAAe,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAI,CAAE,CAAA,CAAA;AACpC,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,GAAG,CAAE,CAAA,CAAA;AACd,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,GAAG,CAAE,CAAA;AACd,CAAC,CAAA,CAAA,CAAG,EAAE,CAAE,CAAA,CAAA;AACR,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAgB,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAE,CAAA,CAAA;AAC9B,CAAA,CAAA,CAAA,CAAI,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAgB,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAC;AAClC,CAAG,CAAA,CAAA;AACH,CAAA,CAAE,IAAI,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAE,CAAA,CAAA;AACxB,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,WAAW,CAAC,CAAA;AACtB,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,EAAE,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,EAAE,CAAI,CAAA,CAAA,CAAA,CAAC,oBAAoB,CAAC,CAAA;AACnD,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,EAAE,CAAG,CAAA,CAAA;AACrB,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC;AACP,CAAG,CAAA,CAAA;AACH,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAC,IAAI,CAAC,CAAC,OAAO,CAAE,CAAA,CAAA;AAC5B,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,WAAW,CAAC,CAAA;AACtB,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,EAAE,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,EAAE,CAAI,CAAA,CAAA,CAAA,CAAC,sBAAsB,CAAC,CAAA;AACrD,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,EAAE,CAAG,CAAA,CAAA;AACrB,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC;AACP,CAAG,CAAA,CAAA;AACH,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC;AAC9C,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,EAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC;AAC/C,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAgB,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAG,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAC,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AAClD,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAA;AACN,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,GAAG,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,gBAAgB,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAC;AAC7C,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,GAAG,CAAC;AACf,CAAA,CAAA,CAAG,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACZ,CAAA,CAAA,CAAA,CAAI,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAgB,CAAC,CAAA,CAAA,CAAA,CAAI,CAAC,CAAC;AAClC,CAAG,CAAA,CAAA;AACH,CAAA;;ACrBA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC;AAC/B,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAc,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC,CAAC;AAK1D,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,UAAU,CAAG,CAAA,CAAA,CAAA;AACtB,CAAA,CAAE,IAAI,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAY,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,OAAO,CAAC,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAoB,IAAI,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,GAAG,CAAE,CAAA,CAAA;AAC7F,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAC;AACb,CAAG,CAAA,CAAA;AACH,CAAA,CAAE,MAAM,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,EAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAC,CAAC,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAC;AAC9D,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,SAAS,CAAE,CAAA,CAAA;AACjB,CAAA,CAAA,CAAA,CAAI,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAA,CAAA,CAAG,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAc,CAAC,CAAA,CAAE,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AACxD,CAAG,CAAA,CAAA;AACH,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAS,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAI,CAAA,CAAA,CAAA,CAAC,MAAM,CAAE,CAAA,CAAA,CAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AAC5C,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAS,CAAE,CAAA,CAAA,CAAE,SAAS,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAE,CAAA,CAAC,CAAC;AAC5C,CAAA,CAAE,OAAO,CAAI,CAAA,CAAA,CAAA,CAAC,SAAS,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAC;AACrC,CAAC;AACD,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAa,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC;AAClB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAa,EAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACpD,CAAA,CAAE,MAAM,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,MAAM,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,EAAE,CAAC;AACpC,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAC,CAAA;AAC1B,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,EAAE,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACnB,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,EAAE,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,KAAK,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,EAAE,CAAG,CAAA,CAAA,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAA,CAAE,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,IAAI,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAE,CAAA;AAClH,CAAA,CAAA,CAAG,CAAC,CAAC;AACL,CAAC,CAAE,CAAA;AACH,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAA;AACnB,CAAA,CAAE,CAAe,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACjB,CAAA,CAAE,CAAkB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACtC,CAAA,CAAA,CAAA,CAAI,MAAM,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAG,CAAA,CAAA,CAAA;AACpC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAC,CAAC,CAAG,CAAA,CAAA,CAAC,OAAO,CAAC,CAAA,CAAA,CAAA,CAAI,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACxD,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,IAAI,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,IAAI,CAAC;AAC7C,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAQ,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAE,EAAE,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAI,CAAA,CAAA,CAAA,CAAA,CAAE,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC,CAAC;AACjE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC;AACR,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC;AACN,CAAA,CAAA,CAAA,CAAI,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACX,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,KAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAC,CAAW,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,MAAM,CAAC,CAAA;AACvC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAc,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACpB,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC;AACN,CAAA,CAAA,CAAG,CAAC;AACJ,CAAC,CAAC;AACF,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAG,CAAA,CAAA,CAAA;AACnB,CAAA,CAAE,CAAqB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACvB,CAAA,CAAE,CAAkB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACtC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,IAAI,CAAG,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAc,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AAC/C,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,OAAO,CAAG,CAAA,CAAA,CAAA;AACpB,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAGK,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAAA,CAAQ,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAA;AACxB,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,MAAM,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,KAAK,CAAC,CAAC,CAAI,CAAA,CAAA,CAAA,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA,CAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAE,CAAA,CAAC,CAAC;AACxE,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC;AACN,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAM,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAC,CAAA,CAAA,CAAA,CAAI,EAAE,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,CAAE,CAAA,CAAC,CAAC;AAC5C,CAAA,CAAA,CAAG,CAAC;AACJ,CAAC,CAAC;AACF,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAuB,EAAE,CAAC;AAC1B,CAAe,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAC,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAE,CAAA,CAAA;AAClC,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,QAAQ,CAAC,CAAA,CAAA,CAAA,CAAA,CAAK,CAAC,CAAQ,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAO,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAC,CAAC;AACzC,CAAC;AACD,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,EAAE,CAAE,CAAA,CAAC,SAAS,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAO,GAAG,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA;AACzC,CAAE,CAAA,CAAA,CAAA,CAAA,CAAI,GAAG,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAG,CAAC,CAAK,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAK,UAAU,CAAE,CAAA,CAAA;AACvC,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAU,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAE,CAAC;AACvB,CAAI,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAU,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAW,CAAC,CAAA,CAAE,KAAK,CAAE,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAM,CAAE,CAAA,CAAC,CAAC;AAC/C,CAAG,CAAA,CAAA;AACH,CAAC,CAAC,CAAA;;ACjFF,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA;AACA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,uDAAA,CAAA;AACA,CAAA,CAAA,CAAA;;;;;"} \ No newline at end of file diff --git a/examples/internal/aws-nitro/.nitro/nitro.json b/examples/internal/aws-nitro/.nitro/nitro.json deleted file mode 100644 index 6c53b17b3d..0000000000 --- a/examples/internal/aws-nitro/.nitro/nitro.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "date": "2024-10-11T19:42:58.132Z", - "preset": "nitro-dev", - "framework": { - "name": "nitro", - "version": "" - }, - "versions": { - "nitro": "2.9.6" - }, - "dev": { - "pid": 3442677, - "workerAddress": { - "socketPath": "/tmp/nitro/worker-3442677-1.sock" - } - } -} \ No newline at end of file diff --git a/examples/internal/aws-nitro/.nitro/types/nitro-config.d.ts b/examples/internal/aws-nitro/.nitro/types/nitro-config.d.ts deleted file mode 100644 index 7eec20a34e..0000000000 --- a/examples/internal/aws-nitro/.nitro/types/nitro-config.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Generated by nitro - -// App Config -import type { Defu } from 'defu' - - - -type UserAppConfig = Defu<{}, []> - -declare module "nitropack/types" { - interface AppConfig extends UserAppConfig {} - interface NitroRuntimeConfig { - streaming: boolean, - } -} -export {} \ No newline at end of file diff --git a/examples/internal/aws-nitro/.nitro/types/nitro-imports.d.ts b/examples/internal/aws-nitro/.nitro/types/nitro-imports.d.ts deleted file mode 100644 index a7aca0b32e..0000000000 --- a/examples/internal/aws-nitro/.nitro/types/nitro-imports.d.ts +++ /dev/null @@ -1,126 +0,0 @@ -declare global { - const appendCorsHeaders: typeof import('../../node_modules/h3')['appendCorsHeaders'] - const appendCorsPreflightHeaders: typeof import('../../node_modules/h3')['appendCorsPreflightHeaders'] - const appendHeader: typeof import('../../node_modules/h3')['appendHeader'] - const appendHeaders: typeof import('../../node_modules/h3')['appendHeaders'] - const appendResponseHeader: typeof import('../../node_modules/h3')['appendResponseHeader'] - const appendResponseHeaders: typeof import('../../node_modules/h3')['appendResponseHeaders'] - const assertMethod: typeof import('../../node_modules/h3')['assertMethod'] - const cachedEventHandler: typeof import('../../../../../../nitro/dist/runtime/index')['cachedEventHandler'] - const cachedFunction: typeof import('../../../../../../nitro/dist/runtime/index')['cachedFunction'] - const callNodeListener: typeof import('../../node_modules/h3')['callNodeListener'] - const clearResponseHeaders: typeof import('../../node_modules/h3')['clearResponseHeaders'] - const clearSession: typeof import('../../node_modules/h3')['clearSession'] - const createApp: typeof import('../../node_modules/h3')['createApp'] - const createAppEventHandler: typeof import('../../node_modules/h3')['createAppEventHandler'] - const createError: typeof import('../../node_modules/h3')['createError'] - const createEvent: typeof import('../../node_modules/h3')['createEvent'] - const createEventStream: typeof import('../../node_modules/h3')['createEventStream'] - const createRouter: typeof import('../../node_modules/h3')['createRouter'] - const defaultContentType: typeof import('../../node_modules/h3')['defaultContentType'] - const defineCachedEventHandler: typeof import('../../../../../../nitro/dist/runtime/index')['defineCachedEventHandler'] - const defineCachedFunction: typeof import('../../../../../../nitro/dist/runtime/index')['defineCachedFunction'] - const defineEventHandler: typeof import('../../node_modules/h3')['defineEventHandler'] - const defineLazyEventHandler: typeof import('../../node_modules/h3')['defineLazyEventHandler'] - const defineNitroErrorHandler: typeof import('../../../../../../nitro/dist/runtime/index')['defineNitroErrorHandler'] - const defineNitroPlugin: typeof import('../../../../../../nitro/dist/runtime/index')['defineNitroPlugin'] - const defineNodeListener: typeof import('../../node_modules/h3')['defineNodeListener'] - const defineNodeMiddleware: typeof import('../../node_modules/h3')['defineNodeMiddleware'] - const defineRenderHandler: typeof import('../../../../../../nitro/dist/runtime/index')['defineRenderHandler'] - const defineRequestMiddleware: typeof import('../../node_modules/h3')['defineRequestMiddleware'] - const defineResponseMiddleware: typeof import('../../node_modules/h3')['defineResponseMiddleware'] - const defineRouteMeta: typeof import('../../../../../../nitro/dist/runtime/index')['defineRouteMeta'] - const defineTask: typeof import('../../../../../../nitro/dist/runtime/index')['defineTask'] - const defineWebSocket: typeof import('../../node_modules/h3')['defineWebSocket'] - const defineWebSocketHandler: typeof import('../../node_modules/h3')['defineWebSocketHandler'] - const deleteCookie: typeof import('../../node_modules/h3')['deleteCookie'] - const dynamicEventHandler: typeof import('../../node_modules/h3')['dynamicEventHandler'] - const eventHandler: typeof import('../../node_modules/h3')['eventHandler'] - const fetchWithEvent: typeof import('../../node_modules/h3')['fetchWithEvent'] - const fromNodeMiddleware: typeof import('../../node_modules/h3')['fromNodeMiddleware'] - const fromPlainHandler: typeof import('../../node_modules/h3')['fromPlainHandler'] - const fromWebHandler: typeof import('../../node_modules/h3')['fromWebHandler'] - const getCookie: typeof import('../../node_modules/h3')['getCookie'] - const getHeader: typeof import('../../node_modules/h3')['getHeader'] - const getHeaders: typeof import('../../node_modules/h3')['getHeaders'] - const getMethod: typeof import('../../node_modules/h3')['getMethod'] - const getProxyRequestHeaders: typeof import('../../node_modules/h3')['getProxyRequestHeaders'] - const getQuery: typeof import('../../node_modules/h3')['getQuery'] - const getRequestFingerprint: typeof import('../../node_modules/h3')['getRequestFingerprint'] - const getRequestHeader: typeof import('../../node_modules/h3')['getRequestHeader'] - const getRequestHeaders: typeof import('../../node_modules/h3')['getRequestHeaders'] - const getRequestHost: typeof import('../../node_modules/h3')['getRequestHost'] - const getRequestIP: typeof import('../../node_modules/h3')['getRequestIP'] - const getRequestPath: typeof import('../../node_modules/h3')['getRequestPath'] - const getRequestProtocol: typeof import('../../node_modules/h3')['getRequestProtocol'] - const getRequestURL: typeof import('../../node_modules/h3')['getRequestURL'] - const getRequestWebStream: typeof import('../../node_modules/h3')['getRequestWebStream'] - const getResponseHeader: typeof import('../../node_modules/h3')['getResponseHeader'] - const getResponseHeaders: typeof import('../../node_modules/h3')['getResponseHeaders'] - const getResponseStatus: typeof import('../../node_modules/h3')['getResponseStatus'] - const getResponseStatusText: typeof import('../../node_modules/h3')['getResponseStatusText'] - const getRouteRules: typeof import('../../../../../../nitro/dist/runtime/index')['getRouteRules'] - const getRouterParam: typeof import('../../node_modules/h3')['getRouterParam'] - const getRouterParams: typeof import('../../node_modules/h3')['getRouterParams'] - const getSession: typeof import('../../node_modules/h3')['getSession'] - const getValidatedQuery: typeof import('../../node_modules/h3')['getValidatedQuery'] - const getValidatedRouterParams: typeof import('../../node_modules/h3')['getValidatedRouterParams'] - const handleCacheHeaders: typeof import('../../node_modules/h3')['handleCacheHeaders'] - const handleCors: typeof import('../../node_modules/h3')['handleCors'] - const isCorsOriginAllowed: typeof import('../../node_modules/h3')['isCorsOriginAllowed'] - const isError: typeof import('../../node_modules/h3')['isError'] - const isEvent: typeof import('../../node_modules/h3')['isEvent'] - const isEventHandler: typeof import('../../node_modules/h3')['isEventHandler'] - const isMethod: typeof import('../../node_modules/h3')['isMethod'] - const isPreflightRequest: typeof import('../../node_modules/h3')['isPreflightRequest'] - const isStream: typeof import('../../node_modules/h3')['isStream'] - const isWebResponse: typeof import('../../node_modules/h3')['isWebResponse'] - const lazyEventHandler: typeof import('../../node_modules/h3')['lazyEventHandler'] - const nitroPlugin: typeof import('../../../../../../nitro/dist/runtime/index')['nitroPlugin'] - const parseCookies: typeof import('../../node_modules/h3')['parseCookies'] - const promisifyNodeListener: typeof import('../../node_modules/h3')['promisifyNodeListener'] - const proxyRequest: typeof import('../../node_modules/h3')['proxyRequest'] - const readBody: typeof import('../../node_modules/h3')['readBody'] - const readFormData: typeof import('../../node_modules/h3')['readFormData'] - const readMultipartFormData: typeof import('../../node_modules/h3')['readMultipartFormData'] - const readRawBody: typeof import('../../node_modules/h3')['readRawBody'] - const readValidatedBody: typeof import('../../node_modules/h3')['readValidatedBody'] - const removeResponseHeader: typeof import('../../node_modules/h3')['removeResponseHeader'] - const runTask: typeof import('../../../../../../nitro/dist/runtime/index')['runTask'] - const sanitizeStatusCode: typeof import('../../node_modules/h3')['sanitizeStatusCode'] - const sanitizeStatusMessage: typeof import('../../node_modules/h3')['sanitizeStatusMessage'] - const sealSession: typeof import('../../node_modules/h3')['sealSession'] - const send: typeof import('../../node_modules/h3')['send'] - const sendError: typeof import('../../node_modules/h3')['sendError'] - const sendIterable: typeof import('../../node_modules/h3')['sendIterable'] - const sendNoContent: typeof import('../../node_modules/h3')['sendNoContent'] - const sendProxy: typeof import('../../node_modules/h3')['sendProxy'] - const sendRedirect: typeof import('../../node_modules/h3')['sendRedirect'] - const sendStream: typeof import('../../node_modules/h3')['sendStream'] - const sendWebResponse: typeof import('../../node_modules/h3')['sendWebResponse'] - const serveStatic: typeof import('../../node_modules/h3')['serveStatic'] - const setCookie: typeof import('../../node_modules/h3')['setCookie'] - const setHeader: typeof import('../../node_modules/h3')['setHeader'] - const setHeaders: typeof import('../../node_modules/h3')['setHeaders'] - const setResponseHeader: typeof import('../../node_modules/h3')['setResponseHeader'] - const setResponseHeaders: typeof import('../../node_modules/h3')['setResponseHeaders'] - const setResponseStatus: typeof import('../../node_modules/h3')['setResponseStatus'] - const splitCookiesString: typeof import('../../node_modules/h3')['splitCookiesString'] - const toEventHandler: typeof import('../../node_modules/h3')['toEventHandler'] - const toNodeListener: typeof import('../../node_modules/h3')['toNodeListener'] - const toPlainHandler: typeof import('../../node_modules/h3')['toPlainHandler'] - const toWebHandler: typeof import('../../node_modules/h3')['toWebHandler'] - const toWebRequest: typeof import('../../node_modules/h3')['toWebRequest'] - const unsealSession: typeof import('../../node_modules/h3')['unsealSession'] - const updateSession: typeof import('../../node_modules/h3')['updateSession'] - const useAppConfig: typeof import('../../../../../../nitro/dist/runtime/index')['useAppConfig'] - const useBase: typeof import('../../node_modules/h3')['useBase'] - const useEvent: typeof import('../../../../../../nitro/dist/runtime/index')['useEvent'] - const useNitroApp: typeof import('../../../../../../nitro/dist/runtime/index')['useNitroApp'] - const useRuntimeConfig: typeof import('../../../../../../nitro/dist/runtime/index')['useRuntimeConfig'] - const useSession: typeof import('../../node_modules/h3')['useSession'] - const useStorage: typeof import('../../../../../../nitro/dist/runtime/index')['useStorage'] - const writeEarlyHints: typeof import('../../node_modules/h3')['writeEarlyHints'] -} -export { defineCachedFunction, defineCachedEventHandler, cachedFunction, cachedEventHandler, useRuntimeConfig, useStorage, useNitroApp, defineNitroPlugin, nitroPlugin, defineRenderHandler, defineRouteMeta, getRouteRules, useAppConfig, useEvent, defineTask, runTask, defineNitroErrorHandler } from 'nitropack/runtime'; -export { appendCorsHeaders, appendCorsPreflightHeaders, appendHeader, appendHeaders, appendResponseHeader, appendResponseHeaders, assertMethod, callNodeListener, clearResponseHeaders, clearSession, createApp, createAppEventHandler, createError, createEvent, createEventStream, createRouter, defaultContentType, defineEventHandler, defineLazyEventHandler, defineNodeListener, defineNodeMiddleware, defineRequestMiddleware, defineResponseMiddleware, defineWebSocket, defineWebSocketHandler, deleteCookie, dynamicEventHandler, eventHandler, fetchWithEvent, fromNodeMiddleware, fromPlainHandler, fromWebHandler, getCookie, getHeader, getHeaders, getMethod, getProxyRequestHeaders, getQuery, getRequestFingerprint, getRequestHeader, getRequestHeaders, getRequestHost, getRequestIP, getRequestPath, getRequestProtocol, getRequestURL, getRequestWebStream, getResponseHeader, getResponseHeaders, getResponseStatus, getResponseStatusText, getRouterParam, getRouterParams, getSession, getValidatedQuery, getValidatedRouterParams, handleCacheHeaders, handleCors, isCorsOriginAllowed, isError, isEvent, isEventHandler, isMethod, isPreflightRequest, isStream, isWebResponse, lazyEventHandler, parseCookies, promisifyNodeListener, proxyRequest, readBody, readFormData, readMultipartFormData, readRawBody, readValidatedBody, removeResponseHeader, sanitizeStatusCode, sanitizeStatusMessage, sealSession, send, sendError, sendIterable, sendNoContent, sendProxy, sendRedirect, sendStream, sendWebResponse, serveStatic, setCookie, setHeader, setHeaders, setResponseHeader, setResponseHeaders, setResponseStatus, splitCookiesString, toEventHandler, toNodeListener, toPlainHandler, toWebHandler, toWebRequest, unsealSession, updateSession, useBase, useSession, writeEarlyHints } from 'h3'; \ No newline at end of file diff --git a/examples/internal/aws-nitro/.nitro/types/nitro-routes.d.ts b/examples/internal/aws-nitro/.nitro/types/nitro-routes.d.ts deleted file mode 100644 index ba51c90878..0000000000 --- a/examples/internal/aws-nitro/.nitro/types/nitro-routes.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Generated by nitro -import type { Serialize, Simplify } from "nitropack/types"; -declare module "nitropack/types" { - type Awaited = T extends PromiseLike ? Awaited : T - interface InternalApi { - '/': { - 'default': Simplify>>> - } - } -} -export {} \ No newline at end of file diff --git a/examples/internal/aws-nitro/.nitro/types/nitro.d.ts b/examples/internal/aws-nitro/.nitro/types/nitro.d.ts deleted file mode 100644 index bf09bd4d33..0000000000 --- a/examples/internal/aws-nitro/.nitro/types/nitro.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -/// -/// -/// \ No newline at end of file diff --git a/examples/internal/aws-nitro/.nitro/types/tsconfig.json b/examples/internal/aws-nitro/.nitro/types/tsconfig.json deleted file mode 100644 index f28d2e8dca..0000000000 --- a/examples/internal/aws-nitro/.nitro/types/tsconfig.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "compilerOptions": { - "forceConsistentCasingInFileNames": true, - "strict": false, - "noEmit": true, - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "allowJs": true, - "resolveJsonModule": true, - "jsx": "preserve", - "allowSyntheticDefaultImports": true, - "jsxFactory": "h", - "jsxFragmentFactory": "Fragment", - "paths": { - "#imports": [ - "./nitro-imports" - ], - "~/*": [ - "../../server/*" - ], - "@/*": [ - "../../server/*" - ], - "~~/*": [ - "../../*" - ], - "@@/*": [ - "../../*" - ] - } - }, - "include": [ - "./nitro.d.ts", - "../../**/*", - "../../server/**/*" - ] -} \ No newline at end of file diff --git a/examples/internal/aws-nitro/.output/nitro.json b/examples/internal/aws-nitro/.output/nitro.json deleted file mode 100644 index c9ca0691b6..0000000000 --- a/examples/internal/aws-nitro/.output/nitro.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "date": "2024-10-11T19:58:57.867Z", - "preset": "aws-lambda", - "framework": { - "name": "nitro", - "version": "" - }, - "versions": { - "nitro": "2.9.6" - }, - "commands": {} -} \ No newline at end of file diff --git a/examples/internal/aws-nitro/.output/server/chunks/routes/index.mjs b/examples/internal/aws-nitro/.output/server/chunks/routes/index.mjs deleted file mode 100644 index 5e26ee4684..0000000000 --- a/examples/internal/aws-nitro/.output/server/chunks/routes/index.mjs +++ /dev/null @@ -1,116 +0,0 @@ -var __defProp$2 = Object.defineProperty; -var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; -var __publicField$2 = (obj, key, value) => { - __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value); - return value; -}; -class H3Error extends Error { - constructor(message, opts = {}) { - super(message, opts); - __publicField$2(this, "statusCode", 500); - __publicField$2(this, "fatal", false); - __publicField$2(this, "unhandled", false); - __publicField$2(this, "statusMessage"); - __publicField$2(this, "data"); - __publicField$2(this, "cause"); - if (opts.cause && !this.cause) { - this.cause = opts.cause; - } - } - toJSON() { - const obj = { - message: this.message, - statusCode: sanitizeStatusCode(this.statusCode, 500) - }; - if (this.statusMessage) { - obj.statusMessage = sanitizeStatusMessage(this.statusMessage); - } - if (this.data !== void 0) { - obj.data = this.data; - } - return obj; - } -} -__publicField$2(H3Error, "__h3_error__", true); - -const DISALLOWED_STATUS_CHARS = /[^\u0009\u0020-\u007E]/g; -function sanitizeStatusMessage(statusMessage = "") { - return statusMessage.replace(DISALLOWED_STATUS_CHARS, ""); -} -function sanitizeStatusCode(statusCode, defaultStatusCode = 200) { - if (!statusCode) { - return defaultStatusCode; - } - if (typeof statusCode === "string") { - statusCode = Number.parseInt(statusCode, 10); - } - if (statusCode < 100 || statusCode > 999) { - return defaultStatusCode; - } - return statusCode; -} - -typeof setImmediate === "undefined" ? (fn) => fn() : setImmediate; - -function defineEventHandler(handler) { - if (typeof handler === "function") { - handler.__is_handler__ = true; - return handler; - } - const _hooks = { - onRequest: _normalizeArray(handler.onRequest), - onBeforeResponse: _normalizeArray(handler.onBeforeResponse) - }; - const _handler = (event) => { - return _callHandler(event, handler.handler, _hooks); - }; - _handler.__is_handler__ = true; - _handler.__resolve__ = handler.handler.__resolve__; - _handler.__websocket__ = handler.websocket; - return _handler; -} -function _normalizeArray(input) { - return input ? Array.isArray(input) ? input : [input] : void 0; -} -async function _callHandler(event, handler, hooks) { - if (hooks.onRequest) { - for (const hook of hooks.onRequest) { - await hook(event); - if (event.handled) { - return; - } - } - } - const body = await handler(event); - const response = { body }; - if (hooks.onBeforeResponse) { - for (const hook of hooks.onBeforeResponse) { - await hook(event, response); - } - } - return response.body; -} -const eventHandler = defineEventHandler; - -const index = eventHandler((event) => { - const stream = new ReadableStream({ - async start(controller) { - for (let i = 1; i <= 10; i++) { - const chunk = `Number: ${i} -`; - controller.enqueue(chunk); - await new Promise((resolve) => setTimeout(resolve, 1e3)); - } - controller.close(); - } - }); - return new Response(stream, { - headers: { - "Content-Type": "text/plain", - "Transfer-Encoding": "chunked" - } - }); -}); - -export { index as default }; -//# sourceMappingURL=index.mjs.map diff --git a/examples/internal/aws-nitro/.output/server/chunks/routes/index.mjs.map b/examples/internal/aws-nitro/.output/server/chunks/routes/index.mjs.map deleted file mode 100644 index 554b7a9a2c..0000000000 --- a/examples/internal/aws-nitro/.output/server/chunks/routes/index.mjs.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.mjs","sources":["../../../../node_modules/h3/dist/index.mjs","../../../../server/routes/index.ts"],"sourcesContent":null,"names":[],"mappings":"","x_google_ignoreList":[0]} \ No newline at end of file diff --git a/examples/internal/aws-nitro/.output/server/index.mjs b/examples/internal/aws-nitro/.output/server/index.mjs deleted file mode 100644 index 51c146bd23..0000000000 --- a/examples/internal/aws-nitro/.output/server/index.mjs +++ /dev/null @@ -1,5529 +0,0 @@ -import process from 'node:process';globalThis._importMeta_={url:import.meta.url,env:process.env};import http from 'node:http'; -import https from 'node:https'; - -const suspectProtoRx$1 = /"(?:_|\\u0{2}5[Ff]){2}(?:p|\\u0{2}70)(?:r|\\u0{2}72)(?:o|\\u0{2}6[Ff])(?:t|\\u0{2}74)(?:o|\\u0{2}6[Ff])(?:_|\\u0{2}5[Ff]){2}"\s*:/; -const suspectConstructorRx$1 = /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/; -const JsonSigRx$1 = /^\s*["[{]|^\s*-?\d{1,16}(\.\d{1,17})?([Ee][+-]?\d+)?\s*$/; -function jsonParseTransform$1(key, value) { - if (key === "__proto__" || key === "constructor" && value && typeof value === "object" && "prototype" in value) { - warnKeyDropped$1(key); - return; - } - return value; -} -function warnKeyDropped$1(key) { - console.warn(`[destr] Dropping "${key}" key to prevent prototype pollution.`); -} -function destr$1(value, options = {}) { - if (typeof value !== "string") { - return value; - } - const _value = value.trim(); - if ( - // eslint-disable-next-line unicorn/prefer-at - value[0] === '"' && value.endsWith('"') && !value.includes("\\") - ) { - return _value.slice(1, -1); - } - if (_value.length <= 9) { - const _lval = _value.toLowerCase(); - if (_lval === "true") { - return true; - } - if (_lval === "false") { - return false; - } - if (_lval === "undefined") { - return void 0; - } - if (_lval === "null") { - return null; - } - if (_lval === "nan") { - return Number.NaN; - } - if (_lval === "infinity") { - return Number.POSITIVE_INFINITY; - } - if (_lval === "-infinity") { - return Number.NEGATIVE_INFINITY; - } - } - if (!JsonSigRx$1.test(value)) { - if (options.strict) { - throw new SyntaxError("[destr] Invalid JSON"); - } - return value; - } - try { - if (suspectProtoRx$1.test(value) || suspectConstructorRx$1.test(value)) { - if (options.strict) { - throw new Error("[destr] Possible prototype pollution"); - } - return JSON.parse(value, jsonParseTransform$1); - } - return JSON.parse(value); - } catch (error) { - if (options.strict) { - throw error; - } - return value; - } -} - -const HASH_RE = /#/g; -const AMPERSAND_RE = /&/g; -const SLASH_RE = /\//g; -const EQUAL_RE = /=/g; -const PLUS_RE = /\+/g; -const ENC_CARET_RE = /%5e/gi; -const ENC_BACKTICK_RE = /%60/gi; -const ENC_PIPE_RE = /%7c/gi; -const ENC_SPACE_RE = /%20/gi; -function encode(text) { - return encodeURI("" + text).replace(ENC_PIPE_RE, "|"); -} -function encodeQueryValue(input) { - return encode(typeof input === "string" ? input : JSON.stringify(input)).replace(PLUS_RE, "%2B").replace(ENC_SPACE_RE, "+").replace(HASH_RE, "%23").replace(AMPERSAND_RE, "%26").replace(ENC_BACKTICK_RE, "`").replace(ENC_CARET_RE, "^").replace(SLASH_RE, "%2F"); -} -function encodeQueryKey(text) { - return encodeQueryValue(text).replace(EQUAL_RE, "%3D"); -} -function decode(text = "") { - try { - return decodeURIComponent("" + text); - } catch { - return "" + text; - } -} -function decodeQueryKey(text) { - return decode(text.replace(PLUS_RE, " ")); -} -function decodeQueryValue(text) { - return decode(text.replace(PLUS_RE, " ")); -} - -function parseQuery(parametersString = "") { - const object = {}; - if (parametersString[0] === "?") { - parametersString = parametersString.slice(1); - } - for (const parameter of parametersString.split("&")) { - const s = parameter.match(/([^=]+)=?(.*)/) || []; - if (s.length < 2) { - continue; - } - const key = decodeQueryKey(s[1]); - if (key === "__proto__" || key === "constructor") { - continue; - } - const value = decodeQueryValue(s[2] || ""); - if (object[key] === void 0) { - object[key] = value; - } else if (Array.isArray(object[key])) { - object[key].push(value); - } else { - object[key] = [object[key], value]; - } - } - return object; -} -function encodeQueryItem(key, value) { - if (typeof value === "number" || typeof value === "boolean") { - value = String(value); - } - if (!value) { - return encodeQueryKey(key); - } - if (Array.isArray(value)) { - return value.map((_value) => `${encodeQueryKey(key)}=${encodeQueryValue(_value)}`).join("&"); - } - return `${encodeQueryKey(key)}=${encodeQueryValue(value)}`; -} -function stringifyQuery(query) { - return Object.keys(query).filter((k) => query[k] !== void 0).map((k) => encodeQueryItem(k, query[k])).filter(Boolean).join("&"); -} - -const PROTOCOL_STRICT_REGEX = /^[\s\w\0+.-]{2,}:([/\\]{1,2})/; -const PROTOCOL_REGEX = /^[\s\w\0+.-]{2,}:([/\\]{2})?/; -const PROTOCOL_RELATIVE_REGEX = /^([/\\]\s*){2,}[^/\\]/; -const JOIN_LEADING_SLASH_RE = /^\.?\//; -function hasProtocol(inputString, opts = {}) { - if (typeof opts === "boolean") { - opts = { acceptRelative: opts }; - } - if (opts.strict) { - return PROTOCOL_STRICT_REGEX.test(inputString); - } - return PROTOCOL_REGEX.test(inputString) || (opts.acceptRelative ? PROTOCOL_RELATIVE_REGEX.test(inputString) : false); -} -function hasTrailingSlash(input = "", respectQueryAndFragment) { - { - return input.endsWith("/"); - } -} -function withoutTrailingSlash(input = "", respectQueryAndFragment) { - { - return (hasTrailingSlash(input) ? input.slice(0, -1) : input) || "/"; - } -} -function withTrailingSlash(input = "", respectQueryAndFragment) { - { - return input.endsWith("/") ? input : input + "/"; - } -} -function hasLeadingSlash(input = "") { - return input.startsWith("/"); -} -function withLeadingSlash(input = "") { - return hasLeadingSlash(input) ? input : "/" + input; -} -function withBase(input, base) { - if (isEmptyURL(base) || hasProtocol(input)) { - return input; - } - const _base = withoutTrailingSlash(base); - if (input.startsWith(_base)) { - return input; - } - return joinURL(_base, input); -} -function withoutBase(input, base) { - if (isEmptyURL(base)) { - return input; - } - const _base = withoutTrailingSlash(base); - if (!input.startsWith(_base)) { - return input; - } - const trimmed = input.slice(_base.length); - return trimmed[0] === "/" ? trimmed : "/" + trimmed; -} -function withQuery(input, query) { - const parsed = parseURL(input); - const mergedQuery = { ...parseQuery(parsed.search), ...query }; - parsed.search = stringifyQuery(mergedQuery); - return stringifyParsedURL(parsed); -} -function getQuery(input) { - return parseQuery(parseURL(input).search); -} -function isEmptyURL(url) { - return !url || url === "/"; -} -function isNonEmptyURL(url) { - return url && url !== "/"; -} -function joinURL(base, ...input) { - let url = base || ""; - for (const segment of input.filter((url2) => isNonEmptyURL(url2))) { - if (url) { - const _segment = segment.replace(JOIN_LEADING_SLASH_RE, ""); - url = withTrailingSlash(url) + _segment; - } else { - url = segment; - } - } - return url; -} - -const protocolRelative = Symbol.for("ufo:protocolRelative"); -function parseURL(input = "", defaultProto) { - const _specialProtoMatch = input.match( - /^[\s\0]*(blob:|data:|javascript:|vbscript:)(.*)/i - ); - if (_specialProtoMatch) { - const [, _proto, _pathname = ""] = _specialProtoMatch; - return { - protocol: _proto.toLowerCase(), - pathname: _pathname, - href: _proto + _pathname, - auth: "", - host: "", - search: "", - hash: "" - }; - } - if (!hasProtocol(input, { acceptRelative: true })) { - return parsePath(input); - } - const [, protocol = "", auth, hostAndPath = ""] = input.replace(/\\/g, "/").match(/^[\s\0]*([\w+.-]{2,}:)?\/\/([^/@]+@)?(.*)/) || []; - let [, host = "", path = ""] = hostAndPath.match(/([^#/?]*)(.*)?/) || []; - if (protocol === "file:") { - path = path.replace(/\/(?=[A-Za-z]:)/, ""); - } - const { pathname, search, hash } = parsePath(path); - return { - protocol: protocol.toLowerCase(), - auth: auth ? auth.slice(0, Math.max(0, auth.length - 1)) : "", - host, - pathname, - search, - hash, - [protocolRelative]: !protocol - }; -} -function parsePath(input = "") { - const [pathname = "", search = "", hash = ""] = (input.match(/([^#?]*)(\?[^#]*)?(#.*)?/) || []).splice(1); - return { - pathname, - search, - hash - }; -} -function stringifyParsedURL(parsed) { - const pathname = parsed.pathname || ""; - const search = parsed.search ? (parsed.search.startsWith("?") ? "" : "?") + parsed.search : ""; - const hash = parsed.hash || ""; - const auth = parsed.auth ? parsed.auth + "@" : ""; - const host = parsed.host || ""; - const proto = parsed.protocol || parsed[protocolRelative] ? (parsed.protocol || "") + "//" : ""; - return proto + auth + host + pathname + search + hash; -} - -const defaults = Object.freeze({ - ignoreUnknown: false, - respectType: false, - respectFunctionNames: false, - respectFunctionProperties: false, - unorderedObjects: true, - unorderedArrays: false, - unorderedSets: false, - excludeKeys: void 0, - excludeValues: void 0, - replacer: void 0 -}); -function objectHash(object, options) { - if (options) { - options = { ...defaults, ...options }; - } else { - options = defaults; - } - const hasher = createHasher(options); - hasher.dispatch(object); - return hasher.toString(); -} -const defaultPrototypesKeys = Object.freeze([ - "prototype", - "__proto__", - "constructor" -]); -function createHasher(options) { - let buff = ""; - let context = /* @__PURE__ */ new Map(); - const write = (str) => { - buff += str; - }; - return { - toString() { - return buff; - }, - getContext() { - return context; - }, - dispatch(value) { - if (options.replacer) { - value = options.replacer(value); - } - const type = value === null ? "null" : typeof value; - return this[type](value); - }, - object(object) { - if (object && typeof object.toJSON === "function") { - return this.object(object.toJSON()); - } - const objString = Object.prototype.toString.call(object); - let objType = ""; - const objectLength = objString.length; - if (objectLength < 10) { - objType = "unknown:[" + objString + "]"; - } else { - objType = objString.slice(8, objectLength - 1); - } - objType = objType.toLowerCase(); - let objectNumber = null; - if ((objectNumber = context.get(object)) === void 0) { - context.set(object, context.size); - } else { - return this.dispatch("[CIRCULAR:" + objectNumber + "]"); - } - if (typeof Buffer !== "undefined" && Buffer.isBuffer && Buffer.isBuffer(object)) { - write("buffer:"); - return write(object.toString("utf8")); - } - if (objType !== "object" && objType !== "function" && objType !== "asyncfunction") { - if (this[objType]) { - this[objType](object); - } else if (!options.ignoreUnknown) { - this.unkown(object, objType); - } - } else { - let keys = Object.keys(object); - if (options.unorderedObjects) { - keys = keys.sort(); - } - let extraKeys = []; - if (options.respectType !== false && !isNativeFunction(object)) { - extraKeys = defaultPrototypesKeys; - } - if (options.excludeKeys) { - keys = keys.filter((key) => { - return !options.excludeKeys(key); - }); - extraKeys = extraKeys.filter((key) => { - return !options.excludeKeys(key); - }); - } - write("object:" + (keys.length + extraKeys.length) + ":"); - const dispatchForKey = (key) => { - this.dispatch(key); - write(":"); - if (!options.excludeValues) { - this.dispatch(object[key]); - } - write(","); - }; - for (const key of keys) { - dispatchForKey(key); - } - for (const key of extraKeys) { - dispatchForKey(key); - } - } - }, - array(arr, unordered) { - unordered = unordered === void 0 ? options.unorderedArrays !== false : unordered; - write("array:" + arr.length + ":"); - if (!unordered || arr.length <= 1) { - for (const entry of arr) { - this.dispatch(entry); - } - return; - } - const contextAdditions = /* @__PURE__ */ new Map(); - const entries = arr.map((entry) => { - const hasher = createHasher(options); - hasher.dispatch(entry); - for (const [key, value] of hasher.getContext()) { - contextAdditions.set(key, value); - } - return hasher.toString(); - }); - context = contextAdditions; - entries.sort(); - return this.array(entries, false); - }, - date(date) { - return write("date:" + date.toJSON()); - }, - symbol(sym) { - return write("symbol:" + sym.toString()); - }, - unkown(value, type) { - write(type); - if (!value) { - return; - } - write(":"); - if (value && typeof value.entries === "function") { - return this.array( - Array.from(value.entries()), - true - /* ordered */ - ); - } - }, - error(err) { - return write("error:" + err.toString()); - }, - boolean(bool) { - return write("bool:" + bool); - }, - string(string) { - write("string:" + string.length + ":"); - write(string); - }, - function(fn) { - write("fn:"); - if (isNativeFunction(fn)) { - this.dispatch("[native]"); - } else { - this.dispatch(fn.toString()); - } - if (options.respectFunctionNames !== false) { - this.dispatch("function-name:" + String(fn.name)); - } - if (options.respectFunctionProperties) { - this.object(fn); - } - }, - number(number) { - return write("number:" + number); - }, - xml(xml) { - return write("xml:" + xml.toString()); - }, - null() { - return write("Null"); - }, - undefined() { - return write("Undefined"); - }, - regexp(regex) { - return write("regex:" + regex.toString()); - }, - uint8array(arr) { - write("uint8array:"); - return this.dispatch(Array.prototype.slice.call(arr)); - }, - uint8clampedarray(arr) { - write("uint8clampedarray:"); - return this.dispatch(Array.prototype.slice.call(arr)); - }, - int8array(arr) { - write("int8array:"); - return this.dispatch(Array.prototype.slice.call(arr)); - }, - uint16array(arr) { - write("uint16array:"); - return this.dispatch(Array.prototype.slice.call(arr)); - }, - int16array(arr) { - write("int16array:"); - return this.dispatch(Array.prototype.slice.call(arr)); - }, - uint32array(arr) { - write("uint32array:"); - return this.dispatch(Array.prototype.slice.call(arr)); - }, - int32array(arr) { - write("int32array:"); - return this.dispatch(Array.prototype.slice.call(arr)); - }, - float32array(arr) { - write("float32array:"); - return this.dispatch(Array.prototype.slice.call(arr)); - }, - float64array(arr) { - write("float64array:"); - return this.dispatch(Array.prototype.slice.call(arr)); - }, - arraybuffer(arr) { - write("arraybuffer:"); - return this.dispatch(new Uint8Array(arr)); - }, - url(url) { - return write("url:" + url.toString()); - }, - map(map) { - write("map:"); - const arr = [...map]; - return this.array(arr, options.unorderedSets !== false); - }, - set(set) { - write("set:"); - const arr = [...set]; - return this.array(arr, options.unorderedSets !== false); - }, - file(file) { - write("file:"); - return this.dispatch([file.name, file.size, file.type, file.lastModfied]); - }, - blob() { - if (options.ignoreUnknown) { - return write("[blob]"); - } - throw new Error( - 'Hashing Blob objects is currently not supported\nUse "options.replacer" or "options.ignoreUnknown"\n' - ); - }, - domwindow() { - return write("domwindow"); - }, - bigint(number) { - return write("bigint:" + number.toString()); - }, - /* Node.js standard native objects */ - process() { - return write("process"); - }, - timer() { - return write("timer"); - }, - pipe() { - return write("pipe"); - }, - tcp() { - return write("tcp"); - }, - udp() { - return write("udp"); - }, - tty() { - return write("tty"); - }, - statwatcher() { - return write("statwatcher"); - }, - securecontext() { - return write("securecontext"); - }, - connection() { - return write("connection"); - }, - zlib() { - return write("zlib"); - }, - context() { - return write("context"); - }, - nodescript() { - return write("nodescript"); - }, - httpparser() { - return write("httpparser"); - }, - dataview() { - return write("dataview"); - }, - signal() { - return write("signal"); - }, - fsevent() { - return write("fsevent"); - }, - tlswrap() { - return write("tlswrap"); - } - }; -} -const nativeFunc = "[native code] }"; -const nativeFuncLength = nativeFunc.length; -function isNativeFunction(f) { - if (typeof f !== "function") { - return false; - } - return Function.prototype.toString.call(f).slice(-nativeFuncLength) === nativeFunc; -} - -var __defProp$1 = Object.defineProperty; -var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; -var __publicField$1 = (obj, key, value) => { - __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value); - return value; -}; -class WordArray { - constructor(words, sigBytes) { - __publicField$1(this, "words"); - __publicField$1(this, "sigBytes"); - words = this.words = words || []; - this.sigBytes = sigBytes === void 0 ? words.length * 4 : sigBytes; - } - toString(encoder) { - return (encoder || Hex).stringify(this); - } - concat(wordArray) { - this.clamp(); - if (this.sigBytes % 4) { - for (let i = 0; i < wordArray.sigBytes; i++) { - const thatByte = wordArray.words[i >>> 2] >>> 24 - i % 4 * 8 & 255; - this.words[this.sigBytes + i >>> 2] |= thatByte << 24 - (this.sigBytes + i) % 4 * 8; - } - } else { - for (let j = 0; j < wordArray.sigBytes; j += 4) { - this.words[this.sigBytes + j >>> 2] = wordArray.words[j >>> 2]; - } - } - this.sigBytes += wordArray.sigBytes; - return this; - } - clamp() { - this.words[this.sigBytes >>> 2] &= 4294967295 << 32 - this.sigBytes % 4 * 8; - this.words.length = Math.ceil(this.sigBytes / 4); - } - clone() { - return new WordArray([...this.words]); - } -} -const Hex = { - stringify(wordArray) { - const hexChars = []; - for (let i = 0; i < wordArray.sigBytes; i++) { - const bite = wordArray.words[i >>> 2] >>> 24 - i % 4 * 8 & 255; - hexChars.push((bite >>> 4).toString(16), (bite & 15).toString(16)); - } - return hexChars.join(""); - } -}; -const Base64 = { - stringify(wordArray) { - const keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - const base64Chars = []; - for (let i = 0; i < wordArray.sigBytes; i += 3) { - const byte1 = wordArray.words[i >>> 2] >>> 24 - i % 4 * 8 & 255; - const byte2 = wordArray.words[i + 1 >>> 2] >>> 24 - (i + 1) % 4 * 8 & 255; - const byte3 = wordArray.words[i + 2 >>> 2] >>> 24 - (i + 2) % 4 * 8 & 255; - const triplet = byte1 << 16 | byte2 << 8 | byte3; - for (let j = 0; j < 4 && i * 8 + j * 6 < wordArray.sigBytes * 8; j++) { - base64Chars.push(keyStr.charAt(triplet >>> 6 * (3 - j) & 63)); - } - } - return base64Chars.join(""); - } -}; -const Latin1 = { - parse(latin1Str) { - const latin1StrLength = latin1Str.length; - const words = []; - for (let i = 0; i < latin1StrLength; i++) { - words[i >>> 2] |= (latin1Str.charCodeAt(i) & 255) << 24 - i % 4 * 8; - } - return new WordArray(words, latin1StrLength); - } -}; -const Utf8 = { - parse(utf8Str) { - return Latin1.parse(unescape(encodeURIComponent(utf8Str))); - } -}; -class BufferedBlockAlgorithm { - constructor() { - __publicField$1(this, "_data", new WordArray()); - __publicField$1(this, "_nDataBytes", 0); - __publicField$1(this, "_minBufferSize", 0); - __publicField$1(this, "blockSize", 512 / 32); - } - reset() { - this._data = new WordArray(); - this._nDataBytes = 0; - } - _append(data) { - if (typeof data === "string") { - data = Utf8.parse(data); - } - this._data.concat(data); - this._nDataBytes += data.sigBytes; - } - _doProcessBlock(_dataWords, _offset) { - } - _process(doFlush) { - let processedWords; - let nBlocksReady = this._data.sigBytes / (this.blockSize * 4); - if (doFlush) { - nBlocksReady = Math.ceil(nBlocksReady); - } else { - nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0); - } - const nWordsReady = nBlocksReady * this.blockSize; - const nBytesReady = Math.min(nWordsReady * 4, this._data.sigBytes); - if (nWordsReady) { - for (let offset = 0; offset < nWordsReady; offset += this.blockSize) { - this._doProcessBlock(this._data.words, offset); - } - processedWords = this._data.words.splice(0, nWordsReady); - this._data.sigBytes -= nBytesReady; - } - return new WordArray(processedWords, nBytesReady); - } -} -class Hasher extends BufferedBlockAlgorithm { - update(messageUpdate) { - this._append(messageUpdate); - this._process(); - return this; - } - finalize(messageUpdate) { - if (messageUpdate) { - this._append(messageUpdate); - } - } -} - -var __defProp$3 = Object.defineProperty; -var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; -var __publicField$3 = (obj, key, value) => { - __defNormalProp$3(obj, key + "" , value); - return value; -}; -const H = [ - 1779033703, - -1150833019, - 1013904242, - -1521486534, - 1359893119, - -1694144372, - 528734635, - 1541459225 -]; -const K = [ - 1116352408, - 1899447441, - -1245643825, - -373957723, - 961987163, - 1508970993, - -1841331548, - -1424204075, - -670586216, - 310598401, - 607225278, - 1426881987, - 1925078388, - -2132889090, - -1680079193, - -1046744716, - -459576895, - -272742522, - 264347078, - 604807628, - 770255983, - 1249150122, - 1555081692, - 1996064986, - -1740746414, - -1473132947, - -1341970488, - -1084653625, - -958395405, - -710438585, - 113926993, - 338241895, - 666307205, - 773529912, - 1294757372, - 1396182291, - 1695183700, - 1986661051, - -2117940946, - -1838011259, - -1564481375, - -1474664885, - -1035236496, - -949202525, - -778901479, - -694614492, - -200395387, - 275423344, - 430227734, - 506948616, - 659060556, - 883997877, - 958139571, - 1322822218, - 1537002063, - 1747873779, - 1955562222, - 2024104815, - -2067236844, - -1933114872, - -1866530822, - -1538233109, - -1090935817, - -965641998 -]; -const W = []; -class SHA256 extends Hasher { - constructor() { - super(...arguments); - __publicField$3(this, "_hash", new WordArray([...H])); - } - /** - * Resets the internal state of the hash object to initial values. - */ - reset() { - super.reset(); - this._hash = new WordArray([...H]); - } - _doProcessBlock(M, offset) { - const H2 = this._hash.words; - let a = H2[0]; - let b = H2[1]; - let c = H2[2]; - let d = H2[3]; - let e = H2[4]; - let f = H2[5]; - let g = H2[6]; - let h = H2[7]; - for (let i = 0; i < 64; i++) { - if (i < 16) { - W[i] = M[offset + i] | 0; - } else { - const gamma0x = W[i - 15]; - const gamma0 = (gamma0x << 25 | gamma0x >>> 7) ^ (gamma0x << 14 | gamma0x >>> 18) ^ gamma0x >>> 3; - const gamma1x = W[i - 2]; - const gamma1 = (gamma1x << 15 | gamma1x >>> 17) ^ (gamma1x << 13 | gamma1x >>> 19) ^ gamma1x >>> 10; - W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16]; - } - const ch = e & f ^ ~e & g; - const maj = a & b ^ a & c ^ b & c; - const sigma0 = (a << 30 | a >>> 2) ^ (a << 19 | a >>> 13) ^ (a << 10 | a >>> 22); - const sigma1 = (e << 26 | e >>> 6) ^ (e << 21 | e >>> 11) ^ (e << 7 | e >>> 25); - const t1 = h + sigma1 + ch + K[i] + W[i]; - const t2 = sigma0 + maj; - h = g; - g = f; - f = e; - e = d + t1 | 0; - d = c; - c = b; - b = a; - a = t1 + t2 | 0; - } - H2[0] = H2[0] + a | 0; - H2[1] = H2[1] + b | 0; - H2[2] = H2[2] + c | 0; - H2[3] = H2[3] + d | 0; - H2[4] = H2[4] + e | 0; - H2[5] = H2[5] + f | 0; - H2[6] = H2[6] + g | 0; - H2[7] = H2[7] + h | 0; - } - /** - * Finishes the hash calculation and returns the hash as a WordArray. - * - * @param {string} messageUpdate - Additional message content to include in the hash. - * @returns {WordArray} The finalised hash as a WordArray. - */ - finalize(messageUpdate) { - super.finalize(messageUpdate); - const nBitsTotal = this._nDataBytes * 8; - const nBitsLeft = this._data.sigBytes * 8; - this._data.words[nBitsLeft >>> 5] |= 128 << 24 - nBitsLeft % 32; - this._data.words[(nBitsLeft + 64 >>> 9 << 4) + 14] = Math.floor( - nBitsTotal / 4294967296 - ); - this._data.words[(nBitsLeft + 64 >>> 9 << 4) + 15] = nBitsTotal; - this._data.sigBytes = this._data.words.length * 4; - this._process(); - return this._hash; - } -} -function sha256base64(message) { - return new SHA256().finalize(message).toString(Base64); -} - -function hash(object, options = {}) { - const hashed = typeof object === "string" ? object : objectHash(object, options); - return sha256base64(hashed).slice(0, 10); -} - -const NODE_TYPES = { - NORMAL: 0, - WILDCARD: 1, - PLACEHOLDER: 2 -}; - -function createRouter$1(options = {}) { - const ctx = { - options, - rootNode: createRadixNode(), - staticRoutesMap: {} - }; - const normalizeTrailingSlash = (p) => options.strictTrailingSlash ? p : p.replace(/\/$/, "") || "/"; - if (options.routes) { - for (const path in options.routes) { - insert(ctx, normalizeTrailingSlash(path), options.routes[path]); - } - } - return { - ctx, - lookup: (path) => lookup(ctx, normalizeTrailingSlash(path)), - insert: (path, data) => insert(ctx, normalizeTrailingSlash(path), data), - remove: (path) => remove(ctx, normalizeTrailingSlash(path)) - }; -} -function lookup(ctx, path) { - const staticPathNode = ctx.staticRoutesMap[path]; - if (staticPathNode) { - return staticPathNode.data; - } - const sections = path.split("/"); - const params = {}; - let paramsFound = false; - let wildcardNode = null; - let node = ctx.rootNode; - let wildCardParam = null; - for (let i = 0; i < sections.length; i++) { - const section = sections[i]; - if (node.wildcardChildNode !== null) { - wildcardNode = node.wildcardChildNode; - wildCardParam = sections.slice(i).join("/"); - } - const nextNode = node.children.get(section); - if (nextNode === void 0) { - if (node && node.placeholderChildren.length > 1) { - const remaining = sections.length - i; - node = node.placeholderChildren.find((c) => c.maxDepth === remaining) || null; - } else { - node = node.placeholderChildren[0] || null; - } - if (!node) { - break; - } - if (node.paramName) { - params[node.paramName] = section; - } - paramsFound = true; - } else { - node = nextNode; - } - } - if ((node === null || node.data === null) && wildcardNode !== null) { - node = wildcardNode; - params[node.paramName || "_"] = wildCardParam; - paramsFound = true; - } - if (!node) { - return null; - } - if (paramsFound) { - return { - ...node.data, - params: paramsFound ? params : void 0 - }; - } - return node.data; -} -function insert(ctx, path, data) { - let isStaticRoute = true; - const sections = path.split("/"); - let node = ctx.rootNode; - let _unnamedPlaceholderCtr = 0; - const matchedNodes = [node]; - for (const section of sections) { - let childNode; - if (childNode = node.children.get(section)) { - node = childNode; - } else { - const type = getNodeType(section); - childNode = createRadixNode({ type, parent: node }); - node.children.set(section, childNode); - if (type === NODE_TYPES.PLACEHOLDER) { - childNode.paramName = section === "*" ? `_${_unnamedPlaceholderCtr++}` : section.slice(1); - node.placeholderChildren.push(childNode); - isStaticRoute = false; - } else if (type === NODE_TYPES.WILDCARD) { - node.wildcardChildNode = childNode; - childNode.paramName = section.slice( - 3 - /* "**:" */ - ) || "_"; - isStaticRoute = false; - } - matchedNodes.push(childNode); - node = childNode; - } - } - for (const [depth, node2] of matchedNodes.entries()) { - node2.maxDepth = Math.max(matchedNodes.length - depth, node2.maxDepth || 0); - } - node.data = data; - if (isStaticRoute === true) { - ctx.staticRoutesMap[path] = node; - } - return node; -} -function remove(ctx, path) { - let success = false; - const sections = path.split("/"); - let node = ctx.rootNode; - for (const section of sections) { - node = node.children.get(section); - if (!node) { - return success; - } - } - if (node.data) { - const lastSection = sections.at(-1) || ""; - node.data = null; - if (Object.keys(node.children).length === 0 && node.parent) { - node.parent.children.delete(lastSection); - node.parent.wildcardChildNode = null; - node.parent.placeholderChildren = []; - } - success = true; - } - return success; -} -function createRadixNode(options = {}) { - return { - type: options.type || NODE_TYPES.NORMAL, - maxDepth: 0, - parent: options.parent || null, - children: /* @__PURE__ */ new Map(), - data: options.data || null, - paramName: options.paramName || null, - wildcardChildNode: null, - placeholderChildren: [] - }; -} -function getNodeType(str) { - if (str.startsWith("**")) { - return NODE_TYPES.WILDCARD; - } - if (str[0] === ":" || str === "*") { - return NODE_TYPES.PLACEHOLDER; - } - return NODE_TYPES.NORMAL; -} - -function toRouteMatcher(router) { - const table = _routerNodeToTable("", router.ctx.rootNode); - return _createMatcher(table, router.ctx.options.strictTrailingSlash); -} -function _createMatcher(table, strictTrailingSlash) { - return { - ctx: { table }, - matchAll: (path) => _matchRoutes(path, table, strictTrailingSlash) - }; -} -function _createRouteTable() { - return { - static: /* @__PURE__ */ new Map(), - wildcard: /* @__PURE__ */ new Map(), - dynamic: /* @__PURE__ */ new Map() - }; -} -function _matchRoutes(path, table, strictTrailingSlash) { - if (strictTrailingSlash !== true && path.endsWith("/")) { - path = path.slice(0, -1) || "/"; - } - const matches = []; - for (const [key, value] of _sortRoutesMap(table.wildcard)) { - if (path === key || path.startsWith(key + "/")) { - matches.push(value); - } - } - for (const [key, value] of _sortRoutesMap(table.dynamic)) { - if (path.startsWith(key + "/")) { - const subPath = "/" + path.slice(key.length).split("/").splice(2).join("/"); - matches.push(..._matchRoutes(subPath, value)); - } - } - const staticMatch = table.static.get(path); - if (staticMatch) { - matches.push(staticMatch); - } - return matches.filter(Boolean); -} -function _sortRoutesMap(m) { - return [...m.entries()].sort((a, b) => a[0].length - b[0].length); -} -function _routerNodeToTable(initialPath, initialNode) { - const table = _createRouteTable(); - function _addNode(path, node) { - if (path) { - if (node.type === NODE_TYPES.NORMAL && !(path.includes("*") || path.includes(":"))) { - if (node.data) { - table.static.set(path, node.data); - } - } else if (node.type === NODE_TYPES.WILDCARD) { - table.wildcard.set(path.replace("/**", ""), node.data); - } else if (node.type === NODE_TYPES.PLACEHOLDER) { - const subTable = _routerNodeToTable("", node); - if (node.data) { - subTable.static.set("/", node.data); - } - table.dynamic.set(path.replace(/\/\*|\/:\w+/, ""), subTable); - return; - } - } - for (const [childPath, child] of node.children.entries()) { - _addNode(`${path}/${childPath}`.replace("//", "/"), child); - } - } - _addNode(initialPath, initialNode); - return table; -} - -function isPlainObject$1(value) { - if (value === null || typeof value !== "object") { - return false; - } - const prototype = Object.getPrototypeOf(value); - if (prototype !== null && prototype !== Object.prototype && Object.getPrototypeOf(prototype) !== null) { - return false; - } - if (Symbol.iterator in value) { - return false; - } - if (Symbol.toStringTag in value) { - return Object.prototype.toString.call(value) === "[object Module]"; - } - return true; -} - -function _defu$1(baseObject, defaults, namespace = ".", merger) { - if (!isPlainObject$1(defaults)) { - return _defu$1(baseObject, {}, namespace, merger); - } - const object = Object.assign({}, defaults); - for (const key in baseObject) { - if (key === "__proto__" || key === "constructor") { - continue; - } - const value = baseObject[key]; - if (value === null || value === void 0) { - continue; - } - if (merger && merger(object, key, value, namespace)) { - continue; - } - if (Array.isArray(value) && Array.isArray(object[key])) { - object[key] = [...value, ...object[key]]; - } else if (isPlainObject$1(value) && isPlainObject$1(object[key])) { - object[key] = _defu$1( - value, - object[key], - (namespace ? `${namespace}.` : "") + key.toString(), - merger - ); - } else { - object[key] = value; - } - } - return object; -} -function createDefu$1(merger) { - return (...arguments_) => ( - // eslint-disable-next-line unicorn/no-array-reduce - arguments_.reduce((p, c) => _defu$1(p, c, "", merger), {}) - ); -} -const defu = createDefu$1(); - -function rawHeaders(headers) { - const rawHeaders2 = []; - for (const key in headers) { - if (Array.isArray(headers[key])) { - for (const h of headers[key]) { - rawHeaders2.push(key, h); - } - } else { - rawHeaders2.push(key, headers[key]); - } - } - return rawHeaders2; -} -function mergeFns(...functions) { - return function(...args) { - for (const fn of functions) { - fn(...args); - } - }; -} -function createNotImplementedError(name) { - throw new Error(`[unenv] ${name} is not implemented yet!`); -} - -let defaultMaxListeners = 10; -let EventEmitter$1 = class EventEmitter { - __unenv__ = true; - _events = /* @__PURE__ */ Object.create(null); - _maxListeners; - static get defaultMaxListeners() { - return defaultMaxListeners; - } - static set defaultMaxListeners(arg) { - if (typeof arg !== "number" || arg < 0 || Number.isNaN(arg)) { - throw new RangeError( - 'The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + "." - ); - } - defaultMaxListeners = arg; - } - setMaxListeners(n) { - if (typeof n !== "number" || n < 0 || Number.isNaN(n)) { - throw new RangeError( - 'The value of "n" is out of range. It must be a non-negative number. Received ' + n + "." - ); - } - this._maxListeners = n; - return this; - } - getMaxListeners() { - return _getMaxListeners(this); - } - emit(type, ...args) { - if (!this._events[type] || this._events[type].length === 0) { - return false; - } - if (type === "error") { - let er; - if (args.length > 0) { - er = args[0]; - } - if (er instanceof Error) { - throw er; - } - const err = new Error( - "Unhandled error." + (er ? " (" + er.message + ")" : "") - ); - err.context = er; - throw err; - } - for (const _listener of this._events[type]) { - (_listener.listener || _listener).apply(this, args); - } - return true; - } - addListener(type, listener) { - return _addListener(this, type, listener, false); - } - on(type, listener) { - return _addListener(this, type, listener, false); - } - prependListener(type, listener) { - return _addListener(this, type, listener, true); - } - once(type, listener) { - return this.on(type, _wrapOnce(this, type, listener)); - } - prependOnceListener(type, listener) { - return this.prependListener(type, _wrapOnce(this, type, listener)); - } - removeListener(type, listener) { - return _removeListener(this, type, listener); - } - off(type, listener) { - return this.removeListener(type, listener); - } - removeAllListeners(type) { - return _removeAllListeners(this, type); - } - listeners(type) { - return _listeners(this, type, true); - } - rawListeners(type) { - return _listeners(this, type, false); - } - listenerCount(type) { - return this.rawListeners(type).length; - } - eventNames() { - return Object.keys(this._events); - } -}; -function _addListener(target, type, listener, prepend) { - _checkListener(listener); - if (target._events.newListener !== void 0) { - target.emit("newListener", type, listener.listener || listener); - } - if (!target._events[type]) { - target._events[type] = []; - } - if (prepend) { - target._events[type].unshift(listener); - } else { - target._events[type].push(listener); - } - const maxListeners = _getMaxListeners(target); - if (maxListeners > 0 && target._events[type].length > maxListeners && !target._events[type].warned) { - target._events[type].warned = true; - const warning = new Error( - `[unenv] Possible EventEmitter memory leak detected. ${target._events[type].length} ${type} listeners added. Use emitter.setMaxListeners() to increase limit` - ); - warning.name = "MaxListenersExceededWarning"; - warning.emitter = target; - warning.type = type; - warning.count = target._events[type]?.length; - console.warn(warning); - } - return target; -} -function _removeListener(target, type, listener) { - _checkListener(listener); - if (!target._events[type] || target._events[type].length === 0) { - return target; - } - const lenBeforeFilter = target._events[type].length; - target._events[type] = target._events[type].filter((fn) => fn !== listener); - if (lenBeforeFilter === target._events[type].length) { - return target; - } - if (target._events.removeListener) { - target.emit("removeListener", type, listener.listener || listener); - } - if (target._events[type].length === 0) { - delete target._events[type]; - } - return target; -} -function _removeAllListeners(target, type) { - if (!target._events[type] || target._events[type].length === 0) { - return target; - } - if (target._events.removeListener) { - for (const _listener of target._events[type]) { - target.emit("removeListener", type, _listener.listener || _listener); - } - } - delete target._events[type]; - return target; -} -function _wrapOnce(target, type, listener) { - let fired = false; - const wrapper = (...args) => { - if (fired) { - return; - } - target.removeListener(type, wrapper); - fired = true; - return args.length === 0 ? listener.call(target) : listener.apply(target, args); - }; - wrapper.listener = listener; - return wrapper; -} -function _getMaxListeners(target) { - return target._maxListeners ?? EventEmitter$1.defaultMaxListeners; -} -function _listeners(target, type, unwrap) { - let listeners = target._events[type]; - if (typeof listeners === "function") { - listeners = [listeners]; - } - return unwrap ? listeners.map((l) => l.listener || l) : listeners; -} -function _checkListener(listener) { - if (typeof listener !== "function") { - throw new TypeError( - 'The "listener" argument must be of type Function. Received type ' + typeof listener - ); - } -} - -const EventEmitter = globalThis.EventEmitter || EventEmitter$1; - -class _Readable extends EventEmitter { - __unenv__ = true; - readableEncoding = null; - readableEnded = true; - readableFlowing = false; - readableHighWaterMark = 0; - readableLength = 0; - readableObjectMode = false; - readableAborted = false; - readableDidRead = false; - closed = false; - errored = null; - readable = false; - destroyed = false; - static from(_iterable, options) { - return new _Readable(options); - } - constructor(_opts) { - super(); - } - _read(_size) { - } - read(_size) { - } - setEncoding(_encoding) { - return this; - } - pause() { - return this; - } - resume() { - return this; - } - isPaused() { - return true; - } - unpipe(_destination) { - return this; - } - unshift(_chunk, _encoding) { - } - wrap(_oldStream) { - return this; - } - push(_chunk, _encoding) { - return false; - } - _destroy(_error, _callback) { - this.removeAllListeners(); - } - destroy(error) { - this.destroyed = true; - this._destroy(error); - return this; - } - pipe(_destenition, _options) { - return {}; - } - compose(stream, options) { - throw new Error("[unenv] Method not implemented."); - } - [Symbol.asyncDispose]() { - this.destroy(); - return Promise.resolve(); - } - // eslint-disable-next-line require-yield - async *[Symbol.asyncIterator]() { - throw createNotImplementedError("Readable.asyncIterator"); - } - iterator(options) { - throw createNotImplementedError("Readable.iterator"); - } - map(fn, options) { - throw createNotImplementedError("Readable.map"); - } - filter(fn, options) { - throw createNotImplementedError("Readable.filter"); - } - forEach(fn, options) { - throw createNotImplementedError("Readable.forEach"); - } - reduce(fn, initialValue, options) { - throw createNotImplementedError("Readable.reduce"); - } - find(fn, options) { - throw createNotImplementedError("Readable.find"); - } - findIndex(fn, options) { - throw createNotImplementedError("Readable.findIndex"); - } - some(fn, options) { - throw createNotImplementedError("Readable.some"); - } - toArray(options) { - throw createNotImplementedError("Readable.toArray"); - } - every(fn, options) { - throw createNotImplementedError("Readable.every"); - } - flatMap(fn, options) { - throw createNotImplementedError("Readable.flatMap"); - } - drop(limit, options) { - throw createNotImplementedError("Readable.drop"); - } - take(limit, options) { - throw createNotImplementedError("Readable.take"); - } - asIndexedPairs(options) { - throw createNotImplementedError("Readable.asIndexedPairs"); - } -} -const Readable = globalThis.Readable || _Readable; - -class _Writable extends EventEmitter { - __unenv__ = true; - writable = true; - writableEnded = false; - writableFinished = false; - writableHighWaterMark = 0; - writableLength = 0; - writableObjectMode = false; - writableCorked = 0; - closed = false; - errored = null; - writableNeedDrain = false; - destroyed = false; - _data; - _encoding = "utf-8"; - constructor(_opts) { - super(); - } - pipe(_destenition, _options) { - return {}; - } - _write(chunk, encoding, callback) { - if (this.writableEnded) { - if (callback) { - callback(); - } - return; - } - if (this._data === void 0) { - this._data = chunk; - } else { - const a = typeof this._data === "string" ? Buffer.from(this._data, this._encoding || encoding || "utf8") : this._data; - const b = typeof chunk === "string" ? Buffer.from(chunk, encoding || this._encoding || "utf8") : chunk; - this._data = Buffer.concat([a, b]); - } - this._encoding = encoding; - if (callback) { - callback(); - } - } - _writev(_chunks, _callback) { - } - _destroy(_error, _callback) { - } - _final(_callback) { - } - write(chunk, arg2, arg3) { - const encoding = typeof arg2 === "string" ? this._encoding : "utf-8"; - const cb = typeof arg2 === "function" ? arg2 : typeof arg3 === "function" ? arg3 : void 0; - this._write(chunk, encoding, cb); - return true; - } - setDefaultEncoding(_encoding) { - return this; - } - end(arg1, arg2, arg3) { - const callback = typeof arg1 === "function" ? arg1 : typeof arg2 === "function" ? arg2 : typeof arg3 === "function" ? arg3 : void 0; - if (this.writableEnded) { - if (callback) { - callback(); - } - return this; - } - const data = arg1 === callback ? void 0 : arg1; - if (data) { - const encoding = arg2 === callback ? void 0 : arg2; - this.write(data, encoding, callback); - } - this.writableEnded = true; - this.writableFinished = true; - this.emit("close"); - this.emit("finish"); - return this; - } - cork() { - } - uncork() { - } - destroy(_error) { - this.destroyed = true; - delete this._data; - this.removeAllListeners(); - return this; - } - compose(stream, options) { - throw new Error("[h3] Method not implemented."); - } -} -const Writable = globalThis.Writable || _Writable; - -const __Duplex = class { - allowHalfOpen = true; - _destroy; - constructor(readable = new Readable(), writable = new Writable()) { - Object.assign(this, readable); - Object.assign(this, writable); - this._destroy = mergeFns(readable._destroy, writable._destroy); - } -}; -function getDuplex() { - Object.assign(__Duplex.prototype, Readable.prototype); - Object.assign(__Duplex.prototype, Writable.prototype); - return __Duplex; -} -const _Duplex = /* @__PURE__ */ getDuplex(); -const Duplex = globalThis.Duplex || _Duplex; - -class Socket extends Duplex { - __unenv__ = true; - bufferSize = 0; - bytesRead = 0; - bytesWritten = 0; - connecting = false; - destroyed = false; - pending = false; - localAddress = ""; - localPort = 0; - remoteAddress = ""; - remoteFamily = ""; - remotePort = 0; - autoSelectFamilyAttemptedAddresses = []; - readyState = "readOnly"; - constructor(_options) { - super(); - } - write(_buffer, _arg1, _arg2) { - return false; - } - connect(_arg1, _arg2, _arg3) { - return this; - } - end(_arg1, _arg2, _arg3) { - return this; - } - setEncoding(_encoding) { - return this; - } - pause() { - return this; - } - resume() { - return this; - } - setTimeout(_timeout, _callback) { - return this; - } - setNoDelay(_noDelay) { - return this; - } - setKeepAlive(_enable, _initialDelay) { - return this; - } - address() { - return {}; - } - unref() { - return this; - } - ref() { - return this; - } - destroySoon() { - this.destroy(); - } - resetAndDestroy() { - const err = new Error("ERR_SOCKET_CLOSED"); - err.code = "ERR_SOCKET_CLOSED"; - this.destroy(err); - return this; - } -} - -class IncomingMessage extends Readable { - __unenv__ = {}; - aborted = false; - httpVersion = "1.1"; - httpVersionMajor = 1; - httpVersionMinor = 1; - complete = true; - connection; - socket; - headers = {}; - trailers = {}; - method = "GET"; - url = "/"; - statusCode = 200; - statusMessage = ""; - closed = false; - errored = null; - readable = false; - constructor(socket) { - super(); - this.socket = this.connection = socket || new Socket(); - } - get rawHeaders() { - return rawHeaders(this.headers); - } - get rawTrailers() { - return []; - } - setTimeout(_msecs, _callback) { - return this; - } - get headersDistinct() { - return _distinct(this.headers); - } - get trailersDistinct() { - return _distinct(this.trailers); - } -} -function _distinct(obj) { - const d = {}; - for (const [key, value] of Object.entries(obj)) { - if (key) { - d[key] = (Array.isArray(value) ? value : [value]).filter( - Boolean - ); - } - } - return d; -} - -class ServerResponse extends Writable { - __unenv__ = true; - statusCode = 200; - statusMessage = ""; - upgrading = false; - chunkedEncoding = false; - shouldKeepAlive = false; - useChunkedEncodingByDefault = false; - sendDate = false; - finished = false; - headersSent = false; - strictContentLength = false; - connection = null; - socket = null; - req; - _headers = {}; - constructor(req) { - super(); - this.req = req; - } - assignSocket(socket) { - socket._httpMessage = this; - this.socket = socket; - this.connection = socket; - this.emit("socket", socket); - this._flush(); - } - _flush() { - this.flushHeaders(); - } - detachSocket(_socket) { - } - writeContinue(_callback) { - } - writeHead(statusCode, arg1, arg2) { - if (statusCode) { - this.statusCode = statusCode; - } - if (typeof arg1 === "string") { - this.statusMessage = arg1; - arg1 = void 0; - } - const headers = arg2 || arg1; - if (headers) { - if (Array.isArray(headers)) ; else { - for (const key in headers) { - this.setHeader(key, headers[key]); - } - } - } - this.headersSent = true; - return this; - } - writeProcessing() { - } - setTimeout(_msecs, _callback) { - return this; - } - appendHeader(name, value) { - name = name.toLowerCase(); - const current = this._headers[name]; - const all = [ - ...Array.isArray(current) ? current : [current], - ...Array.isArray(value) ? value : [value] - ].filter(Boolean); - this._headers[name] = all.length > 1 ? all : all[0]; - return this; - } - setHeader(name, value) { - this._headers[name.toLowerCase()] = value; - return this; - } - getHeader(name) { - return this._headers[name.toLowerCase()]; - } - getHeaders() { - return this._headers; - } - getHeaderNames() { - return Object.keys(this._headers); - } - hasHeader(name) { - return name.toLowerCase() in this._headers; - } - removeHeader(name) { - delete this._headers[name.toLowerCase()]; - } - addTrailers(_headers) { - } - flushHeaders() { - } - writeEarlyHints(_headers, cb) { - if (typeof cb === "function") { - cb(); - } - } -} - -function hasProp(obj, prop) { - try { - return prop in obj; - } catch { - return false; - } -} - -var __defProp$2 = Object.defineProperty; -var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; -var __publicField$2 = (obj, key, value) => { - __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value); - return value; -}; -class H3Error extends Error { - constructor(message, opts = {}) { - super(message, opts); - __publicField$2(this, "statusCode", 500); - __publicField$2(this, "fatal", false); - __publicField$2(this, "unhandled", false); - __publicField$2(this, "statusMessage"); - __publicField$2(this, "data"); - __publicField$2(this, "cause"); - if (opts.cause && !this.cause) { - this.cause = opts.cause; - } - } - toJSON() { - const obj = { - message: this.message, - statusCode: sanitizeStatusCode(this.statusCode, 500) - }; - if (this.statusMessage) { - obj.statusMessage = sanitizeStatusMessage(this.statusMessage); - } - if (this.data !== void 0) { - obj.data = this.data; - } - return obj; - } -} -__publicField$2(H3Error, "__h3_error__", true); -function createError(input) { - if (typeof input === "string") { - return new H3Error(input); - } - if (isError(input)) { - return input; - } - const err = new H3Error(input.message ?? input.statusMessage ?? "", { - cause: input.cause || input - }); - if (hasProp(input, "stack")) { - try { - Object.defineProperty(err, "stack", { - get() { - return input.stack; - } - }); - } catch { - try { - err.stack = input.stack; - } catch { - } - } - } - if (input.data) { - err.data = input.data; - } - if (input.statusCode) { - err.statusCode = sanitizeStatusCode(input.statusCode, err.statusCode); - } else if (input.status) { - err.statusCode = sanitizeStatusCode(input.status, err.statusCode); - } - if (input.statusMessage) { - err.statusMessage = input.statusMessage; - } else if (input.statusText) { - err.statusMessage = input.statusText; - } - if (err.statusMessage) { - const originalMessage = err.statusMessage; - const sanitizedMessage = sanitizeStatusMessage(err.statusMessage); - if (sanitizedMessage !== originalMessage) { - console.warn( - "[h3] Please prefer using `message` for longer error messages instead of `statusMessage`. In the future, `statusMessage` will be sanitized by default." - ); - } - } - if (input.fatal !== void 0) { - err.fatal = input.fatal; - } - if (input.unhandled !== void 0) { - err.unhandled = input.unhandled; - } - return err; -} -function sendError(event, error, debug) { - if (event.handled) { - return; - } - const h3Error = isError(error) ? error : createError(error); - const responseBody = { - statusCode: h3Error.statusCode, - statusMessage: h3Error.statusMessage, - stack: [], - data: h3Error.data - }; - if (debug) { - responseBody.stack = (h3Error.stack || "").split("\n").map((l) => l.trim()); - } - if (event.handled) { - return; - } - const _code = Number.parseInt(h3Error.statusCode); - setResponseStatus(event, _code, h3Error.statusMessage); - event.node.res.setHeader("content-type", MIMES.json); - event.node.res.end(JSON.stringify(responseBody, void 0, 2)); -} -function isError(input) { - return input?.constructor?.__h3_error__ === true; -} -function isMethod(event, expected, allowHead) { - if (typeof expected === "string") { - if (event.method === expected) { - return true; - } - } else if (expected.includes(event.method)) { - return true; - } - return false; -} -function assertMethod(event, expected, allowHead) { - if (!isMethod(event, expected)) { - throw createError({ - statusCode: 405, - statusMessage: "HTTP method is not allowed." - }); - } -} -function getRequestHeaders(event) { - const _headers = {}; - for (const key in event.node.req.headers) { - const val = event.node.req.headers[key]; - _headers[key] = Array.isArray(val) ? val.filter(Boolean).join(", ") : val; - } - return _headers; -} -function getRequestHeader(event, name) { - const headers = getRequestHeaders(event); - const value = headers[name.toLowerCase()]; - return value; -} - -const RawBodySymbol = Symbol.for("h3RawBody"); -const PayloadMethods$1 = ["PATCH", "POST", "PUT", "DELETE"]; -function readRawBody(event, encoding = "utf8") { - assertMethod(event, PayloadMethods$1); - const _rawBody = event._requestBody || event.web?.request?.body || event.node.req[RawBodySymbol] || event.node.req.rawBody || event.node.req.body; - if (_rawBody) { - const promise2 = Promise.resolve(_rawBody).then((_resolved) => { - if (Buffer.isBuffer(_resolved)) { - return _resolved; - } - if (typeof _resolved.pipeTo === "function") { - return new Promise((resolve, reject) => { - const chunks = []; - _resolved.pipeTo( - new WritableStream({ - write(chunk) { - chunks.push(chunk); - }, - close() { - resolve(Buffer.concat(chunks)); - }, - abort(reason) { - reject(reason); - } - }) - ).catch(reject); - }); - } else if (typeof _resolved.pipe === "function") { - return new Promise((resolve, reject) => { - const chunks = []; - _resolved.on("data", (chunk) => { - chunks.push(chunk); - }).on("end", () => { - resolve(Buffer.concat(chunks)); - }).on("error", reject); - }); - } - if (_resolved.constructor === Object) { - return Buffer.from(JSON.stringify(_resolved)); - } - if (_resolved instanceof URLSearchParams) { - return Buffer.from(_resolved.toString()); - } - return Buffer.from(_resolved); - }); - return encoding ? promise2.then((buff) => buff.toString(encoding)) : promise2; - } - if (!Number.parseInt(event.node.req.headers["content-length"] || "") && !String(event.node.req.headers["transfer-encoding"] ?? "").split(",").map((e) => e.trim()).filter(Boolean).includes("chunked")) { - return Promise.resolve(void 0); - } - const promise = event.node.req[RawBodySymbol] = new Promise( - (resolve, reject) => { - const bodyData = []; - event.node.req.on("error", (err) => { - reject(err); - }).on("data", (chunk) => { - bodyData.push(chunk); - }).on("end", () => { - resolve(Buffer.concat(bodyData)); - }); - } - ); - const result = encoding ? promise.then((buff) => buff.toString(encoding)) : promise; - return result; -} -function getRequestWebStream(event) { - if (!PayloadMethods$1.includes(event.method)) { - return; - } - const bodyStream = event.web?.request?.body || event._requestBody; - if (bodyStream) { - return bodyStream; - } - const _hasRawBody = RawBodySymbol in event.node.req || "rawBody" in event.node.req || "body" in event.node.req || "__unenv__" in event.node.req; - if (_hasRawBody) { - return new ReadableStream({ - async start(controller) { - const _rawBody = await readRawBody(event, false); - if (_rawBody) { - controller.enqueue(_rawBody); - } - controller.close(); - } - }); - } - return new ReadableStream({ - start: (controller) => { - event.node.req.on("data", (chunk) => { - controller.enqueue(chunk); - }); - event.node.req.on("end", () => { - controller.close(); - }); - event.node.req.on("error", (err) => { - controller.error(err); - }); - } - }); -} - -function handleCacheHeaders(event, opts) { - const cacheControls = ["public", ...opts.cacheControls || []]; - let cacheMatched = false; - if (opts.maxAge !== void 0) { - cacheControls.push(`max-age=${+opts.maxAge}`, `s-maxage=${+opts.maxAge}`); - } - if (opts.modifiedTime) { - const modifiedTime = new Date(opts.modifiedTime); - const ifModifiedSince = event.node.req.headers["if-modified-since"]; - event.node.res.setHeader("last-modified", modifiedTime.toUTCString()); - if (ifModifiedSince && new Date(ifModifiedSince) >= opts.modifiedTime) { - cacheMatched = true; - } - } - if (opts.etag) { - event.node.res.setHeader("etag", opts.etag); - const ifNonMatch = event.node.req.headers["if-none-match"]; - if (ifNonMatch === opts.etag) { - cacheMatched = true; - } - } - event.node.res.setHeader("cache-control", cacheControls.join(", ")); - if (cacheMatched) { - event.node.res.statusCode = 304; - if (!event.handled) { - event.node.res.end(); - } - return true; - } - return false; -} - -const MIMES = { - html: "text/html", - json: "application/json" -}; - -const DISALLOWED_STATUS_CHARS = /[^\u0009\u0020-\u007E]/g; -function sanitizeStatusMessage(statusMessage = "") { - return statusMessage.replace(DISALLOWED_STATUS_CHARS, ""); -} -function sanitizeStatusCode(statusCode, defaultStatusCode = 200) { - if (!statusCode) { - return defaultStatusCode; - } - if (typeof statusCode === "string") { - statusCode = Number.parseInt(statusCode, 10); - } - if (statusCode < 100 || statusCode > 999) { - return defaultStatusCode; - } - return statusCode; -} -function splitCookiesString(cookiesString) { - if (Array.isArray(cookiesString)) { - return cookiesString.flatMap((c) => splitCookiesString(c)); - } - if (typeof cookiesString !== "string") { - return []; - } - const cookiesStrings = []; - let pos = 0; - let start; - let ch; - let lastComma; - let nextStart; - let cookiesSeparatorFound; - const skipWhitespace = () => { - while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) { - pos += 1; - } - return pos < cookiesString.length; - }; - const notSpecialChar = () => { - ch = cookiesString.charAt(pos); - return ch !== "=" && ch !== ";" && ch !== ","; - }; - while (pos < cookiesString.length) { - start = pos; - cookiesSeparatorFound = false; - while (skipWhitespace()) { - ch = cookiesString.charAt(pos); - if (ch === ",") { - lastComma = pos; - pos += 1; - skipWhitespace(); - nextStart = pos; - while (pos < cookiesString.length && notSpecialChar()) { - pos += 1; - } - if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") { - cookiesSeparatorFound = true; - pos = nextStart; - cookiesStrings.push(cookiesString.slice(start, lastComma)); - start = pos; - } else { - pos = lastComma + 1; - } - } else { - pos += 1; - } - } - if (!cookiesSeparatorFound || pos >= cookiesString.length) { - cookiesStrings.push(cookiesString.slice(start)); - } - } - return cookiesStrings; -} - -const defer = typeof setImmediate === "undefined" ? (fn) => fn() : setImmediate; -function send(event, data, type) { - if (type) { - defaultContentType(event, type); - } - return new Promise((resolve) => { - defer(() => { - if (!event.handled) { - event.node.res.end(data); - } - resolve(); - }); - }); -} -function sendNoContent(event, code) { - if (event.handled) { - return; - } - if (!code && event.node.res.statusCode !== 200) { - code = event.node.res.statusCode; - } - const _code = sanitizeStatusCode(code, 204); - if (_code === 204) { - event.node.res.removeHeader("content-length"); - } - event.node.res.writeHead(_code); - event.node.res.end(); -} -function setResponseStatus(event, code, text) { - if (code) { - event.node.res.statusCode = sanitizeStatusCode( - code, - event.node.res.statusCode - ); - } - if (text) { - event.node.res.statusMessage = sanitizeStatusMessage(text); - } -} -function defaultContentType(event, type) { - if (type && event.node.res.statusCode !== 304 && !event.node.res.getHeader("content-type")) { - event.node.res.setHeader("content-type", type); - } -} -function sendRedirect(event, location, code = 302) { - event.node.res.statusCode = sanitizeStatusCode( - code, - event.node.res.statusCode - ); - event.node.res.setHeader("location", location); - const encodedLoc = location.replace(/"/g, "%22"); - const html = ``; - return send(event, html, MIMES.html); -} -function setResponseHeaders(event, headers) { - for (const [name, value] of Object.entries(headers)) { - event.node.res.setHeader( - name, - value - ); - } -} -const setHeaders = setResponseHeaders; -function setResponseHeader(event, name, value) { - event.node.res.setHeader(name, value); -} -function isStream(data) { - if (!data || typeof data !== "object") { - return false; - } - if (typeof data.pipe === "function") { - if (typeof data._read === "function") { - return true; - } - if (typeof data.abort === "function") { - return true; - } - } - if (typeof data.pipeTo === "function") { - return true; - } - return false; -} -function isWebResponse(data) { - return typeof Response !== "undefined" && data instanceof Response; -} -function sendStream(event, stream) { - if (!stream || typeof stream !== "object") { - throw new Error("[h3] Invalid stream provided."); - } - event.node.res._data = stream; - if (!event.node.res.socket) { - event._handled = true; - return Promise.resolve(); - } - if (hasProp(stream, "pipeTo") && typeof stream.pipeTo === "function") { - return stream.pipeTo( - new WritableStream({ - write(chunk) { - event.node.res.write(chunk); - } - }) - ).then(() => { - event.node.res.end(); - }); - } - if (hasProp(stream, "pipe") && typeof stream.pipe === "function") { - return new Promise((resolve, reject) => { - stream.pipe(event.node.res); - if (stream.on) { - stream.on("end", () => { - event.node.res.end(); - resolve(); - }); - stream.on("error", (error) => { - reject(error); - }); - } - event.node.res.on("close", () => { - if (stream.abort) { - stream.abort(); - } - }); - }); - } - throw new Error("[h3] Invalid or incompatible stream provided."); -} -function sendWebResponse(event, response) { - for (const [key, value] of response.headers) { - if (key === "set-cookie") { - event.node.res.appendHeader(key, splitCookiesString(value)); - } else { - event.node.res.setHeader(key, value); - } - } - if (response.status) { - event.node.res.statusCode = sanitizeStatusCode( - response.status, - event.node.res.statusCode - ); - } - if (response.statusText) { - event.node.res.statusMessage = sanitizeStatusMessage(response.statusText); - } - if (response.redirected) { - event.node.res.setHeader("location", response.url); - } - if (!response.body) { - event.node.res.end(); - return; - } - return sendStream(event, response.body); -} - -const PayloadMethods = /* @__PURE__ */ new Set(["PATCH", "POST", "PUT", "DELETE"]); -const ignoredHeaders = /* @__PURE__ */ new Set([ - "transfer-encoding", - "connection", - "keep-alive", - "upgrade", - "expect", - "host", - "accept" -]); -async function proxyRequest(event, target, opts = {}) { - let body; - let duplex; - if (PayloadMethods.has(event.method)) { - if (opts.streamRequest) { - body = getRequestWebStream(event); - duplex = "half"; - } else { - body = await readRawBody(event, false).catch(() => void 0); - } - } - const method = opts.fetchOptions?.method || event.method; - const fetchHeaders = mergeHeaders$1( - getProxyRequestHeaders(event), - opts.fetchOptions?.headers, - opts.headers - ); - return sendProxy(event, target, { - ...opts, - fetchOptions: { - method, - body, - duplex, - ...opts.fetchOptions, - headers: fetchHeaders - } - }); -} -async function sendProxy(event, target, opts = {}) { - let response; - try { - response = await _getFetch(opts.fetch)(target, { - headers: opts.headers, - ignoreResponseError: true, - // make $ofetch.raw transparent - ...opts.fetchOptions - }); - } catch (error) { - throw createError({ - status: 502, - statusMessage: "Bad Gateway", - cause: error - }); - } - event.node.res.statusCode = sanitizeStatusCode( - response.status, - event.node.res.statusCode - ); - event.node.res.statusMessage = sanitizeStatusMessage(response.statusText); - const cookies = []; - for (const [key, value] of response.headers.entries()) { - if (key === "content-encoding") { - continue; - } - if (key === "content-length") { - continue; - } - if (key === "set-cookie") { - cookies.push(...splitCookiesString(value)); - continue; - } - event.node.res.setHeader(key, value); - } - if (cookies.length > 0) { - event.node.res.setHeader( - "set-cookie", - cookies.map((cookie) => { - if (opts.cookieDomainRewrite) { - cookie = rewriteCookieProperty( - cookie, - opts.cookieDomainRewrite, - "domain" - ); - } - if (opts.cookiePathRewrite) { - cookie = rewriteCookieProperty( - cookie, - opts.cookiePathRewrite, - "path" - ); - } - return cookie; - }) - ); - } - if (opts.onResponse) { - await opts.onResponse(event, response); - } - if (response._data !== void 0) { - return response._data; - } - if (event.handled) { - return; - } - if (opts.sendStream === false) { - const data = new Uint8Array(await response.arrayBuffer()); - return event.node.res.end(data); - } - if (response.body) { - for await (const chunk of response.body) { - event.node.res.write(chunk); - } - } - return event.node.res.end(); -} -function getProxyRequestHeaders(event) { - const headers = /* @__PURE__ */ Object.create(null); - const reqHeaders = getRequestHeaders(event); - for (const name in reqHeaders) { - if (!ignoredHeaders.has(name)) { - headers[name] = reqHeaders[name]; - } - } - return headers; -} -function fetchWithEvent(event, req, init, options) { - return _getFetch(options?.fetch)(req, { - ...init, - context: init?.context || event.context, - headers: { - ...getProxyRequestHeaders(event), - ...init?.headers - } - }); -} -function _getFetch(_fetch) { - if (_fetch) { - return _fetch; - } - if (globalThis.fetch) { - return globalThis.fetch; - } - throw new Error( - "fetch is not available. Try importing `node-fetch-native/polyfill` for Node.js." - ); -} -function rewriteCookieProperty(header, map, property) { - const _map = typeof map === "string" ? { "*": map } : map; - return header.replace( - new RegExp(`(;\\s*${property}=)([^;]+)`, "gi"), - (match, prefix, previousValue) => { - let newValue; - if (previousValue in _map) { - newValue = _map[previousValue]; - } else if ("*" in _map) { - newValue = _map["*"]; - } else { - return match; - } - return newValue ? prefix + newValue : ""; - } - ); -} -function mergeHeaders$1(defaults, ...inputs) { - const _inputs = inputs.filter(Boolean); - if (_inputs.length === 0) { - return defaults; - } - const merged = new Headers(defaults); - for (const input of _inputs) { - for (const [key, value] of Object.entries(input)) { - if (value !== void 0) { - merged.set(key, value); - } - } - } - return merged; -} - -var __defProp = Object.defineProperty; -var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; -var __publicField = (obj, key, value) => { - __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); - return value; -}; -class H3Event { - constructor(req, res) { - __publicField(this, "__is_event__", true); - // Context - __publicField(this, "node"); - // Node - __publicField(this, "web"); - // Web - __publicField(this, "context", {}); - // Shared - // Request - __publicField(this, "_method"); - __publicField(this, "_path"); - __publicField(this, "_headers"); - __publicField(this, "_requestBody"); - // Response - __publicField(this, "_handled", false); - // Hooks - __publicField(this, "_onBeforeResponseCalled"); - __publicField(this, "_onAfterResponseCalled"); - this.node = { req, res }; - } - // --- Request --- - get method() { - if (!this._method) { - this._method = (this.node.req.method || "GET").toUpperCase(); - } - return this._method; - } - get path() { - return this._path || this.node.req.url || "/"; - } - get headers() { - if (!this._headers) { - this._headers = _normalizeNodeHeaders(this.node.req.headers); - } - return this._headers; - } - // --- Respoonse --- - get handled() { - return this._handled || this.node.res.writableEnded || this.node.res.headersSent; - } - respondWith(response) { - return Promise.resolve(response).then( - (_response) => sendWebResponse(this, _response) - ); - } - // --- Utils --- - toString() { - return `[${this.method}] ${this.path}`; - } - toJSON() { - return this.toString(); - } - // --- Deprecated --- - /** @deprecated Please use `event.node.req` instead. */ - get req() { - return this.node.req; - } - /** @deprecated Please use `event.node.res` instead. */ - get res() { - return this.node.res; - } -} -function isEvent(input) { - return hasProp(input, "__is_event__"); -} -function createEvent(req, res) { - return new H3Event(req, res); -} -function _normalizeNodeHeaders(nodeHeaders) { - const headers = new Headers(); - for (const [name, value] of Object.entries(nodeHeaders)) { - if (Array.isArray(value)) { - for (const item of value) { - headers.append(name, item); - } - } else if (value) { - headers.set(name, value); - } - } - return headers; -} - -function defineEventHandler(handler) { - if (typeof handler === "function") { - handler.__is_handler__ = true; - return handler; - } - const _hooks = { - onRequest: _normalizeArray(handler.onRequest), - onBeforeResponse: _normalizeArray(handler.onBeforeResponse) - }; - const _handler = (event) => { - return _callHandler(event, handler.handler, _hooks); - }; - _handler.__is_handler__ = true; - _handler.__resolve__ = handler.handler.__resolve__; - _handler.__websocket__ = handler.websocket; - return _handler; -} -function _normalizeArray(input) { - return input ? Array.isArray(input) ? input : [input] : void 0; -} -async function _callHandler(event, handler, hooks) { - if (hooks.onRequest) { - for (const hook of hooks.onRequest) { - await hook(event); - if (event.handled) { - return; - } - } - } - const body = await handler(event); - const response = { body }; - if (hooks.onBeforeResponse) { - for (const hook of hooks.onBeforeResponse) { - await hook(event, response); - } - } - return response.body; -} -const eventHandler = defineEventHandler; -function isEventHandler(input) { - return hasProp(input, "__is_handler__"); -} -function toEventHandler(input, _, _route) { - if (!isEventHandler(input)) { - console.warn( - "[h3] Implicit event handler conversion is deprecated. Use `eventHandler()` or `fromNodeMiddleware()` to define event handlers.", - _route && _route !== "/" ? ` - Route: ${_route}` : "", - ` - Handler: ${input}` - ); - } - return input; -} -function defineLazyEventHandler(factory) { - let _promise; - let _resolved; - const resolveHandler = () => { - if (_resolved) { - return Promise.resolve(_resolved); - } - if (!_promise) { - _promise = Promise.resolve(factory()).then((r) => { - const handler2 = r.default || r; - if (typeof handler2 !== "function") { - throw new TypeError( - "Invalid lazy handler result. It should be a function:", - handler2 - ); - } - _resolved = { handler: toEventHandler(r.default || r) }; - return _resolved; - }); - } - return _promise; - }; - const handler = eventHandler((event) => { - if (_resolved) { - return _resolved.handler(event); - } - return resolveHandler().then((r) => r.handler(event)); - }); - handler.__resolve__ = resolveHandler; - return handler; -} -const lazyEventHandler = defineLazyEventHandler; - -function createApp(options = {}) { - const stack = []; - const handler = createAppEventHandler(stack, options); - const resolve = createResolver(stack); - handler.__resolve__ = resolve; - const getWebsocket = cachedFn(() => websocketOptions(resolve, options)); - const app = { - // @ts-expect-error - use: (arg1, arg2, arg3) => use(app, arg1, arg2, arg3), - resolve, - handler, - stack, - options, - get websocket() { - return getWebsocket(); - } - }; - return app; -} -function use(app, arg1, arg2, arg3) { - if (Array.isArray(arg1)) { - for (const i of arg1) { - use(app, i, arg2, arg3); - } - } else if (Array.isArray(arg2)) { - for (const i of arg2) { - use(app, arg1, i, arg3); - } - } else if (typeof arg1 === "string") { - app.stack.push( - normalizeLayer({ ...arg3, route: arg1, handler: arg2 }) - ); - } else if (typeof arg1 === "function") { - app.stack.push(normalizeLayer({ ...arg2, handler: arg1 })); - } else { - app.stack.push(normalizeLayer({ ...arg1 })); - } - return app; -} -function createAppEventHandler(stack, options) { - const spacing = options.debug ? 2 : void 0; - return eventHandler(async (event) => { - event.node.req.originalUrl = event.node.req.originalUrl || event.node.req.url || "/"; - const _reqPath = event._path || event.node.req.url || "/"; - let _layerPath; - if (options.onRequest) { - await options.onRequest(event); - } - for (const layer of stack) { - if (layer.route.length > 1) { - if (!_reqPath.startsWith(layer.route)) { - continue; - } - _layerPath = _reqPath.slice(layer.route.length) || "/"; - } else { - _layerPath = _reqPath; - } - if (layer.match && !layer.match(_layerPath, event)) { - continue; - } - event._path = _layerPath; - event.node.req.url = _layerPath; - const val = await layer.handler(event); - const _body = val === void 0 ? void 0 : await val; - if (_body !== void 0) { - const _response = { body: _body }; - if (options.onBeforeResponse) { - event._onBeforeResponseCalled = true; - await options.onBeforeResponse(event, _response); - } - await handleHandlerResponse(event, _response.body, spacing); - if (options.onAfterResponse) { - event._onAfterResponseCalled = true; - await options.onAfterResponse(event, _response); - } - return; - } - if (event.handled) { - if (options.onAfterResponse) { - event._onAfterResponseCalled = true; - await options.onAfterResponse(event, void 0); - } - return; - } - } - if (!event.handled) { - throw createError({ - statusCode: 404, - statusMessage: `Cannot find any path matching ${event.path || "/"}.` - }); - } - if (options.onAfterResponse) { - event._onAfterResponseCalled = true; - await options.onAfterResponse(event, void 0); - } - }); -} -function createResolver(stack) { - return async (path) => { - let _layerPath; - for (const layer of stack) { - if (layer.route === "/" && !layer.handler.__resolve__) { - continue; - } - if (!path.startsWith(layer.route)) { - continue; - } - _layerPath = path.slice(layer.route.length) || "/"; - if (layer.match && !layer.match(_layerPath, void 0)) { - continue; - } - let res = { route: layer.route, handler: layer.handler }; - if (res.handler.__resolve__) { - const _res = await res.handler.__resolve__(_layerPath); - if (!_res) { - continue; - } - res = { - ...res, - ..._res, - route: joinURL(res.route || "/", _res.route || "/") - }; - } - return res; - } - }; -} -function normalizeLayer(input) { - let handler = input.handler; - if (handler.handler) { - handler = handler.handler; - } - if (input.lazy) { - handler = lazyEventHandler(handler); - } else if (!isEventHandler(handler)) { - handler = toEventHandler(handler, void 0, input.route); - } - return { - route: withoutTrailingSlash(input.route), - match: input.match, - handler - }; -} -function handleHandlerResponse(event, val, jsonSpace) { - if (val === null) { - return sendNoContent(event); - } - if (val) { - if (isWebResponse(val)) { - return sendWebResponse(event, val); - } - if (isStream(val)) { - return sendStream(event, val); - } - if (val.buffer) { - return send(event, val); - } - if (val.arrayBuffer && typeof val.arrayBuffer === "function") { - return val.arrayBuffer().then((arrayBuffer) => { - return send(event, Buffer.from(arrayBuffer), val.type); - }); - } - if (val instanceof Error) { - throw createError(val); - } - if (typeof val.end === "function") { - return true; - } - } - const valType = typeof val; - if (valType === "string") { - return send(event, val, MIMES.html); - } - if (valType === "object" || valType === "boolean" || valType === "number") { - return send(event, JSON.stringify(val, void 0, jsonSpace), MIMES.json); - } - if (valType === "bigint") { - return send(event, val.toString(), MIMES.json); - } - throw createError({ - statusCode: 500, - statusMessage: `[h3] Cannot send ${valType} as response.` - }); -} -function cachedFn(fn) { - let cache; - return () => { - if (!cache) { - cache = fn(); - } - return cache; - }; -} -function websocketOptions(evResolver, appOptions) { - return { - ...appOptions.websocket, - async resolve(info) { - const url = info.request?.url || info.url || "/"; - const { pathname } = typeof url === "string" ? parseURL(url) : url; - const resolved = await evResolver(pathname); - return resolved?.handler?.__websocket__ || {}; - } - }; -} - -const RouterMethods = [ - "connect", - "delete", - "get", - "head", - "options", - "post", - "put", - "trace", - "patch" -]; -function createRouter(opts = {}) { - const _router = createRouter$1({}); - const routes = {}; - let _matcher; - const router = {}; - const addRoute = (path, handler, method) => { - let route = routes[path]; - if (!route) { - routes[path] = route = { path, handlers: {} }; - _router.insert(path, route); - } - if (Array.isArray(method)) { - for (const m of method) { - addRoute(path, handler, m); - } - } else { - route.handlers[method] = toEventHandler(handler, void 0, path); - } - return router; - }; - router.use = router.add = (path, handler, method) => addRoute(path, handler, method || "all"); - for (const method of RouterMethods) { - router[method] = (path, handle) => router.add(path, handle, method); - } - const matchHandler = (path = "/", method = "get") => { - const qIndex = path.indexOf("?"); - if (qIndex !== -1) { - path = path.slice(0, Math.max(0, qIndex)); - } - const matched = _router.lookup(path); - if (!matched || !matched.handlers) { - return { - error: createError({ - statusCode: 404, - name: "Not Found", - statusMessage: `Cannot find any route matching ${path || "/"}.` - }) - }; - } - let handler = matched.handlers[method] || matched.handlers.all; - if (!handler) { - if (!_matcher) { - _matcher = toRouteMatcher(_router); - } - const _matches = _matcher.matchAll(path).reverse(); - for (const _match of _matches) { - if (_match.handlers[method]) { - handler = _match.handlers[method]; - matched.handlers[method] = matched.handlers[method] || handler; - break; - } - if (_match.handlers.all) { - handler = _match.handlers.all; - matched.handlers.all = matched.handlers.all || handler; - break; - } - } - } - if (!handler) { - return { - error: createError({ - statusCode: 405, - name: "Method Not Allowed", - statusMessage: `Method ${method} is not allowed on this route.` - }) - }; - } - return { matched, handler }; - }; - const isPreemptive = opts.preemptive || opts.preemtive; - router.handler = eventHandler((event) => { - const match = matchHandler( - event.path, - event.method.toLowerCase() - ); - if ("error" in match) { - if (isPreemptive) { - throw match.error; - } else { - return; - } - } - event.context.matchedRoute = match.matched; - const params = match.matched.params || {}; - event.context.params = params; - return Promise.resolve(match.handler(event)).then((res) => { - if (res === void 0 && isPreemptive) { - return null; - } - return res; - }); - }); - router.handler.__resolve__ = async (path) => { - path = withLeadingSlash(path); - const match = matchHandler(path); - if ("error" in match) { - return; - } - let res = { - route: match.matched.path, - handler: match.handler - }; - if (match.handler.__resolve__) { - const _res = await match.handler.__resolve__(path); - if (!_res) { - return; - } - res = { ...res, ..._res }; - } - return res; - }; - return router; -} -function toNodeListener(app) { - const toNodeHandle = async function(req, res) { - const event = createEvent(req, res); - try { - await app.handler(event); - } catch (_error) { - const error = createError(_error); - if (!isError(_error)) { - error.unhandled = true; - } - setResponseStatus(event, error.statusCode, error.statusMessage); - if (app.options.onError) { - await app.options.onError(error, event); - } - if (event.handled) { - return; - } - if (error.unhandled || error.fatal) { - console.error("[h3]", error.fatal ? "[fatal]" : "[unhandled]", error); - } - if (app.options.onBeforeResponse && !event._onBeforeResponseCalled) { - await app.options.onBeforeResponse(event, { body: error }); - } - await sendError(event, error, !!app.options.debug); - if (app.options.onAfterResponse && !event._onAfterResponseCalled) { - await app.options.onAfterResponse(event, { body: error }); - } - } - }; - return toNodeHandle; -} - -function flatHooks(configHooks, hooks = {}, parentName) { - for (const key in configHooks) { - const subHook = configHooks[key]; - const name = parentName ? `${parentName}:${key}` : key; - if (typeof subHook === "object" && subHook !== null) { - flatHooks(subHook, hooks, name); - } else if (typeof subHook === "function") { - hooks[name] = subHook; - } - } - return hooks; -} -const defaultTask = { run: (function_) => function_() }; -const _createTask = () => defaultTask; -const createTask = typeof console.createTask !== "undefined" ? console.createTask : _createTask; -function serialTaskCaller(hooks, args) { - const name = args.shift(); - const task = createTask(name); - return hooks.reduce( - (promise, hookFunction) => promise.then(() => task.run(() => hookFunction(...args))), - Promise.resolve() - ); -} -function parallelTaskCaller(hooks, args) { - const name = args.shift(); - const task = createTask(name); - return Promise.all(hooks.map((hook) => task.run(() => hook(...args)))); -} -function callEachWith(callbacks, arg0) { - for (const callback of [...callbacks]) { - callback(arg0); - } -} - -class Hookable { - constructor() { - this._hooks = {}; - this._before = void 0; - this._after = void 0; - this._deprecatedMessages = void 0; - this._deprecatedHooks = {}; - this.hook = this.hook.bind(this); - this.callHook = this.callHook.bind(this); - this.callHookWith = this.callHookWith.bind(this); - } - hook(name, function_, options = {}) { - if (!name || typeof function_ !== "function") { - return () => { - }; - } - const originalName = name; - let dep; - while (this._deprecatedHooks[name]) { - dep = this._deprecatedHooks[name]; - name = dep.to; - } - if (dep && !options.allowDeprecated) { - let message = dep.message; - if (!message) { - message = `${originalName} hook has been deprecated` + (dep.to ? `, please use ${dep.to}` : ""); - } - if (!this._deprecatedMessages) { - this._deprecatedMessages = /* @__PURE__ */ new Set(); - } - if (!this._deprecatedMessages.has(message)) { - console.warn(message); - this._deprecatedMessages.add(message); - } - } - if (!function_.name) { - try { - Object.defineProperty(function_, "name", { - get: () => "_" + name.replace(/\W+/g, "_") + "_hook_cb", - configurable: true - }); - } catch { - } - } - this._hooks[name] = this._hooks[name] || []; - this._hooks[name].push(function_); - return () => { - if (function_) { - this.removeHook(name, function_); - function_ = void 0; - } - }; - } - hookOnce(name, function_) { - let _unreg; - let _function = (...arguments_) => { - if (typeof _unreg === "function") { - _unreg(); - } - _unreg = void 0; - _function = void 0; - return function_(...arguments_); - }; - _unreg = this.hook(name, _function); - return _unreg; - } - removeHook(name, function_) { - if (this._hooks[name]) { - const index = this._hooks[name].indexOf(function_); - if (index !== -1) { - this._hooks[name].splice(index, 1); - } - if (this._hooks[name].length === 0) { - delete this._hooks[name]; - } - } - } - deprecateHook(name, deprecated) { - this._deprecatedHooks[name] = typeof deprecated === "string" ? { to: deprecated } : deprecated; - const _hooks = this._hooks[name] || []; - delete this._hooks[name]; - for (const hook of _hooks) { - this.hook(name, hook); - } - } - deprecateHooks(deprecatedHooks) { - Object.assign(this._deprecatedHooks, deprecatedHooks); - for (const name in deprecatedHooks) { - this.deprecateHook(name, deprecatedHooks[name]); - } - } - addHooks(configHooks) { - const hooks = flatHooks(configHooks); - const removeFns = Object.keys(hooks).map( - (key) => this.hook(key, hooks[key]) - ); - return () => { - for (const unreg of removeFns.splice(0, removeFns.length)) { - unreg(); - } - }; - } - removeHooks(configHooks) { - const hooks = flatHooks(configHooks); - for (const key in hooks) { - this.removeHook(key, hooks[key]); - } - } - removeAllHooks() { - for (const key in this._hooks) { - delete this._hooks[key]; - } - } - callHook(name, ...arguments_) { - arguments_.unshift(name); - return this.callHookWith(serialTaskCaller, name, ...arguments_); - } - callHookParallel(name, ...arguments_) { - arguments_.unshift(name); - return this.callHookWith(parallelTaskCaller, name, ...arguments_); - } - callHookWith(caller, name, ...arguments_) { - const event = this._before || this._after ? { name, args: arguments_, context: {} } : void 0; - if (this._before) { - callEachWith(this._before, event); - } - const result = caller( - name in this._hooks ? [...this._hooks[name]] : [], - arguments_ - ); - if (result instanceof Promise) { - return result.finally(() => { - if (this._after && event) { - callEachWith(this._after, event); - } - }); - } - if (this._after && event) { - callEachWith(this._after, event); - } - return result; - } - beforeEach(function_) { - this._before = this._before || []; - this._before.push(function_); - return () => { - if (this._before !== void 0) { - const index = this._before.indexOf(function_); - if (index !== -1) { - this._before.splice(index, 1); - } - } - }; - } - afterEach(function_) { - this._after = this._after || []; - this._after.push(function_); - return () => { - if (this._after !== void 0) { - const index = this._after.indexOf(function_); - if (index !== -1) { - this._after.splice(index, 1); - } - } - }; - } -} -function createHooks() { - return new Hookable(); -} - -const s=globalThis.Headers,i=globalThis.AbortController,l=globalThis.fetch||(()=>{throw new Error("[node-fetch-native] Failed to fetch: `globalThis.fetch` is not available!")}); - -class FetchError extends Error { - constructor(message, opts) { - super(message, opts); - this.name = "FetchError"; - if (opts?.cause && !this.cause) { - this.cause = opts.cause; - } - } -} -function createFetchError(ctx) { - const errorMessage = ctx.error?.message || ctx.error?.toString() || ""; - const method = ctx.request?.method || ctx.options?.method || "GET"; - const url = ctx.request?.url || String(ctx.request) || "/"; - const requestStr = `[${method}] ${JSON.stringify(url)}`; - const statusStr = ctx.response ? `${ctx.response.status} ${ctx.response.statusText}` : ""; - const message = `${requestStr}: ${statusStr}${errorMessage ? ` ${errorMessage}` : ""}`; - const fetchError = new FetchError( - message, - ctx.error ? { cause: ctx.error } : void 0 - ); - for (const key of ["request", "options", "response"]) { - Object.defineProperty(fetchError, key, { - get() { - return ctx[key]; - } - }); - } - for (const [key, refKey] of [ - ["data", "_data"], - ["status", "status"], - ["statusCode", "status"], - ["statusText", "statusText"], - ["statusMessage", "statusText"] - ]) { - Object.defineProperty(fetchError, key, { - get() { - return ctx.response && ctx.response[refKey]; - } - }); - } - return fetchError; -} - -const payloadMethods = new Set( - Object.freeze(["PATCH", "POST", "PUT", "DELETE"]) -); -function isPayloadMethod(method = "GET") { - return payloadMethods.has(method.toUpperCase()); -} -function isJSONSerializable(value) { - if (value === void 0) { - return false; - } - const t = typeof value; - if (t === "string" || t === "number" || t === "boolean" || t === null) { - return true; - } - if (t !== "object") { - return false; - } - if (Array.isArray(value)) { - return true; - } - if (value.buffer) { - return false; - } - return value.constructor && value.constructor.name === "Object" || typeof value.toJSON === "function"; -} -const textTypes = /* @__PURE__ */ new Set([ - "image/svg", - "application/xml", - "application/xhtml", - "application/html" -]); -const JSON_RE = /^application\/(?:[\w!#$%&*.^`~-]*\+)?json(;.+)?$/i; -function detectResponseType(_contentType = "") { - if (!_contentType) { - return "json"; - } - const contentType = _contentType.split(";").shift() || ""; - if (JSON_RE.test(contentType)) { - return "json"; - } - if (textTypes.has(contentType) || contentType.startsWith("text/")) { - return "text"; - } - return "blob"; -} -function resolveFetchOptions(request, input, defaults, Headers) { - const headers = mergeHeaders( - input?.headers ?? request?.headers, - defaults?.headers, - Headers - ); - let query; - if (defaults?.query || defaults?.params || input?.params || input?.query) { - query = { - ...defaults?.params, - ...defaults?.query, - ...input?.params, - ...input?.query - }; - } - return { - ...defaults, - ...input, - query, - params: query, - headers - }; -} -function mergeHeaders(input, defaults, Headers) { - if (!defaults) { - return new Headers(input); - } - const headers = new Headers(defaults); - if (input) { - for (const [key, value] of Symbol.iterator in input || Array.isArray(input) ? input : new Headers(input)) { - headers.set(key, value); - } - } - return headers; -} -async function callHooks(context, hooks) { - if (hooks) { - if (Array.isArray(hooks)) { - for (const hook of hooks) { - await hook(context); - } - } else { - await hooks(context); - } - } -} - -const retryStatusCodes = /* @__PURE__ */ new Set([ - 408, - // Request Timeout - 409, - // Conflict - 425, - // Too Early (Experimental) - 429, - // Too Many Requests - 500, - // Internal Server Error - 502, - // Bad Gateway - 503, - // Service Unavailable - 504 - // Gateway Timeout -]); -const nullBodyResponses$1 = /* @__PURE__ */ new Set([101, 204, 205, 304]); -function createFetch$1(globalOptions = {}) { - const { - fetch = globalThis.fetch, - Headers = globalThis.Headers, - AbortController = globalThis.AbortController - } = globalOptions; - async function onError(context) { - const isAbort = context.error && context.error.name === "AbortError" && !context.options.timeout || false; - if (context.options.retry !== false && !isAbort) { - let retries; - if (typeof context.options.retry === "number") { - retries = context.options.retry; - } else { - retries = isPayloadMethod(context.options.method) ? 0 : 1; - } - const responseCode = context.response && context.response.status || 500; - if (retries > 0 && (Array.isArray(context.options.retryStatusCodes) ? context.options.retryStatusCodes.includes(responseCode) : retryStatusCodes.has(responseCode))) { - const retryDelay = typeof context.options.retryDelay === "function" ? context.options.retryDelay(context) : context.options.retryDelay || 0; - if (retryDelay > 0) { - await new Promise((resolve) => setTimeout(resolve, retryDelay)); - } - return $fetchRaw(context.request, { - ...context.options, - retry: retries - 1 - }); - } - } - const error = createFetchError(context); - if (Error.captureStackTrace) { - Error.captureStackTrace(error, $fetchRaw); - } - throw error; - } - const $fetchRaw = async function $fetchRaw2(_request, _options = {}) { - const context = { - request: _request, - options: resolveFetchOptions( - _request, - _options, - globalOptions.defaults, - Headers - ), - response: void 0, - error: void 0 - }; - if (context.options.method) { - context.options.method = context.options.method.toUpperCase(); - } - if (context.options.onRequest) { - await callHooks(context, context.options.onRequest); - } - if (typeof context.request === "string") { - if (context.options.baseURL) { - context.request = withBase(context.request, context.options.baseURL); - } - if (context.options.query) { - context.request = withQuery(context.request, context.options.query); - delete context.options.query; - } - if ("query" in context.options) { - delete context.options.query; - } - if ("params" in context.options) { - delete context.options.params; - } - } - if (context.options.body && isPayloadMethod(context.options.method)) { - if (isJSONSerializable(context.options.body)) { - context.options.body = typeof context.options.body === "string" ? context.options.body : JSON.stringify(context.options.body); - context.options.headers = new Headers(context.options.headers || {}); - if (!context.options.headers.has("content-type")) { - context.options.headers.set("content-type", "application/json"); - } - if (!context.options.headers.has("accept")) { - context.options.headers.set("accept", "application/json"); - } - } else if ( - // ReadableStream Body - "pipeTo" in context.options.body && typeof context.options.body.pipeTo === "function" || // Node.js Stream Body - typeof context.options.body.pipe === "function" - ) { - if (!("duplex" in context.options)) { - context.options.duplex = "half"; - } - } - } - let abortTimeout; - if (!context.options.signal && context.options.timeout) { - const controller = new AbortController(); - abortTimeout = setTimeout(() => { - const error = new Error( - "[TimeoutError]: The operation was aborted due to timeout" - ); - error.name = "TimeoutError"; - error.code = 23; - controller.abort(error); - }, context.options.timeout); - context.options.signal = controller.signal; - } - try { - context.response = await fetch( - context.request, - context.options - ); - } catch (error) { - context.error = error; - if (context.options.onRequestError) { - await callHooks( - context, - context.options.onRequestError - ); - } - return await onError(context); - } finally { - if (abortTimeout) { - clearTimeout(abortTimeout); - } - } - const hasBody = (context.response.body || // https://github.com/unjs/ofetch/issues/324 - // https://github.com/unjs/ofetch/issues/294 - // https://github.com/JakeChampion/fetch/issues/1454 - context.response._bodyInit) && !nullBodyResponses$1.has(context.response.status) && context.options.method !== "HEAD"; - if (hasBody) { - const responseType = (context.options.parseResponse ? "json" : context.options.responseType) || detectResponseType(context.response.headers.get("content-type") || ""); - switch (responseType) { - case "json": { - const data = await context.response.text(); - const parseFunction = context.options.parseResponse || destr$1; - context.response._data = parseFunction(data); - break; - } - case "stream": { - context.response._data = context.response.body || context.response._bodyInit; - break; - } - default: { - context.response._data = await context.response[responseType](); - } - } - } - if (context.options.onResponse) { - await callHooks( - context, - context.options.onResponse - ); - } - if (!context.options.ignoreResponseError && context.response.status >= 400 && context.response.status < 600) { - if (context.options.onResponseError) { - await callHooks( - context, - context.options.onResponseError - ); - } - return await onError(context); - } - return context.response; - }; - const $fetch = async function $fetch2(request, options) { - const r = await $fetchRaw(request, options); - return r._data; - }; - $fetch.raw = $fetchRaw; - $fetch.native = (...args) => fetch(...args); - $fetch.create = (defaultOptions = {}, customGlobalOptions = {}) => createFetch$1({ - ...globalOptions, - ...customGlobalOptions, - defaults: { - ...globalOptions.defaults, - ...customGlobalOptions.defaults, - ...defaultOptions - } - }); - return $fetch; -} - -function createNodeFetch() { - const useKeepAlive = JSON.parse(process.env.FETCH_KEEP_ALIVE || "false"); - if (!useKeepAlive) { - return l; - } - const agentOptions = { keepAlive: true }; - const httpAgent = new http.Agent(agentOptions); - const httpsAgent = new https.Agent(agentOptions); - const nodeFetchOptions = { - agent(parsedURL) { - return parsedURL.protocol === "http:" ? httpAgent : httpsAgent; - } - }; - return function nodeFetchWithKeepAlive(input, init) { - return l(input, { ...nodeFetchOptions, ...init }); - }; -} -const fetch = globalThis.fetch ? (...args) => globalThis.fetch(...args) : createNodeFetch(); -const Headers$1 = globalThis.Headers || s; -const AbortController = globalThis.AbortController || i; -createFetch$1({ fetch, Headers: Headers$1, AbortController }); - -const nullBodyResponses = /* @__PURE__ */ new Set([101, 204, 205, 304]); -function createCall(handle) { - return function callHandle(context) { - const req = new IncomingMessage(); - const res = new ServerResponse(req); - req.url = context.url || "/"; - req.method = context.method || "GET"; - req.headers = {}; - if (context.headers) { - const headerEntries = typeof context.headers.entries === "function" ? context.headers.entries() : Object.entries(context.headers); - for (const [name, value] of headerEntries) { - if (!value) { - continue; - } - req.headers[name.toLowerCase()] = value; - } - } - req.headers.host = req.headers.host || context.host || "localhost"; - req.connection.encrypted = // @ts-ignore - req.connection.encrypted || context.protocol === "https"; - req.body = context.body || null; - req.__unenv__ = context.context; - return handle(req, res).then(() => { - let body = res._data; - if (nullBodyResponses.has(res.statusCode) || req.method.toUpperCase() === "HEAD") { - body = null; - delete res._headers["content-length"]; - } - const r = { - body, - headers: res._headers, - status: res.statusCode, - statusText: res.statusMessage - }; - req.destroy(); - res.destroy(); - return r; - }); - }; -} - -function createFetch(call, _fetch = global.fetch) { - return async function ufetch(input, init) { - const url = input.toString(); - if (!url.startsWith("/")) { - return _fetch(url, init); - } - try { - const r = await call({ url, ...init }); - return new Response(r.body, { - status: r.status, - statusText: r.statusText, - headers: Object.fromEntries( - Object.entries(r.headers).map(([name, value]) => [ - name, - Array.isArray(value) ? value.join(",") : String(value) || "" - ]) - ) - }); - } catch (error) { - return new Response(error.toString(), { - status: Number.parseInt(error.statusCode || error.code) || 500, - statusText: error.statusText - }); - } - }; -} - -function hasReqHeader(event, name, includes) { - const value = getRequestHeader(event, name); - return value && typeof value === "string" && value.toLowerCase().includes(includes); -} -function isJsonRequest(event) { - if (hasReqHeader(event, "accept", "text/html")) { - return false; - } - return hasReqHeader(event, "accept", "application/json") || hasReqHeader(event, "user-agent", "curl/") || hasReqHeader(event, "user-agent", "httpie/") || hasReqHeader(event, "sec-fetch-mode", "cors") || event.path.startsWith("/api/") || event.path.endsWith(".json"); -} -function normalizeError(error, isDev) { - const cwd = typeof process.cwd === "function" ? process.cwd() : "/"; - const stack = (error.unhandled || error.fatal) ? [] : (error.stack || "").split("\n").splice(1).filter((line) => line.includes("at ")).map((line) => { - const text = line.replace(cwd + "/", "./").replace("webpack:/", "").replace("file://", "").trim(); - return { - text, - internal: line.includes("node_modules") && !line.includes(".cache") || line.includes("internal") || line.includes("new Promise") - }; - }); - const statusCode = error.statusCode || 500; - const statusMessage = error.statusMessage ?? (statusCode === 404 ? "Not Found" : ""); - const message = error.unhandled ? "internal server error" : error.message || error.toString(); - return { - stack, - statusCode, - statusMessage, - message - }; -} -function joinHeaders(value) { - return Array.isArray(value) ? value.join(", ") : String(value); -} -function normalizeFetchResponse(response) { - if (!response.headers.has("set-cookie")) { - return response; - } - return new Response(response.body, { - status: response.status, - statusText: response.statusText, - headers: normalizeCookieHeaders(response.headers) - }); -} -function normalizeCookieHeader(header = "") { - return splitCookiesString(joinHeaders(header)); -} -function normalizeCookieHeaders(headers) { - const outgoingHeaders = new Headers(); - for (const [name, header] of headers) { - if (name === "set-cookie") { - for (const cookie of normalizeCookieHeader(header)) { - outgoingHeaders.append("set-cookie", cookie); - } - } else { - outgoingHeaders.set(name, joinHeaders(header)); - } - } - return outgoingHeaders; -} -function toBuffer(data) { - if ("pipeTo" in data && typeof data.pipeTo === "function") { - return new Promise((resolve, reject) => { - const chunks = []; - data.pipeTo( - new WritableStream({ - write(chunk) { - chunks.push(chunk); - }, - close() { - resolve(Buffer.concat(chunks)); - }, - abort(reason) { - reject(reason); - } - }) - ).catch(reject); - }); - } - if ("pipe" in data && typeof data.pipe === "function") { - return new Promise((resolve, reject) => { - const chunks = []; - data.on("data", (chunk) => { - chunks.push(chunk); - }).on("end", () => { - resolve(Buffer.concat(chunks)); - }).on("error", reject); - }); - } - return Buffer.from(data); -} - -function defineNitroErrorHandler(handler) { - return handler; -} -const errorHandler = defineNitroErrorHandler( - function defaultNitroErrorHandler(error, event) { - const { stack, statusCode, statusMessage, message } = normalizeError( - error); - const errorObject = { - url: event.path || "", - statusCode, - statusMessage, - message, - stack: void 0 - }; - if (error.unhandled || error.fatal) { - const tags = [ - "[nitro]", - "[request error]", - error.unhandled && "[unhandled]", - error.fatal && "[fatal]" - ].filter(Boolean).join(" "); - console.error( - tags, - error.message + "\n" + stack.map((l) => " " + l.text).join(" \n") - ); - } - setResponseStatus(event, statusCode, statusMessage); - if (isJsonRequest(event)) { - setResponseHeader(event, "Content-Type", "application/json"); - return send(event, JSON.stringify(errorObject)); - } - setResponseHeader(event, "Content-Type", "text/html"); - return send(event, renderHTMLError(errorObject)); - } -); -function renderHTMLError(error) { - const statusCode = error.statusCode || 500; - const statusMessage = error.statusMessage || "Request Error"; - return ` - - - - - ${statusCode} ${statusMessage} - - - -
- -
-
-

${statusCode} ${statusMessage}

-
- - ${error.message}

- ${"\n" + (error.stack || []).map((i) => `  ${i}`).join("
")} -
- -
-
-
- - -`; -} - -const plugins = [ - -]; - -const _lazy_ZP4wpu = () => import('./chunks/routes/index.mjs'); - -const handlers = [ - { route: '/', handler: _lazy_ZP4wpu, lazy: true, middleware: false, method: undefined } -]; - -const storageKeyProperties = [ - "hasItem", - "getItem", - "getItemRaw", - "setItem", - "setItemRaw", - "removeItem", - "getMeta", - "setMeta", - "removeMeta", - "getKeys", - "clear", - "mount", - "unmount" -]; -function prefixStorage(storage, base) { - base = normalizeBaseKey$1(base); - if (!base) { - return storage; - } - const nsStorage = { ...storage }; - for (const property of storageKeyProperties) { - nsStorage[property] = (key = "", ...args) => ( - // @ts-ignore - storage[property](base + key, ...args) - ); - } - nsStorage.getKeys = (key = "", ...arguments_) => storage.getKeys(base + key, ...arguments_).then((keys) => keys.map((key2) => key2.slice(base.length))); - return nsStorage; -} -function normalizeKey$2(key) { - if (!key) { - return ""; - } - return key.split("?")[0].replace(/[/\\]/g, ":").replace(/:+/g, ":").replace(/^:|:$/g, ""); -} -function normalizeBaseKey$1(base) { - base = normalizeKey$2(base); - return base ? base + ":" : ""; -} - -const suspectProtoRx = /"(?:_|\\u0{2}5[Ff]){2}(?:p|\\u0{2}70)(?:r|\\u0{2}72)(?:o|\\u0{2}6[Ff])(?:t|\\u0{2}74)(?:o|\\u0{2}6[Ff])(?:_|\\u0{2}5[Ff]){2}"\s*:/; -const suspectConstructorRx = /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/; -const JsonSigRx = /^\s*["[{]|^\s*-?\d{1,16}(\.\d{1,17})?([Ee][+-]?\d+)?\s*$/; -function jsonParseTransform(key, value) { - if (key === "__proto__" || key === "constructor" && value && typeof value === "object" && "prototype" in value) { - warnKeyDropped(key); - return; - } - return value; -} -function warnKeyDropped(key) { - console.warn(`[destr] Dropping "${key}" key to prevent prototype pollution.`); -} -function destr(value, options = {}) { - if (typeof value !== "string") { - return value; - } - const _value = value.trim(); - if ( - // eslint-disable-next-line unicorn/prefer-at - value[0] === '"' && value.endsWith('"') && !value.includes("\\") - ) { - return _value.slice(1, -1); - } - if (_value.length <= 9) { - const _lval = _value.toLowerCase(); - if (_lval === "true") { - return true; - } - if (_lval === "false") { - return false; - } - if (_lval === "undefined") { - return void 0; - } - if (_lval === "null") { - return null; - } - if (_lval === "nan") { - return Number.NaN; - } - if (_lval === "infinity") { - return Number.POSITIVE_INFINITY; - } - if (_lval === "-infinity") { - return Number.NEGATIVE_INFINITY; - } - } - if (!JsonSigRx.test(value)) { - if (options.strict) { - throw new SyntaxError("[destr] Invalid JSON"); - } - return value; - } - try { - if (suspectProtoRx.test(value) || suspectConstructorRx.test(value)) { - if (options.strict) { - throw new Error("[destr] Possible prototype pollution"); - } - return JSON.parse(value, jsonParseTransform); - } - return JSON.parse(value); - } catch (error) { - if (options.strict) { - throw error; - } - return value; - } -} - -function wrapToPromise(value) { - if (!value || typeof value.then !== "function") { - return Promise.resolve(value); - } - return value; -} -function asyncCall(function_, ...arguments_) { - try { - return wrapToPromise(function_(...arguments_)); - } catch (error) { - return Promise.reject(error); - } -} -function isPrimitive(value) { - const type = typeof value; - return value === null || type !== "object" && type !== "function"; -} -function isPureObject(value) { - const proto = Object.getPrototypeOf(value); - return !proto || proto.isPrototypeOf(Object); -} -function stringify(value) { - if (isPrimitive(value)) { - return String(value); - } - if (isPureObject(value) || Array.isArray(value)) { - return JSON.stringify(value); - } - if (typeof value.toJSON === "function") { - return stringify(value.toJSON()); - } - throw new Error("[unstorage] Cannot stringify value!"); -} -function checkBufferSupport() { - if (typeof Buffer === "undefined") { - throw new TypeError("[unstorage] Buffer is not supported!"); - } -} -const BASE64_PREFIX = "base64:"; -function serializeRaw(value) { - if (typeof value === "string") { - return value; - } - checkBufferSupport(); - const base64 = Buffer.from(value).toString("base64"); - return BASE64_PREFIX + base64; -} -function deserializeRaw(value) { - if (typeof value !== "string") { - return value; - } - if (!value.startsWith(BASE64_PREFIX)) { - return value; - } - checkBufferSupport(); - return Buffer.from(value.slice(BASE64_PREFIX.length), "base64"); -} -function normalizeKey$1(key) { - if (!key) { - return ""; - } - return key.split("?")[0].replace(/[/\\]/g, ":").replace(/:+/g, ":").replace(/^:|:$/g, ""); -} -function joinKeys(...keys) { - return normalizeKey$1(keys.join(":")); -} -function normalizeBaseKey(base) { - base = normalizeKey$1(base); - return base ? base + ":" : ""; -} - -function defineDriver(factory) { - return factory; -} - -const DRIVER_NAME = "memory"; -const memory = defineDriver(() => { - const data = /* @__PURE__ */ new Map(); - return { - name: DRIVER_NAME, - getInstance: () => data, - hasItem(key) { - return data.has(key); - }, - getItem(key) { - return data.get(key) ?? null; - }, - getItemRaw(key) { - return data.get(key) ?? null; - }, - setItem(key, value) { - data.set(key, value); - }, - setItemRaw(key, value) { - data.set(key, value); - }, - removeItem(key) { - data.delete(key); - }, - getKeys() { - return [...data.keys()]; - }, - clear() { - data.clear(); - }, - dispose() { - data.clear(); - } - }; -}); - -function createStorage(options = {}) { - const context = { - mounts: { "": options.driver || memory() }, - mountpoints: [""], - watching: false, - watchListeners: [], - unwatch: {} - }; - const getMount = (key) => { - for (const base of context.mountpoints) { - if (key.startsWith(base)) { - return { - base, - relativeKey: key.slice(base.length), - driver: context.mounts[base] - }; - } - } - return { - base: "", - relativeKey: key, - driver: context.mounts[""] - }; - }; - const getMounts = (base, includeParent) => { - return context.mountpoints.filter( - (mountpoint) => mountpoint.startsWith(base) || includeParent && base.startsWith(mountpoint) - ).map((mountpoint) => ({ - relativeBase: base.length > mountpoint.length ? base.slice(mountpoint.length) : void 0, - mountpoint, - driver: context.mounts[mountpoint] - })); - }; - const onChange = (event, key) => { - if (!context.watching) { - return; - } - key = normalizeKey$1(key); - for (const listener of context.watchListeners) { - listener(event, key); - } - }; - const startWatch = async () => { - if (context.watching) { - return; - } - context.watching = true; - for (const mountpoint in context.mounts) { - context.unwatch[mountpoint] = await watch( - context.mounts[mountpoint], - onChange, - mountpoint - ); - } - }; - const stopWatch = async () => { - if (!context.watching) { - return; - } - for (const mountpoint in context.unwatch) { - await context.unwatch[mountpoint](); - } - context.unwatch = {}; - context.watching = false; - }; - const runBatch = (items, commonOptions, cb) => { - const batches = /* @__PURE__ */ new Map(); - const getBatch = (mount) => { - let batch = batches.get(mount.base); - if (!batch) { - batch = { - driver: mount.driver, - base: mount.base, - items: [] - }; - batches.set(mount.base, batch); - } - return batch; - }; - for (const item of items) { - const isStringItem = typeof item === "string"; - const key = normalizeKey$1(isStringItem ? item : item.key); - const value = isStringItem ? void 0 : item.value; - const options2 = isStringItem || !item.options ? commonOptions : { ...commonOptions, ...item.options }; - const mount = getMount(key); - getBatch(mount).items.push({ - key, - value, - relativeKey: mount.relativeKey, - options: options2 - }); - } - return Promise.all([...batches.values()].map((batch) => cb(batch))).then( - (r) => r.flat() - ); - }; - const storage = { - // Item - hasItem(key, opts = {}) { - key = normalizeKey$1(key); - const { relativeKey, driver } = getMount(key); - return asyncCall(driver.hasItem, relativeKey, opts); - }, - getItem(key, opts = {}) { - key = normalizeKey$1(key); - const { relativeKey, driver } = getMount(key); - return asyncCall(driver.getItem, relativeKey, opts).then( - (value) => destr(value) - ); - }, - getItems(items, commonOptions) { - return runBatch(items, commonOptions, (batch) => { - if (batch.driver.getItems) { - return asyncCall( - batch.driver.getItems, - batch.items.map((item) => ({ - key: item.relativeKey, - options: item.options - })), - commonOptions - ).then( - (r) => r.map((item) => ({ - key: joinKeys(batch.base, item.key), - value: destr(item.value) - })) - ); - } - return Promise.all( - batch.items.map((item) => { - return asyncCall( - batch.driver.getItem, - item.relativeKey, - item.options - ).then((value) => ({ - key: item.key, - value: destr(value) - })); - }) - ); - }); - }, - getItemRaw(key, opts = {}) { - key = normalizeKey$1(key); - const { relativeKey, driver } = getMount(key); - if (driver.getItemRaw) { - return asyncCall(driver.getItemRaw, relativeKey, opts); - } - return asyncCall(driver.getItem, relativeKey, opts).then( - (value) => deserializeRaw(value) - ); - }, - async setItem(key, value, opts = {}) { - if (value === void 0) { - return storage.removeItem(key); - } - key = normalizeKey$1(key); - const { relativeKey, driver } = getMount(key); - if (!driver.setItem) { - return; - } - await asyncCall(driver.setItem, relativeKey, stringify(value), opts); - if (!driver.watch) { - onChange("update", key); - } - }, - async setItems(items, commonOptions) { - await runBatch(items, commonOptions, async (batch) => { - if (batch.driver.setItems) { - return asyncCall( - batch.driver.setItems, - batch.items.map((item) => ({ - key: item.relativeKey, - value: stringify(item.value), - options: item.options - })), - commonOptions - ); - } - if (!batch.driver.setItem) { - return; - } - await Promise.all( - batch.items.map((item) => { - return asyncCall( - batch.driver.setItem, - item.relativeKey, - stringify(item.value), - item.options - ); - }) - ); - }); - }, - async setItemRaw(key, value, opts = {}) { - if (value === void 0) { - return storage.removeItem(key, opts); - } - key = normalizeKey$1(key); - const { relativeKey, driver } = getMount(key); - if (driver.setItemRaw) { - await asyncCall(driver.setItemRaw, relativeKey, value, opts); - } else if (driver.setItem) { - await asyncCall(driver.setItem, relativeKey, serializeRaw(value), opts); - } else { - return; - } - if (!driver.watch) { - onChange("update", key); - } - }, - async removeItem(key, opts = {}) { - if (typeof opts === "boolean") { - opts = { removeMeta: opts }; - } - key = normalizeKey$1(key); - const { relativeKey, driver } = getMount(key); - if (!driver.removeItem) { - return; - } - await asyncCall(driver.removeItem, relativeKey, opts); - if (opts.removeMeta || opts.removeMata) { - await asyncCall(driver.removeItem, relativeKey + "$", opts); - } - if (!driver.watch) { - onChange("remove", key); - } - }, - // Meta - async getMeta(key, opts = {}) { - if (typeof opts === "boolean") { - opts = { nativeOnly: opts }; - } - key = normalizeKey$1(key); - const { relativeKey, driver } = getMount(key); - const meta = /* @__PURE__ */ Object.create(null); - if (driver.getMeta) { - Object.assign(meta, await asyncCall(driver.getMeta, relativeKey, opts)); - } - if (!opts.nativeOnly) { - const value = await asyncCall( - driver.getItem, - relativeKey + "$", - opts - ).then((value_) => destr(value_)); - if (value && typeof value === "object") { - if (typeof value.atime === "string") { - value.atime = new Date(value.atime); - } - if (typeof value.mtime === "string") { - value.mtime = new Date(value.mtime); - } - Object.assign(meta, value); - } - } - return meta; - }, - setMeta(key, value, opts = {}) { - return this.setItem(key + "$", value, opts); - }, - removeMeta(key, opts = {}) { - return this.removeItem(key + "$", opts); - }, - // Keys - async getKeys(base, opts = {}) { - base = normalizeBaseKey(base); - const mounts = getMounts(base, true); - let maskedMounts = []; - const allKeys = []; - for (const mount of mounts) { - const rawKeys = await asyncCall( - mount.driver.getKeys, - mount.relativeBase, - opts - ); - for (const key of rawKeys) { - const fullKey = mount.mountpoint + normalizeKey$1(key); - if (!maskedMounts.some((p) => fullKey.startsWith(p))) { - allKeys.push(fullKey); - } - } - maskedMounts = [ - mount.mountpoint, - ...maskedMounts.filter((p) => !p.startsWith(mount.mountpoint)) - ]; - } - return base ? allKeys.filter( - (key) => key.startsWith(base) && key[key.length - 1] !== "$" - ) : allKeys.filter((key) => key[key.length - 1] !== "$"); - }, - // Utils - async clear(base, opts = {}) { - base = normalizeBaseKey(base); - await Promise.all( - getMounts(base, false).map(async (m) => { - if (m.driver.clear) { - return asyncCall(m.driver.clear, m.relativeBase, opts); - } - if (m.driver.removeItem) { - const keys = await m.driver.getKeys(m.relativeBase || "", opts); - return Promise.all( - keys.map((key) => m.driver.removeItem(key, opts)) - ); - } - }) - ); - }, - async dispose() { - await Promise.all( - Object.values(context.mounts).map((driver) => dispose(driver)) - ); - }, - async watch(callback) { - await startWatch(); - context.watchListeners.push(callback); - return async () => { - context.watchListeners = context.watchListeners.filter( - (listener) => listener !== callback - ); - if (context.watchListeners.length === 0) { - await stopWatch(); - } - }; - }, - async unwatch() { - context.watchListeners = []; - await stopWatch(); - }, - // Mount - mount(base, driver) { - base = normalizeBaseKey(base); - if (base && context.mounts[base]) { - throw new Error(`already mounted at ${base}`); - } - if (base) { - context.mountpoints.push(base); - context.mountpoints.sort((a, b) => b.length - a.length); - } - context.mounts[base] = driver; - if (context.watching) { - Promise.resolve(watch(driver, onChange, base)).then((unwatcher) => { - context.unwatch[base] = unwatcher; - }).catch(console.error); - } - return storage; - }, - async unmount(base, _dispose = true) { - base = normalizeBaseKey(base); - if (!base || !context.mounts[base]) { - return; - } - if (context.watching && base in context.unwatch) { - context.unwatch[base](); - delete context.unwatch[base]; - } - if (_dispose) { - await dispose(context.mounts[base]); - } - context.mountpoints = context.mountpoints.filter((key) => key !== base); - delete context.mounts[base]; - }, - getMount(key = "") { - key = normalizeKey$1(key) + ":"; - const m = getMount(key); - return { - driver: m.driver, - base: m.base - }; - }, - getMounts(base = "", opts = {}) { - base = normalizeKey$1(base); - const mounts = getMounts(base, opts.parents); - return mounts.map((m) => ({ - driver: m.driver, - base: m.mountpoint - })); - }, - // Aliases - keys: (base, opts = {}) => storage.getKeys(base, opts), - get: (key, opts = {}) => storage.getItem(key, opts), - set: (key, value, opts = {}) => storage.setItem(key, value, opts), - has: (key, opts = {}) => storage.hasItem(key, opts), - del: (key, opts = {}) => storage.removeItem(key, opts), - remove: (key, opts = {}) => storage.removeItem(key, opts) - }; - return storage; -} -function watch(driver, onChange, base) { - return driver.watch ? driver.watch((event, key) => onChange(event, base + key)) : () => { - }; -} -async function dispose(driver) { - if (typeof driver.dispose === "function") { - await asyncCall(driver.dispose); - } -} - -const _assets = { - -}; - -const normalizeKey = function normalizeKey(key) { - if (!key) { - return ""; - } - return key.split("?")[0].replace(/[/\\]/g, ":").replace(/:+/g, ":").replace(/^:|:$/g, ""); -}; - -const assets = { - getKeys() { - return Promise.resolve(Object.keys(_assets)) - }, - hasItem (id) { - id = normalizeKey(id); - return Promise.resolve(id in _assets) - }, - getItem (id) { - id = normalizeKey(id); - return Promise.resolve(_assets[id] ? _assets[id].import() : null) - }, - getMeta (id) { - id = normalizeKey(id); - return Promise.resolve(_assets[id] ? _assets[id].meta : {}) - } -}; - -const storage = createStorage({}); - -storage.mount('/assets', assets); - -function useStorage(base = "") { - return base ? prefixStorage(storage, base) : storage; -} - -function defaultCacheOptions() { - return { - name: "_", - base: "/cache", - swr: true, - maxAge: 1 - }; -} -function defineCachedFunction(fn, opts = {}) { - opts = { ...defaultCacheOptions(), ...opts }; - const pending = {}; - const group = opts.group || "nitro/functions"; - const name = opts.name || fn.name || "_"; - const integrity = opts.integrity || hash([fn, opts]); - const validate = opts.validate || ((entry) => entry.value !== void 0); - async function get(key, resolver, shouldInvalidateCache, event) { - const cacheKey = [opts.base, group, name, key + ".json"].filter(Boolean).join(":").replace(/:\/$/, ":index"); - let entry = await useStorage().getItem(cacheKey) || {}; - if (typeof entry !== "object") { - entry = {}; - const error = new Error("Malformed data read from cache."); - console.error("[nitro] [cache]", error); - useNitroApp().captureError(error, { event, tags: ["cache"] }); - } - const ttl = (opts.maxAge ?? 0) * 1e3; - if (ttl) { - entry.expires = Date.now() + ttl; - } - const expired = shouldInvalidateCache || entry.integrity !== integrity || ttl && Date.now() - (entry.mtime || 0) > ttl || validate(entry) === false; - const _resolve = async () => { - const isPending = pending[key]; - if (!isPending) { - if (entry.value !== void 0 && (opts.staleMaxAge || 0) >= 0 && opts.swr === false) { - entry.value = void 0; - entry.integrity = void 0; - entry.mtime = void 0; - entry.expires = void 0; - } - pending[key] = Promise.resolve(resolver()); - } - try { - entry.value = await pending[key]; - } catch (error) { - if (!isPending) { - delete pending[key]; - } - throw error; - } - if (!isPending) { - entry.mtime = Date.now(); - entry.integrity = integrity; - delete pending[key]; - if (validate(entry) !== false) { - const promise = useStorage().setItem(cacheKey, entry).catch((error) => { - console.error(`[nitro] [cache] Cache write error.`, error); - useNitroApp().captureError(error, { event, tags: ["cache"] }); - }); - if (event?.waitUntil) { - event.waitUntil(promise); - } - } - } - }; - const _resolvePromise = expired ? _resolve() : Promise.resolve(); - if (entry.value === void 0) { - await _resolvePromise; - } else if (expired && event && event.waitUntil) { - event.waitUntil(_resolvePromise); - } - if (opts.swr && validate(entry) !== false) { - _resolvePromise.catch((error) => { - console.error(`[nitro] [cache] SWR handler error.`, error); - useNitroApp().captureError(error, { event, tags: ["cache"] }); - }); - return entry; - } - return _resolvePromise.then(() => entry); - } - return async (...args) => { - const shouldBypassCache = await opts.shouldBypassCache?.(...args); - if (shouldBypassCache) { - return fn(...args); - } - const key = await (opts.getKey || getKey)(...args); - const shouldInvalidateCache = await opts.shouldInvalidateCache?.(...args); - const entry = await get( - key, - () => fn(...args), - shouldInvalidateCache, - args[0] && isEvent(args[0]) ? args[0] : void 0 - ); - let value = entry.value; - if (opts.transform) { - value = await opts.transform(entry, ...args) || value; - } - return value; - }; -} -function cachedFunction(fn, opts = {}) { - return defineCachedFunction(fn, opts); -} -function getKey(...args) { - return args.length > 0 ? hash(args, {}) : ""; -} -function escapeKey(key) { - return String(key).replace(/\W/g, ""); -} -function defineCachedEventHandler(handler, opts = defaultCacheOptions()) { - const variableHeaderNames = (opts.varies || []).filter(Boolean).map((h) => h.toLowerCase()).sort(); - const _opts = { - ...opts, - getKey: async (event) => { - const customKey = await opts.getKey?.(event); - if (customKey) { - return escapeKey(customKey); - } - const _path = event.node.req.originalUrl || event.node.req.url || event.path; - const _pathname = escapeKey(decodeURI(parseURL(_path).pathname)).slice(0, 16) || "index"; - const _hashedPath = `${_pathname}.${hash(_path)}`; - const _headers = variableHeaderNames.map((header) => [header, event.node.req.headers[header]]).map(([name, value]) => `${escapeKey(name)}.${hash(value)}`); - return [_hashedPath, ..._headers].join(":"); - }, - validate: (entry) => { - if (!entry.value) { - return false; - } - if (entry.value.code >= 400) { - return false; - } - if (entry.value.body === void 0) { - return false; - } - if (entry.value.headers.etag === "undefined" || entry.value.headers["last-modified"] === "undefined") { - return false; - } - return true; - }, - group: opts.group || "nitro/handlers", - integrity: opts.integrity || hash([handler, opts]) - }; - const _cachedHandler = cachedFunction( - async (incomingEvent) => { - const variableHeaders = {}; - for (const header of variableHeaderNames) { - const value = incomingEvent.node.req.headers[header]; - if (value !== void 0) { - variableHeaders[header] = value; - } - } - const reqProxy = cloneWithProxy(incomingEvent.node.req, { - headers: variableHeaders - }); - const resHeaders = {}; - let _resSendBody; - const resProxy = cloneWithProxy(incomingEvent.node.res, { - statusCode: 200, - writableEnded: false, - writableFinished: false, - headersSent: false, - closed: false, - getHeader(name) { - return resHeaders[name]; - }, - setHeader(name, value) { - resHeaders[name] = value; - return this; - }, - getHeaderNames() { - return Object.keys(resHeaders); - }, - hasHeader(name) { - return name in resHeaders; - }, - removeHeader(name) { - delete resHeaders[name]; - }, - getHeaders() { - return resHeaders; - }, - end(chunk, arg2, arg3) { - if (typeof chunk === "string") { - _resSendBody = chunk; - } - if (typeof arg2 === "function") { - arg2(); - } - if (typeof arg3 === "function") { - arg3(); - } - return this; - }, - write(chunk, arg2, arg3) { - if (typeof chunk === "string") { - _resSendBody = chunk; - } - if (typeof arg2 === "function") { - arg2(void 0); - } - if (typeof arg3 === "function") { - arg3(); - } - return true; - }, - writeHead(statusCode, headers2) { - this.statusCode = statusCode; - if (headers2) { - if (Array.isArray(headers2) || typeof headers2 === "string") { - throw new TypeError("Raw headers is not supported."); - } - for (const header in headers2) { - const value = headers2[header]; - if (value !== void 0) { - this.setHeader( - header, - value - ); - } - } - } - return this; - } - }); - const event = createEvent(reqProxy, resProxy); - event.fetch = (url, fetchOptions) => fetchWithEvent(event, url, fetchOptions, { - fetch: useNitroApp().localFetch - }); - event.$fetch = (url, fetchOptions) => fetchWithEvent(event, url, fetchOptions, { - fetch: globalThis.$fetch - }); - event.context = incomingEvent.context; - event.context.cache = { - options: _opts - }; - const body = await handler(event) || _resSendBody; - const headers = event.node.res.getHeaders(); - headers.etag = String( - headers.Etag || headers.etag || `W/"${hash(body)}"` - ); - headers["last-modified"] = String( - headers["Last-Modified"] || headers["last-modified"] || (/* @__PURE__ */ new Date()).toUTCString() - ); - const cacheControl = []; - if (opts.swr) { - if (opts.maxAge) { - cacheControl.push(`s-maxage=${opts.maxAge}`); - } - if (opts.staleMaxAge) { - cacheControl.push(`stale-while-revalidate=${opts.staleMaxAge}`); - } else { - cacheControl.push("stale-while-revalidate"); - } - } else if (opts.maxAge) { - cacheControl.push(`max-age=${opts.maxAge}`); - } - if (cacheControl.length > 0) { - headers["cache-control"] = cacheControl.join(", "); - } - const cacheEntry = { - code: event.node.res.statusCode, - headers, - body - }; - return cacheEntry; - }, - _opts - ); - return defineEventHandler(async (event) => { - if (opts.headersOnly) { - if (handleCacheHeaders(event, { maxAge: opts.maxAge })) { - return; - } - return handler(event); - } - const response = await _cachedHandler( - event - ); - if (event.node.res.headersSent || event.node.res.writableEnded) { - return response.body; - } - if (handleCacheHeaders(event, { - modifiedTime: new Date(response.headers["last-modified"]), - etag: response.headers.etag, - maxAge: opts.maxAge - })) { - return; - } - event.node.res.statusCode = response.code; - for (const name in response.headers) { - const value = response.headers[name]; - if (name === "set-cookie") { - event.node.res.appendHeader( - name, - splitCookiesString(value) - ); - } else { - if (value !== void 0) { - event.node.res.setHeader(name, value); - } - } - } - return response.body; - }); -} -function cloneWithProxy(obj, overrides) { - return new Proxy(obj, { - get(target, property, receiver) { - if (property in overrides) { - return overrides[property]; - } - return Reflect.get(target, property, receiver); - }, - set(target, property, value, receiver) { - if (property in overrides) { - overrides[property] = value; - return true; - } - return Reflect.set(target, property, value, receiver); - } - }); -} -const cachedEventHandler = defineCachedEventHandler; - -function klona(x) { - if (typeof x !== 'object') return x; - - var k, tmp, str=Object.prototype.toString.call(x); - - if (str === '[object Object]') { - if (x.constructor !== Object && typeof x.constructor === 'function') { - tmp = new x.constructor(); - for (k in x) { - if (x.hasOwnProperty(k) && tmp[k] !== x[k]) { - tmp[k] = klona(x[k]); - } - } - } else { - tmp = {}; // null - for (k in x) { - if (k === '__proto__') { - Object.defineProperty(tmp, k, { - value: klona(x[k]), - configurable: true, - enumerable: true, - writable: true, - }); - } else { - tmp[k] = klona(x[k]); - } - } - } - return tmp; - } - - if (str === '[object Array]') { - k = x.length; - for (tmp=Array(k); k--;) { - tmp[k] = klona(x[k]); - } - return tmp; - } - - if (str === '[object Set]') { - tmp = new Set; - x.forEach(function (val) { - tmp.add(klona(val)); - }); - return tmp; - } - - if (str === '[object Map]') { - tmp = new Map; - x.forEach(function (val, key) { - tmp.set(klona(key), klona(val)); - }); - return tmp; - } - - if (str === '[object Date]') { - return new Date(+x); - } - - if (str === '[object RegExp]') { - tmp = new RegExp(x.source, x.flags); - tmp.lastIndex = x.lastIndex; - return tmp; - } - - if (str === '[object DataView]') { - return new x.constructor( klona(x.buffer) ); - } - - if (str === '[object ArrayBuffer]') { - return x.slice(0); - } - - // ArrayBuffer.isView(x) - // ~> `new` bcuz `Buffer.slice` => ref - if (str.slice(-6) === 'Array]') { - return new x.constructor(x); - } - - return x; -} - -function isPlainObject(value) { - if (value === null || typeof value !== "object") { - return false; - } - const prototype = Object.getPrototypeOf(value); - if (prototype !== null && prototype !== Object.prototype && Object.getPrototypeOf(prototype) !== null) { - return false; - } - if (Symbol.iterator in value) { - return false; - } - if (Symbol.toStringTag in value) { - return Object.prototype.toString.call(value) === "[object Module]"; - } - return true; -} - -function _defu(baseObject, defaults, namespace = ".", merger) { - if (!isPlainObject(defaults)) { - return _defu(baseObject, {}, namespace, merger); - } - const object = Object.assign({}, defaults); - for (const key in baseObject) { - if (key === "__proto__" || key === "constructor") { - continue; - } - const value = baseObject[key]; - if (value === null || value === void 0) { - continue; - } - if (merger && merger(object, key, value, namespace)) { - continue; - } - if (Array.isArray(value) && Array.isArray(object[key])) { - object[key] = [...value, ...object[key]]; - } else if (isPlainObject(value) && isPlainObject(object[key])) { - object[key] = _defu( - value, - object[key], - (namespace ? `${namespace}.` : "") + key.toString(), - merger - ); - } else { - object[key] = value; - } - } - return object; -} -function createDefu(merger) { - return (...arguments_) => ( - // eslint-disable-next-line unicorn/no-array-reduce - arguments_.reduce((p, c) => _defu(p, c, "", merger), {}) - ); -} -const defuFn = createDefu((object, key, currentValue) => { - if (object[key] !== void 0 && typeof currentValue === "function") { - object[key] = currentValue(object[key]); - return true; - } -}); - -const inlineAppConfig = {}; - - - -const appConfig = defuFn(inlineAppConfig); - -const NUMBER_CHAR_RE = /\d/; -const STR_SPLITTERS = ["-", "_", "/", "."]; -function isUppercase(char = "") { - if (NUMBER_CHAR_RE.test(char)) { - return void 0; - } - return char !== char.toLowerCase(); -} -function splitByCase(str, separators) { - const splitters = STR_SPLITTERS; - const parts = []; - if (!str || typeof str !== "string") { - return parts; - } - let buff = ""; - let previousUpper; - let previousSplitter; - for (const char of str) { - const isSplitter = splitters.includes(char); - if (isSplitter === true) { - parts.push(buff); - buff = ""; - previousUpper = void 0; - continue; - } - const isUpper = isUppercase(char); - if (previousSplitter === false) { - if (previousUpper === false && isUpper === true) { - parts.push(buff); - buff = char; - previousUpper = isUpper; - continue; - } - if (previousUpper === true && isUpper === false && buff.length > 1) { - const lastChar = buff.at(-1); - parts.push(buff.slice(0, Math.max(0, buff.length - 1))); - buff = lastChar + char; - previousUpper = isUpper; - continue; - } - } - buff += char; - previousUpper = isUpper; - previousSplitter = isSplitter; - } - parts.push(buff); - return parts; -} -function kebabCase(str, joiner) { - return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => p.toLowerCase()).join(joiner) : ""; -} -function snakeCase(str) { - return kebabCase(str || "", "_"); -} - -function getEnv(key, opts) { - const envKey = snakeCase(key).toUpperCase(); - return destr$1( - process.env[opts.prefix + envKey] ?? process.env[opts.altPrefix + envKey] - ); -} -function _isObject(input) { - return typeof input === "object" && !Array.isArray(input); -} -function applyEnv(obj, opts, parentKey = "") { - for (const key in obj) { - const subKey = parentKey ? `${parentKey}_${key}` : key; - const envValue = getEnv(subKey, opts); - if (_isObject(obj[key])) { - if (_isObject(envValue)) { - obj[key] = { ...obj[key], ...envValue }; - applyEnv(obj[key], opts, subKey); - } else if (envValue === void 0) { - applyEnv(obj[key], opts, subKey); - } else { - obj[key] = envValue ?? obj[key]; - } - } else { - obj[key] = envValue ?? obj[key]; - } - if (opts.envExpansion && typeof obj[key] === "string") { - obj[key] = _expandFromEnv(obj[key]); - } - } - return obj; -} -const envExpandRx = /{{(.*?)}}/g; -function _expandFromEnv(value) { - return value.replace(envExpandRx, (match, key) => { - return process.env[key] || match; - }); -} - -const _inlineRuntimeConfig = { - "app": { - "baseURL": "/" - }, - "nitro": { - "routeRules": {} - }, - "streaming": true -}; -const envOptions = { - prefix: "NITRO_", - altPrefix: _inlineRuntimeConfig.nitro.envPrefix ?? process.env.NITRO_ENV_PREFIX ?? "_", - envExpansion: _inlineRuntimeConfig.nitro.envExpansion ?? process.env.NITRO_ENV_EXPANSION ?? false -}; -const _sharedRuntimeConfig = _deepFreeze( - applyEnv(klona(_inlineRuntimeConfig), envOptions) -); -function useRuntimeConfig(event) { - { - return _sharedRuntimeConfig; - } -} -_deepFreeze(klona(appConfig)); -function _deepFreeze(object) { - const propNames = Object.getOwnPropertyNames(object); - for (const name of propNames) { - const value = object[name]; - if (value && typeof value === "object") { - _deepFreeze(value); - } - } - return Object.freeze(object); -} -new Proxy(/* @__PURE__ */ Object.create(null), { - get: (_, prop) => { - console.warn( - "Please use `useRuntimeConfig()` instead of accessing config directly." - ); - const runtimeConfig = useRuntimeConfig(); - if (prop in runtimeConfig) { - return runtimeConfig[prop]; - } - return void 0; - } -}); - -function createContext(opts = {}) { - let currentInstance; - let isSingleton = false; - const checkConflict = (instance) => { - if (currentInstance && currentInstance !== instance) { - throw new Error("Context conflict"); - } - }; - let als; - if (opts.asyncContext) { - const _AsyncLocalStorage = opts.AsyncLocalStorage || globalThis.AsyncLocalStorage; - if (_AsyncLocalStorage) { - als = new _AsyncLocalStorage(); - } else { - console.warn("[unctx] `AsyncLocalStorage` is not provided."); - } - } - const _getCurrentInstance = () => { - if (als && currentInstance === void 0) { - const instance = als.getStore(); - if (instance !== void 0) { - return instance; - } - } - return currentInstance; - }; - return { - use: () => { - const _instance = _getCurrentInstance(); - if (_instance === void 0) { - throw new Error("Context is not available"); - } - return _instance; - }, - tryUse: () => { - return _getCurrentInstance(); - }, - set: (instance, replace) => { - if (!replace) { - checkConflict(instance); - } - currentInstance = instance; - isSingleton = true; - }, - unset: () => { - currentInstance = void 0; - isSingleton = false; - }, - call: (instance, callback) => { - checkConflict(instance); - currentInstance = instance; - try { - return als ? als.run(instance, callback) : callback(); - } finally { - if (!isSingleton) { - currentInstance = void 0; - } - } - }, - async callAsync(instance, callback) { - currentInstance = instance; - const onRestore = () => { - currentInstance = instance; - }; - const onLeave = () => currentInstance === instance ? onRestore : void 0; - asyncHandlers.add(onLeave); - try { - const r = als ? als.run(instance, callback) : callback(); - if (!isSingleton) { - currentInstance = void 0; - } - return await r; - } finally { - asyncHandlers.delete(onLeave); - } - } - }; -} -function createNamespace(defaultOpts = {}) { - const contexts = {}; - return { - get(key, opts = {}) { - if (!contexts[key]) { - contexts[key] = createContext({ ...defaultOpts, ...opts }); - } - contexts[key]; - return contexts[key]; - } - }; -} -const _globalThis = typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : typeof global !== "undefined" ? global : {}; -const globalKey = "__unctx__"; -const defaultNamespace = _globalThis[globalKey] || (_globalThis[globalKey] = createNamespace()); -const getContext = (key, opts = {}) => defaultNamespace.get(key, opts); -const asyncHandlersKey = "__unctx_async_handlers__"; -const asyncHandlers = _globalThis[asyncHandlersKey] || (_globalThis[asyncHandlersKey] = /* @__PURE__ */ new Set()); - -getContext("nitro-app", { - asyncContext: undefined, - AsyncLocalStorage: void 0 -}); - -const config = useRuntimeConfig(); -const _routeRulesMatcher = toRouteMatcher( - createRouter$1({ routes: config.nitro.routeRules }) -); -function createRouteRulesHandler(ctx) { - return eventHandler((event) => { - const routeRules = getRouteRules(event); - if (routeRules.headers) { - setHeaders(event, routeRules.headers); - } - if (routeRules.redirect) { - let target = routeRules.redirect.to; - if (target.endsWith("/**")) { - let targetPath = event.path; - const strpBase = routeRules.redirect._redirectStripBase; - if (strpBase) { - targetPath = withoutBase(targetPath, strpBase); - } - target = joinURL(target.slice(0, -3), targetPath); - } else if (event.path.includes("?")) { - const query = getQuery(event.path); - target = withQuery(target, query); - } - return sendRedirect(event, target, routeRules.redirect.statusCode); - } - if (routeRules.proxy) { - let target = routeRules.proxy.to; - if (target.endsWith("/**")) { - let targetPath = event.path; - const strpBase = routeRules.proxy._proxyStripBase; - if (strpBase) { - targetPath = withoutBase(targetPath, strpBase); - } - target = joinURL(target.slice(0, -3), targetPath); - } else if (event.path.includes("?")) { - const query = getQuery(event.path); - target = withQuery(target, query); - } - return proxyRequest(event, target, { - fetch: ctx.localFetch, - ...routeRules.proxy - }); - } - }); -} -function getRouteRules(event) { - event.context._nitro = event.context._nitro || {}; - if (!event.context._nitro.routeRules) { - event.context._nitro.routeRules = getRouteRulesForPath( - withoutBase(event.path.split("?")[0], useRuntimeConfig().app.baseURL) - ); - } - return event.context._nitro.routeRules; -} -function getRouteRulesForPath(path) { - return defu({}, ..._routeRulesMatcher.matchAll(path).reverse()); -} - -function createNitroApp() { - const config = useRuntimeConfig(); - const hooks = createHooks(); - const captureError = (error, context = {}) => { - const promise = hooks.callHookParallel("error", error, context).catch((error_) => { - console.error("Error while capturing another error", error_); - }); - if (context.event && isEvent(context.event)) { - const errors = context.event.context.nitro?.errors; - if (errors) { - errors.push({ error, context }); - } - if (context.event.waitUntil) { - context.event.waitUntil(promise); - } - } - }; - const h3App = createApp({ - debug: destr$1(false), - onError: (error, event) => { - captureError(error, { event, tags: ["request"] }); - return errorHandler(error, event); - }, - onRequest: async (event) => { - await nitroApp$1.hooks.callHook("request", event).catch((error) => { - captureError(error, { event, tags: ["request"] }); - }); - }, - onBeforeResponse: async (event, response) => { - await nitroApp$1.hooks.callHook("beforeResponse", event, response).catch((error) => { - captureError(error, { event, tags: ["request", "response"] }); - }); - }, - onAfterResponse: async (event, response) => { - await nitroApp$1.hooks.callHook("afterResponse", event, response).catch((error) => { - captureError(error, { event, tags: ["request", "response"] }); - }); - } - }); - const router = createRouter({ - preemptive: true - }); - const localCall = createCall(toNodeListener(h3App)); - const _localFetch = createFetch(localCall, globalThis.fetch); - const localFetch = (input, init) => _localFetch(input, init).then( - (response) => normalizeFetchResponse(response) - ); - const $fetch = createFetch$1({ - fetch: localFetch, - Headers: Headers$1, - defaults: { baseURL: config.app.baseURL } - }); - globalThis.$fetch = $fetch; - h3App.use(createRouteRulesHandler({ localFetch })); - h3App.use( - eventHandler((event) => { - event.context.nitro = event.context.nitro || { errors: [] }; - const envContext = event.node.req?.__unenv__; - if (envContext) { - Object.assign(event.context, envContext); - } - event.fetch = (req, init) => fetchWithEvent(event, req, init, { fetch: localFetch }); - event.$fetch = (req, init) => fetchWithEvent(event, req, init, { - fetch: $fetch - }); - event.waitUntil = (promise) => { - if (!event.context.nitro._waitUntilPromises) { - event.context.nitro._waitUntilPromises = []; - } - event.context.nitro._waitUntilPromises.push(promise); - if (envContext?.waitUntil) { - envContext.waitUntil(promise); - } - }; - event.captureError = (error, context) => { - captureError(error, { event, ...context }); - }; - }) - ); - for (const h of handlers) { - let handler = h.lazy ? lazyEventHandler(h.handler) : h.handler; - if (h.middleware || !h.route) { - const middlewareBase = (config.app.baseURL + (h.route || "/")).replace( - /\/+/g, - "/" - ); - h3App.use(middlewareBase, handler); - } else { - const routeRules = getRouteRulesForPath( - h.route.replace(/:\w+|\*\*/g, "_") - ); - if (routeRules.cache) { - handler = cachedEventHandler(handler, { - group: "nitro/routes", - ...routeRules.cache - }); - } - router.use(h.route, handler, h.method); - } - } - h3App.use(config.app.baseURL, router.handler); - const app = { - hooks, - h3App, - router, - localCall, - localFetch, - captureError - }; - return app; -} -function runNitroPlugins(nitroApp2) { - for (const plugin of plugins) { - try { - plugin(nitroApp2); - } catch (error) { - nitroApp2.captureError(error, { tags: ["plugin"] }); - throw error; - } - } -} -const nitroApp$1 = createNitroApp(); -function useNitroApp() { - return nitroApp$1; -} -runNitroPlugins(nitroApp$1); - -function normalizeLambdaIncomingHeaders(headers) { - return Object.fromEntries( - Object.entries(headers || {}).map(([key, value]) => [ - key.toLowerCase(), - value - ]) - ); -} -function normalizeLambdaOutgoingHeaders(headers, stripCookies = false) { - const entries = stripCookies ? Object.entries(headers).filter(([key]) => !["set-cookie"].includes(key)) : Object.entries(headers); - return Object.fromEntries( - entries.map(([k, v]) => [k, Array.isArray(v) ? v.join(",") : String(v)]) - ); -} -async function normalizeLambdaOutgoingBody(body, headers) { - if (typeof body === "string") { - return { type: "text", body }; - } - if (!body) { - return { type: "text", body: "" }; - } - const buffer = await toBuffer(body); - const contentType = headers["content-type"] || ""; - return isTextType(contentType) ? { type: "text", body: buffer.toString("utf8") } : { type: "binary", body: buffer.toString("base64") }; -} -const TEXT_TYPE_RE = /^text\/|\/(javascript|json|xml)|utf-?8/; -function isTextType(contentType = "") { - return TEXT_TYPE_RE.test(contentType); -} - -const nitroApp = useNitroApp(); -async function bufferedHandler(event, context) { - const query = { - ...event.queryStringParameters, - ...event.multiValueQueryStringParameters - }; - const url = withQuery( - event.path || event.rawPath, - query - ); - const method = event.httpMethod || event.requestContext?.http?.method || "get"; - if ("cookies" in event && event.cookies) { - event.headers.cookie = event.cookies.join(";"); - } - const r = await nitroApp.localCall({ - event, - url, - context, - headers: normalizeLambdaIncomingHeaders(event.headers), - method, - query, - body: event.isBase64Encoded ? Buffer.from(event.body || "", "base64").toString("utf8") : event.body - }); - const isApiGwV2 = "cookies" in event || "rawPath" in event; - const awsBody = await normalizeLambdaOutgoingBody(r.body, r.headers); - const cookies = normalizeCookieHeader(r.headers["set-cookie"]); - return { - ...cookies.length > 0 && { - ...isApiGwV2 ? { cookies } : { multiValueHeaders: { "set-cookie": cookies } } - }, - statusCode: r.status, - headers: normalizeLambdaOutgoingHeaders(r.headers, true), - body: awsBody.body, - isBase64Encoded: awsBody.type === "binary" - }; -} -const streamingHandler = () => awslambda.streamifyResponse( - async (event, responseStream, context) => { - const query = { - ...event.queryStringParameters - }; - const url = withQuery(event.rawPath, query); - const method = event.requestContext?.http?.method || "get"; - if ("cookies" in event && event.cookies) { - event.headers.cookie = event.cookies.join(";"); - } - const r = await nitroApp.localCall({ - event, - url, - context, - headers: normalizeLambdaIncomingHeaders(event.headers), - method, - query, - body: event.isBase64Encoded ? Buffer.from(event.body || "", "base64").toString("utf8") : event.body - }); - const httpResponseMetadata = { - statusCode: r.status, - headers: { - ...normalizeLambdaOutgoingHeaders(r.headers, true), - "Transfer-Encoding": "chunked" - } - }; - if (r.body) { - const writer = awslambda.HttpResponseStream.from( - responseStream, - httpResponseMetadata - ); - if (!r.body.getReader) { - writer.write(r.body); - writer.end(); - return; - } - const reader = r.body.getReader(); - await streamToNodeStream(reader, responseStream); - writer.end(); - } - } -); -const streamToNodeStream = async (reader, writer) => { - let readResult = await reader.read(); - while (!readResult.done) { - writer.write(readResult.value); - readResult = await reader.read(); - } - writer.end(); -}; -const handler = useRuntimeConfig().streaming ? streamingHandler() : bufferedHandler; - -export { bufferedHandler, handler }; -//# sourceMappingURL=index.mjs.map diff --git a/examples/internal/aws-nitro/.output/server/index.mjs.map b/examples/internal/aws-nitro/.output/server/index.mjs.map deleted file mode 100644 index 1a2d2337fd..0000000000 --- a/examples/internal/aws-nitro/.output/server/index.mjs.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.mjs","sources":["../../../../../../nitro/node_modules/.pnpm/destr@2.0.3/node_modules/destr/dist/index.mjs","../../../../../../nitro/node_modules/.pnpm/ufo@1.5.4/node_modules/ufo/dist/index.mjs","../../../../../../nitro/node_modules/.pnpm/ohash@1.1.4/node_modules/ohash/dist/index.mjs","../../../../../../nitro/node_modules/.pnpm/radix3@1.1.2/node_modules/radix3/dist/index.mjs","../../../../../../nitro/node_modules/.pnpm/defu@6.1.4/node_modules/defu/dist/defu.mjs","../../../../../../nitro/node_modules/.pnpm/unenv@1.10.0/node_modules/unenv/runtime/_internal/utils.mjs","../../../../../../nitro/node_modules/.pnpm/unenv@1.10.0/node_modules/unenv/runtime/node/events/_events.mjs","../../../../../../nitro/node_modules/.pnpm/unenv@1.10.0/node_modules/unenv/runtime/node/events/index.mjs","../../../../../../nitro/node_modules/.pnpm/unenv@1.10.0/node_modules/unenv/runtime/node/stream/readable.mjs","../../../../../../nitro/node_modules/.pnpm/unenv@1.10.0/node_modules/unenv/runtime/node/stream/writable.mjs","../../../../../../nitro/node_modules/.pnpm/unenv@1.10.0/node_modules/unenv/runtime/node/stream/duplex.mjs","../../../../../../nitro/node_modules/.pnpm/unenv@1.10.0/node_modules/unenv/runtime/node/net/socket.mjs","../../../../../../nitro/node_modules/.pnpm/unenv@1.10.0/node_modules/unenv/runtime/node/http/_request.mjs","../../../../../../nitro/node_modules/.pnpm/unenv@1.10.0/node_modules/unenv/runtime/node/http/_response.mjs","../../../../../../nitro/node_modules/.pnpm/h3@1.13.0/node_modules/h3/dist/index.mjs","../../../../../../nitro/node_modules/.pnpm/hookable@5.5.3/node_modules/hookable/dist/index.mjs","../../../../../../nitro/node_modules/.pnpm/node-fetch-native@1.6.4/node_modules/node-fetch-native/dist/native.mjs","../../../../../../nitro/node_modules/.pnpm/ofetch@1.4.1/node_modules/ofetch/dist/shared/ofetch.03887fc3.mjs","../../../../../../nitro/node_modules/.pnpm/ofetch@1.4.1/node_modules/ofetch/dist/node.mjs","../../../../../../nitro/node_modules/.pnpm/unenv@1.10.0/node_modules/unenv/runtime/fetch/call.mjs","../../../../../../nitro/node_modules/.pnpm/unenv@1.10.0/node_modules/unenv/runtime/fetch/index.mjs","../../../../../../nitro/dist/runtime/internal/utils.mjs","../../../../../../nitro/dist/runtime/internal/error.mjs","../../../../../../nitro/node_modules/.pnpm/unstorage@1.12.0_ioredis@5.4.1/node_modules/unstorage/dist/shared/unstorage.d569726e.mjs","../../node_modules/destr/dist/index.mjs","../../node_modules/unstorage/dist/shared/unstorage.d569726e.mjs","../../node_modules/unstorage/dist/index.mjs","../../../../../../nitro/dist/runtime/internal/storage.mjs","../../../../../../nitro/dist/runtime/internal/cache.mjs","../../../../../../nitro/node_modules/.pnpm/klona@2.0.6/node_modules/klona/dist/index.mjs","../../node_modules/defu/dist/defu.mjs","../../../../../../nitro/node_modules/.pnpm/scule@1.3.0/node_modules/scule/dist/index.mjs","../../../../../../nitro/dist/runtime/internal/utils.env.mjs","../../../../../../nitro/dist/runtime/internal/config.mjs","../../../../../../nitro/node_modules/.pnpm/unctx@2.3.1/node_modules/unctx/dist/index.mjs","../../../../../../nitro/dist/runtime/internal/context.mjs","../../../../../../nitro/dist/runtime/internal/route-rules.mjs","../../../../../../nitro/dist/runtime/internal/app.mjs","../../../../../../nitro/dist/runtime/internal/utils.lambda.mjs","../../../../../../nitro/dist/presets/aws-lambda/runtime/aws-lambda.mjs"],"sourcesContent":null,"names":["suspectProtoRx","suspectConstructorRx","JsonSigRx","jsonParseTransform","warnKeyDropped","destr","__defProp","__defNormalProp","__publicField","createRouter","isPlainObject","_defu","createDefu","EventEmitter","_EventEmitter","mergeHeaders","nullBodyResponses","createFetch","nodeFetch","Headers","Headers$1","AbortController$1","normalizeBaseKey","normalizeKey","_inlineAppConfig","createRadixRouter","nitroApp","createLocalFetch"],"mappings":"","x_google_ignoreList":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,23,24,25,26,29,30,31,34]} \ No newline at end of file diff --git a/examples/internal/aws-nitro/.output/server/package.json b/examples/internal/aws-nitro/.output/server/package.json deleted file mode 100644 index 7ca3ef69ca..0000000000 --- a/examples/internal/aws-nitro/.output/server/package.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"server-prod","version":"0.0.0","type":"module","private":true,"dependencies":{}} \ No newline at end of file diff --git a/examples/internal/big/bun.lock b/examples/internal/big/bun.lock deleted file mode 100644 index af8aad7307..0000000000 --- a/examples/internal/big/bun.lock +++ /dev/null @@ -1,40 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "base-ts", - "dependencies": { - "sst": "^3.0.4", - }, - }, - }, - "packages": { - "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], - - "jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="], - - "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - - "object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], - - "oidc-token-hash": ["oidc-token-hash@5.0.3", "", {}, "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw=="], - - "openid-client": ["openid-client@5.6.4", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="], - - "sst": ["sst@3.6.20", "", { "dependencies": { "aws4fetch": "^1.0.18", "jose": "5.2.3", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.6.20", "sst-darwin-x64": "3.6.20", "sst-linux-arm64": "3.6.20", "sst-linux-x64": "3.6.20", "sst-linux-x86": "3.6.20" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-NVbQXW7ZVEt6AWqs1aWvKvrz0rM3sZjS2vm0LTobvvxWQ3tkyopB7ZHWQG2Fd81/u9SYCay/bVUVC/gqixbuow=="], - - "sst-darwin-arm64": ["sst-darwin-arm64@3.6.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jh/dsGSvL7r3I/o/32t2JnRHY/t+6P+PzdTsFjn8lZRglfEYp28zLYxeZqHoTOZnF9SKqC/GoK/eHKppMSUNKA=="], - - "sst-darwin-x64": ["sst-darwin-x64@3.6.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-MyIfwSFekKobjVmMHjyQLeFheYxkC0HPb36nJXi0F1RD29TuVKu1ocPGzVaHzmguf9nNGllItg4Kv8h8VmavHA=="], - - "sst-linux-arm64": ["sst-linux-arm64@3.6.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-LQwEP47w1Uk7rmZ3I0ECgpKygpxyY+7tpFNrNA18LAo4dyOcY6pH2lUPdzf0t++/gcAN7rfd4X272HnIjEkliw=="], - - "sst-linux-x64": ["sst-linux-x64@3.6.20", "", { "os": "linux", "cpu": "x64" }, "sha512-vvoTzY84Yh5jnPMO2RfsnH209rnuTD4c6BkTMcHP0nS17Z7OhC4kPg4I6T4kCfQO85fC95pf4XMkv64S3zbu1g=="], - - "sst-linux-x86": ["sst-linux-x86@3.6.20", "", { "os": "linux", "cpu": "none" }, "sha512-BnU00LKa+F9zcc0ns1swvltfBt4iNScnzxDT3ecNABmKkfSfteNiwf9HqxRbZSShOdyqD13iNU2cXBYN8lYxaQ=="], - - "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - - "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], - } -} diff --git a/examples/internal/big/index.ts b/examples/internal/big/index.ts deleted file mode 100644 index 5bbba25719..0000000000 --- a/examples/internal/big/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function handler() { - return "hello world"; -} diff --git a/examples/internal/big/package.json b/examples/internal/big/package.json deleted file mode 100644 index 2444208c90..0000000000 --- a/examples/internal/big/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "base-ts", - "version": "0.0.0", - "dependencies": { - "sst": "*" - } -} diff --git a/examples/internal/big/sst-env.d.ts b/examples/internal/big/sst-env.d.ts deleted file mode 100644 index c1b62bdd43..0000000000 --- a/examples/internal/big/sst-env.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -declare module "sst" { - export interface Resource { - } -} -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/big/sst.config.ts b/examples/internal/big/sst.config.ts deleted file mode 100644 index 6f7f297702..0000000000 --- a/examples/internal/big/sst.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -/// - -export default $config({ - app(input) { - return { - name: "aws-big", - removal: input?.stage === "production" ? "retain" : "remove", - home: "aws", - }; - }, - async run() { - for (let i = 0; i < 10; i++) { - new sst.aws.Function("MyFunction" + i, { - handler: "index.handler", - }); - } - }, -}); diff --git a/examples/internal/cloudflare-remix-cf-template/.eslintrc.cjs b/examples/internal/cloudflare-remix-cf-template/.eslintrc.cjs deleted file mode 100644 index 4f6f59eee1..0000000000 --- a/examples/internal/cloudflare-remix-cf-template/.eslintrc.cjs +++ /dev/null @@ -1,84 +0,0 @@ -/** - * This is intended to be a basic starting point for linting in your app. - * It relies on recommended configs out of the box for simplicity, but you can - * and should modify this configuration to best suit your team's needs. - */ - -/** @type {import('eslint').Linter.Config} */ -module.exports = { - root: true, - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - ecmaFeatures: { - jsx: true, - }, - }, - env: { - browser: true, - commonjs: true, - es6: true, - }, - ignorePatterns: ["!**/.server", "!**/.client"], - - // Base config - extends: ["eslint:recommended"], - - overrides: [ - // React - { - files: ["**/*.{js,jsx,ts,tsx}"], - plugins: ["react", "jsx-a11y"], - extends: [ - "plugin:react/recommended", - "plugin:react/jsx-runtime", - "plugin:react-hooks/recommended", - "plugin:jsx-a11y/recommended", - ], - settings: { - react: { - version: "detect", - }, - formComponents: ["Form"], - linkComponents: [ - { name: "Link", linkAttribute: "to" }, - { name: "NavLink", linkAttribute: "to" }, - ], - "import/resolver": { - typescript: {}, - }, - }, - }, - - // Typescript - { - files: ["**/*.{ts,tsx}"], - plugins: ["@typescript-eslint", "import"], - parser: "@typescript-eslint/parser", - settings: { - "import/internal-regex": "^~/", - "import/resolver": { - node: { - extensions: [".ts", ".tsx"], - }, - typescript: { - alwaysTryTypes: true, - }, - }, - }, - extends: [ - "plugin:@typescript-eslint/recommended", - "plugin:import/recommended", - "plugin:import/typescript", - ], - }, - - // Node - { - files: [".eslintrc.cjs"], - env: { - node: true, - }, - }, - ], -}; diff --git a/examples/internal/cloudflare-remix-cf-template/.gitignore b/examples/internal/cloudflare-remix-cf-template/.gitignore deleted file mode 100644 index 9b131cd40c..0000000000 --- a/examples/internal/cloudflare-remix-cf-template/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -node_modules - -/.cache -/build -.env - -.wrangler - -# sst -.sst diff --git a/examples/internal/cloudflare-remix-cf-template/README.md b/examples/internal/cloudflare-remix-cf-template/README.md deleted file mode 100644 index 22cba04fe8..0000000000 --- a/examples/internal/cloudflare-remix-cf-template/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Welcome to Remix + Vite! - -πŸ“– See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/future/vite) for details on supported features. - -## Typegen - -Generate types for your Cloudflare bindings in `wrangler.toml`: - -```sh -npm run typegen -``` - -You will need to rerun typegen whenever you make changes to `wrangler.toml`. - -## Development - -Run the Vite dev server: - -```sh -npm run dev -``` - -To run Wrangler: - -```sh -npm run build -npm run start -``` - -## Deployment - -> [!WARNING] -> Cloudflare does _not_ use `wrangler.toml` to configure deployment bindings. -> You **MUST** [configure deployment bindings manually in the Cloudflare dashboard][bindings]. - -First, build your app for production: - -```sh -npm run build -``` - -Then, deploy your app to Cloudflare Pages: - -```sh -npm run deploy -``` - -[bindings]: https://developers.cloudflare.com/pages/functions/bindings/ diff --git a/examples/internal/cloudflare-remix-cf-template/app/entry.client.tsx b/examples/internal/cloudflare-remix-cf-template/app/entry.client.tsx deleted file mode 100644 index 94d5dc0de0..0000000000 --- a/examples/internal/cloudflare-remix-cf-template/app/entry.client.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/** - * By default, Remix will handle hydrating your app on the client for you. - * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ - * For more information, see https://remix.run/file-conventions/entry.client - */ - -import { RemixBrowser } from "@remix-run/react"; -import { startTransition, StrictMode } from "react"; -import { hydrateRoot } from "react-dom/client"; - -startTransition(() => { - hydrateRoot( - document, - - - - ); -}); diff --git a/examples/internal/cloudflare-remix-cf-template/app/entry.server.tsx b/examples/internal/cloudflare-remix-cf-template/app/entry.server.tsx deleted file mode 100644 index 0d5c40a755..0000000000 --- a/examples/internal/cloudflare-remix-cf-template/app/entry.server.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/** - * By default, Remix will handle generating the HTTP Response for you. - * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ - * For more information, see https://remix.run/file-conventions/entry.server - */ - -import type { AppLoadContext, EntryContext } from "@remix-run/cloudflare"; -import { RemixServer } from "@remix-run/react"; -import { isbot } from "isbot"; -import { renderToReadableStream } from "react-dom/server"; - -export default async function handleRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext, - // This is ignored so we can keep it in the template for visibility. Feel - // free to delete this parameter in your app if you're not using it! - // eslint-disable-next-line @typescript-eslint/no-unused-vars - loadContext: AppLoadContext -) { - const body = await renderToReadableStream( - , - { - signal: request.signal, - onError(error: unknown) { - // Log streaming rendering errors from inside the shell - console.error(error); - responseStatusCode = 500; - }, - } - ); - - if (isbot(request.headers.get("user-agent") || "")) { - await body.allReady; - } - - responseHeaders.set("Content-Type", "text/html"); - return new Response(body, { - headers: responseHeaders, - status: responseStatusCode, - }); -} diff --git a/examples/internal/cloudflare-remix-cf-template/app/root.tsx b/examples/internal/cloudflare-remix-cf-template/app/root.tsx deleted file mode 100644 index e82f26fd17..0000000000 --- a/examples/internal/cloudflare-remix-cf-template/app/root.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { - Links, - Meta, - Outlet, - Scripts, - ScrollRestoration, -} from "@remix-run/react"; - -export function Layout({ children }: { children: React.ReactNode }) { - return ( - - - - - - - - - {children} - - - - - ); -} - -export default function App() { - return ; -} diff --git a/examples/internal/cloudflare-remix-cf-template/app/routes/_index.tsx b/examples/internal/cloudflare-remix-cf-template/app/routes/_index.tsx deleted file mode 100644 index 4780eadc46..0000000000 --- a/examples/internal/cloudflare-remix-cf-template/app/routes/_index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import type { MetaFunction } from "@remix-run/cloudflare"; - -export const meta: MetaFunction = () => { - return [ - { title: "New Remix App" }, - { - name: "description", - content: "Welcome to Remix! Using Vite and Cloudflare!", - }, - ]; -}; - -export default function Index() { - return ( -
-

Welcome to Remix (with Vite and Cloudflare)

- -
- ); -} diff --git a/examples/internal/cloudflare-remix-cf-template/functions/[[path]].ts b/examples/internal/cloudflare-remix-cf-template/functions/[[path]].ts deleted file mode 100644 index 26a7de6b0e..0000000000 --- a/examples/internal/cloudflare-remix-cf-template/functions/[[path]].ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages"; - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore - the server build file is generated by `remix vite:build` -// eslint-disable-next-line import/no-unresolved -import * as build from "../build/server"; - -export const onRequest = createPagesFunctionHandler({ build }); diff --git a/examples/internal/cloudflare-remix-cf-template/load-context.ts b/examples/internal/cloudflare-remix-cf-template/load-context.ts deleted file mode 100644 index 2777ca18c0..0000000000 --- a/examples/internal/cloudflare-remix-cf-template/load-context.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { type PlatformProxy } from "wrangler"; - -// When using `wrangler.toml` to configure bindings, -// `wrangler types` will generate types for those bindings -// into the global `Env` interface. -// Need this empty interface so that typechecking passes -// even if no `wrangler.toml` exists. -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface Env {} - -type Cloudflare = Omit, "dispose">; - -declare module "@remix-run/cloudflare" { - interface AppLoadContext { - cloudflare: Cloudflare; - } -} diff --git a/examples/internal/cloudflare-remix-cf-template/package.json b/examples/internal/cloudflare-remix-cf-template/package.json deleted file mode 100644 index 15803f7b55..0000000000 --- a/examples/internal/cloudflare-remix-cf-template/package.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "cf-remix", - "private": true, - "sideEffects": false, - "type": "module", - "scripts": { - "build": "remix vite:build", - "deploy": "wrangler pages deploy ./build/client", - "dev": "sst dev remix dev", - "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", - "start": "wrangler pages dev ./build/client", - "typecheck": "tsc", - "typegen": "wrangler types" - }, - "dependencies": { - "@remix-run/cloudflare": "^2.8.1", - "@remix-run/cloudflare-pages": "^2.8.1", - "@remix-run/react": "^2.8.1", - "isbot": "^4.1.0", - "miniflare": "^3.20231030.4", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "sst": "^3.0.1" - }, - "devDependencies": { - "@cloudflare/workers-types": "^4.20230518.0", - "@remix-run/dev": "^2.8.1", - "@types/react": "^18.2.20", - "@types/react-dom": "^18.2.7", - "@typescript-eslint/eslint-plugin": "^6.7.4", - "@typescript-eslint/parser": "^6.7.4", - "eslint": "^8.38.0", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", - "node-fetch": "^3.3.2", - "typescript": "^5.1.6", - "vite": "^5.1.0", - "vite-tsconfig-paths": "^4.2.1", - "wrangler": "^3.24.0" - }, - "engines": { - "node": ">=18.0.0" - } -} diff --git a/examples/internal/cloudflare-remix-cf-template/public/favicon.ico b/examples/internal/cloudflare-remix-cf-template/public/favicon.ico deleted file mode 100644 index 8830cf6821..0000000000 Binary files a/examples/internal/cloudflare-remix-cf-template/public/favicon.ico and /dev/null differ diff --git a/examples/internal/cloudflare-remix-cf-template/sst.config.ts b/examples/internal/cloudflare-remix-cf-template/sst.config.ts deleted file mode 100644 index 4edc521799..0000000000 --- a/examples/internal/cloudflare-remix-cf-template/sst.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -/// - -export default $config({ - app(input) { - return { - name: "cf-remix", - removal: input?.stage === "production" ? "retain" : "remove", - home: "cloudflare", - }; - }, - async run() { - new sst.cloudflare.Remix("MyWeb"); - }, -}); diff --git a/examples/internal/cloudflare-remix-cf-template/tsconfig.json b/examples/internal/cloudflare-remix-cf-template/tsconfig.json deleted file mode 100644 index a61e663a76..0000000000 --- a/examples/internal/cloudflare-remix-cf-template/tsconfig.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "include": [ - "**/*.ts", - "**/*.tsx", - "**/.server/**/*.ts", - "**/.server/**/*.tsx", - "**/.client/**/*.ts", - "**/.client/**/*.tsx" - ], - "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2022"], - "types": ["@remix-run/cloudflare", "vite/client"], - "isolatedModules": true, - "esModuleInterop": true, - "jsx": "react-jsx", - "module": "ESNext", - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "target": "ES2022", - "strict": true, - "allowJs": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "baseUrl": ".", - "paths": { - "~/*": ["./app/*"] - }, - - // Vite takes care of building everything, not tsc. - "noEmit": true - } -} diff --git a/examples/internal/cloudflare-remix-cf-template/vite.config.ts b/examples/internal/cloudflare-remix-cf-template/vite.config.ts deleted file mode 100644 index 37b93b6d03..0000000000 --- a/examples/internal/cloudflare-remix-cf-template/vite.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { - vitePlugin as remix, - cloudflareDevProxyVitePlugin as remixCloudflareDevProxy, -} from "@remix-run/dev"; -import { defineConfig } from "vite"; -import tsconfigPaths from "vite-tsconfig-paths"; - -export default defineConfig({ - plugins: [remixCloudflareDevProxy(), remix(), tsconfigPaths()], -}); diff --git a/examples/internal/cloudflare-remix/.eslintrc.cjs b/examples/internal/cloudflare-remix/.eslintrc.cjs deleted file mode 100644 index 4f6f59eee1..0000000000 --- a/examples/internal/cloudflare-remix/.eslintrc.cjs +++ /dev/null @@ -1,84 +0,0 @@ -/** - * This is intended to be a basic starting point for linting in your app. - * It relies on recommended configs out of the box for simplicity, but you can - * and should modify this configuration to best suit your team's needs. - */ - -/** @type {import('eslint').Linter.Config} */ -module.exports = { - root: true, - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - ecmaFeatures: { - jsx: true, - }, - }, - env: { - browser: true, - commonjs: true, - es6: true, - }, - ignorePatterns: ["!**/.server", "!**/.client"], - - // Base config - extends: ["eslint:recommended"], - - overrides: [ - // React - { - files: ["**/*.{js,jsx,ts,tsx}"], - plugins: ["react", "jsx-a11y"], - extends: [ - "plugin:react/recommended", - "plugin:react/jsx-runtime", - "plugin:react-hooks/recommended", - "plugin:jsx-a11y/recommended", - ], - settings: { - react: { - version: "detect", - }, - formComponents: ["Form"], - linkComponents: [ - { name: "Link", linkAttribute: "to" }, - { name: "NavLink", linkAttribute: "to" }, - ], - "import/resolver": { - typescript: {}, - }, - }, - }, - - // Typescript - { - files: ["**/*.{ts,tsx}"], - plugins: ["@typescript-eslint", "import"], - parser: "@typescript-eslint/parser", - settings: { - "import/internal-regex": "^~/", - "import/resolver": { - node: { - extensions: [".ts", ".tsx"], - }, - typescript: { - alwaysTryTypes: true, - }, - }, - }, - extends: [ - "plugin:@typescript-eslint/recommended", - "plugin:import/recommended", - "plugin:import/typescript", - ], - }, - - // Node - { - files: [".eslintrc.cjs"], - env: { - node: true, - }, - }, - ], -}; diff --git a/examples/internal/cloudflare-remix/.gitignore b/examples/internal/cloudflare-remix/.gitignore deleted file mode 100644 index 6a1b35d13a..0000000000 --- a/examples/internal/cloudflare-remix/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -node_modules - -/.cache -/build -.env - -# sst -.sst diff --git a/examples/internal/cloudflare-remix/README.md b/examples/internal/cloudflare-remix/README.md deleted file mode 100644 index c05e097d92..0000000000 --- a/examples/internal/cloudflare-remix/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Welcome to Remix + Vite! - -πŸ“– See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/future/vite) for details on supported features. - -## Development - -Run the Vite dev server: - -```shellscript -npm run dev -``` - -## Deployment - -First, build your app for production: - -```sh -npm run build -``` - -Then run the app in production mode: - -```sh -npm start -``` - -Now you'll need to pick a host to deploy it to. - -### DIY - -If you're familiar with deploying Node applications, the built-in Remix app server is production-ready. - -Make sure to deploy the output of `npm run build` - -- `build/server` -- `build/client` diff --git a/examples/internal/cloudflare-remix/app/entry.client.tsx b/examples/internal/cloudflare-remix/app/entry.client.tsx deleted file mode 100644 index 94d5dc0de0..0000000000 --- a/examples/internal/cloudflare-remix/app/entry.client.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/** - * By default, Remix will handle hydrating your app on the client for you. - * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ - * For more information, see https://remix.run/file-conventions/entry.client - */ - -import { RemixBrowser } from "@remix-run/react"; -import { startTransition, StrictMode } from "react"; -import { hydrateRoot } from "react-dom/client"; - -startTransition(() => { - hydrateRoot( - document, - - - - ); -}); diff --git a/examples/internal/cloudflare-remix/app/entry.server.tsx b/examples/internal/cloudflare-remix/app/entry.server.tsx deleted file mode 100644 index 45db3229c6..0000000000 --- a/examples/internal/cloudflare-remix/app/entry.server.tsx +++ /dev/null @@ -1,140 +0,0 @@ -/** - * By default, Remix will handle generating the HTTP Response for you. - * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ - * For more information, see https://remix.run/file-conventions/entry.server - */ - -import { PassThrough } from "node:stream"; - -import type { AppLoadContext, EntryContext } from "@remix-run/node"; -import { createReadableStreamFromReadable } from "@remix-run/node"; -import { RemixServer } from "@remix-run/react"; -import { isbot } from "isbot"; -import { renderToPipeableStream } from "react-dom/server"; - -const ABORT_DELAY = 5_000; - -export default function handleRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext, - // This is ignored so we can keep it in the template for visibility. Feel - // free to delete this parameter in your app if you're not using it! - // eslint-disable-next-line @typescript-eslint/no-unused-vars - loadContext: AppLoadContext -) { - return isbot(request.headers.get("user-agent") || "") - ? handleBotRequest( - request, - responseStatusCode, - responseHeaders, - remixContext - ) - : handleBrowserRequest( - request, - responseStatusCode, - responseHeaders, - remixContext - ); -} - -function handleBotRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext -) { - return new Promise((resolve, reject) => { - let shellRendered = false; - const { pipe, abort } = renderToPipeableStream( - , - { - onAllReady() { - shellRendered = true; - const body = new PassThrough(); - const stream = createReadableStreamFromReadable(body); - - responseHeaders.set("Content-Type", "text/html"); - - resolve( - new Response(stream, { - headers: responseHeaders, - status: responseStatusCode, - }) - ); - - pipe(body); - }, - onShellError(error: unknown) { - reject(error); - }, - onError(error: unknown) { - responseStatusCode = 500; - // Log streaming rendering errors from inside the shell. Don't log - // errors encountered during initial shell rendering since they'll - // reject and get logged in handleDocumentRequest. - if (shellRendered) { - console.error(error); - } - }, - } - ); - - setTimeout(abort, ABORT_DELAY); - }); -} - -function handleBrowserRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext -) { - return new Promise((resolve, reject) => { - let shellRendered = false; - const { pipe, abort } = renderToPipeableStream( - , - { - onShellReady() { - shellRendered = true; - const body = new PassThrough(); - const stream = createReadableStreamFromReadable(body); - - responseHeaders.set("Content-Type", "text/html"); - - resolve( - new Response(stream, { - headers: responseHeaders, - status: responseStatusCode, - }) - ); - - pipe(body); - }, - onShellError(error: unknown) { - reject(error); - }, - onError(error: unknown) { - responseStatusCode = 500; - // Log streaming rendering errors from inside the shell. Don't log - // errors encountered during initial shell rendering since they'll - // reject and get logged in handleDocumentRequest. - if (shellRendered) { - console.error(error); - } - }, - } - ); - - setTimeout(abort, ABORT_DELAY); - }); -} diff --git a/examples/internal/cloudflare-remix/app/root.tsx b/examples/internal/cloudflare-remix/app/root.tsx deleted file mode 100644 index e82f26fd17..0000000000 --- a/examples/internal/cloudflare-remix/app/root.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { - Links, - Meta, - Outlet, - Scripts, - ScrollRestoration, -} from "@remix-run/react"; - -export function Layout({ children }: { children: React.ReactNode }) { - return ( - - - - - - - - - {children} - - - - - ); -} - -export default function App() { - return ; -} diff --git a/examples/internal/cloudflare-remix/app/routes/_index.tsx b/examples/internal/cloudflare-remix/app/routes/_index.tsx deleted file mode 100644 index 5347369230..0000000000 --- a/examples/internal/cloudflare-remix/app/routes/_index.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import type { MetaFunction } from "@remix-run/node"; - -export const meta: MetaFunction = () => { - return [ - { title: "New Remix App" }, - { name: "description", content: "Welcome to Remix!" }, - ]; -}; - -export default function Index() { - return ( - - ); -} diff --git a/examples/internal/cloudflare-remix/package.json b/examples/internal/cloudflare-remix/package.json deleted file mode 100644 index ba420122fe..0000000000 --- a/examples/internal/cloudflare-remix/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "cf-remix", - "private": true, - "sideEffects": false, - "type": "module", - "scripts": { - "build": "remix vite:build", - "dev": "sst dev remix dev", - "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", - "start": "remix-serve ./build/server/index.js", - "typecheck": "tsc" - }, - "dependencies": { - "@remix-run/node": "^2.8.1", - "@remix-run/react": "^2.8.1", - "@remix-run/serve": "^2.8.1", - "isbot": "^4.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "sst": "^3.0.1" - }, - "devDependencies": { - "@remix-run/dev": "^2.8.1", - "@types/react": "^18.2.20", - "@types/react-dom": "^18.2.7", - "@typescript-eslint/eslint-plugin": "^6.7.4", - "@typescript-eslint/parser": "^6.7.4", - "eslint": "^8.38.0", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", - "typescript": "^5.1.6", - "vite": "^5.1.0", - "vite-tsconfig-paths": "^4.2.1" - }, - "engines": { - "node": ">=18.0.0" - } -} diff --git a/examples/internal/cloudflare-remix/public/favicon.ico b/examples/internal/cloudflare-remix/public/favicon.ico deleted file mode 100644 index 8830cf6821..0000000000 Binary files a/examples/internal/cloudflare-remix/public/favicon.ico and /dev/null differ diff --git a/examples/internal/cloudflare-remix/sst-env.d.ts b/examples/internal/cloudflare-remix/sst-env.d.ts deleted file mode 100644 index d11d29b5b1..0000000000 --- a/examples/internal/cloudflare-remix/sst-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/examples/internal/cloudflare-remix/sst.config.ts b/examples/internal/cloudflare-remix/sst.config.ts deleted file mode 100644 index ac08d22d40..0000000000 --- a/examples/internal/cloudflare-remix/sst.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -/// - -export default $config({ - app(input) { - return { - name: "cloudflare-remix", - removal: input?.stage === "production" ? "retain" : "remove", - home: "cloudflare", - }; - }, - async run() { - new sst.cloudflare.Remix("MyWeb"); - }, -}); diff --git a/examples/internal/cloudflare-remix/tsconfig.json b/examples/internal/cloudflare-remix/tsconfig.json deleted file mode 100644 index 9d87dd378f..0000000000 --- a/examples/internal/cloudflare-remix/tsconfig.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "include": [ - "**/*.ts", - "**/*.tsx", - "**/.server/**/*.ts", - "**/.server/**/*.tsx", - "**/.client/**/*.ts", - "**/.client/**/*.tsx" - ], - "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2022"], - "types": ["@remix-run/node", "vite/client"], - "isolatedModules": true, - "esModuleInterop": true, - "jsx": "react-jsx", - "module": "ESNext", - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "target": "ES2022", - "strict": true, - "allowJs": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "baseUrl": ".", - "paths": { - "~/*": ["./app/*"] - }, - - // Vite takes care of building everything, not tsc. - "noEmit": true - } -} diff --git a/examples/internal/cloudflare-remix/vite.config.ts b/examples/internal/cloudflare-remix/vite.config.ts deleted file mode 100644 index 2b6aff9872..0000000000 --- a/examples/internal/cloudflare-remix/vite.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { vitePlugin as remix } from "@remix-run/dev"; -import { installGlobals } from "@remix-run/node"; -import { defineConfig } from "vite"; -import tsconfigPaths from "vite-tsconfig-paths"; - -installGlobals(); - -export default defineConfig({ - plugins: [remix(), tsconfigPaths()], -}); diff --git a/examples/internal/cloudflare-static-site/index.html b/examples/internal/cloudflare-static-site/index.html deleted file mode 100644 index a1c47c6f46..0000000000 --- a/examples/internal/cloudflare-static-site/index.html +++ /dev/null @@ -1,3 +0,0 @@ - - Hello - diff --git a/examples/internal/cloudflare-static-site/sst.config.ts b/examples/internal/cloudflare-static-site/sst.config.ts deleted file mode 100644 index d5223ea484..0000000000 --- a/examples/internal/cloudflare-static-site/sst.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -/// - -export default $config({ - app(input) { - return { - name: "cloudflare-static-site", - removal: input?.stage === "production" ? "retain" : "remove", - providers: { - cloudflare: true, - }, - home: "aws", - }; - }, - async run() { - new sst.cloudflare.StaticSite("MySite", { - domain: "static.sstion.com", - }); - }, -}); diff --git a/examples/internal/cloudflare-vite/.eslintrc.cjs b/examples/internal/cloudflare-vite/.eslintrc.cjs deleted file mode 100644 index d6c9537953..0000000000 --- a/examples/internal/cloudflare-vite/.eslintrc.cjs +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, es2020: true }, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:react-hooks/recommended', - ], - ignorePatterns: ['dist', '.eslintrc.cjs'], - parser: '@typescript-eslint/parser', - plugins: ['react-refresh'], - rules: { - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - }, -} diff --git a/examples/internal/cloudflare-vite/.gitignore b/examples/internal/cloudflare-vite/.gitignore deleted file mode 100644 index a547bf36d8..0000000000 --- a/examples/internal/cloudflare-vite/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/examples/internal/cloudflare-vite/README.md b/examples/internal/cloudflare-vite/README.md deleted file mode 100644 index 0d6babeddb..0000000000 --- a/examples/internal/cloudflare-vite/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - -- Configure the top-level `parserOptions` property like this: - -```js -export default { - // other rules... - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['./tsconfig.json', './tsconfig.node.json'], - tsconfigRootDir: __dirname, - }, -} -``` - -- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` -- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/examples/internal/cloudflare-vite/index.html b/examples/internal/cloudflare-vite/index.html deleted file mode 100644 index e4b78eae12..0000000000 --- a/examples/internal/cloudflare-vite/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/examples/internal/cloudflare-vite/package.json b/examples/internal/cloudflare-vite/package.json deleted file mode 100644 index 509498c485..0000000000 --- a/examples/internal/cloudflare-vite/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "cf-vite", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" - }, - "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@types/react": "^18.2.56", - "@types/react-dom": "^18.2.19", - "@typescript-eslint/eslint-plugin": "^7.0.2", - "@typescript-eslint/parser": "^7.0.2", - "@vitejs/plugin-react": "^4.2.1", - "eslint": "^8.56.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.5", - "typescript": "^5.2.2", - "vite": "^5.1.4" - } -} diff --git a/examples/internal/cloudflare-vite/public/vite.svg b/examples/internal/cloudflare-vite/public/vite.svg deleted file mode 100644 index e7b8dfb1b2..0000000000 --- a/examples/internal/cloudflare-vite/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/internal/cloudflare-vite/src/App.css b/examples/internal/cloudflare-vite/src/App.css deleted file mode 100644 index b9d355df2a..0000000000 --- a/examples/internal/cloudflare-vite/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/examples/internal/cloudflare-vite/src/App.tsx b/examples/internal/cloudflare-vite/src/App.tsx deleted file mode 100644 index 9cb2808cab..0000000000 --- a/examples/internal/cloudflare-vite/src/App.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useState } from "react"; -import reactLogo from "./assets/react.svg"; -import viteLogo from "/vite.svg"; -import "./App.css"; - -function App() { - const [count, setCount] = useState(0); - - return ( - <> - -

Vite + React v3

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ); -} - -export default App; diff --git a/examples/internal/cloudflare-vite/src/assets/react.svg b/examples/internal/cloudflare-vite/src/assets/react.svg deleted file mode 100644 index 6c87de9bb3..0000000000 --- a/examples/internal/cloudflare-vite/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/internal/cloudflare-vite/src/index.css b/examples/internal/cloudflare-vite/src/index.css deleted file mode 100644 index 6119ad9a8f..0000000000 --- a/examples/internal/cloudflare-vite/src/index.css +++ /dev/null @@ -1,68 +0,0 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} diff --git a/examples/internal/cloudflare-vite/src/sst-env.d.ts b/examples/internal/cloudflare-vite/src/sst-env.d.ts deleted file mode 100644 index 1fadcdf703..0000000000 --- a/examples/internal/cloudflare-vite/src/sst-env.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/// - interface ImportMetaEnv { - - } - interface ImportMeta { - readonly env: ImportMetaEnv - } \ No newline at end of file diff --git a/examples/internal/cloudflare-vite/src/vite-env.d.ts b/examples/internal/cloudflare-vite/src/vite-env.d.ts deleted file mode 100644 index 11f02fe2a0..0000000000 --- a/examples/internal/cloudflare-vite/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/examples/internal/cloudflare-vite/sst.config.ts b/examples/internal/cloudflare-vite/sst.config.ts deleted file mode 100644 index 2c2dec1356..0000000000 --- a/examples/internal/cloudflare-vite/sst.config.ts +++ /dev/null @@ -1,24 +0,0 @@ -/// - -export default $config({ - app(input) { - return { - name: "cloudflare-vite", - removal: input?.stage === "production" ? "retain" : "remove", - providers: { - aws: {}, - cloudflare: {}, - }, - home: "aws", - }; - }, - async run() { - new sst.cloudflare.StaticSite("Web", { - build: { - command: "pnpm run build", - output: "dist", - }, - domain: "vite.sstion.com", - }); - }, -}); diff --git a/examples/internal/database-benchmark/.gitignore b/examples/internal/database-benchmark/.gitignore deleted file mode 100644 index cc54d25a17..0000000000 --- a/examples/internal/database-benchmark/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ - -# sst -.sst diff --git a/examples/internal/database-benchmark/README.md b/examples/internal/database-benchmark/README.md deleted file mode 100644 index f22d9229bb..0000000000 --- a/examples/internal/database-benchmark/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# database-benchmark - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run index.ts -``` - -This project was created using `bun init` in bun v1.1.17. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/examples/internal/database-benchmark/bun.lockb b/examples/internal/database-benchmark/bun.lockb deleted file mode 100755 index 8a1c7b5990..0000000000 Binary files a/examples/internal/database-benchmark/bun.lockb and /dev/null differ diff --git a/examples/internal/database-benchmark/drizzle.config.ts b/examples/internal/database-benchmark/drizzle.config.ts deleted file mode 100644 index f1018a6c21..0000000000 --- a/examples/internal/database-benchmark/drizzle.config.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { defineConfig } from "drizzle-kit"; -import { Resource } from "sst"; - -const choice = process.env.DRIZZLE_CONFIG; -if (!choice) { - throw new Error("DRIZZLE_CONFIG must be set"); -} - -console.log(Resource.Planetscale); -const configs = { - "pg-data-api": defineConfig({ - driver: "aws-data-api", - schema: "./src/schema/postgres.sql.ts", - dialect: "postgresql", - dbCredentials: { - database: Resource.Postgres.database, - resourceArn: Resource.Postgres.clusterArn, - secretArn: Resource.Postgres.secretArn, - }, - }), - planetscale: defineConfig({ - dialect: "mysql", - schema: "./src/schema/mysql.sql.ts", - dbCredentials: { - url: `mysql://${Resource.Planetscale.username}:${Resource.Planetscale.password}@${Resource.Planetscale.host}/${Resource.Planetscale.database}`, - }, - }), -}; - -const match = configs[choice]; -if (!match) { - throw new Error(`Unknown DRIZZLE_CONFIG: ${choice}`); -} - -export default match; diff --git a/examples/internal/database-benchmark/package.json b/examples/internal/database-benchmark/package.json deleted file mode 100644 index 5f334a9229..0000000000 --- a/examples/internal/database-benchmark/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "database-benchmark", - "module": "index.ts", - "type": "module", - "scripts": { - "db": "sst shell drizzle-kit" - }, - "devDependencies": { - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "dependencies": { - "@aws-sdk/client-rds-data": "^3.600.0", - "@planetscale/database": "^1.18.0", - "drizzle-kit": "^0.22.7", - "drizzle-orm": "^0.31.2" - } -} diff --git a/examples/internal/database-benchmark/src/drizzle/pg-data-api.ts b/examples/internal/database-benchmark/src/drizzle/pg-data-api.ts deleted file mode 100644 index a5445e72f4..0000000000 --- a/examples/internal/database-benchmark/src/drizzle/pg-data-api.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Resource } from "sst"; -import { drizzle } from "drizzle-orm/aws-data-api/pg"; -import { RDSDataClient } from "@aws-sdk/client-rds-data"; - -export function pgDataApi() { - const client = new RDSDataClient({}); - - return drizzle(client, { - database: Resource.Postgres.database, - secretArn: Resource.Postgres.secretArn, - resourceArn: Resource.Postgres.clusterArn, - }); -} diff --git a/examples/internal/database-benchmark/src/drizzle/planetscale.ts b/examples/internal/database-benchmark/src/drizzle/planetscale.ts deleted file mode 100644 index a175aef32a..0000000000 --- a/examples/internal/database-benchmark/src/drizzle/planetscale.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { drizzle } from "drizzle-orm/planetscale-serverless"; -import { Resource } from "sst"; -import { Client } from "@planetscale/database"; -export * from "drizzle-orm"; - -export function planetscale() { - const client = new Client({ - host: Resource.Planetscale.host, - username: Resource.Planetscale.username, - password: Resource.Planetscale.password, - fetch: (url, init) => { - delete init["cache"]; - return fetch(url, init); - }, - }); - return drizzle(client); -} diff --git a/examples/internal/database-benchmark/src/lambda.ts b/examples/internal/database-benchmark/src/lambda.ts deleted file mode 100644 index 22e32d77f3..0000000000 --- a/examples/internal/database-benchmark/src/lambda.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { sql } from "drizzle-orm"; -import { pgDataApi } from "./drizzle/pg-data-api"; -import { APIGatewayProxyStructuredResultV2 } from "aws-lambda"; -import { planetscale } from "./drizzle/planetscale"; - -export async function handler(): Promise { - const results = {} as Record; - - for (const connect of [pgDataApi, planetscale]) { - const db = connect(); - // @ts-ignore - await db.execute(sql`SELECT 1`); - const time = Date.now(); - // @ts-ignore - await db.execute(sql`SELECT 1`); - const elapsed = Date.now() - time; - results[connect.name] = elapsed; - } - - return { - statusCode: 200, - body: JSON.stringify(results), - headers: { - "content-type": "application/json", - }, - }; -} diff --git a/examples/internal/database-benchmark/src/schema/mysql.sql.ts b/examples/internal/database-benchmark/src/schema/mysql.sql.ts deleted file mode 100644 index 8fb1cdb2f8..0000000000 --- a/examples/internal/database-benchmark/src/schema/mysql.sql.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { int, mysqlTable, text, timestamp } from "drizzle-orm/mysql-core"; - -export const todoTable = mysqlTable("todo", { - id: int("id").autoincrement().primaryKey(), - title: text("title"), - description: text("description"), - timeCreated: timestamp("time_created").notNull().defaultNow(), - timeCompleted: timestamp("time_completed"), -}); diff --git a/examples/internal/database-benchmark/src/schema/postgres.sql.ts b/examples/internal/database-benchmark/src/schema/postgres.sql.ts deleted file mode 100644 index 734adce4d9..0000000000 --- a/examples/internal/database-benchmark/src/schema/postgres.sql.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { pgTable, smallserial, text, timestamp } from "drizzle-orm/pg-core"; - -export const todoTable = pgTable("todo", { - id: smallserial("id").primaryKey(), - title: text("title"), - description: text("description"), - timeCreated: timestamp("time_created").notNull().defaultNow(), - timeCompleted: timestamp("time_completed"), -}); diff --git a/examples/internal/database-benchmark/src/worker.ts b/examples/internal/database-benchmark/src/worker.ts deleted file mode 100644 index 37b1fe5d2d..0000000000 --- a/examples/internal/database-benchmark/src/worker.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { sql } from "drizzle-orm"; -import { planetscale } from "./drizzle/planetscale"; - -export default { - async fetch(event: any) { - const results = {} as Record; - - for (const connect of [planetscale]) { - const db = connect(); - // @ts-ignore - await db.execute(sql`SELECT 1`); - const time = Date.now(); - // @ts-ignore - await db.execute(sql`SELECT 1`); - const elapsed = Date.now() - time; - results[connect.name] = elapsed; - } - - return new Response(JSON.stringify(results), { - status: 200, - headers: { - "content-type": "application/json", - }, - }); - }, -}; diff --git a/examples/internal/database-benchmark/sst-env.d.ts b/examples/internal/database-benchmark/sst-env.d.ts deleted file mode 100644 index f110f33043..0000000000 --- a/examples/internal/database-benchmark/sst-env.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - } -} -export {} diff --git a/examples/internal/database-benchmark/sst.config.ts b/examples/internal/database-benchmark/sst.config.ts deleted file mode 100644 index 157c205e1d..0000000000 --- a/examples/internal/database-benchmark/sst.config.ts +++ /dev/null @@ -1,66 +0,0 @@ -/// -export default $config({ - app(input) { - return { - name: "database-benchmark", - removal: input?.stage === "production" ? "retain" : "remove", - home: "aws", - providers: { planetscale: true, cloudflare: true }, - }; - }, - async run() { - const vpc = new sst.aws.Vpc("Vpc"); - - const pscaleDB = new planetscale.Database("PlanetscaleDatabase", { - name: $app.name, - organization: "sst", - clusterSize: "PS_10", - region: "us-east", - }); - - const pscalePassword = new planetscale.Password("PlanetscalePassword", { - organization: pscaleDB.organization, - database: pscaleDB.name, - branch: pscaleDB.defaultBranch, - name: $app.stage, - }); - - const pscale = new sst.Resource("Planetscale", { - host: pscalePassword.accessHostUrl, - database: pscalePassword.database, - username: pscalePassword.username, - password: pscalePassword.plaintext, - }); - - const postgres = new sst.aws.Postgres("Postgres", { - vpc, - }); - - const worker = new sst.cloudflare.Worker("Worker", { - link: [pscale], - handler: "./src/worker.ts", - transform: { - worker: { - placements: [ - { - mode: "smart", - }, - ], - }, - }, - url: true, - }); - - const lambda = new sst.aws.Function("Lambda", { - dev: false, - url: true, - link: [postgres, pscale], - handler: "./src/lambda.handler", - }); - - return { - lambda: lambda.url, - worker: worker.url, - }; - }, -}); diff --git a/examples/internal/database-benchmark/tsconfig.json b/examples/internal/database-benchmark/tsconfig.json deleted file mode 100644 index 2c63c08510..0000000000 --- a/examples/internal/database-benchmark/tsconfig.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} diff --git a/examples/internal/fly/.gitignore b/examples/internal/fly/.gitignore deleted file mode 100644 index cc54d25a17..0000000000 --- a/examples/internal/fly/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ - -# sst -.sst diff --git a/examples/internal/fly/package.json b/examples/internal/fly/package.json deleted file mode 100644 index 3501d2923a..0000000000 --- a/examples/internal/fly/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "base-ts", - "version": "0.0.0", - "dependencies": { - "sst": "^3.0.4" - } -} diff --git a/examples/internal/fly/sst-env.d.ts b/examples/internal/fly/sst-env.d.ts deleted file mode 100644 index 5213abcbfa..0000000000 --- a/examples/internal/fly/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -export {} -declare module "sst" { - export interface Resource { - } -} diff --git a/examples/internal/fly/sst.config.ts b/examples/internal/fly/sst.config.ts deleted file mode 100644 index d67fde0f48..0000000000 --- a/examples/internal/fly/sst.config.ts +++ /dev/null @@ -1,28 +0,0 @@ -/// -export default $config({ - app(input) { - return { - name: "fly", - removal: input?.stage === "production" ? "retain" : "remove", - home: "local", - providers: { fly: "0.1.17" }, - }; - }, - async run() { - const app = new fly.App("App", { - name: "sst-demo", - }); - const machine = new fly.Machine("Machine", { - app: app.name, - image: "nginx:latest", - region: "mia", - services: [ - { - protocol: "tcp", - internalPort: 80, - ports: [{ port: 80, handlers: ["http"] }], - }, - ], - }); - }, -}); diff --git a/examples/internal/fly/tsconfig.json b/examples/internal/fly/tsconfig.json deleted file mode 100644 index 0967ef424b..0000000000 --- a/examples/internal/fly/tsconfig.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/examples/internal/playground/.vscode/settings.json b/examples/internal/playground/.vscode/settings.json deleted file mode 100644 index 23830fb423..0000000000 --- a/examples/internal/playground/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "git.ignoreLimitWarning": true -} diff --git a/examples/internal/playground/functions/apiv1/index.ts b/examples/internal/playground/functions/apiv1/index.ts deleted file mode 100644 index 757682bcc0..0000000000 --- a/examples/internal/playground/functions/apiv1/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const handler = async (event) => { - return { - statusCode: 200, - body: JSON.stringify({ event }, null, 2), - }; -}; diff --git a/examples/internal/playground/functions/apiv2/index.ts b/examples/internal/playground/functions/apiv2/index.ts deleted file mode 100644 index 614aa75e57..0000000000 --- a/examples/internal/playground/functions/apiv2/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Resource } from "sst"; - -export const authorizer = async () => { - return { - isAuthorized: true, - context: { - userId: "123", - }, - }; -}; - -export const handler = async (event) => { - return { - statusCode: 200, - body: JSON.stringify({ event, resources: Resource.MyBucket }, null, 2), - }; -}; diff --git a/examples/internal/playground/functions/apiws/index.ts b/examples/internal/playground/functions/apiws/index.ts deleted file mode 100644 index 64ba89dd6c..0000000000 --- a/examples/internal/playground/functions/apiws/index.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Resource } from "sst"; -import { - ApiGatewayManagementApiClient, - PostToConnectionCommand, -} from "@aws-sdk/client-apigatewaymanagementapi"; - -export async function connect(event) { - console.log("!!! connect"); - - // Handle subprotocols - const protocolHeader = event.headers["Sec-WebSocket-Protocol"]; - if (protocolHeader) { - const subprotocols = protocolHeader.split(",").map((p) => p.trim()); - return subprotocols.includes("MY_ALLOWED_PROTOCOL") - ? { - statusCode: 200, - headers: { "Sec-WebSocket-Protocol": "MY_ALLOWED_PROTOCOL" }, - } - : { statusCode: 400 }; - } - - return { statusCode: 200 }; -} - -export async function disconnect(event) { - console.log("!!! disconnect"); - return { statusCode: 200 }; -} - -export async function sendMessage(event) { - console.log("!!! sendMessage"); - return { statusCode: 200 }; -} - -export async function catchAll(event) { - console.log("!!! default"); - - // Send a message back to the client - const client = new ApiGatewayManagementApiClient({ - endpoint: Resource.MyApiWebsocket.managementEndpoint, - }); - await client.send( - new PostToConnectionCommand({ - ConnectionId: event.requestContext.connectionId, - Data: "Hey! What is this?", - }) - ); - - return { statusCode: 200 }; -} - -export async function authorizer(event, context) { - console.log("!!! authorizer"); - return { - principalId: "*", - policyDocument: { - Version: "2012-10-17", - Statement: [ - { - Action: "execute-api:Invoke", - Effect: "Allow", - Resource: "*", - }, - ], - }, - }; -} diff --git a/examples/internal/playground/functions/auth/index.ts b/examples/internal/playground/functions/auth/index.ts deleted file mode 100644 index 34831e9856..0000000000 --- a/examples/internal/playground/functions/auth/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Resource } from "sst"; -import { authorizer } from "@openauthjs/openauth"; -import { handle } from "hono/aws-lambda"; -import { subjects } from "./subjects.js"; -import { PasswordAdapter } from "@openauthjs/openauth/adapter/password"; -import { PasswordUI } from "@openauthjs/openauth/ui/password"; -import { GoogleAdapter } from "@openauthjs/openauth/adapter/google"; -import { DynamoStorage } from "@openauthjs/openauth/storage/dynamo"; - -const app = authorizer({ - subjects, - storage: DynamoStorage({ - table: Resource.RouterAuthStorage.name, - pk: "pk", - sk: "sk", - }), - providers: { - password: PasswordAdapter( - PasswordUI({ - sendCode: async (email, code) => { - console.log(email, code); - }, - }) - ), - google: GoogleAdapter({ - clientID: Resource.GOOGLE_CLIENT_ID.value, - clientSecret: Resource.GOOGLE_CLIENT_SECRET.value, - scopes: ["email"], - }), - }, - success: async (ctx, value) => { - if (value.provider === "password") { - return ctx.subject("user", { - email: value.email, - }); - } - if (value.provider === "google") { - return ctx.subject("user", { - email: value.tokenset.access, - }); - } - throw new Error("Invalid provider"); - }, -}); - -// @ts-ignore -export const handler = handle(app); diff --git a/examples/internal/playground/functions/auth/subjects.ts b/examples/internal/playground/functions/auth/subjects.ts deleted file mode 100644 index c370bd81f0..0000000000 --- a/examples/internal/playground/functions/auth/subjects.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { object, string } from "valibot"; -import { createSubjects } from "@openauthjs/openauth"; - -export const subjects = createSubjects({ - user: object({ - email: string(), - }), -}); diff --git a/examples/internal/playground/functions/bucket/index.ts b/examples/internal/playground/functions/bucket/index.ts deleted file mode 100644 index 118f4371ad..0000000000 --- a/examples/internal/playground/functions/bucket/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const handler = async (event) => { - console.log(event); - return "ok"; -}; diff --git a/examples/internal/playground/functions/bundled-example/index.js b/examples/internal/playground/functions/bundled-example/index.js deleted file mode 100644 index c85c858bca..0000000000 --- a/examples/internal/playground/functions/bundled-example/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Resource } from "sst"; - -export const handler = async (event) => { - return { - statusCode: 200, - body: JSON.stringify({ event, resources: Resource.MyBucket }, null, 2), - }; -}; diff --git a/examples/internal/playground/functions/bundled-example/package.json b/examples/internal/playground/functions/bundled-example/package.json deleted file mode 100644 index ccef9f5de5..0000000000 --- a/examples/internal/playground/functions/bundled-example/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "bundled-function", - "version": "1.0.0", - "description": "", - "type": "module", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "sst": "3.0.1-9" - } -} diff --git a/examples/internal/playground/functions/bundled-example/sst-env.d.ts b/examples/internal/playground/functions/bundled-example/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/internal/playground/functions/bundled-example/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/playground/functions/bus/index.ts b/examples/internal/playground/functions/bus/index.ts deleted file mode 100644 index b196ccb338..0000000000 --- a/examples/internal/playground/functions/bus/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Resource } from "sst"; -import { - EventBridgeClient, - PutEventsCommand, -} from "@aws-sdk/client-eventbridge"; - -const client = new EventBridgeClient(); - -export const publisher = async () => { - await client.send( - new PutEventsCommand({ - Entries: [ - { - EventBusName: Resource.MyBus.name, - Source: "app.myevent", - DetailType: "MyEvent", - Detail: JSON.stringify({ foo: "bar" }), - }, - ], - }) - ); - return { - statusCode: 200, - }; -}; - -export const subscriber = async () => { - console.log("bus subscriber: message received"); -}; diff --git a/examples/internal/playground/functions/cron/index.ts b/examples/internal/playground/functions/cron/index.ts deleted file mode 100644 index 40a60c8143..0000000000 --- a/examples/internal/playground/functions/cron/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Resource } from "sst"; - -export const handler = async (event) => { - console.log("event", event); - console.log("Resource.MyBucket", Resource.MyBucket); -}; diff --git a/examples/internal/playground/functions/efs/index.ts b/examples/internal/playground/functions/efs/index.ts deleted file mode 100644 index 1bcce15276..0000000000 --- a/examples/internal/playground/functions/efs/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { stat, readFile, writeFile } from "fs/promises"; - -export const handler = async () => { - const dir = await stat("/mnt/efs"); - console.log("DIR STAT", dir); - if (!dir.isDirectory()) { - return { - statusCode: 500, - body: "/mnt/efs not mounted", - }; - } - - let value; - try { - const content = await readFile("/mnt/efs/counter", "utf8"); - value = parseInt(content) + 1; - } catch (e) { - console.log("READ ERROR", e); - value = 1; - } - await writeFile("/mnt/efs/counter", value.toString()); - - return { - statusCode: 200, - body: value.toString(), - }; -}; diff --git a/examples/internal/playground/functions/email/index.ts b/examples/internal/playground/functions/email/index.ts deleted file mode 100644 index 6c8e5f442a..0000000000 --- a/examples/internal/playground/functions/email/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Resource } from "sst"; -import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2"; - -const client = new SESv2Client(); - -export const sender = async () => { - await client.send( - new SendEmailCommand({ - FromEmailAddress: Resource.MyEmail.sender, - Destination: { - ToAddresses: [Resource.MyEmail.sender], - }, - Content: { - Simple: { - Subject: { - Data: "Hello World!", - }, - Body: { - Text: { - Data: "Sent from my SST app.", - }, - }, - }, - }, - }) - ); - - return { - statusCode: 200, - body: "Sent!", - }; -}; - -export const notification = async (event: any) => { - console.log(JSON.stringify(event, null, 2)); - return { - statusCode: 200, - body: "Received!", - }; -}; diff --git a/examples/internal/playground/functions/handler-example/index.ts b/examples/internal/playground/functions/handler-example/index.ts deleted file mode 100644 index c85c858bca..0000000000 --- a/examples/internal/playground/functions/handler-example/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Resource } from "sst"; - -export const handler = async (event) => { - return { - statusCode: 200, - body: JSON.stringify({ event, resources: Resource.MyBucket }, null, 2), - }; -}; diff --git a/examples/internal/playground/functions/mysql/index.ts b/examples/internal/playground/functions/mysql/index.ts deleted file mode 100644 index e2eb500d1a..0000000000 --- a/examples/internal/playground/functions/mysql/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Resource } from "sst"; -import mysql from "mysql2/promise"; - -const connection = await mysql.createConnection({ - user: Resource.MyMysql.username, - password: Resource.MyMysql.password, - database: Resource.MyMysql.database, - host: Resource.MyMysql.host, - port: Resource.MyMysql.port, -}); - -export async function handler() { - const res = await connection.execute("SELECT NOW() AS now"); - return { - statusCode: 200, - body: JSON.stringify(res[0]), - }; -} diff --git a/examples/internal/playground/functions/open-control/index.ts b/examples/internal/playground/functions/open-control/index.ts deleted file mode 100644 index 5e67f4dc1a..0000000000 --- a/examples/internal/playground/functions/open-control/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { handle } from "hono/aws-lambda"; -import { create } from "opencontrol"; -import { tools } from "sst/opencontrol"; - -const app = create({ - key: process.env.OPENCONTROL_KEY, - tools, -}); - -export const handler = handle(app); - -//const tools = [workout, aws, ...sstTools]; -//const batch = tool({ -// name: "batch", -// description: "Run another tool multiple times with different arguments", -// args: z.object({ -// tool: z -// .enum(tools.map((t) => t.name) as [string, ...string[]]) -// .describe("The tool to run"), -// args: z -// .array(z.any()) -// .describe("An array of arguments to pass to the tool"), -// }), -// async run(input) { -// const tool = tools.find((t) => t.name === input.tool); -// if (!tool) throw new Error(`Tool ${input.tool} not found`); -// -// // Validate each argument against the tool's schema if it exists -// if (tool.args) { -// input.args.forEach((arg, index) => { -// try { -// tool.args.parse(arg); -// } catch (error) { -// throw new Error( -// `Argument at index ${index} is invalid for tool ${input.tool}: ${error.message}` -// ); -// } -// }); -// } -// -// return await Promise.all(input.args.map((arg) => tool.run(arg))); -// }, -//}); diff --git a/examples/internal/playground/functions/open-search/index.ts b/examples/internal/playground/functions/open-search/index.ts deleted file mode 100644 index 06599bd5a1..0000000000 --- a/examples/internal/playground/functions/open-search/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Resource } from "sst"; -import { Client } from "@opensearch-project/opensearch"; - -const client = new Client({ - node: Resource.MyOpenSearch.url, - auth: { - username: Resource.MyOpenSearch.username, - password: Resource.MyOpenSearch.password, - }, -}); - -export async function handler() { - // Index a document - await client.index({ - index: "my-index", - id: "1", - body: { user: "foo", message: "Hello World!" }, - refresh: true, // make it visible to search immediately - }); - - // Search for a document - const result = await client.search({ - index: "my-index", - body: { - query: { match: { message: "World" } }, - }, - }); - - return { - statusCode: 200, - body: JSON.stringify(result.body.hits, null, 2), - }; -} diff --git a/examples/internal/playground/functions/postgres/index.ts b/examples/internal/playground/functions/postgres/index.ts deleted file mode 100644 index a4cccdc57d..0000000000 --- a/examples/internal/playground/functions/postgres/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import pg from "pg"; -import { Resource } from "sst"; -const { Client } = pg; -const client = new Client({ - user: Resource.MyPostgres.username, - password: Resource.MyPostgres.password, - database: Resource.MyPostgres.database, - host: Resource.MyPostgres.host, - port: Resource.MyPostgres.port, -}); -await client.connect(); - -export async function handler() { - const res = await client.query("SELECT $1::text as message", [ - "Hello world!", - ]); - return { - statusCode: 200, - body: res.rows[0].message, - }; -} diff --git a/examples/internal/playground/functions/queue/index.ts b/examples/internal/playground/functions/queue/index.ts deleted file mode 100644 index 1a4fcadb8f..0000000000 --- a/examples/internal/playground/functions/queue/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Resource } from "sst"; -import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs"; -const client = new SQSClient(); - -export const publisher = async () => { - await client.send( - new SendMessageCommand({ - QueueUrl: Resource.MyQueue.url, - MessageBody: JSON.stringify({ foo: "bar" }), - }) - ); - - return { - statusCode: 200, - body: JSON.stringify({ status: "sent" }, null, 2), - }; -}; - -export const subscriber = async () => { - console.log("queue subscriber: message received"); -}; diff --git a/examples/internal/playground/functions/redis/cluster-index.ts b/examples/internal/playground/functions/redis/cluster-index.ts deleted file mode 100644 index 07da57201f..0000000000 --- a/examples/internal/playground/functions/redis/cluster-index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Cluster } from "ioredis"; -import { Resource } from "sst"; - -const client = new Cluster( - [ - { - host: Resource.MyRedis.host, - port: Resource.MyRedis.port, - }, - ], - { - redisOptions: { - tls: { - checkServerIdentity: () => undefined, - }, - username: Resource.MyRedis.username, - password: Resource.MyRedis.password, - }, - } -); - -export async function handler() { - await client.set("foo", `bar-${Date.now()}`); - return { - statusCode: 200, - body: JSON.stringify({ - foo: await client.get("foo"), - }), - }; -} diff --git a/examples/internal/playground/functions/redis/instance-index.ts b/examples/internal/playground/functions/redis/instance-index.ts deleted file mode 100644 index 46adb7a692..0000000000 --- a/examples/internal/playground/functions/redis/instance-index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Redis from "ioredis"; -import { Resource } from "sst"; - -const client = new Redis({ - host: Resource.MyRedis.host, - port: Resource.MyRedis.port, - username: Resource.MyRedis.username, - password: Resource.MyRedis.password, - tls: { - checkServerIdentity: () => undefined, - }, -}); - -export async function handler() { - await client.set("foo", `bar-${Date.now()}`); - return { - statusCode: 200, - body: JSON.stringify({ - foo: await client.get("foo"), - }), - }; -} diff --git a/examples/internal/playground/functions/router/index.ts b/examples/internal/playground/functions/router/index.ts deleted file mode 100644 index c9d3e9e72f..0000000000 --- a/examples/internal/playground/functions/router/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const handler = async (event: any) => { - return { - statusCode: 200, - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(event, null, 2), - }; -}; diff --git a/examples/internal/playground/functions/task/index.ts b/examples/internal/playground/functions/task/index.ts deleted file mode 100644 index 868b8bf706..0000000000 --- a/examples/internal/playground/functions/task/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Resource } from "/Users/frank/Sites/sst/sdk/js/src/resource"; -import { task } from "/Users/frank/Sites/sst/sdk/js/src/aws/task"; - -export const handler = async () => { - const ret = await task.run(Resource.MyTask); - - if (ret.response.tasks?.length) { - return { - taskArn: ret.response.tasks[0].taskArn, - }; - } - - return JSON.stringify( - { - ARN: ret.response.tasks[0]?.taskArn, - response: ret.response, - }, - null, - 2 - ); - - //const ret = await task.describe(Resource.MyTask, t); - - //const ret = await task.stop(Resource.MyTask, t); - //console.log(ret.task?.lastStatus); -}; diff --git a/examples/internal/playground/functions/topic/index.ts b/examples/internal/playground/functions/topic/index.ts deleted file mode 100644 index 7b4d5aedda..0000000000 --- a/examples/internal/playground/functions/topic/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Resource } from "sst"; -import { SNSClient, PublishCommand } from "@aws-sdk/client-sns"; -const client = new SNSClient(); - -export const publisher = async () => { - await client.send( - new PublishCommand({ - TargetArn: Resource.MyTopic.arn, - Message: JSON.stringify({ foo: "bar" }), - MessageAttributes: { foo: { DataType: "String", StringValue: "bar" } }, - }) - ); - - return { - statusCode: 200, - body: JSON.stringify({ status: "sent" }, null, 2), - }; -}; - -export const subscriber = async () => { - console.log("topic subscriber: message received"); -}; diff --git a/examples/internal/playground/images/sidecar/Dockerfile b/examples/internal/playground/images/sidecar/Dockerfile deleted file mode 100644 index 9cfff20241..0000000000 --- a/examples/internal/playground/images/sidecar/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM node:18-bullseye-slim - -WORKDIR /app/ - -COPY package.json /app -RUN npm install - -COPY index.mjs /app - -ENTRYPOINT ["node", "index.mjs"] \ No newline at end of file diff --git a/examples/internal/playground/images/sidecar/index.mjs b/examples/internal/playground/images/sidecar/index.mjs deleted file mode 100644 index 6377e697ef..0000000000 --- a/examples/internal/playground/images/sidecar/index.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import express from "express"; - -const PORT = 8080; - -const app = express(); - -app.get("/", async (req, res) => { - res.send("I'm a sidecar"); -}); - -app.listen(PORT, () => { - console.log(`Server is running on http://localhost:${PORT}`); -}); diff --git a/examples/internal/playground/images/sidecar/package.json b/examples/internal/playground/images/sidecar/package.json deleted file mode 100644 index 2b120522d9..0000000000 --- a/examples/internal/playground/images/sidecar/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "cluster", - "version": "1.0.0", - "description": "", - "main": "index.js", - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "express": "^4.19.2", - "sst": "latest" - } -} diff --git a/examples/internal/playground/images/sidecar/sst-env.d.ts b/examples/internal/playground/images/sidecar/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/internal/playground/images/sidecar/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/playground/images/task/Dockerfile b/examples/internal/playground/images/task/Dockerfile deleted file mode 100644 index 9cfff20241..0000000000 --- a/examples/internal/playground/images/task/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM node:18-bullseye-slim - -WORKDIR /app/ - -COPY package.json /app -RUN npm install - -COPY index.mjs /app - -ENTRYPOINT ["node", "index.mjs"] \ No newline at end of file diff --git a/examples/internal/playground/images/task/index.mjs b/examples/internal/playground/images/task/index.mjs deleted file mode 100644 index be107113bf..0000000000 --- a/examples/internal/playground/images/task/index.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import { Resource } from "sst"; - -console.log({ - sdk: Resource.MyBucket.name, - env: process.env.SST_RESOURCE_MyBucket, -}); diff --git a/examples/internal/playground/images/task/package.json b/examples/internal/playground/images/task/package.json deleted file mode 100644 index 60514e41c6..0000000000 --- a/examples/internal/playground/images/task/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "job", - "version": "1.0.0", - "description": "", - "main": "index.js", - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "sst": "latest" - } -} diff --git a/examples/internal/playground/images/task/sst-env.d.ts b/examples/internal/playground/images/task/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/internal/playground/images/task/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/playground/images/web/Dockerfile b/examples/internal/playground/images/web/Dockerfile deleted file mode 100644 index 1cd70fbcd7..0000000000 --- a/examples/internal/playground/images/web/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM node:18-bullseye-slim - -WORKDIR /app/ - -COPY package.json /app -RUN npm install - -# Ensure linked resources are available at build time -COPY build.mjs /app -RUN --mount=type=secret,id=SST_RESOURCE_MyBucket,env=SST_RESOURCE_MyBucket \ - node build.mjs - -COPY index.mjs /app - -ENTRYPOINT ["node", "index.mjs"] \ No newline at end of file diff --git a/examples/internal/playground/images/web/build.mjs b/examples/internal/playground/images/web/build.mjs deleted file mode 100644 index 2b66fb7e19..0000000000 --- a/examples/internal/playground/images/web/build.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import { Resource } from "sst"; - -console.log("SDK", Resource.MyBucket.name); diff --git a/examples/internal/playground/images/web/index.mjs b/examples/internal/playground/images/web/index.mjs deleted file mode 100644 index f3369d8804..0000000000 --- a/examples/internal/playground/images/web/index.mjs +++ /dev/null @@ -1,19 +0,0 @@ -import express from "express"; -import { Resource } from "sst"; - -const PORT = 80; - -const app = express(); - -app.get("/", async (req, res) => { - res.send( - JSON.stringify({ - sdk: Resource.MyBucket.name, - env: process.env.SST_RESOURCE_MyBucket, - }) - ); -}); - -app.listen(PORT, () => { - console.log(`Server is running on http://localhost:${PORT}`); -}); diff --git a/examples/internal/playground/images/web/package.json b/examples/internal/playground/images/web/package.json deleted file mode 100644 index 2b120522d9..0000000000 --- a/examples/internal/playground/images/web/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "cluster", - "version": "1.0.0", - "description": "", - "main": "index.js", - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "express": "^4.19.2", - "sst": "latest" - } -} diff --git a/examples/internal/playground/images/web/sst-env.d.ts b/examples/internal/playground/images/web/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/internal/playground/images/web/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/playground/infra-cf/sites/solid-start/.gitignore b/examples/internal/playground/infra-cf/sites/solid-start/.gitignore deleted file mode 100644 index 8ebae30246..0000000000 --- a/examples/internal/playground/infra-cf/sites/solid-start/.gitignore +++ /dev/null @@ -1,32 +0,0 @@ - -dist -.solid -.output -.vercel -.netlify -.vinxi -app.config.timestamp_*.js - -# Environment -.env -.env*.local - -# dependencies -/node_modules - -# IDEs and editors -/.idea -.project -.classpath -*.launch -.settings/ - -# Temp -gitignore - -# System Files -.DS_Store -Thumbs.db - -# sst -.sst diff --git a/examples/internal/playground/infra-cf/sites/solid-start/README.md b/examples/internal/playground/infra-cf/sites/solid-start/README.md deleted file mode 100644 index a84af3943c..0000000000 --- a/examples/internal/playground/infra-cf/sites/solid-start/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# SolidStart - -Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com); - -## Creating a project - -```bash -# create a new project in the current directory -npm init solid@latest - -# create a new project in my-app -npm init solid@latest my-app -``` - -## Developing - -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: - -```bash -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` - -## Building - -Solid apps are built with _presets_, which optimise your project for deployment to different environments. - -By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`. - -## This project was created with the [Solid CLI](https://solid-cli.netlify.app) diff --git a/examples/internal/playground/infra-cf/sites/solid-start/package.json b/examples/internal/playground/infra-cf/sites/solid-start/package.json deleted file mode 100644 index 4fbeae91a4..0000000000 --- a/examples/internal/playground/infra-cf/sites/solid-start/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "example-basic", - "type": "module", - "scripts": { - "build": "vinxi build", - "dev": "vinxi dev", - "dev:remote": "go run ../../../../cmd/sst shell --target=MySolidStart npm run dev", - "start": "vinxi start", - "version": "vinxi version" - }, - "dependencies": { - "@solidjs/meta": "^0.29.4", - "@solidjs/router": "^0.15.0", - "@solidjs/start": "^1.1.0", - "solid-js": "^1.9.5", - "sst": "latest", - "vinxi": "^0.5.7" - }, - "engines": { - "node": ">=22" - } -} diff --git a/examples/internal/playground/infra-cf/sites/solid-start/public/favicon.ico b/examples/internal/playground/infra-cf/sites/solid-start/public/favicon.ico deleted file mode 100644 index fb282da071..0000000000 Binary files a/examples/internal/playground/infra-cf/sites/solid-start/public/favicon.ico and /dev/null differ diff --git a/examples/internal/playground/infra-cf/sites/solid-start/src/app.css b/examples/internal/playground/infra-cf/sites/solid-start/src/app.css deleted file mode 100644 index 8596998a49..0000000000 --- a/examples/internal/playground/infra-cf/sites/solid-start/src/app.css +++ /dev/null @@ -1,39 +0,0 @@ -body { - font-family: Gordita, Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; -} - -a { - margin-right: 1rem; -} - -main { - text-align: center; - padding: 1em; - margin: 0 auto; -} - -h1 { - color: #335d92; - text-transform: uppercase; - font-size: 4rem; - font-weight: 100; - line-height: 1.1; - margin: 4rem auto; - max-width: 14rem; -} - -p { - max-width: 14rem; - margin: 2rem auto; - line-height: 1.35; -} - -@media (min-width: 480px) { - h1 { - max-width: none; - } - - p { - max-width: none; - } -} diff --git a/examples/internal/playground/infra-cf/sites/solid-start/src/app.tsx b/examples/internal/playground/infra-cf/sites/solid-start/src/app.tsx deleted file mode 100644 index b77c0e25f5..0000000000 --- a/examples/internal/playground/infra-cf/sites/solid-start/src/app.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { MetaProvider, Title } from "@solidjs/meta"; -import { A, Router } from "@solidjs/router"; -import { FileRoutes } from "@solidjs/start/router"; -import { Suspense } from "solid-js"; -import "./app.css"; - -export default function App() { - return ( - ( - - SolidStart - Basic - Index - About - {props.children} - - )} - > - - - ); -} diff --git a/examples/internal/playground/infra-cf/sites/solid-start/src/components/Counter.css b/examples/internal/playground/infra-cf/sites/solid-start/src/components/Counter.css deleted file mode 100644 index 220e179460..0000000000 --- a/examples/internal/playground/infra-cf/sites/solid-start/src/components/Counter.css +++ /dev/null @@ -1,21 +0,0 @@ -.increment { - font-family: inherit; - font-size: inherit; - padding: 1em 2em; - color: #335d92; - background-color: rgba(68, 107, 158, 0.1); - border-radius: 2em; - border: 2px solid rgba(68, 107, 158, 0); - outline: none; - width: 200px; - font-variant-numeric: tabular-nums; - cursor: pointer; -} - -.increment:focus { - border: 2px solid #335d92; -} - -.increment:active { - background-color: rgba(68, 107, 158, 0.2); -} \ No newline at end of file diff --git a/examples/internal/playground/infra-cf/sites/solid-start/src/components/Counter.tsx b/examples/internal/playground/infra-cf/sites/solid-start/src/components/Counter.tsx deleted file mode 100644 index 091fc5d0bc..0000000000 --- a/examples/internal/playground/infra-cf/sites/solid-start/src/components/Counter.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createSignal } from "solid-js"; -import "./Counter.css"; - -export default function Counter() { - const [count, setCount] = createSignal(0); - return ( - - ); -} diff --git a/examples/internal/playground/infra-cf/sites/solid-start/src/entry-server.tsx b/examples/internal/playground/infra-cf/sites/solid-start/src/entry-server.tsx deleted file mode 100644 index 401eff83fd..0000000000 --- a/examples/internal/playground/infra-cf/sites/solid-start/src/entry-server.tsx +++ /dev/null @@ -1,21 +0,0 @@ -// @refresh reload -import { createHandler, StartServer } from "@solidjs/start/server"; - -export default createHandler(() => ( - ( - - - - - - {assets} - - -
{children}
- {scripts} - - - )} - /> -)); diff --git a/examples/internal/playground/infra-cf/sites/solid-start/src/global.d.ts b/examples/internal/playground/infra-cf/sites/solid-start/src/global.d.ts deleted file mode 100644 index 4802ce5466..0000000000 --- a/examples/internal/playground/infra-cf/sites/solid-start/src/global.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -declare module "cloudflare:workers"; diff --git a/examples/internal/playground/infra-cf/sites/solid-start/src/routes/about.tsx b/examples/internal/playground/infra-cf/sites/solid-start/src/routes/about.tsx deleted file mode 100644 index 8371d911cd..0000000000 --- a/examples/internal/playground/infra-cf/sites/solid-start/src/routes/about.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Title } from "@solidjs/meta"; - -export default function Home() { - return ( -
- About -

About

-
- ); -} diff --git a/examples/internal/playground/infra-cf/sites/solid-start/src/routes/api.ts b/examples/internal/playground/infra-cf/sites/solid-start/src/routes/api.ts deleted file mode 100644 index 58112aadbd..0000000000 --- a/examples/internal/playground/infra-cf/sites/solid-start/src/routes/api.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { APIEvent } from "@solidjs/start/server"; -import { Resource } from "../util/resource"; - -export async function GET(input: APIEvent) { - return Response.json( - { - secret: Resource.MySecret.value, - foo: process.env.FOO, - }, - { status: 200 } - ); -} diff --git a/examples/internal/playground/infra-cf/sites/solid-start/src/routes/index.tsx b/examples/internal/playground/infra-cf/sites/solid-start/src/routes/index.tsx deleted file mode 100644 index 07438a8fa3..0000000000 --- a/examples/internal/playground/infra-cf/sites/solid-start/src/routes/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -//import { Resource } from "sst"; -import { createAsync } from "@solidjs/router"; - -async function loadBucket() { - "use server"; - return { - //bucket: Resource.MyBucket.name, - bucket: "test", - foo: process.env.FOO, - }; -} - -export const route = { - load: () => loadBucket(), -}; - -export default function Home() { - const bucket = createAsync(() => loadBucket()); - - return ( -
-

Hello world!

-

Bucket: {bucket()?.bucket}

-

Foo: {bucket()?.foo}

-
- ); -} diff --git a/examples/internal/playground/infra-cf/sites/solid-start/src/util/resource.ts b/examples/internal/playground/infra-cf/sites/solid-start/src/util/resource.ts deleted file mode 100644 index 1543145dcd..0000000000 --- a/examples/internal/playground/infra-cf/sites/solid-start/src/util/resource.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { env } from "cloudflare:workers"; - -export const Resource = new Proxy( - {}, - { - get(_target, prop: string) { - if (prop in env) { - const value = env[prop]; - return typeof value === "string" ? JSON.parse(value) : value; - } - throw new Error(`"${prop}" is not linked in your sst.config.ts`); - }, - } -) as Record; diff --git a/examples/internal/playground/infra-cf/sites/solid-start/sst-env.d.ts b/examples/internal/playground/infra-cf/sites/solid-start/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/internal/playground/infra-cf/sites/solid-start/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/playground/infra-cf/sst-env.d.ts b/examples/internal/playground/infra-cf/sst-env.d.ts deleted file mode 100644 index 1b29c78785..0000000000 --- a/examples/internal/playground/infra-cf/sst-env.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -declare module "sst" { - export interface Resource { - "ExtBucket": { - "type": "sst.cloudflare.Bucket" - } - "MyBucket": { - "type": "sst.cloudflare.Bucket" - } - } -} -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/playground/infra-cf/sst.config.ts b/examples/internal/playground/infra-cf/sst.config.ts deleted file mode 100644 index 9bb9630ab2..0000000000 --- a/examples/internal/playground/infra-cf/sst.config.ts +++ /dev/null @@ -1,67 +0,0 @@ -/// - -export default $config({ - app(input) { - return { - name: "play-cf", - home: "cloudflare", - }; - }, - async run() { - const ret: Record = {}; - const secret = new sst.Secret("MySecret", "xyz123"); - const bucket = createBucket(); - const worker = createWorker(); - createAstro(); - createSolidStart(); - createStatic(); - return ret; - - function createBucket() { - const bucket = new sst.cloudflare.Bucket("MyBucket"); - ret.bucket = bucket.name; - return bucket; - } - - function createWorker() { - const worker = new sst.cloudflare.Worker("MyWorker", { - handler: "workers/worker.ts", - link: [bucket], - url: true, - }); - ret.worker = worker.url; - return worker; - } - - function createAstro() { - new sst.cloudflare.x.Astro("MyAstro", { - path: "../sites/astro5", - link: [bucket], - environment: { - FOO: "hello", - }, - }); - } - - function createSolidStart() { - new sst.cloudflare.x.SolidStart("MySolidStart", { - path: "sites/solid-start", - link: [secret, bucket], - environment: { - FOO: "hello", - }, - }); - } - - function createStatic() { - new sst.cloudflare.x.StaticSite("MyAstroStatic", { - errorPage: "404.html", - path: "../sites/astro5-static", - build: { - command: "npm run build:cf", - output: "dist", - }, - }); - } - }, -}); diff --git a/examples/internal/playground/infra-cf/workers/worker.ts b/examples/internal/playground/infra-cf/workers/worker.ts deleted file mode 100644 index d5db783704..0000000000 --- a/examples/internal/playground/infra-cf/workers/worker.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Resource } from "sst"; - -export default { - async fetch(req: Request) { - return new Response( - JSON.stringify({ time: new Date().toISOString() }, null, 2) - ); - }, -}; diff --git a/examples/internal/playground/package.json b/examples/internal/playground/package.json deleted file mode 100644 index a2c01fc7b3..0000000000 --- a/examples/internal/playground/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "playground", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "@aws-sdk/client-apigatewaymanagementapi": "^3.712.0", - "@aws-sdk/client-ecs": "^3.713.0", - "@aws-sdk/client-eventbridge": "^3.679.0", - "@aws-sdk/client-sesv2": "^3.515.0", - "@aws-sdk/client-sns": "^3.679.0", - "@aws-sdk/client-sqs": "^3.682.0", - "@aws-sdk/client-ssm": "^3.759.0", - "@openauthjs/openauth": "^0.0.19", - "@opensearch-project/opensearch": "^3.5.1", - "aws-sdk": "^2.1692.0", - "hono": "^4.7.4", - "ioredis": "^5.4.1", - "mysql2": "^3.14.0", - "opencontrol": "0.0.6", - "pg": "^8.13.1", - "sst": "3.9.32", - "valibot": "1.0.0-beta.9", - "zod": "^3.24.2" - } -} diff --git a/examples/internal/playground/sites/analog/.editorconfig b/examples/internal/playground/sites/analog/.editorconfig deleted file mode 100644 index 59d9a3a3e7..0000000000 --- a/examples/internal/playground/sites/analog/.editorconfig +++ /dev/null @@ -1,16 +0,0 @@ -# Editor configuration, see https://editorconfig.org -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 2 -insert_final_newline = true -trim_trailing_whitespace = true - -[*.ts] -quote_type = single - -[*.md] -max_line_length = off -trim_trailing_whitespace = false diff --git a/examples/internal/playground/sites/analog/.gitignore b/examples/internal/playground/sites/analog/.gitignore deleted file mode 100644 index c48ce743bf..0000000000 --- a/examples/internal/playground/sites/analog/.gitignore +++ /dev/null @@ -1,47 +0,0 @@ -# See http://help.github.com/ignore-files/ for more about ignoring files. - -# Compiled output -/dist -/tmp -/out-tsc -/bazel-out - -# Node -/node_modules -npm-debug.log -yarn-error.log - -# IDEs and editors -.idea/ -.project -.classpath -.c9/ -*.launch -.settings/ -*.sublime-workspace - -# Visual Studio Code -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -.history/* - -# Miscellaneous -/.angular/cache -/.nx/cache -/.nx/workspace-data -.sass-cache/ -/connect.lock -/coverage -/libpeerconnection.log -testem.log -/typings - -# System files -.DS_Store -Thumbs.db - -# sst -.sst diff --git a/examples/internal/playground/sites/analog/.vscode/extensions.json b/examples/internal/playground/sites/analog/.vscode/extensions.json deleted file mode 100644 index e4679f326a..0000000000 --- a/examples/internal/playground/sites/analog/.vscode/extensions.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 - "recommendations": ["angular.ng-template", "analogjs.vscode-analog"] -} diff --git a/examples/internal/playground/sites/analog/.vscode/launch.json b/examples/internal/playground/sites/analog/.vscode/launch.json deleted file mode 100644 index 57dbf761ec..0000000000 --- a/examples/internal/playground/sites/analog/.vscode/launch.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "ng serve", - "type": "chrome", - "request": "launch", - "preLaunchTask": "npm: start", - "url": "http://localhost:5173/" - }, - { - "name": "ng test", - "type": "chrome", - "request": "launch", - "preLaunchTask": "npm: test" - } - ] -} diff --git a/examples/internal/playground/sites/analog/.vscode/tasks.json b/examples/internal/playground/sites/analog/.vscode/tasks.json deleted file mode 100644 index a298b5bd87..0000000000 --- a/examples/internal/playground/sites/analog/.vscode/tasks.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 - "version": "2.0.0", - "tasks": [ - { - "type": "npm", - "script": "start", - "isBackground": true, - "problemMatcher": { - "owner": "typescript", - "pattern": "$tsc", - "background": { - "activeOnStart": true, - "beginsPattern": { - "regexp": "(.*?)" - }, - "endsPattern": { - "regexp": "bundle generation complete" - } - } - } - }, - { - "type": "npm", - "script": "test", - "isBackground": true, - "problemMatcher": { - "owner": "typescript", - "pattern": "$tsc", - "background": { - "activeOnStart": true, - "beginsPattern": { - "regexp": "(.*?)" - }, - "endsPattern": { - "regexp": "bundle generation complete" - } - } - } - } - ] -} diff --git a/examples/internal/playground/sites/analog/README.md b/examples/internal/playground/sites/analog/README.md deleted file mode 100644 index 684df4a42f..0000000000 --- a/examples/internal/playground/sites/analog/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Analog App - -This project was generated with [Analog](https://analogjs.org), the fullstack meta-framework for Angular. - -## Setup - -Run `npm install` to install the application dependencies. - -## Development - -Run `npm start` for a dev server. Navigate to `http://localhost:5173/`. The application automatically reloads if you change any of the source files. - -## Build - -Run `npm run build` to build the client/server project. The client build artifacts are located in the `dist/analog/public` directory. The server for the API build artifacts are located in the `dist/analog/server` directory. - -## Test - -Run `npm run test` to run unit tests with [Vitest](https://vitest.dev). - -## Community - -- Visit and Star the [GitHub Repo](https://github.com/analogjs/analog) -- Join the [Discord](https://chat.analogjs.org) -- Follow us on [Twitter](https://twitter.com/analogjs) -- Become a [Sponsor](https://github.com/sponsors/brandonroberts) diff --git a/examples/internal/playground/sites/analog/angular.json b/examples/internal/playground/sites/analog/angular.json deleted file mode 100644 index d8c37010b7..0000000000 --- a/examples/internal/playground/sites/analog/angular.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "$schema": "./node_modules/@angular/cli/lib/config/schema.json", - "version": 1, - "newProjectRoot": "projects", - "projects": { - "my-app": { - "projectType": "application", - "root": ".", - "sourceRoot": "src", - "prefix": "app", - "architect": { - "build": { - "builder": "@analogjs/platform:vite", - "options": { - "configFile": "vite.config.ts", - "main": "src/main.ts", - "outputPath": "dist/client", - "tsConfig": "tsconfig.app.json" - }, - "defaultConfiguration": "production", - "configurations": { - "development": { - "mode": "development" - }, - "production": { - "sourcemap": false, - "mode": "production" - } - } - }, - "serve": { - "builder": "@analogjs/platform:vite-dev-server", - "defaultConfiguration": "development", - "options": { - "buildTarget": "my-app:build", - "port": 5173 - }, - "configurations": { - "development": { - "buildTarget": "my-app:build:development", - "hmr": true - }, - "production": { - "buildTarget": "my-app:build:production" - } - } - }, - "test": { - "builder": "@analogjs/vitest-angular:test" - } - } - } - }, - "cli": { - "analytics": false - } -} diff --git a/examples/internal/playground/sites/analog/index.html b/examples/internal/playground/sites/analog/index.html deleted file mode 100644 index d8c7a95f47..0000000000 --- a/examples/internal/playground/sites/analog/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - My App - - - - - - - - - - diff --git a/examples/internal/playground/sites/analog/package.json b/examples/internal/playground/sites/analog/package.json deleted file mode 100644 index 10b471ad16..0000000000 --- a/examples/internal/playground/sites/analog/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "aws-analog", - "version": "0.0.0", - "type": "module", - "engines": { - "node": ">=18.19.1" - }, - "scripts": { - "build": "sed -i '' 's/base: .*/base: undefined,/' vite.config.ts && sed -i '' 's/apiPrefix: .*/apiPrefix: undefined,/' vite.config.ts && ng build", - "build-base": "sed -i '' 's/base: .*/base: \"\\/analog\",/' vite.config.ts && sed -i '' 's/apiPrefix: .*/apiPrefix: \"analog\\/api\",/' vite.config.ts && ng build", - "dev": "ng serve", - "ng": "ng", - "start": "npm run dev", - "test": "ng test", - "watch": "ng build --watch --configuration development" - }, - "private": true, - "dependencies": { - "@analogjs/content": "^1.14.1", - "@analogjs/router": "^1.14.1", - "@angular/animations": "^19.0.0", - "@angular/common": "^19.0.0", - "@angular/compiler": "^19.0.0", - "@angular/core": "^19.0.0", - "@angular/forms": "^19.0.0", - "@angular/platform-browser": "^19.0.0", - "@angular/platform-browser-dynamic": "^19.0.0", - "@angular/platform-server": "^19.0.0", - "@angular/router": "^19.0.0", - "@aws-sdk/client-s3": "^3.654.0", - "@aws-sdk/s3-request-presigner": "^3.654.0", - "front-matter": "^4.0.2", - "marked": "^5.0.2", - "marked-gfm-heading-id": "^3.1.0", - "marked-highlight": "^2.0.1", - "marked-mangle": "^1.1.7", - "prismjs": "^1.29.0", - "rxjs": "~7.8.0", - "sst": "latest", - "tslib": "^2.3.0", - "zone.js": "~0.15.0" - }, - "devDependencies": { - "@analogjs/platform": "^1.14.1", - "@analogjs/vite-plugin-angular": "^1.14.1", - "@analogjs/vitest-angular": "^1.14.1", - "@angular-devkit/build-angular": "^19.0.0", - "@angular/build": "^19.0.0", - "@angular/cli": "^19.0.0", - "@angular/compiler-cli": "^19.0.0", - "jsdom": "^22.0.0", - "typescript": "~5.8.0", - "vite": "^6.0.0", - "vite-tsconfig-paths": "^4.2.0", - "vitest": "^3.0.0" - } -} diff --git a/examples/internal/playground/sites/analog/public/analog.svg b/examples/internal/playground/sites/analog/public/analog.svg deleted file mode 100644 index e4f555aa7d..0000000000 --- a/examples/internal/playground/sites/analog/public/analog.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/internal/playground/sites/analog/public/favicon.ico b/examples/internal/playground/sites/analog/public/favicon.ico deleted file mode 100644 index 997406ad22..0000000000 Binary files a/examples/internal/playground/sites/analog/public/favicon.ico and /dev/null differ diff --git a/examples/internal/playground/sites/analog/public/vite.svg b/examples/internal/playground/sites/analog/public/vite.svg deleted file mode 100644 index e7b8dfb1b2..0000000000 --- a/examples/internal/playground/sites/analog/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/internal/playground/sites/analog/src/app/app.component.spec.ts b/examples/internal/playground/sites/analog/src/app/app.component.spec.ts deleted file mode 100644 index 9a08b4b25f..0000000000 --- a/examples/internal/playground/sites/analog/src/app/app.component.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { provideRouter } from '@angular/router'; -import { provideLocationMocks } from '@angular/common/testing'; - -import { AppComponent } from './app.component'; - -describe('AppComponent', () => { - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [AppComponent], - providers: [provideRouter([]), provideLocationMocks()], - }).compileComponents(); - }); - - it('should create the app', () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app).toBeTruthy(); - }); -}); diff --git a/examples/internal/playground/sites/analog/src/app/app.component.ts b/examples/internal/playground/sites/analog/src/app/app.component.ts deleted file mode 100644 index 2e02d16044..0000000000 --- a/examples/internal/playground/sites/analog/src/app/app.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Component } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; - -@Component({ - selector: 'app-root', - standalone: true, - imports: [RouterOutlet], - template: ` `, - styles: [ - ` - :host { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; - } - `, - ], -}) -export class AppComponent {} diff --git a/examples/internal/playground/sites/analog/src/app/app.config.server.ts b/examples/internal/playground/sites/analog/src/app/app.config.server.ts deleted file mode 100644 index 0da63b09b0..0000000000 --- a/examples/internal/playground/sites/analog/src/app/app.config.server.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; -import { provideServerRendering } from '@angular/platform-server'; - -import { appConfig } from './app.config'; - -const serverConfig: ApplicationConfig = { - providers: [provideServerRendering()], -}; - -export const config = mergeApplicationConfig(appConfig, serverConfig); diff --git a/examples/internal/playground/sites/analog/src/app/app.config.ts b/examples/internal/playground/sites/analog/src/app/app.config.ts deleted file mode 100644 index 486fb32acc..0000000000 --- a/examples/internal/playground/sites/analog/src/app/app.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { - provideHttpClient, - withFetch, - withInterceptors, -} from '@angular/common/http'; -import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; -import { provideClientHydration } from '@angular/platform-browser'; -import { provideFileRouter, requestContextInterceptor } from '@analogjs/router'; - -export const appConfig: ApplicationConfig = { - providers: [ - provideZoneChangeDetection({ eventCoalescing: true }), - provideFileRouter(), - provideHttpClient( - withFetch(), - withInterceptors([requestContextInterceptor]), - ), - provideClientHydration(), - ], -}; diff --git a/examples/internal/playground/sites/analog/src/app/pages/index.page.ts b/examples/internal/playground/sites/analog/src/app/pages/index.page.ts deleted file mode 100644 index 56b2cfb557..0000000000 --- a/examples/internal/playground/sites/analog/src/app/pages/index.page.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Component } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { injectLoad } from '@analogjs/router'; -import { toSignal } from '@angular/core/rxjs-interop'; - -import { load } from './index.server'; - -@Component({ - selector: 'app-home', - standalone: true, - imports: [FormsModule], - template: ` -
- - -
- `, -}) -export default class HomeComponent { - data = toSignal(injectLoad(), { requireSync: true }); - - async onSubmit(event: Event): Promise { - const file = (event.target as HTMLFormElement)['file'].files?.[0]!; - - const image = await fetch(this.data().url, { - body: file, - method: 'PUT', - headers: { - 'Content-Type': file.type, - 'Content-Disposition': `attachment; filename="${file.name}"`, - }, - }); - - window.location.href = image.url.split('?')[0]; - } -} diff --git a/examples/internal/playground/sites/analog/src/app/pages/index.server.ts b/examples/internal/playground/sites/analog/src/app/pages/index.server.ts deleted file mode 100644 index a120302365..0000000000 --- a/examples/internal/playground/sites/analog/src/app/pages/index.server.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Resource } from 'sst'; -import { PageServerLoad } from '@analogjs/router'; -import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; - -export const load = async ({ }: PageServerLoad) => { - const command = new PutObjectCommand({ - Key: crypto.randomUUID(), - // @ts-ignore: Generated on deploy - Bucket: Resource.MyBucket.name, - }); - - const url = await getSignedUrl(new S3Client({}), command); - - return { - url - }; -}; diff --git a/examples/internal/playground/sites/analog/src/main.server.ts b/examples/internal/playground/sites/analog/src/main.server.ts deleted file mode 100644 index 2b6d4d14b5..0000000000 --- a/examples/internal/playground/sites/analog/src/main.server.ts +++ /dev/null @@ -1,32 +0,0 @@ -import 'zone.js/node'; -import '@angular/platform-server/init'; -import { enableProdMode } from '@angular/core'; -import { bootstrapApplication } from '@angular/platform-browser'; -import { renderApplication } from '@angular/platform-server'; -import { provideServerContext } from '@analogjs/router/server'; -import { ServerContext } from '@analogjs/router/tokens'; - -import { config } from './app/app.config.server'; -import { AppComponent } from './app/app.component'; - -if (import.meta.env.PROD) { - enableProdMode(); -} - -export function bootstrap() { - return bootstrapApplication(AppComponent, config); -} - -export default async function render( - url: string, - document: string, - serverContext: ServerContext -) { - const html = await renderApplication(bootstrap, { - document, - url, - platformProviders: [provideServerContext(serverContext)], - }); - - return html; -} diff --git a/examples/internal/playground/sites/analog/src/main.ts b/examples/internal/playground/sites/analog/src/main.ts deleted file mode 100644 index e774611399..0000000000 --- a/examples/internal/playground/sites/analog/src/main.ts +++ /dev/null @@ -1,7 +0,0 @@ -import 'zone.js'; -import { bootstrapApplication } from '@angular/platform-browser'; - -import { AppComponent } from './app/app.component'; -import { appConfig } from './app/app.config'; - -bootstrapApplication(AppComponent, appConfig); diff --git a/examples/internal/playground/sites/analog/src/server/routes/v1/hello.ts b/examples/internal/playground/sites/analog/src/server/routes/v1/hello.ts deleted file mode 100644 index 594c5d716d..0000000000 --- a/examples/internal/playground/sites/analog/src/server/routes/v1/hello.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { defineEventHandler } from 'h3'; - -export default defineEventHandler(() => ({ message: 'Hello World' })); diff --git a/examples/internal/playground/sites/analog/src/styles.css b/examples/internal/playground/sites/analog/src/styles.css deleted file mode 100644 index 8e92f8fcdc..0000000000 --- a/examples/internal/playground/sites/analog/src/styles.css +++ /dev/null @@ -1,75 +0,0 @@ -/* You can add global styles to this file, and also import other style files */ -:root { - font-family: Inter, Avenir, Helvetica, Arial, sans-serif; - font-size: 16px; - line-height: 24px; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -.card { - padding: 2em; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} diff --git a/examples/internal/playground/sites/analog/src/test-setup.ts b/examples/internal/playground/sites/analog/src/test-setup.ts deleted file mode 100644 index 318c3b9d71..0000000000 --- a/examples/internal/playground/sites/analog/src/test-setup.ts +++ /dev/null @@ -1,12 +0,0 @@ -import '@analogjs/vitest-angular/setup-zone'; - -import { - BrowserDynamicTestingModule, - platformBrowserDynamicTesting, -} from '@angular/platform-browser-dynamic/testing'; -import { getTestBed } from '@angular/core/testing'; - -getTestBed().initTestEnvironment( - BrowserDynamicTestingModule, - platformBrowserDynamicTesting() -); diff --git a/examples/internal/playground/sites/analog/src/vite-env.d.ts b/examples/internal/playground/sites/analog/src/vite-env.d.ts deleted file mode 100644 index 11f02fe2a0..0000000000 --- a/examples/internal/playground/sites/analog/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/examples/internal/playground/sites/analog/sst-env.d.ts b/examples/internal/playground/sites/analog/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/internal/playground/sites/analog/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/playground/sites/analog/tsconfig.app.json b/examples/internal/playground/sites/analog/tsconfig.app.json deleted file mode 100644 index ad965f67de..0000000000 --- a/examples/internal/playground/sites/analog/tsconfig.app.json +++ /dev/null @@ -1,14 +0,0 @@ -/* To learn more about this file see: https://angular.io/config/tsconfig. */ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./out-tsc/app", - "types": [] - }, - "files": ["src/main.ts", "src/main.server.ts"], - "include": [ - "src/**/*.d.ts", - "src/app/pages/**/*.page.ts", - "src/server/middleware/**/*.ts" - ] -} diff --git a/examples/internal/playground/sites/analog/tsconfig.json b/examples/internal/playground/sites/analog/tsconfig.json deleted file mode 100644 index 94e11863a1..0000000000 --- a/examples/internal/playground/sites/analog/tsconfig.json +++ /dev/null @@ -1,31 +0,0 @@ -/* To learn more about this file see: https://angular.io/config/tsconfig. */ -{ - "compileOnSave": false, - "compilerOptions": { - "baseUrl": "./", - "outDir": "./dist/out-tsc", - "forceConsistentCasingInFileNames": true, - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "sourceMap": true, - "declaration": false, - "downlevelIteration": true, - "experimentalDecorators": true, - "moduleResolution": "node", - "importHelpers": true, - "target": "ES2022", - "module": "ES2022", - "lib": ["ES2022", "dom"], - "useDefineForClassFields": false, - "skipLibCheck": true - }, - "angularCompilerOptions": { - "enableI18nLegacyMessageIdFormat": false, - "strictInjectionParameters": true, - "strictInputAccessModifiers": true, - "strictTemplates": true - } -} diff --git a/examples/internal/playground/sites/analog/tsconfig.spec.json b/examples/internal/playground/sites/analog/tsconfig.spec.json deleted file mode 100644 index 06eb7cae61..0000000000 --- a/examples/internal/playground/sites/analog/tsconfig.spec.json +++ /dev/null @@ -1,11 +0,0 @@ -/* To learn more about this file see: https://angular.io/config/tsconfig. */ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./out-tsc/spec", - "target": "es2016", - "types": ["node", "vitest/globals"] - }, - "files": ["src/test-setup.ts"], - "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] -} diff --git a/examples/internal/playground/sites/analog/vite.config.ts b/examples/internal/playground/sites/analog/vite.config.ts deleted file mode 100644 index 9e9779fd3f..0000000000 --- a/examples/internal/playground/sites/analog/vite.config.ts +++ /dev/null @@ -1,33 +0,0 @@ -/// - -import { defineConfig } from 'vite'; -import analog from '@analogjs/platform'; - -// https://vitejs.dev/config/ -export default defineConfig(({ mode }) => ({ - build: { - target: ['es2020'], - }, - resolve: { - mainFields: ['module'], - }, - plugins: [ - analog({ - nitro: { - preset: 'aws-lambda', - }, - apiPrefix: "analog/api", - }), - ], - test: { - globals: true, - environment: 'jsdom', - setupFiles: ['src/test-setup.ts'], - include: ['**/*.spec.ts'], - reporters: ['default'], - }, - define: { - 'import.meta.vitest': mode !== 'production', - }, - base: "/analog", -})); diff --git a/examples/internal/playground/sites/astro4/.gitignore b/examples/internal/playground/sites/astro4/.gitignore deleted file mode 100644 index 9a746a2f70..0000000000 --- a/examples/internal/playground/sites/astro4/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# build output -dist/ - -# generated types -.astro/ - -# dependencies -node_modules/ - -# logs -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* - -# environment variables -.env -.env.production - -# macOS-specific files -.DS_Store - -# sst -.sst diff --git a/examples/internal/playground/sites/astro4/.vscode/extensions.json b/examples/internal/playground/sites/astro4/.vscode/extensions.json deleted file mode 100644 index 22a15055d6..0000000000 --- a/examples/internal/playground/sites/astro4/.vscode/extensions.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "recommendations": ["astro-build.astro-vscode"], - "unwantedRecommendations": [] -} diff --git a/examples/internal/playground/sites/astro4/.vscode/launch.json b/examples/internal/playground/sites/astro4/.vscode/launch.json deleted file mode 100644 index d642209762..0000000000 --- a/examples/internal/playground/sites/astro4/.vscode/launch.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "command": "./node_modules/.bin/astro dev", - "name": "Development server", - "request": "launch", - "type": "node-terminal" - } - ] -} diff --git a/examples/internal/playground/sites/astro4/README.md b/examples/internal/playground/sites/astro4/README.md deleted file mode 100644 index 1db3fb3991..0000000000 --- a/examples/internal/playground/sites/astro4/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# Astro Starter Kit: Basics - -```sh -npm create astro@latest -- --template basics -``` - -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) -[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) - -> πŸ§‘β€πŸš€ **Seasoned astronaut?** Delete this file. Have fun! - -![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) - -## πŸš€ Project Structure - -Inside of your Astro project, you'll see the following folders and files: - -```text -/ -β”œβ”€β”€ public/ -β”‚ └── favicon.svg -β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ components/ -β”‚ β”‚ └── Card.astro -β”‚ β”œβ”€β”€ layouts/ -β”‚ β”‚ └── Layout.astro -β”‚ └── pages/ -β”‚ └── index.astro -└── package.json -``` - -Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. - -There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. - -Any static assets, like images, can be placed in the `public/` directory. - -## 🧞 Commands - -All commands are run from the root of the project, from a terminal: - -| Command | Action | -| :------------------------ | :----------------------------------------------- | -| `npm install` | Installs dependencies | -| `npm run dev` | Starts local dev server at `localhost:4321` | -| `npm run build` | Build your production site to `./dist/` | -| `npm run preview` | Preview your build locally, before deploying | -| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | -| `npm run astro -- --help` | Get help using the Astro CLI | - -## πŸ‘€ Want to learn more? - -Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). diff --git a/examples/internal/playground/sites/astro4/astro.config.mjs b/examples/internal/playground/sites/astro4/astro.config.mjs deleted file mode 100644 index 489f779a36..0000000000 --- a/examples/internal/playground/sites/astro4/astro.config.mjs +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from "astro/config"; -import aws from "astro-sst"; - -export default defineConfig({ - output: "server", - adapter: aws({ - serverRoutes: ["/_actions/getGreeting"], - }), -}); diff --git a/examples/internal/playground/sites/astro4/package.json b/examples/internal/playground/sites/astro4/package.json deleted file mode 100644 index e08c57e006..0000000000 --- a/examples/internal/playground/sites/astro4/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "aws-astro", - "type": "module", - "version": "0.0.1", - "scripts": { - "dev": "astro dev", - "start": "astro dev", - "build": "astro check && astro build", - "preview": "astro preview", - "astro": "astro" - }, - "dependencies": { - "@astrojs/check": "^0.5.10", - "@aws-sdk/client-s3": "^3.540.0", - "@aws-sdk/s3-request-presigner": "^3.540.0", - "astro": "^4.5.9", - "astro-sst": "^2.41.2", - "sst": "latest", - "typescript": "^5.4.3" - } -} diff --git a/examples/internal/playground/sites/astro4/public/favicon.svg b/examples/internal/playground/sites/astro4/public/favicon.svg deleted file mode 100644 index f157bd1c5e..0000000000 --- a/examples/internal/playground/sites/astro4/public/favicon.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - diff --git a/examples/internal/playground/sites/astro4/src/actions/index.ts b/examples/internal/playground/sites/astro4/src/actions/index.ts deleted file mode 100644 index c64a39dd92..0000000000 --- a/examples/internal/playground/sites/astro4/src/actions/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineAction } from "astro:actions"; -import { z } from "astro:schema"; - -export const server = { - getGreeting: defineAction({ - input: z.object({ - name: z.string(), - }), - handler: async (input) => { - return `Hello, ${input.name}!`; - }, - }), -}; diff --git a/examples/internal/playground/sites/astro4/src/components/Card.astro b/examples/internal/playground/sites/astro4/src/components/Card.astro deleted file mode 100644 index bd6d5971eb..0000000000 --- a/examples/internal/playground/sites/astro4/src/components/Card.astro +++ /dev/null @@ -1,61 +0,0 @@ ---- -interface Props { - title: string; - body: string; - href: string; -} - -const { href, title, body } = Astro.props; ---- - - - diff --git a/examples/internal/playground/sites/astro4/src/env.d.ts b/examples/internal/playground/sites/astro4/src/env.d.ts deleted file mode 100644 index acef35f175..0000000000 --- a/examples/internal/playground/sites/astro4/src/env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/examples/internal/playground/sites/astro4/src/layouts/Layout.astro b/examples/internal/playground/sites/astro4/src/layouts/Layout.astro deleted file mode 100644 index 7b552be19b..0000000000 --- a/examples/internal/playground/sites/astro4/src/layouts/Layout.astro +++ /dev/null @@ -1,51 +0,0 @@ ---- -interface Props { - title: string; -} - -const { title } = Astro.props; ---- - - - - - - - - - - {title} - - - - - - diff --git a/examples/internal/playground/sites/astro4/src/pages/404.astro b/examples/internal/playground/sites/astro4/src/pages/404.astro deleted file mode 100644 index e2ab6b47cd..0000000000 --- a/examples/internal/playground/sites/astro4/src/pages/404.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -export const prerender = true; ---- - -

404 - Page Not Found

diff --git a/examples/internal/playground/sites/astro4/src/pages/500.astro b/examples/internal/playground/sites/astro4/src/pages/500.astro deleted file mode 100644 index bb1af3b0cb..0000000000 --- a/examples/internal/playground/sites/astro4/src/pages/500.astro +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -

500 - Internal Server Error

\ No newline at end of file diff --git a/examples/internal/playground/sites/astro4/src/pages/bad.astro b/examples/internal/playground/sites/astro4/src/pages/bad.astro deleted file mode 100644 index 56a86c74e8..0000000000 --- a/examples/internal/playground/sites/astro4/src/pages/bad.astro +++ /dev/null @@ -1,6 +0,0 @@ ---- -// @ts-nocheck -throw new Error("error"); ---- - -

error page

\ No newline at end of file diff --git a/examples/internal/playground/sites/astro4/src/pages/index.astro b/examples/internal/playground/sites/astro4/src/pages/index.astro deleted file mode 100644 index 831f2c226f..0000000000 --- a/examples/internal/playground/sites/astro4/src/pages/index.astro +++ /dev/null @@ -1,80 +0,0 @@ ---- -// @ts-nocheck -import { Resource } from "sst"; -import Layout from '../layouts/Layout.astro'; -import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; -import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; - -const command = new PutObjectCommand({ - Key: crypto.randomUUID(), - Bucket: Resource.MyBucket.name, -}); -const url = await getSignedUrl(new S3Client({}), command); ---- - - -
- -
- - -
- -
-
- - diff --git a/examples/internal/playground/sites/astro4/src/pages/login.astro b/examples/internal/playground/sites/astro4/src/pages/login.astro deleted file mode 100644 index e7e92e6085..0000000000 --- a/examples/internal/playground/sites/astro4/src/pages/login.astro +++ /dev/null @@ -1,15 +0,0 @@ ---- ---- - - - - \ No newline at end of file diff --git a/examples/internal/playground/sites/astro4/sst-env.d.ts b/examples/internal/playground/sites/astro4/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/internal/playground/sites/astro4/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/playground/sites/astro4/tsconfig.json b/examples/internal/playground/sites/astro4/tsconfig.json deleted file mode 100644 index 77da9dd009..0000000000 --- a/examples/internal/playground/sites/astro4/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "astro/tsconfigs/strict" -} \ No newline at end of file diff --git a/examples/internal/playground/sites/astro5-static/.gitignore b/examples/internal/playground/sites/astro5-static/.gitignore deleted file mode 100644 index 016b59ea14..0000000000 --- a/examples/internal/playground/sites/astro5-static/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# build output -dist/ - -# generated types -.astro/ - -# dependencies -node_modules/ - -# logs -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* - -# environment variables -.env -.env.production - -# macOS-specific files -.DS_Store - -# jetbrains setting folder -.idea/ diff --git a/examples/internal/playground/sites/astro5-static/.vscode/extensions.json b/examples/internal/playground/sites/astro5-static/.vscode/extensions.json deleted file mode 100644 index 22a15055d6..0000000000 --- a/examples/internal/playground/sites/astro5-static/.vscode/extensions.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "recommendations": ["astro-build.astro-vscode"], - "unwantedRecommendations": [] -} diff --git a/examples/internal/playground/sites/astro5-static/.vscode/launch.json b/examples/internal/playground/sites/astro5-static/.vscode/launch.json deleted file mode 100644 index d642209762..0000000000 --- a/examples/internal/playground/sites/astro5-static/.vscode/launch.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "command": "./node_modules/.bin/astro dev", - "name": "Development server", - "request": "launch", - "type": "node-terminal" - } - ] -} diff --git a/examples/internal/playground/sites/astro5-static/README.md b/examples/internal/playground/sites/astro5-static/README.md deleted file mode 100644 index ff19a3e7ec..0000000000 --- a/examples/internal/playground/sites/astro5-static/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Astro Starter Kit: Basics - -```sh -npm create astro@latest -- --template basics -``` - -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) -[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) - -> πŸ§‘β€πŸš€ **Seasoned astronaut?** Delete this file. Have fun! - -![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) - -## πŸš€ Project Structure - -Inside of your Astro project, you'll see the following folders and files: - -```text -/ -β”œβ”€β”€ public/ -β”‚ └── favicon.svg -β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ layouts/ -β”‚ β”‚ └── Layout.astro -β”‚ └── pages/ -β”‚ └── index.astro -└── package.json -``` - -To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/). - -## 🧞 Commands - -All commands are run from the root of the project, from a terminal: - -| Command | Action | -| :------------------------ | :----------------------------------------------- | -| `npm install` | Installs dependencies | -| `npm run dev` | Starts local dev server at `localhost:4321` | -| `npm run build` | Build your production site to `./dist/` | -| `npm run preview` | Preview your build locally, before deploying | -| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | -| `npm run astro -- --help` | Get help using the Astro CLI | - -## πŸ‘€ Want to learn more? - -Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). diff --git a/examples/internal/playground/sites/astro5-static/astro.config.mjs b/examples/internal/playground/sites/astro5-static/astro.config.mjs deleted file mode 100644 index 9f025b5fca..0000000000 --- a/examples/internal/playground/sites/astro5-static/astro.config.mjs +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-check -import { defineConfig } from "astro/config"; -import aws from "astro-sst"; -import cloudflare from "@astrojs/cloudflare"; -//import aws from "../../../../../../astro-sst/packages/astro-sst/dist/adapter"; - -// https://astro.build/config -export default defineConfig({ - image: { - domains: ["sst.dev"], - }, - output: "static", - adapter: aws(), - redirects: { - "/redirect-to-route": "/prerendered", - "/redirect-to-url": "https://www.google.com", - "/redirect/[slug]": "/sub/[slug]", - }, -}); \ No newline at end of file diff --git a/examples/internal/playground/sites/astro5-static/package.json b/examples/internal/playground/sites/astro5-static/package.json deleted file mode 100644 index 2497f7b516..0000000000 --- a/examples/internal/playground/sites/astro5-static/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "astro5", - "type": "module", - "version": "0.0.1", - "scripts": { - "dev": "astro dev", - "use:cf": "sed -i '' 's/adapter: .*/adapter: undefined,/' astro.config.mjs", - "use:aws": "sed -i '' 's/adapter: .*/adapter: aws(),/' astro.config.mjs", - "build:cf": "npm run use:cf && astro build", - "build": "npm run use:aws && astro build", - "preview": "astro preview", - "astro": "astro" - }, - "dependencies": { - "@astrojs/cloudflare": "^12.5.3", - "astro": "5.8.1", - "astro-sst": "3.1.4", - "sst": "3.9.25" - } -} diff --git a/examples/internal/playground/sites/astro5-static/public/_headers b/examples/internal/playground/sites/astro5-static/public/_headers deleted file mode 100644 index c269214ab2..0000000000 --- a/examples/internal/playground/sites/astro5-static/public/_headers +++ /dev/null @@ -1,2 +0,0 @@ -/* - Access-Control-Allow-Origin: * \ No newline at end of file diff --git a/examples/internal/playground/sites/astro5-static/public/favicon.svg b/examples/internal/playground/sites/astro5-static/public/favicon.svg deleted file mode 100644 index f157bd1c5e..0000000000 --- a/examples/internal/playground/sites/astro5-static/public/favicon.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - diff --git a/examples/internal/playground/sites/astro5-static/public/spongebob-public.jpg b/examples/internal/playground/sites/astro5-static/public/spongebob-public.jpg deleted file mode 100644 index 02740004a3..0000000000 Binary files a/examples/internal/playground/sites/astro5-static/public/spongebob-public.jpg and /dev/null differ diff --git a/examples/internal/playground/sites/astro5-static/src/assets/astro.svg b/examples/internal/playground/sites/astro5-static/src/assets/astro.svg deleted file mode 100644 index 8cf8fb0c7d..0000000000 --- a/examples/internal/playground/sites/astro5-static/src/assets/astro.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/examples/internal/playground/sites/astro5-static/src/assets/background.svg b/examples/internal/playground/sites/astro5-static/src/assets/background.svg deleted file mode 100644 index 4b2be0ac0e..0000000000 --- a/examples/internal/playground/sites/astro5-static/src/assets/background.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/examples/internal/playground/sites/astro5-static/src/assets/spongebob-src.jpg b/examples/internal/playground/sites/astro5-static/src/assets/spongebob-src.jpg deleted file mode 100644 index d52035d35b..0000000000 Binary files a/examples/internal/playground/sites/astro5-static/src/assets/spongebob-src.jpg and /dev/null differ diff --git a/examples/internal/playground/sites/astro5-static/src/components/Welcome.astro b/examples/internal/playground/sites/astro5-static/src/components/Welcome.astro deleted file mode 100644 index 6b7b9c70e8..0000000000 --- a/examples/internal/playground/sites/astro5-static/src/components/Welcome.astro +++ /dev/null @@ -1,209 +0,0 @@ ---- -import astroLogo from '../assets/astro.svg'; -import background from '../assets/background.svg'; ---- - - - - diff --git a/examples/internal/playground/sites/astro5-static/src/layouts/Layout.astro b/examples/internal/playground/sites/astro5-static/src/layouts/Layout.astro deleted file mode 100644 index e455c61067..0000000000 --- a/examples/internal/playground/sites/astro5-static/src/layouts/Layout.astro +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - Astro Basics - - - - - - - diff --git a/examples/internal/playground/sites/astro5-static/src/pages/404.astro b/examples/internal/playground/sites/astro5-static/src/pages/404.astro deleted file mode 100644 index 3b62608079..0000000000 --- a/examples/internal/playground/sites/astro5-static/src/pages/404.astro +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -

404 - Page Not Found

diff --git a/examples/internal/playground/sites/astro5-static/src/pages/500.astro b/examples/internal/playground/sites/astro5-static/src/pages/500.astro deleted file mode 100644 index bb1af3b0cb..0000000000 --- a/examples/internal/playground/sites/astro5-static/src/pages/500.astro +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -

500 - Internal Server Error

\ No newline at end of file diff --git a/examples/internal/playground/sites/astro5-static/src/pages/dynamic-redirect.astro b/examples/internal/playground/sites/astro5-static/src/pages/dynamic-redirect.astro deleted file mode 100644 index ef4d1765fb..0000000000 --- a/examples/internal/playground/sites/astro5-static/src/pages/dynamic-redirect.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -return Astro.redirect("/prerendered"); ---- - -

This page will be redirected

\ No newline at end of file diff --git a/examples/internal/playground/sites/astro5-static/src/pages/index.astro b/examples/internal/playground/sites/astro5-static/src/pages/index.astro deleted file mode 100644 index e075a123f7..0000000000 --- a/examples/internal/playground/sites/astro5-static/src/pages/index.astro +++ /dev/null @@ -1,23 +0,0 @@ ---- -import Welcome from '../components/Welcome.astro'; -import Layout from '../layouts/Layout.astro'; - -// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build -// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh. ---- - - - - - diff --git a/examples/internal/playground/sites/astro5-static/src/pages/prerendered.astro b/examples/internal/playground/sites/astro5-static/src/pages/prerendered.astro deleted file mode 100644 index add042410f..0000000000 --- a/examples/internal/playground/sites/astro5-static/src/pages/prerendered.astro +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -

This page is prerendered

\ No newline at end of file diff --git a/examples/internal/playground/sites/astro5-static/src/pages/public-image.astro b/examples/internal/playground/sites/astro5-static/src/pages/public-image.astro deleted file mode 100644 index d4d0b2971a..0000000000 --- a/examples/internal/playground/sites/astro5-static/src/pages/public-image.astro +++ /dev/null @@ -1,7 +0,0 @@ ---- -import { Image } from 'astro:assets'; -import myImage from '../../public/spongebob-public.jpg'; ---- - -

This image is in public/ with dimentions 2992x2248.

-Spongebob \ No newline at end of file diff --git a/examples/internal/playground/sites/astro5-static/src/pages/remote-image.astro b/examples/internal/playground/sites/astro5-static/src/pages/remote-image.astro deleted file mode 100644 index 132682400f..0000000000 --- a/examples/internal/playground/sites/astro5-static/src/pages/remote-image.astro +++ /dev/null @@ -1,6 +0,0 @@ ---- -import { Image } from 'astro:assets'; ---- - -

This image is remote url.

-SST Logo \ No newline at end of file diff --git a/examples/internal/playground/sites/astro5-static/src/pages/src-image.astro b/examples/internal/playground/sites/astro5-static/src/pages/src-image.astro deleted file mode 100644 index a5409e344e..0000000000 --- a/examples/internal/playground/sites/astro5-static/src/pages/src-image.astro +++ /dev/null @@ -1,7 +0,0 @@ ---- -import { Image } from 'astro:assets'; -import myImage from '../assets/spongebob-src.jpg'; ---- - -

This image is in src/assets with dimentions 2992x2248. This image SHOULD be transformed.

-Spongebob \ No newline at end of file diff --git a/examples/internal/playground/sites/astro5-static/src/pages/sub/[slug].astro b/examples/internal/playground/sites/astro5-static/src/pages/sub/[slug].astro deleted file mode 100644 index 71f250cf9b..0000000000 --- a/examples/internal/playground/sites/astro5-static/src/pages/sub/[slug].astro +++ /dev/null @@ -1,10 +0,0 @@ ---- -export function getStaticPaths() { - return [ - { params: { slug: "page" }}, - ]; -} - -const { slug } = Astro.params; ---- -

This {slug} is inside /sub

\ No newline at end of file diff --git a/examples/internal/playground/sites/astro5-static/sst-env.d.ts b/examples/internal/playground/sites/astro5-static/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/internal/playground/sites/astro5-static/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/playground/sites/astro5-static/wrangler.jsonc b/examples/internal/playground/sites/astro5-static/wrangler.jsonc deleted file mode 100644 index 6425f4df6c..0000000000 --- a/examples/internal/playground/sites/astro5-static/wrangler.jsonc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "my-astro-static-app", - // Update to today's date - "compatibility_date": "2025-03-25", - "assets": { - "directory": "./dist" - } -} diff --git a/examples/internal/playground/sites/astro5/.gitignore b/examples/internal/playground/sites/astro5/.gitignore deleted file mode 100644 index 016b59ea14..0000000000 --- a/examples/internal/playground/sites/astro5/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# build output -dist/ - -# generated types -.astro/ - -# dependencies -node_modules/ - -# logs -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* - -# environment variables -.env -.env.production - -# macOS-specific files -.DS_Store - -# jetbrains setting folder -.idea/ diff --git a/examples/internal/playground/sites/astro5/.vscode/extensions.json b/examples/internal/playground/sites/astro5/.vscode/extensions.json deleted file mode 100644 index 22a15055d6..0000000000 --- a/examples/internal/playground/sites/astro5/.vscode/extensions.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "recommendations": ["astro-build.astro-vscode"], - "unwantedRecommendations": [] -} diff --git a/examples/internal/playground/sites/astro5/.vscode/launch.json b/examples/internal/playground/sites/astro5/.vscode/launch.json deleted file mode 100644 index d642209762..0000000000 --- a/examples/internal/playground/sites/astro5/.vscode/launch.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "command": "./node_modules/.bin/astro dev", - "name": "Development server", - "request": "launch", - "type": "node-terminal" - } - ] -} diff --git a/examples/internal/playground/sites/astro5/README.md b/examples/internal/playground/sites/astro5/README.md deleted file mode 100644 index ff19a3e7ec..0000000000 --- a/examples/internal/playground/sites/astro5/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Astro Starter Kit: Basics - -```sh -npm create astro@latest -- --template basics -``` - -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) -[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) - -> πŸ§‘β€πŸš€ **Seasoned astronaut?** Delete this file. Have fun! - -![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) - -## πŸš€ Project Structure - -Inside of your Astro project, you'll see the following folders and files: - -```text -/ -β”œβ”€β”€ public/ -β”‚ └── favicon.svg -β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ layouts/ -β”‚ β”‚ └── Layout.astro -β”‚ └── pages/ -β”‚ └── index.astro -└── package.json -``` - -To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/). - -## 🧞 Commands - -All commands are run from the root of the project, from a terminal: - -| Command | Action | -| :------------------------ | :----------------------------------------------- | -| `npm install` | Installs dependencies | -| `npm run dev` | Starts local dev server at `localhost:4321` | -| `npm run build` | Build your production site to `./dist/` | -| `npm run preview` | Preview your build locally, before deploying | -| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | -| `npm run astro -- --help` | Get help using the Astro CLI | - -## πŸ‘€ Want to learn more? - -Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). diff --git a/examples/internal/playground/sites/astro5/astro.config.mjs b/examples/internal/playground/sites/astro5/astro.config.mjs deleted file mode 100644 index d61a298657..0000000000 --- a/examples/internal/playground/sites/astro5/astro.config.mjs +++ /dev/null @@ -1,20 +0,0 @@ -// @ts-check -import { defineConfig } from "astro/config"; -import aws from "astro-sst"; -import cloudflare from "@astrojs/cloudflare"; -//import aws from "../../../../../../astro-sst/packages/astro-sst/dist/adapter"; - -// https://astro.build/config -export default defineConfig({ - image: { - domains: ["sst.dev"], - }, - output: "server", - adapter: cloudflare(), - redirects: { - "/redirect-to-route": "/prerendered", - "/redirect-to-url": "https://www.google.com", - "/redirect/[slug]": "/sub/[slug]", - }, - base: undefined, -}); diff --git a/examples/internal/playground/sites/astro5/package.json b/examples/internal/playground/sites/astro5/package.json deleted file mode 100644 index 2bda1fd143..0000000000 --- a/examples/internal/playground/sites/astro5/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "astro5", - "type": "module", - "version": "0.0.1", - "scripts": { - "dev": "astro dev", - "use:base": "sed -i '' 's/base: .*/base: \"\\/astro5\",/' astro.config.mjs", - "use:no-base": "sed -i '' 's/base: .*/base: undefined,/' astro.config.mjs", - "use:cf": "sed -i '' 's/adapter: .*/adapter: cloudflare(),/' astro.config.mjs", - "use:aws": "sed -i '' 's/adapter: .*/adapter: aws(),/' astro.config.mjs", - "build": "astro build", - "build:cf:base": "npm run use:cf && npm run use:base && astro build", - "build:cf:no-base": "npm run use:cf && npm run use:no-base && astro build", - "build:base": "npm run use:aws && npm run use:base && astro build", - "build:no-base": "npm run use:aws && npm run use:no-base && astro build", - "preview": "astro preview", - "astro": "astro" - }, - "dependencies": { - "@astrojs/cloudflare": "^12.5.3", - "astro": "5.8.1", - "astro-sst": "3.1.4", - "sst": "3.9.25" - } -} diff --git a/examples/internal/playground/sites/astro5/public/.assetsignore b/examples/internal/playground/sites/astro5/public/.assetsignore deleted file mode 100644 index 1b006a0f9e..0000000000 --- a/examples/internal/playground/sites/astro5/public/.assetsignore +++ /dev/null @@ -1,2 +0,0 @@ -_worker.js -_routes.json \ No newline at end of file diff --git a/examples/internal/playground/sites/astro5/public/favicon.svg b/examples/internal/playground/sites/astro5/public/favicon.svg deleted file mode 100644 index f157bd1c5e..0000000000 --- a/examples/internal/playground/sites/astro5/public/favicon.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - diff --git a/examples/internal/playground/sites/astro5/public/spongebob-public.jpg b/examples/internal/playground/sites/astro5/public/spongebob-public.jpg deleted file mode 100644 index 02740004a3..0000000000 Binary files a/examples/internal/playground/sites/astro5/public/spongebob-public.jpg and /dev/null differ diff --git a/examples/internal/playground/sites/astro5/src/actions/index.ts b/examples/internal/playground/sites/astro5/src/actions/index.ts deleted file mode 100644 index c64a39dd92..0000000000 --- a/examples/internal/playground/sites/astro5/src/actions/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineAction } from "astro:actions"; -import { z } from "astro:schema"; - -export const server = { - getGreeting: defineAction({ - input: z.object({ - name: z.string(), - }), - handler: async (input) => { - return `Hello, ${input.name}!`; - }, - }), -}; diff --git a/examples/internal/playground/sites/astro5/src/assets/astro.svg b/examples/internal/playground/sites/astro5/src/assets/astro.svg deleted file mode 100644 index 8cf8fb0c7d..0000000000 --- a/examples/internal/playground/sites/astro5/src/assets/astro.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/examples/internal/playground/sites/astro5/src/assets/background.svg b/examples/internal/playground/sites/astro5/src/assets/background.svg deleted file mode 100644 index 4b2be0ac0e..0000000000 --- a/examples/internal/playground/sites/astro5/src/assets/background.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/examples/internal/playground/sites/astro5/src/assets/spongebob-src.jpg b/examples/internal/playground/sites/astro5/src/assets/spongebob-src.jpg deleted file mode 100644 index d52035d35b..0000000000 Binary files a/examples/internal/playground/sites/astro5/src/assets/spongebob-src.jpg and /dev/null differ diff --git a/examples/internal/playground/sites/astro5/src/components/Welcome.astro b/examples/internal/playground/sites/astro5/src/components/Welcome.astro deleted file mode 100644 index 6b7b9c70e8..0000000000 --- a/examples/internal/playground/sites/astro5/src/components/Welcome.astro +++ /dev/null @@ -1,209 +0,0 @@ ---- -import astroLogo from '../assets/astro.svg'; -import background from '../assets/background.svg'; ---- - - - - diff --git a/examples/internal/playground/sites/astro5/src/layouts/Layout.astro b/examples/internal/playground/sites/astro5/src/layouts/Layout.astro deleted file mode 100644 index e455c61067..0000000000 --- a/examples/internal/playground/sites/astro5/src/layouts/Layout.astro +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - Astro Basics - - - - - - - diff --git a/examples/internal/playground/sites/astro5/src/pages/404.astro b/examples/internal/playground/sites/astro5/src/pages/404.astro deleted file mode 100644 index e2ab6b47cd..0000000000 --- a/examples/internal/playground/sites/astro5/src/pages/404.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -export const prerender = true; ---- - -

404 - Page Not Found

diff --git a/examples/internal/playground/sites/astro5/src/pages/500.astro b/examples/internal/playground/sites/astro5/src/pages/500.astro deleted file mode 100644 index bb1af3b0cb..0000000000 --- a/examples/internal/playground/sites/astro5/src/pages/500.astro +++ /dev/null @@ -1,4 +0,0 @@ ---- ---- - -

500 - Internal Server Error

\ No newline at end of file diff --git a/examples/internal/playground/sites/astro5/src/pages/action.astro b/examples/internal/playground/sites/astro5/src/pages/action.astro deleted file mode 100644 index e7e92e6085..0000000000 --- a/examples/internal/playground/sites/astro5/src/pages/action.astro +++ /dev/null @@ -1,15 +0,0 @@ ---- ---- - - - - \ No newline at end of file diff --git a/examples/internal/playground/sites/astro5/src/pages/dynamic-redirect.astro b/examples/internal/playground/sites/astro5/src/pages/dynamic-redirect.astro deleted file mode 100644 index ef4d1765fb..0000000000 --- a/examples/internal/playground/sites/astro5/src/pages/dynamic-redirect.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -return Astro.redirect("/prerendered"); ---- - -

This page will be redirected

\ No newline at end of file diff --git a/examples/internal/playground/sites/astro5/src/pages/error.astro b/examples/internal/playground/sites/astro5/src/pages/error.astro deleted file mode 100644 index 56a86c74e8..0000000000 --- a/examples/internal/playground/sites/astro5/src/pages/error.astro +++ /dev/null @@ -1,6 +0,0 @@ ---- -// @ts-nocheck -throw new Error("error"); ---- - -

error page

\ No newline at end of file diff --git a/examples/internal/playground/sites/astro5/src/pages/index.astro b/examples/internal/playground/sites/astro5/src/pages/index.astro deleted file mode 100644 index 7d619578c7..0000000000 --- a/examples/internal/playground/sites/astro5/src/pages/index.astro +++ /dev/null @@ -1,25 +0,0 @@ ---- -import Welcome from '../components/Welcome.astro'; -import Layout from '../layouts/Layout.astro'; - -const base = import.meta.env.BASE_URL === "/" ? "" : import.meta.env.BASE_URL; ---- - - - - - diff --git a/examples/internal/playground/sites/astro5/src/pages/prerendered.astro b/examples/internal/playground/sites/astro5/src/pages/prerendered.astro deleted file mode 100644 index 3ac4801d2d..0000000000 --- a/examples/internal/playground/sites/astro5/src/pages/prerendered.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -export const prerender = true; ---- - -

This page is prerendered

\ No newline at end of file diff --git a/examples/internal/playground/sites/astro5/src/pages/public-image.astro b/examples/internal/playground/sites/astro5/src/pages/public-image.astro deleted file mode 100644 index d4d0b2971a..0000000000 --- a/examples/internal/playground/sites/astro5/src/pages/public-image.astro +++ /dev/null @@ -1,7 +0,0 @@ ---- -import { Image } from 'astro:assets'; -import myImage from '../../public/spongebob-public.jpg'; ---- - -

This image is in public/ with dimentions 2992x2248.

-Spongebob \ No newline at end of file diff --git a/examples/internal/playground/sites/astro5/src/pages/remote-image.astro b/examples/internal/playground/sites/astro5/src/pages/remote-image.astro deleted file mode 100644 index 132682400f..0000000000 --- a/examples/internal/playground/sites/astro5/src/pages/remote-image.astro +++ /dev/null @@ -1,6 +0,0 @@ ---- -import { Image } from 'astro:assets'; ---- - -

This image is remote url.

-SST Logo \ No newline at end of file diff --git a/examples/internal/playground/sites/astro5/src/pages/server-rendered.astro b/examples/internal/playground/sites/astro5/src/pages/server-rendered.astro deleted file mode 100644 index d62b647e83..0000000000 --- a/examples/internal/playground/sites/astro5/src/pages/server-rendered.astro +++ /dev/null @@ -1,24 +0,0 @@ ---- -import { Resource } from "sst"; - -const { env } = Astro.locals.runtime; -const localsRuntime = { - FOO: env.FOO, - files: JSON.stringify(await env.MyBucket.list(), null, 2), -}; - -const importMeta = { - FOO: import.meta.env.FOO, -}; - -//const files2 = JSON.stringify(await Resource.MyBucket.list(), null, 2); ---- - -

This page is server rendered

- -

Accessed via Astro.locals.runtime.dev

-
FOO: {localsRuntime.FOO}
-
files: {localsRuntime.files}
- -

Accessed via import.meta.env

-
FOO: {importMeta.FOO}
\ No newline at end of file diff --git a/examples/internal/playground/sites/astro5/src/pages/src-image.astro b/examples/internal/playground/sites/astro5/src/pages/src-image.astro deleted file mode 100644 index a5409e344e..0000000000 --- a/examples/internal/playground/sites/astro5/src/pages/src-image.astro +++ /dev/null @@ -1,7 +0,0 @@ ---- -import { Image } from 'astro:assets'; -import myImage from '../assets/spongebob-src.jpg'; ---- - -

This image is in src/assets with dimentions 2992x2248. This image SHOULD be transformed.

-Spongebob \ No newline at end of file diff --git a/examples/internal/playground/sites/astro5/src/pages/sub/[slug].astro b/examples/internal/playground/sites/astro5/src/pages/sub/[slug].astro deleted file mode 100644 index 71f250cf9b..0000000000 --- a/examples/internal/playground/sites/astro5/src/pages/sub/[slug].astro +++ /dev/null @@ -1,10 +0,0 @@ ---- -export function getStaticPaths() { - return [ - { params: { slug: "page" }}, - ]; -} - -const { slug } = Astro.params; ---- -

This {slug} is inside /sub

\ No newline at end of file diff --git a/examples/internal/playground/sites/astro5/sst-env.d.ts b/examples/internal/playground/sites/astro5/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/internal/playground/sites/astro5/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/playground/sites/astro5/wrangler.jsonc b/examples/internal/playground/sites/astro5/wrangler.jsonc deleted file mode 100644 index d22923b163..0000000000 --- a/examples/internal/playground/sites/astro5/wrangler.jsonc +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "my-astro-app", - "main": "./dist/_worker.js/index.js", - // Update to today's date - "compatibility_date": "2025-03-25", - "compatibility_flags": ["nodejs_compat"], - "assets": { - "binding": "ASSETS", - "directory": "./dist" - }, - "observability": { - "enabled": true - } -} diff --git a/examples/internal/playground/sites/nextjs/.gitignore b/examples/internal/playground/sites/nextjs/.gitignore deleted file mode 100644 index 69d7f97469..0000000000 --- a/examples/internal/playground/sites/nextjs/.gitignore +++ /dev/null @@ -1,42 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - -# sst -.sst - -# open-next -.open-next diff --git a/examples/internal/playground/sites/nextjs/README.md b/examples/internal/playground/sites/nextjs/README.md deleted file mode 100644 index e215bc4ccf..0000000000 --- a/examples/internal/playground/sites/nextjs/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/examples/internal/playground/sites/nextjs/app/favicon.ico b/examples/internal/playground/sites/nextjs/app/favicon.ico deleted file mode 100644 index 718d6fea48..0000000000 Binary files a/examples/internal/playground/sites/nextjs/app/favicon.ico and /dev/null differ diff --git a/examples/internal/playground/sites/nextjs/app/fonts/GeistMonoVF.woff b/examples/internal/playground/sites/nextjs/app/fonts/GeistMonoVF.woff deleted file mode 100644 index f2ae185cbf..0000000000 Binary files a/examples/internal/playground/sites/nextjs/app/fonts/GeistMonoVF.woff and /dev/null differ diff --git a/examples/internal/playground/sites/nextjs/app/fonts/GeistVF.woff b/examples/internal/playground/sites/nextjs/app/fonts/GeistVF.woff deleted file mode 100644 index 1b62daacff..0000000000 Binary files a/examples/internal/playground/sites/nextjs/app/fonts/GeistVF.woff and /dev/null differ diff --git a/examples/internal/playground/sites/nextjs/app/globals.css b/examples/internal/playground/sites/nextjs/app/globals.css deleted file mode 100644 index e3734be15e..0000000000 --- a/examples/internal/playground/sites/nextjs/app/globals.css +++ /dev/null @@ -1,42 +0,0 @@ -:root { - --background: #ffffff; - --foreground: #171717; -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - -html, -body { - max-width: 100vw; - overflow-x: hidden; -} - -body { - color: var(--foreground); - background: var(--background); - font-family: Arial, Helvetica, sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -* { - box-sizing: border-box; - padding: 0; - margin: 0; -} - -a { - color: inherit; - text-decoration: none; -} - -@media (prefers-color-scheme: dark) { - html { - color-scheme: dark; - } -} diff --git a/examples/internal/playground/sites/nextjs/app/image-test/page.tsx b/examples/internal/playground/sites/nextjs/app/image-test/page.tsx deleted file mode 100644 index e7b1dbeb1c..0000000000 --- a/examples/internal/playground/sites/nextjs/app/image-test/page.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import Image from "next/image"; -import Link from "next/link"; -import styles from "../page.module.css"; - -export default function ImageTest() { - return ( -
-
-

Next.js Image Optimization Test

-
- - Back to Home - -
- -

Image from public folder:

-
- Black square -
- -

Remote image (will use optimization):

-
- Remote image -
-
-
- ); -} diff --git a/examples/internal/playground/sites/nextjs/app/layout.tsx b/examples/internal/playground/sites/nextjs/app/layout.tsx deleted file mode 100644 index dca06aee77..0000000000 --- a/examples/internal/playground/sites/nextjs/app/layout.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import type { Metadata } from "next"; -import localFont from "next/font/local"; -import "./globals.css"; - -const geistSans = localFont({ - src: "./fonts/GeistVF.woff", - variable: "--font-geist-sans", - weight: "100 900", -}); -const geistMono = localFont({ - src: "./fonts/GeistMonoVF.woff", - variable: "--font-geist-mono", - weight: "100 900", -}); - -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - {children} - - - ); -} diff --git a/examples/internal/playground/sites/nextjs/app/page.module.css b/examples/internal/playground/sites/nextjs/app/page.module.css deleted file mode 100644 index 8a460419f9..0000000000 --- a/examples/internal/playground/sites/nextjs/app/page.module.css +++ /dev/null @@ -1,165 +0,0 @@ -.page { - --gray-rgb: 0, 0, 0; - --gray-alpha-200: rgba(var(--gray-rgb), 0.08); - --gray-alpha-100: rgba(var(--gray-rgb), 0.05); - - --button-primary-hover: #383838; - --button-secondary-hover: #f2f2f2; - - display: grid; - grid-template-rows: 20px 1fr 20px; - align-items: center; - justify-items: center; - min-height: 100svh; - padding: 80px; - gap: 64px; - font-family: var(--font-geist-sans); -} - -@media (prefers-color-scheme: dark) { - .page { - --gray-rgb: 255, 255, 255; - --gray-alpha-200: rgba(var(--gray-rgb), 0.145); - --gray-alpha-100: rgba(var(--gray-rgb), 0.06); - - --button-primary-hover: #ccc; - --button-secondary-hover: #1a1a1a; - } -} - -.main { - display: flex; - flex-direction: column; - gap: 32px; - grid-row-start: 2; -} - -.main ol { - font-family: var(--font-geist-mono); - padding-left: 0; - margin: 0; - font-size: 14px; - line-height: 24px; - letter-spacing: -0.01em; - list-style-position: inside; -} - -.main li:not(:last-of-type) { - margin-bottom: 8px; -} - -.main code { - font-family: inherit; - background: var(--gray-alpha-100); - padding: 2px 4px; - border-radius: 4px; - font-weight: 600; -} - -.ctas { - display: flex; - gap: 16px; -} - -.ctas a { - appearance: none; - border-radius: 128px; - height: 48px; - padding: 0 20px; - border: none; - border: 1px solid transparent; - transition: background 0.2s, color 0.2s, border-color 0.2s; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 16px; - line-height: 20px; - font-weight: 500; -} - -a.primary { - background: var(--foreground); - color: var(--background); - gap: 8px; -} - -a.secondary { - border-color: var(--gray-alpha-200); - min-width: 180px; -} - -.footer { - grid-row-start: 3; - display: flex; - gap: 24px; -} - -.footer a { - display: flex; - align-items: center; - gap: 8px; -} - -.footer img { - flex-shrink: 0; -} - -/* Enable hover only on non-touch devices */ -@media (hover: hover) and (pointer: fine) { - a.primary:hover { - background: var(--button-primary-hover); - border-color: transparent; - } - - a.secondary:hover { - background: var(--button-secondary-hover); - border-color: transparent; - } - - .footer a:hover { - text-decoration: underline; - text-underline-offset: 4px; - } -} - -@media (max-width: 600px) { - .page { - padding: 32px; - padding-bottom: 80px; - } - - .main { - align-items: center; - } - - .main ol { - text-align: center; - } - - .ctas { - flex-direction: column; - } - - .ctas a { - font-size: 14px; - height: 40px; - padding: 0 16px; - } - - a.secondary { - min-width: auto; - } - - .footer { - flex-wrap: wrap; - align-items: center; - justify-content: center; - } -} - -@media (prefers-color-scheme: dark) { - .logo { - filter: invert(); - } -} diff --git a/examples/internal/playground/sites/nextjs/app/page.tsx b/examples/internal/playground/sites/nextjs/app/page.tsx deleted file mode 100644 index 7f355635e5..0000000000 --- a/examples/internal/playground/sites/nextjs/app/page.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Resource } from "sst"; -import Form from "@/components/form"; -import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; -import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; -import Link from "next/link"; -import styles from "./page.module.css"; - -export const dynamic = "force-dynamic"; - -export default async function Home() { - const command = new PutObjectCommand({ - Key: crypto.randomUUID(), - // @ts-ignore - Bucket: Resource.MyBucket.name, - }); - const url = await getSignedUrl(new S3Client({}), command); - - return ( -
-
-
-
- - Image Optimization Test - -
-
-

Test Links

- -
- ); -} diff --git a/examples/internal/playground/sites/nextjs/components/form.module.css b/examples/internal/playground/sites/nextjs/components/form.module.css deleted file mode 100644 index 6a12c9ae58..0000000000 --- a/examples/internal/playground/sites/nextjs/components/form.module.css +++ /dev/null @@ -1,24 +0,0 @@ -.form { - padding: 2rem; - border-radius: 0.5rem; - background-color: var(--gray-alpha-100); -} - -.form input { - margin-right: 1rem; -} - -.form button { - appearance: none; - padding: 0.5rem 0.75rem; - font-weight: 500; - font-size: 0.875rem; - border-radius: 0.375rem; - background-color: transparent; - font-family: var(--font-geist-sans); - border: 1px solid var(--gray-alpha-200); -} - -.form button:active:enabled { - background-color: var(--gray-alpha-200); -} diff --git a/examples/internal/playground/sites/nextjs/components/form.tsx b/examples/internal/playground/sites/nextjs/components/form.tsx deleted file mode 100644 index adaa22afdc..0000000000 --- a/examples/internal/playground/sites/nextjs/components/form.tsx +++ /dev/null @@ -1,30 +0,0 @@ -"use client"; - -import styles from "./form.module.css"; - -export default function Form({ url }: { url: string }) { - return ( - { - e.preventDefault(); - - const file = (e.target as HTMLFormElement).file.files?.[0] ?? null; - - const image = await fetch(url, { - body: file, - method: "PUT", - headers: { - "Content-Type": file.type, - "Content-Disposition": `attachment; filename="${file.name}"`, - }, - }); - - window.location.href = image.url.split("?")[0]; - }} - > - - - - ); -} diff --git a/examples/internal/playground/sites/nextjs/next.config.mjs b/examples/internal/playground/sites/nextjs/next.config.mjs deleted file mode 100644 index 6e819d6d34..0000000000 --- a/examples/internal/playground/sites/nextjs/next.config.mjs +++ /dev/null @@ -1,9 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - basePath: undefined, - images: { - domains: ["images.unsplash.com"], - }, -}; - -export default nextConfig; diff --git a/examples/internal/playground/sites/nextjs/package.json b/examples/internal/playground/sites/nextjs/package.json deleted file mode 100644 index d62917fc66..0000000000 --- a/examples/internal/playground/sites/nextjs/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "aws-nextjs", - "version": "0.1.0", - "private": true, - "scripts": { - "build": "next build", - "onbuild": "sed -i '' 's/basePath: .*/basePath: undefined,/' next.config.mjs && npx -y @opennextjs/aws@latest build", - "onbuild-base": "sed -i '' 's/basePath: .*/basePath: \"\\/nextjs\",/' next.config.mjs && npx -y @opennextjs/aws@latest build", - "dev": "next dev", - "lint": "next lint", - "start": "next start" - }, - "dependencies": { - "@aws-sdk/client-s3": "^3.668.0", - "@aws-sdk/s3-request-presigner": "^3.668.0", - "next": "14.2.15", - "react": "^18", - "react-dom": "^18", - "sst": "3.3.47" - }, - "devDependencies": { - "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", - "typescript": "^5" - } -} diff --git a/examples/internal/playground/sites/nextjs/public/.well-known/apple-app-site-association b/examples/internal/playground/sites/nextjs/public/.well-known/apple-app-site-association deleted file mode 100644 index 8bfec6612f..0000000000 --- a/examples/internal/playground/sites/nextjs/public/.well-known/apple-app-site-association +++ /dev/null @@ -1 +0,0 @@ -{ "name": "apple-app-site-association" } \ No newline at end of file diff --git a/examples/internal/playground/sites/nextjs/public/.well-known/assetlinks.json b/examples/internal/playground/sites/nextjs/public/.well-known/assetlinks.json deleted file mode 100644 index c1e01cb5fa..0000000000 --- a/examples/internal/playground/sites/nextjs/public/.well-known/assetlinks.json +++ /dev/null @@ -1 +0,0 @@ -{ "name": "assetLinks.json" } diff --git a/examples/internal/playground/sites/nextjs/public/black.jpg b/examples/internal/playground/sites/nextjs/public/black.jpg deleted file mode 100644 index 0a3caba751..0000000000 Binary files a/examples/internal/playground/sites/nextjs/public/black.jpg and /dev/null differ diff --git a/examples/internal/playground/sites/nextjs/public/next.svg b/examples/internal/playground/sites/nextjs/public/next.svg deleted file mode 100644 index 5174b28c56..0000000000 --- a/examples/internal/playground/sites/nextjs/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/internal/playground/sites/nextjs/public/vercel.svg b/examples/internal/playground/sites/nextjs/public/vercel.svg deleted file mode 100644 index d2f8422273..0000000000 --- a/examples/internal/playground/sites/nextjs/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/internal/playground/sites/nextjs/sst-env.d.ts b/examples/internal/playground/sites/nextjs/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/internal/playground/sites/nextjs/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/playground/sites/nextjs/tsconfig.json b/examples/internal/playground/sites/nextjs/tsconfig.json deleted file mode 100644 index 56433afeb2..0000000000 --- a/examples/internal/playground/sites/nextjs/tsconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules","sst.config.ts"] -} diff --git a/examples/internal/playground/sites/nuxt/README.md b/examples/internal/playground/sites/nuxt/README.md deleted file mode 100644 index f5db2a2dbf..0000000000 --- a/examples/internal/playground/sites/nuxt/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# Nuxt 3 Minimal Starter - -Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. - -## Setup - -Make sure to install the dependencies: - -```bash -# npm -npm install - -# pnpm -pnpm install - -# yarn -yarn install - -# bun -bun install -``` - -## Development Server - -Start the development server on `http://localhost:3000`: - -```bash -# npm -npm run dev - -# pnpm -pnpm run dev - -# yarn -yarn dev - -# bun -bun run dev -``` - -## Production - -Build the application for production: - -```bash -# npm -npm run build - -# pnpm -pnpm run build - -# yarn -yarn build - -# bun -bun run build -``` - -Locally preview production build: - -```bash -# npm -npm run preview - -# pnpm -pnpm run preview - -# yarn -yarn preview - -# bun -bun run preview -``` - -Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. diff --git a/examples/internal/playground/sites/nuxt/app.vue b/examples/internal/playground/sites/nuxt/app.vue deleted file mode 100644 index 5947ef77d1..0000000000 --- a/examples/internal/playground/sites/nuxt/app.vue +++ /dev/null @@ -1,24 +0,0 @@ - - diff --git a/examples/internal/playground/sites/nuxt/nuxt.config.ts b/examples/internal/playground/sites/nuxt/nuxt.config.ts deleted file mode 100644 index dc1add8c48..0000000000 --- a/examples/internal/playground/sites/nuxt/nuxt.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -// https://nuxt.com/docs/api/configuration/nuxt-config -export default defineNuxtConfig({ - compatibilityDate: "2024-04-03", - nitro: { - preset: "aws-lambda", - }, - devtools: { enabled: true }, - app: { - baseURL: "/nuxt", - }, -}); diff --git a/examples/internal/playground/sites/nuxt/package.json b/examples/internal/playground/sites/nuxt/package.json deleted file mode 100644 index f4622964ca..0000000000 --- a/examples/internal/playground/sites/nuxt/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "nuxt-app", - "private": true, - "type": "module", - "scripts": { - "build": "sed -i '' 's/baseURL: .*/baseURL: undefined,/' nuxt.config.ts && nuxt build", - "build-base": "sed -i '' 's/baseURL: .*/baseURL: \"\\/nuxt\",/' nuxt.config.ts && nuxt build", - "dev": "nuxt dev", - "generate": "nuxt generate", - "postinstall": "nuxt prepare", - "preview": "nuxt preview" - }, - "dependencies": { - "@aws-sdk/client-s3": "^3.651.1", - "@aws-sdk/s3-request-presigner": "^3.651.1", - "nuxt": "^3.13.0", - "sst": "latest", - "vue": "latest", - "vue-router": "latest" - } -} diff --git a/examples/internal/playground/sites/nuxt/server/api/presigned.ts b/examples/internal/playground/sites/nuxt/server/api/presigned.ts deleted file mode 100644 index b8c29a2e5b..0000000000 --- a/examples/internal/playground/sites/nuxt/server/api/presigned.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Resource } from "sst"; -import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; -import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; - -export default defineEventHandler(async () => { - const command = new PutObjectCommand({ - Key: crypto.randomUUID(), - Bucket: Resource.MyBucket.name, - }); - - return await getSignedUrl(new S3Client({}), command); -}) diff --git a/examples/internal/playground/sites/nuxt/sst-env.d.ts b/examples/internal/playground/sites/nuxt/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/internal/playground/sites/nuxt/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/playground/sites/react-router-7-csr/.dockerignore b/examples/internal/playground/sites/react-router-7-csr/.dockerignore deleted file mode 100644 index 9b8d514712..0000000000 --- a/examples/internal/playground/sites/react-router-7-csr/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -.react-router -build -node_modules -README.md \ No newline at end of file diff --git a/examples/internal/playground/sites/react-router-7-csr/.gitignore b/examples/internal/playground/sites/react-router-7-csr/.gitignore deleted file mode 100644 index 9b7c041f96..0000000000 --- a/examples/internal/playground/sites/react-router-7-csr/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -.DS_Store -/node_modules/ - -# React Router -/.react-router/ -/build/ diff --git a/examples/internal/playground/sites/react-router-7-csr/Dockerfile b/examples/internal/playground/sites/react-router-7-csr/Dockerfile deleted file mode 100644 index 207bf937e3..0000000000 --- a/examples/internal/playground/sites/react-router-7-csr/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM node:20-alpine AS development-dependencies-env -COPY . /app -WORKDIR /app -RUN npm ci - -FROM node:20-alpine AS production-dependencies-env -COPY ./package.json package-lock.json /app/ -WORKDIR /app -RUN npm ci --omit=dev - -FROM node:20-alpine AS build-env -COPY . /app/ -COPY --from=development-dependencies-env /app/node_modules /app/node_modules -WORKDIR /app -RUN npm run build - -FROM node:20-alpine -COPY ./package.json package-lock.json /app/ -COPY --from=production-dependencies-env /app/node_modules /app/node_modules -COPY --from=build-env /app/build /app/build -WORKDIR /app -CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/examples/internal/playground/sites/react-router-7-csr/README.md b/examples/internal/playground/sites/react-router-7-csr/README.md deleted file mode 100644 index e0d20664ec..0000000000 --- a/examples/internal/playground/sites/react-router-7-csr/README.md +++ /dev/null @@ -1,100 +0,0 @@ -# Welcome to React Router! - -A modern, production-ready template for building full-stack React applications using React Router. - -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default) - -## Features - -- πŸš€ Server-side rendering -- ⚑️ Hot Module Replacement (HMR) -- πŸ“¦ Asset bundling and optimization -- πŸ”„ Data loading and mutations -- πŸ”’ TypeScript by default -- πŸŽ‰ TailwindCSS for styling -- πŸ“– [React Router docs](https://reactrouter.com/) - -## Getting Started - -### Installation - -Install the dependencies: - -```bash -npm install -``` - -### Development - -Start the development server with HMR: - -```bash -npm run dev -``` - -Your application will be available at `http://localhost:5173`. - -## Building for Production - -Create a production build: - -```bash -npm run build -``` - -## Deployment - -### Docker Deployment - -This template includes three Dockerfiles optimized for different package managers: - -- `Dockerfile` - for npm -- `Dockerfile.pnpm` - for pnpm -- `Dockerfile.bun` - for bun - -To build and run using Docker: - -```bash -# For npm -docker build -t my-app . - -# For pnpm -docker build -f Dockerfile.pnpm -t my-app . - -# For bun -docker build -f Dockerfile.bun -t my-app . - -# Run the container -docker run -p 3000:3000 my-app -``` - -The containerized application can be deployed to any platform that supports Docker, including: - -- AWS ECS -- Google Cloud Run -- Azure Container Apps -- Digital Ocean App Platform -- Fly.io -- Railway - -### DIY Deployment - -If you're familiar with deploying Node applications, the built-in app server is production-ready. - -Make sure to deploy the output of `npm run build` - -``` -β”œβ”€β”€ package.json -β”œβ”€β”€ package-lock.json (or pnpm-lock.yaml, or bun.lockb) -β”œβ”€β”€ build/ -β”‚ β”œβ”€β”€ client/ # Static assets -β”‚ └── server/ # Server-side code -``` - -## Styling - -This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. - ---- - -Built with ❀️ using React Router. diff --git a/examples/internal/playground/sites/react-router-7-csr/app/app.css b/examples/internal/playground/sites/react-router-7-csr/app/app.css deleted file mode 100644 index 99345d8218..0000000000 --- a/examples/internal/playground/sites/react-router-7-csr/app/app.css +++ /dev/null @@ -1,15 +0,0 @@ -@import "tailwindcss"; - -@theme { - --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; -} - -html, -body { - @apply bg-white dark:bg-gray-950; - - @media (prefers-color-scheme: dark) { - color-scheme: dark; - } -} diff --git a/examples/internal/playground/sites/react-router-7-csr/app/routes.ts b/examples/internal/playground/sites/react-router-7-csr/app/routes.ts deleted file mode 100644 index 4fcdb4f3ac..0000000000 --- a/examples/internal/playground/sites/react-router-7-csr/app/routes.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { type RouteConfig, index, route } from "@react-router/dev/routes"; - -export default [ - index("routes/home.tsx"), - route("client-rendered", "./routes/client-rendered.tsx"), - route("prerendered", "./routes/prerendered.tsx"), - route("plus+sign-in-path", "./routes/plus-sign-in-path.tsx"), -] satisfies RouteConfig; diff --git a/examples/internal/playground/sites/react-router-7-csr/app/routes/client-rendered.tsx b/examples/internal/playground/sites/react-router-7-csr/app/routes/client-rendered.tsx deleted file mode 100644 index 990ec416b3..0000000000 --- a/examples/internal/playground/sites/react-router-7-csr/app/routes/client-rendered.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { Route } from "./+types/server-rendered"; - -export async function clientLoader() { - return { - message: "Hello from the clientLoader()", - }; -} - -export default function MyRouteComponent({ loaderData }: Route.ComponentProps) { - return ( -
-

This page is client rendered

-

{loaderData.message}

-
- ); -} diff --git a/examples/internal/playground/sites/react-router-7-csr/app/routes/home.tsx b/examples/internal/playground/sites/react-router-7-csr/app/routes/home.tsx deleted file mode 100644 index 8e6e61e06b..0000000000 --- a/examples/internal/playground/sites/react-router-7-csr/app/routes/home.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import type { Route } from "./+types/home"; -import { Welcome } from "../welcome/welcome"; - -export function meta({}: Route.MetaArgs) { - return [ - { title: "New React Router App" }, - { name: "description", content: "Welcome to React Router!" }, - ]; -} - -export default function Home() { - return ( - - ); -} diff --git a/examples/internal/playground/sites/react-router-7-csr/app/routes/plus-sign-in-path.tsx b/examples/internal/playground/sites/react-router-7-csr/app/routes/plus-sign-in-path.tsx deleted file mode 100644 index add90391bf..0000000000 --- a/examples/internal/playground/sites/react-router-7-csr/app/routes/plus-sign-in-path.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function MyRouteComponent() { - return ( -
-

This page has a plus sign in the path

-
- ); -} diff --git a/examples/internal/playground/sites/react-router-7-csr/app/routes/prerendered.tsx b/examples/internal/playground/sites/react-router-7-csr/app/routes/prerendered.tsx deleted file mode 100644 index 02865895cb..0000000000 --- a/examples/internal/playground/sites/react-router-7-csr/app/routes/prerendered.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function MyRouteComponent() { - return ( -
-

This page is static prerendered

-
- ); -} diff --git a/examples/internal/playground/sites/react-router-7-csr/app/welcome/welcome.tsx b/examples/internal/playground/sites/react-router-7-csr/app/welcome/welcome.tsx deleted file mode 100644 index 8ac6e1d30b..0000000000 --- a/examples/internal/playground/sites/react-router-7-csr/app/welcome/welcome.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import logoDark from "./logo-dark.svg"; -import logoLight from "./logo-light.svg"; - -export function Welcome() { - return ( -
-
-
-
- React Router - React Router -
-
-
- -
-
-
- ); -} - -const resources = [ - { - href: "https://reactrouter.com/docs", - text: "React Router Docs", - icon: ( - - - - ), - }, - { - href: "https://rmx.as/discord", - text: "Join Discord", - icon: ( - - - - ), - }, -]; diff --git a/examples/internal/playground/sites/react-router-7-csr/package.json b/examples/internal/playground/sites/react-router-7-csr/package.json deleted file mode 100644 index affed18836..0000000000 --- a/examples/internal/playground/sites/react-router-7-csr/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "react-router-7", - "private": true, - "type": "module", - "scripts": { - "build": "react-router build", - "dev": "react-router dev", - "start": "react-router-serve ./build/server/index.js", - "typecheck": "react-router typegen && tsc" - }, - "dependencies": { - "@react-router/node": "^7.2.0", - "@react-router/serve": "^7.2.0", - "isbot": "^5.1.17", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router": "^7.2.0" - }, - "devDependencies": { - "@react-router/dev": "^7.2.0", - "@tailwindcss/vite": "^4.0.0", - "@types/node": "^20", - "@types/react": "^19.0.1", - "@types/react-dom": "^19.0.1", - "react-router-devtools": "^1.1.0", - "tailwindcss": "^4.0.0", - "typescript": "^5.7.2", - "vite": "^5.4.11", - "vite-tsconfig-paths": "^5.1.4" - } -} \ No newline at end of file diff --git a/examples/internal/playground/sites/react-router-7-csr/react-router.config.ts b/examples/internal/playground/sites/react-router-7-csr/react-router.config.ts deleted file mode 100644 index 75240f83ac..0000000000 --- a/examples/internal/playground/sites/react-router-7-csr/react-router.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Config } from "@react-router/dev/config"; - -export default { - // Config options... - // Server-side render by default, to enable SPA mode set this to `false` - ssr: false, - async prerender() { - return ["/prerendered"]; - }, -} satisfies Config; diff --git a/examples/internal/playground/sites/react-router-7-csr/sst-env.d.ts b/examples/internal/playground/sites/react-router-7-csr/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/internal/playground/sites/react-router-7-csr/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/playground/sites/react-router-7-csr/tsconfig.json b/examples/internal/playground/sites/react-router-7-csr/tsconfig.json deleted file mode 100644 index dc391a45f7..0000000000 --- a/examples/internal/playground/sites/react-router-7-csr/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "include": [ - "**/*", - "**/.server/**/*", - "**/.client/**/*", - ".react-router/types/**/*" - ], - "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2022"], - "types": ["node", "vite/client"], - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "bundler", - "jsx": "react-jsx", - "rootDirs": [".", "./.react-router/types"], - "baseUrl": ".", - "paths": { - "~/*": ["./app/*"] - }, - "esModuleInterop": true, - "verbatimModuleSyntax": true, - "noEmit": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "strict": true - } -} diff --git a/examples/internal/playground/sites/react-router-7-csr/vite.config.ts b/examples/internal/playground/sites/react-router-7-csr/vite.config.ts deleted file mode 100644 index 4a88d5871c..0000000000 --- a/examples/internal/playground/sites/react-router-7-csr/vite.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { reactRouter } from "@react-router/dev/vite"; -import tailwindcss from "@tailwindcss/vite"; -import { defineConfig } from "vite"; -import tsconfigPaths from "vite-tsconfig-paths"; - -export default defineConfig({ - plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], -}); diff --git a/examples/internal/playground/sites/react-router-7-ssr/.dockerignore b/examples/internal/playground/sites/react-router-7-ssr/.dockerignore deleted file mode 100644 index 9b8d514712..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -.react-router -build -node_modules -README.md \ No newline at end of file diff --git a/examples/internal/playground/sites/react-router-7-ssr/.gitignore b/examples/internal/playground/sites/react-router-7-ssr/.gitignore deleted file mode 100644 index 9b7c041f96..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -.DS_Store -/node_modules/ - -# React Router -/.react-router/ -/build/ diff --git a/examples/internal/playground/sites/react-router-7-ssr/Dockerfile b/examples/internal/playground/sites/react-router-7-ssr/Dockerfile deleted file mode 100644 index 207bf937e3..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM node:20-alpine AS development-dependencies-env -COPY . /app -WORKDIR /app -RUN npm ci - -FROM node:20-alpine AS production-dependencies-env -COPY ./package.json package-lock.json /app/ -WORKDIR /app -RUN npm ci --omit=dev - -FROM node:20-alpine AS build-env -COPY . /app/ -COPY --from=development-dependencies-env /app/node_modules /app/node_modules -WORKDIR /app -RUN npm run build - -FROM node:20-alpine -COPY ./package.json package-lock.json /app/ -COPY --from=production-dependencies-env /app/node_modules /app/node_modules -COPY --from=build-env /app/build /app/build -WORKDIR /app -CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/examples/internal/playground/sites/react-router-7-ssr/README.md b/examples/internal/playground/sites/react-router-7-ssr/README.md deleted file mode 100644 index e0d20664ec..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/README.md +++ /dev/null @@ -1,100 +0,0 @@ -# Welcome to React Router! - -A modern, production-ready template for building full-stack React applications using React Router. - -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default) - -## Features - -- πŸš€ Server-side rendering -- ⚑️ Hot Module Replacement (HMR) -- πŸ“¦ Asset bundling and optimization -- πŸ”„ Data loading and mutations -- πŸ”’ TypeScript by default -- πŸŽ‰ TailwindCSS for styling -- πŸ“– [React Router docs](https://reactrouter.com/) - -## Getting Started - -### Installation - -Install the dependencies: - -```bash -npm install -``` - -### Development - -Start the development server with HMR: - -```bash -npm run dev -``` - -Your application will be available at `http://localhost:5173`. - -## Building for Production - -Create a production build: - -```bash -npm run build -``` - -## Deployment - -### Docker Deployment - -This template includes three Dockerfiles optimized for different package managers: - -- `Dockerfile` - for npm -- `Dockerfile.pnpm` - for pnpm -- `Dockerfile.bun` - for bun - -To build and run using Docker: - -```bash -# For npm -docker build -t my-app . - -# For pnpm -docker build -f Dockerfile.pnpm -t my-app . - -# For bun -docker build -f Dockerfile.bun -t my-app . - -# Run the container -docker run -p 3000:3000 my-app -``` - -The containerized application can be deployed to any platform that supports Docker, including: - -- AWS ECS -- Google Cloud Run -- Azure Container Apps -- Digital Ocean App Platform -- Fly.io -- Railway - -### DIY Deployment - -If you're familiar with deploying Node applications, the built-in app server is production-ready. - -Make sure to deploy the output of `npm run build` - -``` -β”œβ”€β”€ package.json -β”œβ”€β”€ package-lock.json (or pnpm-lock.yaml, or bun.lockb) -β”œβ”€β”€ build/ -β”‚ β”œβ”€β”€ client/ # Static assets -β”‚ └── server/ # Server-side code -``` - -## Styling - -This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. - ---- - -Built with ❀️ using React Router. diff --git a/examples/internal/playground/sites/react-router-7-ssr/app/app.css b/examples/internal/playground/sites/react-router-7-ssr/app/app.css deleted file mode 100644 index 99345d8218..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/app/app.css +++ /dev/null @@ -1,15 +0,0 @@ -@import "tailwindcss"; - -@theme { - --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; -} - -html, -body { - @apply bg-white dark:bg-gray-950; - - @media (prefers-color-scheme: dark) { - color-scheme: dark; - } -} diff --git a/examples/internal/playground/sites/react-router-7-ssr/app/root.tsx b/examples/internal/playground/sites/react-router-7-ssr/app/root.tsx deleted file mode 100644 index 3c4684d836..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/app/root.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { - isRouteErrorResponse, - Links, - Meta, - Outlet, - Scripts, - ScrollRestoration, -} from "react-router"; - -import type { Route } from "./+types/root"; -import "./app.css"; - -export const links: Route.LinksFunction = () => [ - { rel: "preconnect", href: "https://fonts.googleapis.com" }, - { - rel: "preconnect", - href: "https://fonts.gstatic.com", - crossOrigin: "anonymous", - }, - { - rel: "stylesheet", - href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", - }, - { - rel: "icon", - href: import.meta.env.BASE_URL + "/favicon.ico", - }, -]; - -export function Layout({ children }: { children: React.ReactNode }) { - return ( - - - - - - - - - {children} - - - - - ); -} - -export default function App() { - return ; -} - -export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { - let message = "Oops!"; - let details = "An unexpected error occurred."; - let stack: string | undefined; - - if (isRouteErrorResponse(error)) { - message = error.status === 404 ? "404" : "Error"; - details = - error.status === 404 - ? "The requested page could not be found." - : error.statusText || details; - } else if (import.meta.env.DEV && error && error instanceof Error) { - details = error.message; - stack = error.stack; - } - - return ( -
-

{message}

-

{details}

- {stack && ( -
-          {stack}
-        
- )} -
- ); -} diff --git a/examples/internal/playground/sites/react-router-7-ssr/app/routes.ts b/examples/internal/playground/sites/react-router-7-ssr/app/routes.ts deleted file mode 100644 index 49be7f889b..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/app/routes.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { type RouteConfig, index, route } from "@react-router/dev/routes"; - -export default [ - index("routes/home.tsx"), - route("server-rendered", "./routes/server-rendered.tsx"), - route("client-rendered", "./routes/client-rendered.tsx"), - route("prerendered", "./routes/prerendered.tsx"), - route("plus+sign-in-path", "./routes/plus-sign-in-path.tsx"), - route("error", "./routes/error.tsx"), -] satisfies RouteConfig; diff --git a/examples/internal/playground/sites/react-router-7-ssr/app/routes/client-rendered.tsx b/examples/internal/playground/sites/react-router-7-ssr/app/routes/client-rendered.tsx deleted file mode 100644 index 990ec416b3..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/app/routes/client-rendered.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { Route } from "./+types/server-rendered"; - -export async function clientLoader() { - return { - message: "Hello from the clientLoader()", - }; -} - -export default function MyRouteComponent({ loaderData }: Route.ComponentProps) { - return ( -
-

This page is client rendered

-

{loaderData.message}

-
- ); -} diff --git a/examples/internal/playground/sites/react-router-7-ssr/app/routes/error.tsx b/examples/internal/playground/sites/react-router-7-ssr/app/routes/error.tsx deleted file mode 100644 index f0ad1501d7..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/app/routes/error.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { Route } from "./+types/error"; - -export async function loader() { - throw new Error("This is a test error"); - return { - message: "Hello from the loader()", - }; -} - -export default function MyRouteComponent({ loaderData }: Route.ComponentProps) { - return ( -
-

This page is server rendered

-

{loaderData.message}

-
- ); -} diff --git a/examples/internal/playground/sites/react-router-7-ssr/app/routes/home.tsx b/examples/internal/playground/sites/react-router-7-ssr/app/routes/home.tsx deleted file mode 100644 index 6fbed4183d..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/app/routes/home.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import type { Route } from "./+types/home"; -import { Welcome } from "../welcome/welcome"; -import { NavLink } from "react-router"; - -export function meta({}: Route.MetaArgs) { - return [ - { title: "New React Router App" }, - { name: "description", content: "Welcome to React Router!" }, - ]; -} - -export default function Home() { - return ( -
- - -
- ); -} diff --git a/examples/internal/playground/sites/react-router-7-ssr/app/routes/plus-sign-in-path.tsx b/examples/internal/playground/sites/react-router-7-ssr/app/routes/plus-sign-in-path.tsx deleted file mode 100644 index add90391bf..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/app/routes/plus-sign-in-path.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function MyRouteComponent() { - return ( -
-

This page has a plus sign in the path

-
- ); -} diff --git a/examples/internal/playground/sites/react-router-7-ssr/app/routes/prerendered.tsx b/examples/internal/playground/sites/react-router-7-ssr/app/routes/prerendered.tsx deleted file mode 100644 index 1c0f84faa9..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/app/routes/prerendered.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function MyRouteComponent() { - return ( -
-

This page is prerendered

-
- ); -} diff --git a/examples/internal/playground/sites/react-router-7-ssr/app/routes/server-rendered.tsx b/examples/internal/playground/sites/react-router-7-ssr/app/routes/server-rendered.tsx deleted file mode 100644 index a54323d428..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/app/routes/server-rendered.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { Route } from "./+types/server-rendered"; -import { v4 } from "uuid"; - -export async function loader() { - return { - message: "Hello from the loader() with uuid: " + v4(), - }; -} - -export default function MyRouteComponent({ loaderData }: Route.ComponentProps) { - return ( -
-

This page is server rendered

-

{loaderData.message}

-
- ); -} diff --git a/examples/internal/playground/sites/react-router-7-ssr/app/welcome/logo-dark.svg b/examples/internal/playground/sites/react-router-7-ssr/app/welcome/logo-dark.svg deleted file mode 100644 index dd82028944..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/app/welcome/logo-dark.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/internal/playground/sites/react-router-7-ssr/app/welcome/logo-light.svg b/examples/internal/playground/sites/react-router-7-ssr/app/welcome/logo-light.svg deleted file mode 100644 index 73284929d3..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/app/welcome/logo-light.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/internal/playground/sites/react-router-7-ssr/app/welcome/welcome.tsx b/examples/internal/playground/sites/react-router-7-ssr/app/welcome/welcome.tsx deleted file mode 100644 index 8ac6e1d30b..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/app/welcome/welcome.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import logoDark from "./logo-dark.svg"; -import logoLight from "./logo-light.svg"; - -export function Welcome() { - return ( -
-
-
-
- React Router - React Router -
-
-
- -
-
-
- ); -} - -const resources = [ - { - href: "https://reactrouter.com/docs", - text: "React Router Docs", - icon: ( - - - - ), - }, - { - href: "https://rmx.as/discord", - text: "Join Discord", - icon: ( - - - - ), - }, -]; diff --git a/examples/internal/playground/sites/react-router-7-ssr/package.json b/examples/internal/playground/sites/react-router-7-ssr/package.json deleted file mode 100644 index b4a26d94fa..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "react-router-7", - "private": true, - "type": "module", - "scripts": { - "build": "sed -i '' 's/basename: .*/basename: undefined,/' react-router.config.ts && sed -i '' 's/base: .*/base: undefined,/' vite.config.ts && react-router build", - "build:base": "sed -i '' 's/basename: .*/basename: \"\\/rr7\",/' react-router.config.ts && sed -i '' 's/base: .*/base: \"\\/rr7\\/\",/' vite.config.ts && react-router build", - "dev": "react-router dev", - "start": "react-router-serve ./build/server/index.js", - "typecheck": "react-router typegen && tsc" - }, - "dependencies": { - "@react-router/node": "^7.2.0", - "@react-router/serve": "^7.2.0", - "isbot": "^5.1.17", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router": "^7.2.0", - "uuid": "^11.1.0" - }, - "devDependencies": { - "@react-router/dev": "^7.2.0", - "@tailwindcss/vite": "^4.0.0", - "@types/node": "^20", - "@types/react": "^19.0.1", - "@types/react-dom": "^19.0.1", - "react-router-devtools": "^1.1.0", - "tailwindcss": "^4.0.0", - "typescript": "^5.7.2", - "vite": "^5.4.11", - "vite-tsconfig-paths": "^5.1.4" - } -} diff --git a/examples/internal/playground/sites/react-router-7-ssr/public/favicon.ico b/examples/internal/playground/sites/react-router-7-ssr/public/favicon.ico deleted file mode 100644 index 5dbdfcddcb..0000000000 Binary files a/examples/internal/playground/sites/react-router-7-ssr/public/favicon.ico and /dev/null differ diff --git a/examples/internal/playground/sites/react-router-7-ssr/react-router.config.ts b/examples/internal/playground/sites/react-router-7-ssr/react-router.config.ts deleted file mode 100644 index b5c268e9e3..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/react-router.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Config } from "@react-router/dev/config"; - -export default { - // Config options... - // Server-side render by default, to enable SPA mode set this to `false` - ssr: true, - async prerender() { - return ["/client-rendered", "/prerendered"]; - }, - basename: "/rr7", -} satisfies Config; diff --git a/examples/internal/playground/sites/react-router-7-ssr/sst-env.d.ts b/examples/internal/playground/sites/react-router-7-ssr/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/playground/sites/react-router-7-ssr/tsconfig.json b/examples/internal/playground/sites/react-router-7-ssr/tsconfig.json deleted file mode 100644 index dc391a45f7..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "include": [ - "**/*", - "**/.server/**/*", - "**/.client/**/*", - ".react-router/types/**/*" - ], - "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2022"], - "types": ["node", "vite/client"], - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "bundler", - "jsx": "react-jsx", - "rootDirs": [".", "./.react-router/types"], - "baseUrl": ".", - "paths": { - "~/*": ["./app/*"] - }, - "esModuleInterop": true, - "verbatimModuleSyntax": true, - "noEmit": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "strict": true - } -} diff --git a/examples/internal/playground/sites/react-router-7-ssr/vite.config.ts b/examples/internal/playground/sites/react-router-7-ssr/vite.config.ts deleted file mode 100644 index 18f91b24c5..0000000000 --- a/examples/internal/playground/sites/react-router-7-ssr/vite.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { reactRouter } from "@react-router/dev/vite"; -import tailwindcss from "@tailwindcss/vite"; -import { defineConfig } from "vite"; -import tsconfigPaths from "vite-tsconfig-paths"; - -export default defineConfig({ - plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], - base: "/rr7/", -}); diff --git a/examples/internal/playground/sites/remix/.eslintrc.cjs b/examples/internal/playground/sites/remix/.eslintrc.cjs deleted file mode 100644 index 4f6f59eee1..0000000000 --- a/examples/internal/playground/sites/remix/.eslintrc.cjs +++ /dev/null @@ -1,84 +0,0 @@ -/** - * This is intended to be a basic starting point for linting in your app. - * It relies on recommended configs out of the box for simplicity, but you can - * and should modify this configuration to best suit your team's needs. - */ - -/** @type {import('eslint').Linter.Config} */ -module.exports = { - root: true, - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - ecmaFeatures: { - jsx: true, - }, - }, - env: { - browser: true, - commonjs: true, - es6: true, - }, - ignorePatterns: ["!**/.server", "!**/.client"], - - // Base config - extends: ["eslint:recommended"], - - overrides: [ - // React - { - files: ["**/*.{js,jsx,ts,tsx}"], - plugins: ["react", "jsx-a11y"], - extends: [ - "plugin:react/recommended", - "plugin:react/jsx-runtime", - "plugin:react-hooks/recommended", - "plugin:jsx-a11y/recommended", - ], - settings: { - react: { - version: "detect", - }, - formComponents: ["Form"], - linkComponents: [ - { name: "Link", linkAttribute: "to" }, - { name: "NavLink", linkAttribute: "to" }, - ], - "import/resolver": { - typescript: {}, - }, - }, - }, - - // Typescript - { - files: ["**/*.{ts,tsx}"], - plugins: ["@typescript-eslint", "import"], - parser: "@typescript-eslint/parser", - settings: { - "import/internal-regex": "^~/", - "import/resolver": { - node: { - extensions: [".ts", ".tsx"], - }, - typescript: { - alwaysTryTypes: true, - }, - }, - }, - extends: [ - "plugin:@typescript-eslint/recommended", - "plugin:import/recommended", - "plugin:import/typescript", - ], - }, - - // Node - { - files: [".eslintrc.cjs"], - env: { - node: true, - }, - }, - ], -}; diff --git a/examples/internal/playground/sites/remix/.gitignore b/examples/internal/playground/sites/remix/.gitignore deleted file mode 100644 index 6a1b35d13a..0000000000 --- a/examples/internal/playground/sites/remix/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -node_modules - -/.cache -/build -.env - -# sst -.sst diff --git a/examples/internal/playground/sites/remix/README.md b/examples/internal/playground/sites/remix/README.md deleted file mode 100644 index 6c4d2168fa..0000000000 --- a/examples/internal/playground/sites/remix/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# Welcome to Remix! - -- πŸ“– [Remix docs](https://remix.run/docs) - -## Development - -Run the dev server: - -```shellscript -npm run dev -``` - -## Deployment - -First, build your app for production: - -```sh -npm run build -``` - -Then run the app in production mode: - -```sh -npm start -``` - -Now you'll need to pick a host to deploy it to. - -### DIY - -If you're familiar with deploying Node applications, the built-in Remix app server is production-ready. - -Make sure to deploy the output of `npm run build` - -- `build/server` -- `build/client` - -## Styling - -This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever css framework you prefer. See the [Vite docs on css](https://vitejs.dev/guide/features.html#css) for more information. diff --git a/examples/internal/playground/sites/remix/app/entry.client.tsx b/examples/internal/playground/sites/remix/app/entry.client.tsx deleted file mode 100644 index 94d5dc0de0..0000000000 --- a/examples/internal/playground/sites/remix/app/entry.client.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/** - * By default, Remix will handle hydrating your app on the client for you. - * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ - * For more information, see https://remix.run/file-conventions/entry.client - */ - -import { RemixBrowser } from "@remix-run/react"; -import { startTransition, StrictMode } from "react"; -import { hydrateRoot } from "react-dom/client"; - -startTransition(() => { - hydrateRoot( - document, - - - - ); -}); diff --git a/examples/internal/playground/sites/remix/app/entry.server.tsx b/examples/internal/playground/sites/remix/app/entry.server.tsx deleted file mode 100644 index 45db3229c6..0000000000 --- a/examples/internal/playground/sites/remix/app/entry.server.tsx +++ /dev/null @@ -1,140 +0,0 @@ -/** - * By default, Remix will handle generating the HTTP Response for you. - * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ - * For more information, see https://remix.run/file-conventions/entry.server - */ - -import { PassThrough } from "node:stream"; - -import type { AppLoadContext, EntryContext } from "@remix-run/node"; -import { createReadableStreamFromReadable } from "@remix-run/node"; -import { RemixServer } from "@remix-run/react"; -import { isbot } from "isbot"; -import { renderToPipeableStream } from "react-dom/server"; - -const ABORT_DELAY = 5_000; - -export default function handleRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext, - // This is ignored so we can keep it in the template for visibility. Feel - // free to delete this parameter in your app if you're not using it! - // eslint-disable-next-line @typescript-eslint/no-unused-vars - loadContext: AppLoadContext -) { - return isbot(request.headers.get("user-agent") || "") - ? handleBotRequest( - request, - responseStatusCode, - responseHeaders, - remixContext - ) - : handleBrowserRequest( - request, - responseStatusCode, - responseHeaders, - remixContext - ); -} - -function handleBotRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext -) { - return new Promise((resolve, reject) => { - let shellRendered = false; - const { pipe, abort } = renderToPipeableStream( - , - { - onAllReady() { - shellRendered = true; - const body = new PassThrough(); - const stream = createReadableStreamFromReadable(body); - - responseHeaders.set("Content-Type", "text/html"); - - resolve( - new Response(stream, { - headers: responseHeaders, - status: responseStatusCode, - }) - ); - - pipe(body); - }, - onShellError(error: unknown) { - reject(error); - }, - onError(error: unknown) { - responseStatusCode = 500; - // Log streaming rendering errors from inside the shell. Don't log - // errors encountered during initial shell rendering since they'll - // reject and get logged in handleDocumentRequest. - if (shellRendered) { - console.error(error); - } - }, - } - ); - - setTimeout(abort, ABORT_DELAY); - }); -} - -function handleBrowserRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext -) { - return new Promise((resolve, reject) => { - let shellRendered = false; - const { pipe, abort } = renderToPipeableStream( - , - { - onShellReady() { - shellRendered = true; - const body = new PassThrough(); - const stream = createReadableStreamFromReadable(body); - - responseHeaders.set("Content-Type", "text/html"); - - resolve( - new Response(stream, { - headers: responseHeaders, - status: responseStatusCode, - }) - ); - - pipe(body); - }, - onShellError(error: unknown) { - reject(error); - }, - onError(error: unknown) { - responseStatusCode = 500; - // Log streaming rendering errors from inside the shell. Don't log - // errors encountered during initial shell rendering since they'll - // reject and get logged in handleDocumentRequest. - if (shellRendered) { - console.error(error); - } - }, - } - ); - - setTimeout(abort, ABORT_DELAY); - }); -} diff --git a/examples/internal/playground/sites/remix/app/root.tsx b/examples/internal/playground/sites/remix/app/root.tsx deleted file mode 100644 index 61c8b983d2..0000000000 --- a/examples/internal/playground/sites/remix/app/root.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { - Links, - Meta, - Outlet, - Scripts, - ScrollRestoration, -} from "@remix-run/react"; -import type { LinksFunction } from "@remix-run/node"; - -import "./tailwind.css"; - -export const links: LinksFunction = () => [ - { rel: "preconnect", href: "https://fonts.googleapis.com" }, - { - rel: "preconnect", - href: "https://fonts.gstatic.com", - crossOrigin: "anonymous", - }, - { - rel: "stylesheet", - href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", - }, -]; - -export function Layout({ children }: { children: React.ReactNode }) { - return ( - - - - - - - - - {children} - - - - - ); -} - -export default function App() { - return ; -} diff --git a/examples/internal/playground/sites/remix/app/routes/_index.tsx b/examples/internal/playground/sites/remix/app/routes/_index.tsx deleted file mode 100644 index 38db41f396..0000000000 --- a/examples/internal/playground/sites/remix/app/routes/_index.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Resource } from "sst"; -import { useLoaderData } from "@remix-run/react"; -import type { MetaFunction } from "@remix-run/node"; -import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; -import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; - -export const meta: MetaFunction = () => { - return [ - { title: "New Remix App" }, - { name: "description", content: "Welcome to Remix!" }, - ]; -}; - -export async function loader() { - const command = new PutObjectCommand({ - Key: crypto.randomUUID(), - Bucket: Resource.MyBucket.name, - }); - const url = await getSignedUrl(new S3Client({}), command); - - return { url }; -} - -export default function Index() { - const data = useLoaderData(); - return ( -
-
-

- Welcome to Remix -

-
{ - e.preventDefault(); - - const file = (e.target as HTMLFormElement).file.files?.[0]!; - - const image = await fetch(data.url, { - body: file, - method: "PUT", - headers: { - "Content-Type": file.type, - "Content-Disposition": `attachment; filename="${file.name}"`, - }, - }); - - window.location.href = image.url.split("?")[0]; - }} - > - - -
-
-
- ); -} diff --git a/examples/internal/playground/sites/remix/app/tailwind.css b/examples/internal/playground/sites/remix/app/tailwind.css deleted file mode 100644 index 303fe158fc..0000000000 --- a/examples/internal/playground/sites/remix/app/tailwind.css +++ /dev/null @@ -1,12 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -html, -body { - @apply bg-white dark:bg-gray-950; - - @media (prefers-color-scheme: dark) { - color-scheme: dark; - } -} diff --git a/examples/internal/playground/sites/remix/package.json b/examples/internal/playground/sites/remix/package.json deleted file mode 100644 index 1750ce329e..0000000000 --- a/examples/internal/playground/sites/remix/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "aws-remix", - "private": true, - "sideEffects": false, - "type": "module", - "scripts": { - "build": "sed -i '' 's/base: .*/base: undefined,/' vite.config.ts && sed -i '' 's/basename: .*/basename: undefined,/' vite.config.ts && remix vite:build", - "build-base": "sed -i '' 's/base: .*/base: \"\\/remix\\/\",/' vite.config.ts && sed -i '' 's/basename: .*/basename: \"\\/remix\",/' vite.config.ts && remix vite:build", - "dev": "remix vite:dev", - "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", - "start": "remix-serve ./build/server/index.js", - "typecheck": "tsc" - }, - "dependencies": { - "@aws-sdk/client-s3": "^3.699.0", - "@aws-sdk/s3-request-presigner": "^3.699.0", - "@remix-run/node": "^2.15.0", - "@remix-run/react": "^2.15.0", - "@remix-run/serve": "^2.15.0", - "isbot": "^4.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "sst": "latest" - }, - "devDependencies": { - "@remix-run/dev": "^2.15.0", - "@types/react": "^18.2.20", - "@types/react-dom": "^18.2.7", - "@typescript-eslint/eslint-plugin": "^6.7.4", - "@typescript-eslint/parser": "^6.7.4", - "autoprefixer": "^10.4.19", - "eslint": "^8.38.0", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", - "postcss": "^8.4.38", - "tailwindcss": "^3.4.4", - "typescript": "^5.1.6", - "vite": "^5.1.0", - "vite-tsconfig-paths": "^4.2.1" - }, - "engines": { - "node": ">=20.0.0" - }, - "overrides": { - "valibot": "^0.41.0" - } -} diff --git a/examples/internal/playground/sites/remix/postcss.config.js b/examples/internal/playground/sites/remix/postcss.config.js deleted file mode 100644 index 2aa7205d4b..0000000000 --- a/examples/internal/playground/sites/remix/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/examples/internal/playground/sites/remix/public/favicon.ico b/examples/internal/playground/sites/remix/public/favicon.ico deleted file mode 100644 index 8830cf6821..0000000000 Binary files a/examples/internal/playground/sites/remix/public/favicon.ico and /dev/null differ diff --git a/examples/internal/playground/sites/remix/public/logo-dark.png b/examples/internal/playground/sites/remix/public/logo-dark.png deleted file mode 100644 index b24c7aee3a..0000000000 Binary files a/examples/internal/playground/sites/remix/public/logo-dark.png and /dev/null differ diff --git a/examples/internal/playground/sites/remix/public/logo-light.png b/examples/internal/playground/sites/remix/public/logo-light.png deleted file mode 100644 index 4490ae7930..0000000000 Binary files a/examples/internal/playground/sites/remix/public/logo-light.png and /dev/null differ diff --git a/examples/internal/playground/sites/remix/sst-env.d.ts b/examples/internal/playground/sites/remix/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/internal/playground/sites/remix/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/playground/sites/remix/tailwind.config.ts b/examples/internal/playground/sites/remix/tailwind.config.ts deleted file mode 100644 index 5f06ad4ba5..0000000000 --- a/examples/internal/playground/sites/remix/tailwind.config.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Config } from "tailwindcss"; - -export default { - content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"], - theme: { - extend: { - fontFamily: { - sans: [ - "Inter", - "ui-sans-serif", - "system-ui", - "sans-serif", - "Apple Color Emoji", - "Segoe UI Emoji", - "Segoe UI Symbol", - "Noto Color Emoji", - ], - }, - }, - }, - plugins: [], -} satisfies Config; diff --git a/examples/internal/playground/sites/remix/tsconfig.json b/examples/internal/playground/sites/remix/tsconfig.json deleted file mode 100644 index 9d87dd378f..0000000000 --- a/examples/internal/playground/sites/remix/tsconfig.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "include": [ - "**/*.ts", - "**/*.tsx", - "**/.server/**/*.ts", - "**/.server/**/*.tsx", - "**/.client/**/*.ts", - "**/.client/**/*.tsx" - ], - "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2022"], - "types": ["@remix-run/node", "vite/client"], - "isolatedModules": true, - "esModuleInterop": true, - "jsx": "react-jsx", - "module": "ESNext", - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "target": "ES2022", - "strict": true, - "allowJs": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "baseUrl": ".", - "paths": { - "~/*": ["./app/*"] - }, - - // Vite takes care of building everything, not tsc. - "noEmit": true - } -} diff --git a/examples/internal/playground/sites/remix/vite.config.ts b/examples/internal/playground/sites/remix/vite.config.ts deleted file mode 100644 index c418ec6c63..0000000000 --- a/examples/internal/playground/sites/remix/vite.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { vitePlugin as remix } from "@remix-run/dev"; -import { defineConfig } from "vite"; -import tsconfigPaths from "vite-tsconfig-paths"; - -declare module "@remix-run/node" { - interface Future { - v3_singleFetch: true; - } -} - -export default defineConfig({ - plugins: [ - remix({ - basename: "/remix", - future: { - v3_fetcherPersist: true, - v3_relativeSplatPath: true, - v3_throwAbortReason: true, - v3_singleFetch: true, - v3_lazyRouteDiscovery: true, - }, - }), - tsconfigPaths(), - ], - base: "/remix/", -}); diff --git a/examples/internal/playground/sites/solid-start/.gitignore b/examples/internal/playground/sites/solid-start/.gitignore deleted file mode 100644 index 8ebae30246..0000000000 --- a/examples/internal/playground/sites/solid-start/.gitignore +++ /dev/null @@ -1,32 +0,0 @@ - -dist -.solid -.output -.vercel -.netlify -.vinxi -app.config.timestamp_*.js - -# Environment -.env -.env*.local - -# dependencies -/node_modules - -# IDEs and editors -/.idea -.project -.classpath -*.launch -.settings/ - -# Temp -gitignore - -# System Files -.DS_Store -Thumbs.db - -# sst -.sst diff --git a/examples/internal/playground/sites/solid-start/README.md b/examples/internal/playground/sites/solid-start/README.md deleted file mode 100644 index a84af3943c..0000000000 --- a/examples/internal/playground/sites/solid-start/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# SolidStart - -Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com); - -## Creating a project - -```bash -# create a new project in the current directory -npm init solid@latest - -# create a new project in my-app -npm init solid@latest my-app -``` - -## Developing - -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: - -```bash -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` - -## Building - -Solid apps are built with _presets_, which optimise your project for deployment to different environments. - -By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`. - -## This project was created with the [Solid CLI](https://solid-cli.netlify.app) diff --git a/examples/internal/playground/sites/solid-start/app.config.ts b/examples/internal/playground/sites/solid-start/app.config.ts deleted file mode 100644 index 361a70541e..0000000000 --- a/examples/internal/playground/sites/solid-start/app.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from "@solidjs/start/config"; - -export default defineConfig({ - server: { - preset: "aws-lambda", - awsLambda: { - streaming: true, - }, - baseURL: "/solid", - }, -}); diff --git a/examples/internal/playground/sites/solid-start/package.json b/examples/internal/playground/sites/solid-start/package.json deleted file mode 100644 index b3552b6c1b..0000000000 --- a/examples/internal/playground/sites/solid-start/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "example-basic", - "type": "module", - "scripts": { - "use:base": "sed -i '' 's/baseURL: .*/baseURL: \"\\/solid\",/' app.config.ts", - "use:no-base": "sed -i '' 's/baseURL: .*/baseURL: undefined,/' app.config.ts", - "build": "vinxi build", - "build:base": "npm run use:base && npm run build", - "build:no-base": "npm run use:no-base && npm run build", - "dev": "vinxi dev", - "start": "vinxi start", - "version": "vinxi version" - }, - "dependencies": { - "@aws-sdk/client-s3": "^3.670.0", - "@aws-sdk/s3-request-presigner": "^3.670.0", - "@solidjs/meta": "^0.29.4", - "@solidjs/router": "^0.15.0", - "@solidjs/start": "^1.1.0", - "solid-js": "^1.9.5", - "sst": "latest", - "vinxi": "^0.5.3" - }, - "engines": { - "node": ">=22" - } -} diff --git a/examples/internal/playground/sites/solid-start/public/favicon.ico b/examples/internal/playground/sites/solid-start/public/favicon.ico deleted file mode 100644 index fb282da071..0000000000 Binary files a/examples/internal/playground/sites/solid-start/public/favicon.ico and /dev/null differ diff --git a/examples/internal/playground/sites/solid-start/src/app.css b/examples/internal/playground/sites/solid-start/src/app.css deleted file mode 100644 index 8596998a49..0000000000 --- a/examples/internal/playground/sites/solid-start/src/app.css +++ /dev/null @@ -1,39 +0,0 @@ -body { - font-family: Gordita, Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; -} - -a { - margin-right: 1rem; -} - -main { - text-align: center; - padding: 1em; - margin: 0 auto; -} - -h1 { - color: #335d92; - text-transform: uppercase; - font-size: 4rem; - font-weight: 100; - line-height: 1.1; - margin: 4rem auto; - max-width: 14rem; -} - -p { - max-width: 14rem; - margin: 2rem auto; - line-height: 1.35; -} - -@media (min-width: 480px) { - h1 { - max-width: none; - } - - p { - max-width: none; - } -} diff --git a/examples/internal/playground/sites/solid-start/src/app.tsx b/examples/internal/playground/sites/solid-start/src/app.tsx deleted file mode 100644 index b77c0e25f5..0000000000 --- a/examples/internal/playground/sites/solid-start/src/app.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { MetaProvider, Title } from "@solidjs/meta"; -import { A, Router } from "@solidjs/router"; -import { FileRoutes } from "@solidjs/start/router"; -import { Suspense } from "solid-js"; -import "./app.css"; - -export default function App() { - return ( - ( - - SolidStart - Basic - Index - About - {props.children} - - )} - > - - - ); -} diff --git a/examples/internal/playground/sites/solid-start/src/components/Counter.css b/examples/internal/playground/sites/solid-start/src/components/Counter.css deleted file mode 100644 index 220e179460..0000000000 --- a/examples/internal/playground/sites/solid-start/src/components/Counter.css +++ /dev/null @@ -1,21 +0,0 @@ -.increment { - font-family: inherit; - font-size: inherit; - padding: 1em 2em; - color: #335d92; - background-color: rgba(68, 107, 158, 0.1); - border-radius: 2em; - border: 2px solid rgba(68, 107, 158, 0); - outline: none; - width: 200px; - font-variant-numeric: tabular-nums; - cursor: pointer; -} - -.increment:focus { - border: 2px solid #335d92; -} - -.increment:active { - background-color: rgba(68, 107, 158, 0.2); -} \ No newline at end of file diff --git a/examples/internal/playground/sites/solid-start/src/components/Counter.tsx b/examples/internal/playground/sites/solid-start/src/components/Counter.tsx deleted file mode 100644 index 091fc5d0bc..0000000000 --- a/examples/internal/playground/sites/solid-start/src/components/Counter.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createSignal } from "solid-js"; -import "./Counter.css"; - -export default function Counter() { - const [count, setCount] = createSignal(0); - return ( - - ); -} diff --git a/examples/internal/playground/sites/solid-start/src/entry-client.tsx b/examples/internal/playground/sites/solid-start/src/entry-client.tsx deleted file mode 100644 index 0ca4e3c300..0000000000 --- a/examples/internal/playground/sites/solid-start/src/entry-client.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// @refresh reload -import { mount, StartClient } from "@solidjs/start/client"; - -mount(() => , document.getElementById("app")!); diff --git a/examples/internal/playground/sites/solid-start/src/entry-server.tsx b/examples/internal/playground/sites/solid-start/src/entry-server.tsx deleted file mode 100644 index 401eff83fd..0000000000 --- a/examples/internal/playground/sites/solid-start/src/entry-server.tsx +++ /dev/null @@ -1,21 +0,0 @@ -// @refresh reload -import { createHandler, StartServer } from "@solidjs/start/server"; - -export default createHandler(() => ( - ( - - - - - - {assets} - - -
{children}
- {scripts} - - - )} - /> -)); diff --git a/examples/internal/playground/sites/solid-start/src/routes/[...404].tsx b/examples/internal/playground/sites/solid-start/src/routes/[...404].tsx deleted file mode 100644 index 4ea71ec7fe..0000000000 --- a/examples/internal/playground/sites/solid-start/src/routes/[...404].tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Title } from "@solidjs/meta"; -import { HttpStatusCode } from "@solidjs/start"; - -export default function NotFound() { - return ( -
- Not Found - -

Page Not Found

-

- Visit{" "} - - start.solidjs.com - {" "} - to learn how to build SolidStart apps. -

-
- ); -} diff --git a/examples/internal/playground/sites/solid-start/src/routes/about.tsx b/examples/internal/playground/sites/solid-start/src/routes/about.tsx deleted file mode 100644 index 8371d911cd..0000000000 --- a/examples/internal/playground/sites/solid-start/src/routes/about.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Title } from "@solidjs/meta"; - -export default function Home() { - return ( -
- About -

About

-
- ); -} diff --git a/examples/internal/playground/sites/solid-start/src/routes/index.tsx b/examples/internal/playground/sites/solid-start/src/routes/index.tsx deleted file mode 100644 index 9d59f86dee..0000000000 --- a/examples/internal/playground/sites/solid-start/src/routes/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Resource } from "sst"; -import { createAsync } from "@solidjs/router"; -import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; -import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; - -async function presignedUrl() { - "use server"; - const command = new PutObjectCommand({ - Key: crypto.randomUUID(), - Bucket: Resource.MyBucket.name, - }); - return await getSignedUrl(new S3Client({}), command); -} - -export const route = { - load: () => presignedUrl(), -}; - -export default function Home() { - const url = createAsync(() => presignedUrl()); - - return ( -
-

Hello world!

-
{ - e.preventDefault(); - - const file = (e.target as HTMLFormElement).file.files?.[0]!; - - const image = await fetch(url() as string, { - body: file, - method: "PUT", - headers: { - "Content-Type": file.type, - "Content-Disposition": `attachment; filename="${file.name}"`, - }, - }); - - window.location.href = image.url.split("?")[0]; - }} - > - - -
-
- ); -} diff --git a/examples/internal/playground/sites/solid-start/sst-env.d.ts b/examples/internal/playground/sites/solid-start/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/internal/playground/sites/solid-start/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/playground/sites/solid-start/tsconfig.json b/examples/internal/playground/sites/solid-start/tsconfig.json deleted file mode 100644 index 7d5871a07a..0000000000 --- a/examples/internal/playground/sites/solid-start/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "jsx": "preserve", - "jsxImportSource": "solid-js", - "allowJs": true, - "strict": true, - "noEmit": true, - "types": ["vinxi/types/client"], - "isolatedModules": true, - "paths": { - "~/*": ["./src/*"] - } - } -} diff --git a/examples/internal/playground/sites/svelte-kit/.gitignore b/examples/internal/playground/sites/svelte-kit/.gitignore deleted file mode 100644 index 82ed85c1d6..0000000000 --- a/examples/internal/playground/sites/svelte-kit/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -node_modules - -# Output -.output -.vercel -/.svelte-kit -/build - -# OS -.DS_Store -Thumbs.db - -# Env -.env -.env.* -!.env.example -!.env.test - -# Vite -vite.config.js.timestamp-* -vite.config.ts.timestamp-* - -# sst -.sst diff --git a/examples/internal/playground/sites/svelte-kit/.npmrc b/examples/internal/playground/sites/svelte-kit/.npmrc deleted file mode 100644 index b6f27f1359..0000000000 --- a/examples/internal/playground/sites/svelte-kit/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true diff --git a/examples/internal/playground/sites/svelte-kit/README.md b/examples/internal/playground/sites/svelte-kit/README.md deleted file mode 100644 index b5b295070b..0000000000 --- a/examples/internal/playground/sites/svelte-kit/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# sv - -Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). - -## Creating a project - -If you're seeing this, you've probably already done this step. Congrats! - -```bash -# create a new project in the current directory -npx sv create - -# create a new project in my-app -npx sv create my-app -``` - -## Developing - -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: - -```bash -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` - -## Building - -To create a production version of your app: - -```bash -npm run build -``` - -You can preview the production build with `npm run preview`. - -> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/examples/internal/playground/sites/svelte-kit/package.json b/examples/internal/playground/sites/svelte-kit/package.json deleted file mode 100644 index c8cd18dda0..0000000000 --- a/examples/internal/playground/sites/svelte-kit/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "aws-svelte-kit", - "version": "0.0.1", - "type": "module", - "scripts": { - "build": "sed -i '' 's/base: .*/base: undefined,/' svelte.config.js && vite build", - "build-base": "sed -i '' 's/base: .*/base: \"\\/svelte\",/' svelte.config.js && vite build", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "dev": "vite dev", - "preview": "vite preview" - }, - "devDependencies": { - "@sveltejs/adapter-auto": "^3.0.0", - "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^4.0.0", - "svelte": "^5.0.0", - "svelte-check": "^4.0.0", - "typescript": "^5.0.0", - "vite": "^5.0.3" - }, - "dependencies": { - "@aws-sdk/client-s3": "^3.700.0", - "@aws-sdk/s3-request-presigner": "^3.700.0", - "sst": "latest", - "svelte-kit-sst": "2.43.5" - } -} diff --git a/examples/internal/playground/sites/svelte-kit/src/app.d.ts b/examples/internal/playground/sites/svelte-kit/src/app.d.ts deleted file mode 100644 index da08e6da59..0000000000 --- a/examples/internal/playground/sites/svelte-kit/src/app.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// See https://svelte.dev/docs/kit/types#app.d.ts -// for information about these interfaces -declare global { - namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } -} - -export {}; diff --git a/examples/internal/playground/sites/svelte-kit/src/app.html b/examples/internal/playground/sites/svelte-kit/src/app.html deleted file mode 100644 index 77a5ff52c9..0000000000 --- a/examples/internal/playground/sites/svelte-kit/src/app.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - %sveltekit.head% - - -
%sveltekit.body%
- - diff --git a/examples/internal/playground/sites/svelte-kit/src/lib/index.ts b/examples/internal/playground/sites/svelte-kit/src/lib/index.ts deleted file mode 100644 index 856f2b6c38..0000000000 --- a/examples/internal/playground/sites/svelte-kit/src/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder. diff --git a/examples/internal/playground/sites/svelte-kit/src/routes/+page.server.ts b/examples/internal/playground/sites/svelte-kit/src/routes/+page.server.ts deleted file mode 100644 index d5ff41caed..0000000000 --- a/examples/internal/playground/sites/svelte-kit/src/routes/+page.server.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Resource } from "sst"; -import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; -import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; - -/** @type {import('./$types').PageServerLoad} */ -export async function load() { - const command = new PutObjectCommand({ - Key: crypto.randomUUID(), - Bucket: Resource.MyBucket.name, - }); - const url = await getSignedUrl(new S3Client({}), command); - - return { url }; -} diff --git a/examples/internal/playground/sites/svelte-kit/src/routes/+page.svelte b/examples/internal/playground/sites/svelte-kit/src/routes/+page.svelte deleted file mode 100644 index cbc3260f8b..0000000000 --- a/examples/internal/playground/sites/svelte-kit/src/routes/+page.svelte +++ /dev/null @@ -1,38 +0,0 @@ - - - - -
-
- - -
-
diff --git a/examples/internal/playground/sites/svelte-kit/sst-env.d.ts b/examples/internal/playground/sites/svelte-kit/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/internal/playground/sites/svelte-kit/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/playground/sites/svelte-kit/static/favicon.png b/examples/internal/playground/sites/svelte-kit/static/favicon.png deleted file mode 100644 index 825b9e65af..0000000000 Binary files a/examples/internal/playground/sites/svelte-kit/static/favicon.png and /dev/null differ diff --git a/examples/internal/playground/sites/svelte-kit/svelte.config.js b/examples/internal/playground/sites/svelte-kit/svelte.config.js deleted file mode 100644 index f5c288ad86..0000000000 --- a/examples/internal/playground/sites/svelte-kit/svelte.config.js +++ /dev/null @@ -1,21 +0,0 @@ -import adapter from "svelte-kit-sst"; -import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - // Consult https://svelte.dev/docs/kit/integrations - // for more information about preprocessors - preprocess: vitePreprocess(), - - kit: { - // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://svelte.dev/docs/kit/adapters for more information about adapters. - adapter: adapter(), - paths: { - base: "/svelte", - }, - }, -}; - -export default config; diff --git a/examples/internal/playground/sites/svelte-kit/tsconfig.json b/examples/internal/playground/sites/svelte-kit/tsconfig.json deleted file mode 100644 index 0b2d8865f4..0000000000 --- a/examples/internal/playground/sites/svelte-kit/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "moduleResolution": "bundler" - } - // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias - // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files - // - // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes - // from the referenced tsconfig.json - TypeScript does not merge them in -} diff --git a/examples/internal/playground/sites/svelte-kit/vite.config.ts b/examples/internal/playground/sites/svelte-kit/vite.config.ts deleted file mode 100644 index bbf8c7da43..0000000000 --- a/examples/internal/playground/sites/svelte-kit/vite.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - plugins: [sveltekit()] -}); diff --git a/examples/internal/playground/sites/tanstack-start/.gitignore b/examples/internal/playground/sites/tanstack-start/.gitignore deleted file mode 100644 index be342025da..0000000000 --- a/examples/internal/playground/sites/tanstack-start/.gitignore +++ /dev/null @@ -1,22 +0,0 @@ -node_modules -package-lock.json -yarn.lock - -.DS_Store -.cache -.env -.vercel -.output -.vinxi - -/build/ -/api/ -/server/build -/public/build -.vinxi -# Sentry Config File -.env.sentry-build-plugin -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/.cache/ diff --git a/examples/internal/playground/sites/tanstack-start/.prettierignore b/examples/internal/playground/sites/tanstack-start/.prettierignore deleted file mode 100644 index 2be5eaa6ec..0000000000 --- a/examples/internal/playground/sites/tanstack-start/.prettierignore +++ /dev/null @@ -1,4 +0,0 @@ -**/build -**/public -pnpm-lock.yaml -routeTree.gen.ts \ No newline at end of file diff --git a/examples/internal/playground/sites/tanstack-start/README.md b/examples/internal/playground/sites/tanstack-start/README.md deleted file mode 100644 index 90cba4aac1..0000000000 --- a/examples/internal/playground/sites/tanstack-start/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# Welcome to TanStack.com! - -This site is built with TanStack Router! - -- [TanStack Router Docs](https://tanstack.com/router) - -It's deployed automagically with Netlify! - -- [Netlify](https://netlify.com/) - -## Development - -From your terminal: - -```sh -pnpm install -pnpm dev -``` - -This starts your app in development mode, rebuilding assets on file changes. - -## Editing and previewing the docs of TanStack projects locally - -The documentations for all TanStack projects except for `React Charts` are hosted on [https://tanstack.com](https://tanstack.com), powered by this TanStack Router app. -In production, the markdown doc pages are fetched from the GitHub repos of the projects, but in development they are read from the local file system. - -Follow these steps if you want to edit the doc pages of a project (in these steps we'll assume it's [`TanStack/form`](https://github.com/tanstack/form)) and preview them locally : - -1. Create a new directory called `tanstack`. - -```sh -mkdir tanstack -``` - -2. Enter the directory and clone this repo and the repo of the project there. - -```sh -cd tanstack -git clone git@github.com:TanStack/tanstack.com.git -git clone git@github.com:TanStack/form.git -``` - -> [!NOTE] -> Your `tanstack` directory should look like this: -> -> ``` -> tanstack/ -> | -> +-- form/ -> | -> +-- tanstack.com/ -> ``` - -> [!WARNING] -> Make sure the name of the directory in your local file system matches the name of the project's repo. For example, `tanstack/form` must be cloned into `form` (this is the default) instead of `some-other-name`, because that way, the doc pages won't be found. - -3. Enter the `tanstack/tanstack.com` directory, install the dependencies and run the app in dev mode: - -```sh -cd tanstack.com -pnpm i -# The app will run on https://localhost:3000 by default -pnpm dev -``` - -4. Now you can visit http://localhost:3000/form/latest/docs/overview in the browser and see the changes you make in `tanstack/form/docs`. - -> [!NOTE] -> The updated pages need to be manually reloaded in the browser. - -> [!WARNING] -> You will need to update the `docs/config.json` file (in the project's repo) if you add a new doc page! diff --git a/examples/internal/playground/sites/tanstack-start/app.config.timestamp_1741135234606.js b/examples/internal/playground/sites/tanstack-start/app.config.timestamp_1741135234606.js deleted file mode 100644 index aa656726f3..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app.config.timestamp_1741135234606.js +++ /dev/null @@ -1,15 +0,0 @@ -// app.config.ts -import { defineConfig } from "@tanstack/react-start/config"; -import tsConfigPaths from "vite-tsconfig-paths"; -var app_config_default = defineConfig({ - vite: { - plugins: [ - tsConfigPaths({ - projects: ["./tsconfig.json"] - }) - ] - } -}); -export { - app_config_default as default -}; diff --git a/examples/internal/playground/sites/tanstack-start/app.config.ts b/examples/internal/playground/sites/tanstack-start/app.config.ts deleted file mode 100644 index 5253ad3bda..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { defineConfig } from "@tanstack/react-start/config"; -import tsConfigPaths from "vite-tsconfig-paths"; - -export default defineConfig({ - server: { - preset: "aws-lambda", - awsLambda: { - streaming: true, - }, - }, - vite: { - plugins: [ - tsConfigPaths({ - projects: ["./tsconfig.json"], - }), - ], - base: "/tan", - }, -}); diff --git a/examples/internal/playground/sites/tanstack-start/app/api.ts b/examples/internal/playground/sites/tanstack-start/app/api.ts deleted file mode 100644 index 8b9fef1667..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/api.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { - createStartAPIHandler, - defaultAPIFileRouteHandler, -} from '@tanstack/react-start/api' - -export default createStartAPIHandler(defaultAPIFileRouteHandler) diff --git a/examples/internal/playground/sites/tanstack-start/app/client.tsx b/examples/internal/playground/sites/tanstack-start/app/client.tsx deleted file mode 100644 index 1593d1b3c7..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/client.tsx +++ /dev/null @@ -1,8 +0,0 @@ -/// -import { hydrateRoot } from 'react-dom/client' -import { StartClient } from '@tanstack/react-start' -import { createRouter } from './router' - -const router = createRouter() - -hydrateRoot(document, ) diff --git a/examples/internal/playground/sites/tanstack-start/app/components/DefaultCatchBoundary.tsx b/examples/internal/playground/sites/tanstack-start/app/components/DefaultCatchBoundary.tsx deleted file mode 100644 index f750e7bd2b..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/components/DefaultCatchBoundary.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { - ErrorComponent, - Link, - rootRouteId, - useMatch, - useRouter, -} from '@tanstack/react-router' -import type { ErrorComponentProps } from '@tanstack/react-router' - -export function DefaultCatchBoundary({ error }: ErrorComponentProps) { - const router = useRouter() - const isRoot = useMatch({ - strict: false, - select: (state) => state.id === rootRouteId, - }) - - console.error('DefaultCatchBoundary Error:', error) - - return ( -
- -
- - {isRoot ? ( - - Home - - ) : ( - { - e.preventDefault() - window.history.back() - }} - > - Go Back - - )} -
-
- ) -} diff --git a/examples/internal/playground/sites/tanstack-start/app/components/NotFound.tsx b/examples/internal/playground/sites/tanstack-start/app/components/NotFound.tsx deleted file mode 100644 index 7b54fa5680..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/components/NotFound.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Link } from '@tanstack/react-router' - -export function NotFound({ children }: { children?: any }) { - return ( -
-
- {children ||

The page you are looking for does not exist.

} -
-

- - - Start Over - -

-
- ) -} diff --git a/examples/internal/playground/sites/tanstack-start/app/components/PostError.tsx b/examples/internal/playground/sites/tanstack-start/app/components/PostError.tsx deleted file mode 100644 index 3573f46964..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/components/PostError.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { ErrorComponent, ErrorComponentProps } from '@tanstack/react-router' - -export function PostErrorComponent({ error }: ErrorComponentProps) { - return -} diff --git a/examples/internal/playground/sites/tanstack-start/app/components/UserError.tsx b/examples/internal/playground/sites/tanstack-start/app/components/UserError.tsx deleted file mode 100644 index ebea2f6213..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/components/UserError.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { ErrorComponent, ErrorComponentProps } from '@tanstack/react-router' - -export function UserErrorComponent({ error }: ErrorComponentProps) { - return -} diff --git a/examples/internal/playground/sites/tanstack-start/app/global-middleware.ts b/examples/internal/playground/sites/tanstack-start/app/global-middleware.ts deleted file mode 100644 index c7e7af599f..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/global-middleware.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { registerGlobalMiddleware } from '@tanstack/react-start' -import { logMiddleware } from './utils/loggingMiddleware' - -registerGlobalMiddleware({ - middleware: [logMiddleware], -}) diff --git a/examples/internal/playground/sites/tanstack-start/app/routeTree.gen.ts b/examples/internal/playground/sites/tanstack-start/app/routeTree.gen.ts deleted file mode 100644 index 4dca20ee9c..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/routeTree.gen.ts +++ /dev/null @@ -1,483 +0,0 @@ -/* eslint-disable */ - -// @ts-nocheck - -// noinspection JSUnusedGlobalSymbols - -// This file was automatically generated by TanStack Router. -// You should NOT make any changes in this file as it will be overwritten. -// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. - -// Import Routes - -import { Route as rootRoute } from './routes/__root' -import { Route as RedirectImport } from './routes/redirect' -import { Route as DeferredImport } from './routes/deferred' -import { Route as PathlessLayoutImport } from './routes/_pathlessLayout' -import { Route as UsersRouteImport } from './routes/users.route' -import { Route as PostsRouteImport } from './routes/posts.route' -import { Route as IndexImport } from './routes/index' -import { Route as UsersIndexImport } from './routes/users.index' -import { Route as PostsIndexImport } from './routes/posts.index' -import { Route as UsersUserIdImport } from './routes/users.$userId' -import { Route as PostsPostIdImport } from './routes/posts.$postId' -import { Route as PathlessLayoutNestedLayoutImport } from './routes/_pathlessLayout/_nested-layout' -import { Route as PostsPostIdDeepImport } from './routes/posts_.$postId.deep' -import { Route as PathlessLayoutNestedLayoutRouteBImport } from './routes/_pathlessLayout/_nested-layout/route-b' -import { Route as PathlessLayoutNestedLayoutRouteAImport } from './routes/_pathlessLayout/_nested-layout/route-a' - -// Create/Update Routes - -const RedirectRoute = RedirectImport.update({ - id: '/redirect', - path: '/redirect', - getParentRoute: () => rootRoute, -} as any) - -const DeferredRoute = DeferredImport.update({ - id: '/deferred', - path: '/deferred', - getParentRoute: () => rootRoute, -} as any) - -const PathlessLayoutRoute = PathlessLayoutImport.update({ - id: '/_pathlessLayout', - getParentRoute: () => rootRoute, -} as any) - -const UsersRouteRoute = UsersRouteImport.update({ - id: '/users', - path: '/users', - getParentRoute: () => rootRoute, -} as any) - -const PostsRouteRoute = PostsRouteImport.update({ - id: '/posts', - path: '/posts', - getParentRoute: () => rootRoute, -} as any) - -const IndexRoute = IndexImport.update({ - id: '/', - path: '/', - getParentRoute: () => rootRoute, -} as any) - -const UsersIndexRoute = UsersIndexImport.update({ - id: '/', - path: '/', - getParentRoute: () => UsersRouteRoute, -} as any) - -const PostsIndexRoute = PostsIndexImport.update({ - id: '/', - path: '/', - getParentRoute: () => PostsRouteRoute, -} as any) - -const UsersUserIdRoute = UsersUserIdImport.update({ - id: '/$userId', - path: '/$userId', - getParentRoute: () => UsersRouteRoute, -} as any) - -const PostsPostIdRoute = PostsPostIdImport.update({ - id: '/$postId', - path: '/$postId', - getParentRoute: () => PostsRouteRoute, -} as any) - -const PathlessLayoutNestedLayoutRoute = PathlessLayoutNestedLayoutImport.update( - { - id: '/_nested-layout', - getParentRoute: () => PathlessLayoutRoute, - } as any, -) - -const PostsPostIdDeepRoute = PostsPostIdDeepImport.update({ - id: '/posts_/$postId/deep', - path: '/posts/$postId/deep', - getParentRoute: () => rootRoute, -} as any) - -const PathlessLayoutNestedLayoutRouteBRoute = - PathlessLayoutNestedLayoutRouteBImport.update({ - id: '/route-b', - path: '/route-b', - getParentRoute: () => PathlessLayoutNestedLayoutRoute, - } as any) - -const PathlessLayoutNestedLayoutRouteARoute = - PathlessLayoutNestedLayoutRouteAImport.update({ - id: '/route-a', - path: '/route-a', - getParentRoute: () => PathlessLayoutNestedLayoutRoute, - } as any) - -// Populate the FileRoutesByPath interface - -declare module '@tanstack/react-router' { - interface FileRoutesByPath { - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexImport - parentRoute: typeof rootRoute - } - '/posts': { - id: '/posts' - path: '/posts' - fullPath: '/posts' - preLoaderRoute: typeof PostsRouteImport - parentRoute: typeof rootRoute - } - '/users': { - id: '/users' - path: '/users' - fullPath: '/users' - preLoaderRoute: typeof UsersRouteImport - parentRoute: typeof rootRoute - } - '/_pathlessLayout': { - id: '/_pathlessLayout' - path: '' - fullPath: '' - preLoaderRoute: typeof PathlessLayoutImport - parentRoute: typeof rootRoute - } - '/deferred': { - id: '/deferred' - path: '/deferred' - fullPath: '/deferred' - preLoaderRoute: typeof DeferredImport - parentRoute: typeof rootRoute - } - '/redirect': { - id: '/redirect' - path: '/redirect' - fullPath: '/redirect' - preLoaderRoute: typeof RedirectImport - parentRoute: typeof rootRoute - } - '/_pathlessLayout/_nested-layout': { - id: '/_pathlessLayout/_nested-layout' - path: '' - fullPath: '' - preLoaderRoute: typeof PathlessLayoutNestedLayoutImport - parentRoute: typeof PathlessLayoutImport - } - '/posts/$postId': { - id: '/posts/$postId' - path: '/$postId' - fullPath: '/posts/$postId' - preLoaderRoute: typeof PostsPostIdImport - parentRoute: typeof PostsRouteImport - } - '/users/$userId': { - id: '/users/$userId' - path: '/$userId' - fullPath: '/users/$userId' - preLoaderRoute: typeof UsersUserIdImport - parentRoute: typeof UsersRouteImport - } - '/posts/': { - id: '/posts/' - path: '/' - fullPath: '/posts/' - preLoaderRoute: typeof PostsIndexImport - parentRoute: typeof PostsRouteImport - } - '/users/': { - id: '/users/' - path: '/' - fullPath: '/users/' - preLoaderRoute: typeof UsersIndexImport - parentRoute: typeof UsersRouteImport - } - '/_pathlessLayout/_nested-layout/route-a': { - id: '/_pathlessLayout/_nested-layout/route-a' - path: '/route-a' - fullPath: '/route-a' - preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteAImport - parentRoute: typeof PathlessLayoutNestedLayoutImport - } - '/_pathlessLayout/_nested-layout/route-b': { - id: '/_pathlessLayout/_nested-layout/route-b' - path: '/route-b' - fullPath: '/route-b' - preLoaderRoute: typeof PathlessLayoutNestedLayoutRouteBImport - parentRoute: typeof PathlessLayoutNestedLayoutImport - } - '/posts_/$postId/deep': { - id: '/posts_/$postId/deep' - path: '/posts/$postId/deep' - fullPath: '/posts/$postId/deep' - preLoaderRoute: typeof PostsPostIdDeepImport - parentRoute: typeof rootRoute - } - } -} - -// Create and export the route tree - -interface PostsRouteRouteChildren { - PostsPostIdRoute: typeof PostsPostIdRoute - PostsIndexRoute: typeof PostsIndexRoute -} - -const PostsRouteRouteChildren: PostsRouteRouteChildren = { - PostsPostIdRoute: PostsPostIdRoute, - PostsIndexRoute: PostsIndexRoute, -} - -const PostsRouteRouteWithChildren = PostsRouteRoute._addFileChildren( - PostsRouteRouteChildren, -) - -interface UsersRouteRouteChildren { - UsersUserIdRoute: typeof UsersUserIdRoute - UsersIndexRoute: typeof UsersIndexRoute -} - -const UsersRouteRouteChildren: UsersRouteRouteChildren = { - UsersUserIdRoute: UsersUserIdRoute, - UsersIndexRoute: UsersIndexRoute, -} - -const UsersRouteRouteWithChildren = UsersRouteRoute._addFileChildren( - UsersRouteRouteChildren, -) - -interface PathlessLayoutNestedLayoutRouteChildren { - PathlessLayoutNestedLayoutRouteARoute: typeof PathlessLayoutNestedLayoutRouteARoute - PathlessLayoutNestedLayoutRouteBRoute: typeof PathlessLayoutNestedLayoutRouteBRoute -} - -const PathlessLayoutNestedLayoutRouteChildren: PathlessLayoutNestedLayoutRouteChildren = - { - PathlessLayoutNestedLayoutRouteARoute: - PathlessLayoutNestedLayoutRouteARoute, - PathlessLayoutNestedLayoutRouteBRoute: - PathlessLayoutNestedLayoutRouteBRoute, - } - -const PathlessLayoutNestedLayoutRouteWithChildren = - PathlessLayoutNestedLayoutRoute._addFileChildren( - PathlessLayoutNestedLayoutRouteChildren, - ) - -interface PathlessLayoutRouteChildren { - PathlessLayoutNestedLayoutRoute: typeof PathlessLayoutNestedLayoutRouteWithChildren -} - -const PathlessLayoutRouteChildren: PathlessLayoutRouteChildren = { - PathlessLayoutNestedLayoutRoute: PathlessLayoutNestedLayoutRouteWithChildren, -} - -const PathlessLayoutRouteWithChildren = PathlessLayoutRoute._addFileChildren( - PathlessLayoutRouteChildren, -) - -export interface FileRoutesByFullPath { - '/': typeof IndexRoute - '/posts': typeof PostsRouteRouteWithChildren - '/users': typeof UsersRouteRouteWithChildren - '': typeof PathlessLayoutNestedLayoutRouteWithChildren - '/deferred': typeof DeferredRoute - '/redirect': typeof RedirectRoute - '/posts/$postId': typeof PostsPostIdRoute - '/users/$userId': typeof UsersUserIdRoute - '/posts/': typeof PostsIndexRoute - '/users/': typeof UsersIndexRoute - '/route-a': typeof PathlessLayoutNestedLayoutRouteARoute - '/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute - '/posts/$postId/deep': typeof PostsPostIdDeepRoute -} - -export interface FileRoutesByTo { - '/': typeof IndexRoute - '': typeof PathlessLayoutNestedLayoutRouteWithChildren - '/deferred': typeof DeferredRoute - '/redirect': typeof RedirectRoute - '/posts/$postId': typeof PostsPostIdRoute - '/users/$userId': typeof UsersUserIdRoute - '/posts': typeof PostsIndexRoute - '/users': typeof UsersIndexRoute - '/route-a': typeof PathlessLayoutNestedLayoutRouteARoute - '/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute - '/posts/$postId/deep': typeof PostsPostIdDeepRoute -} - -export interface FileRoutesById { - __root__: typeof rootRoute - '/': typeof IndexRoute - '/posts': typeof PostsRouteRouteWithChildren - '/users': typeof UsersRouteRouteWithChildren - '/_pathlessLayout': typeof PathlessLayoutRouteWithChildren - '/deferred': typeof DeferredRoute - '/redirect': typeof RedirectRoute - '/_pathlessLayout/_nested-layout': typeof PathlessLayoutNestedLayoutRouteWithChildren - '/posts/$postId': typeof PostsPostIdRoute - '/users/$userId': typeof UsersUserIdRoute - '/posts/': typeof PostsIndexRoute - '/users/': typeof UsersIndexRoute - '/_pathlessLayout/_nested-layout/route-a': typeof PathlessLayoutNestedLayoutRouteARoute - '/_pathlessLayout/_nested-layout/route-b': typeof PathlessLayoutNestedLayoutRouteBRoute - '/posts_/$postId/deep': typeof PostsPostIdDeepRoute -} - -export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: - | '/' - | '/posts' - | '/users' - | '' - | '/deferred' - | '/redirect' - | '/posts/$postId' - | '/users/$userId' - | '/posts/' - | '/users/' - | '/route-a' - | '/route-b' - | '/posts/$postId/deep' - fileRoutesByTo: FileRoutesByTo - to: - | '/' - | '' - | '/deferred' - | '/redirect' - | '/posts/$postId' - | '/users/$userId' - | '/posts' - | '/users' - | '/route-a' - | '/route-b' - | '/posts/$postId/deep' - id: - | '__root__' - | '/' - | '/posts' - | '/users' - | '/_pathlessLayout' - | '/deferred' - | '/redirect' - | '/_pathlessLayout/_nested-layout' - | '/posts/$postId' - | '/users/$userId' - | '/posts/' - | '/users/' - | '/_pathlessLayout/_nested-layout/route-a' - | '/_pathlessLayout/_nested-layout/route-b' - | '/posts_/$postId/deep' - fileRoutesById: FileRoutesById -} - -export interface RootRouteChildren { - IndexRoute: typeof IndexRoute - PostsRouteRoute: typeof PostsRouteRouteWithChildren - UsersRouteRoute: typeof UsersRouteRouteWithChildren - PathlessLayoutRoute: typeof PathlessLayoutRouteWithChildren - DeferredRoute: typeof DeferredRoute - RedirectRoute: typeof RedirectRoute - PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute -} - -const rootRouteChildren: RootRouteChildren = { - IndexRoute: IndexRoute, - PostsRouteRoute: PostsRouteRouteWithChildren, - UsersRouteRoute: UsersRouteRouteWithChildren, - PathlessLayoutRoute: PathlessLayoutRouteWithChildren, - DeferredRoute: DeferredRoute, - RedirectRoute: RedirectRoute, - PostsPostIdDeepRoute: PostsPostIdDeepRoute, -} - -export const routeTree = rootRoute - ._addFileChildren(rootRouteChildren) - ._addFileTypes() - -/* ROUTE_MANIFEST_START -{ - "routes": { - "__root__": { - "filePath": "__root.tsx", - "children": [ - "/", - "/posts", - "/users", - "/_pathlessLayout", - "/deferred", - "/redirect", - "/posts_/$postId/deep" - ] - }, - "/": { - "filePath": "index.tsx" - }, - "/posts": { - "filePath": "posts.route.tsx", - "children": [ - "/posts/$postId", - "/posts/" - ] - }, - "/users": { - "filePath": "users.route.tsx", - "children": [ - "/users/$userId", - "/users/" - ] - }, - "/_pathlessLayout": { - "filePath": "_pathlessLayout.tsx", - "children": [ - "/_pathlessLayout/_nested-layout" - ] - }, - "/deferred": { - "filePath": "deferred.tsx" - }, - "/redirect": { - "filePath": "redirect.tsx" - }, - "/_pathlessLayout/_nested-layout": { - "filePath": "_pathlessLayout/_nested-layout.tsx", - "parent": "/_pathlessLayout", - "children": [ - "/_pathlessLayout/_nested-layout/route-a", - "/_pathlessLayout/_nested-layout/route-b" - ] - }, - "/posts/$postId": { - "filePath": "posts.$postId.tsx", - "parent": "/posts" - }, - "/users/$userId": { - "filePath": "users.$userId.tsx", - "parent": "/users" - }, - "/posts/": { - "filePath": "posts.index.tsx", - "parent": "/posts" - }, - "/users/": { - "filePath": "users.index.tsx", - "parent": "/users" - }, - "/_pathlessLayout/_nested-layout/route-a": { - "filePath": "_pathlessLayout/_nested-layout/route-a.tsx", - "parent": "/_pathlessLayout/_nested-layout" - }, - "/_pathlessLayout/_nested-layout/route-b": { - "filePath": "_pathlessLayout/_nested-layout/route-b.tsx", - "parent": "/_pathlessLayout/_nested-layout" - }, - "/posts_/$postId/deep": { - "filePath": "posts_.$postId.deep.tsx" - } - } -} -ROUTE_MANIFEST_END */ diff --git a/examples/internal/playground/sites/tanstack-start/app/router.tsx b/examples/internal/playground/sites/tanstack-start/app/router.tsx deleted file mode 100644 index 20d0b9a5f1..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/router.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { createRouter as createTanStackRouter } from "@tanstack/react-router"; -import { routeTree } from "./routeTree.gen"; -import { DefaultCatchBoundary } from "./components/DefaultCatchBoundary"; -import { NotFound } from "./components/NotFound"; - -export function createRouter() { - const router = createTanStackRouter({ - routeTree, - defaultPreload: "intent", - defaultErrorComponent: DefaultCatchBoundary, - defaultNotFoundComponent: () => , - scrollRestoration: true, - basepath: "/tan", - }); - - return router; -} - -declare module "@tanstack/react-router" { - interface Register { - router: ReturnType; - } -} diff --git a/examples/internal/playground/sites/tanstack-start/app/routes/__root.tsx b/examples/internal/playground/sites/tanstack-start/app/routes/__root.tsx deleted file mode 100644 index 30f0a42b4c..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/routes/__root.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { - HeadContent, - Link, - Outlet, - Scripts, - createRootRoute, -} from '@tanstack/react-router' -import { TanStackRouterDevtools } from '@tanstack/router-devtools' -import * as React from 'react' -import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' -import { NotFound } from '~/components/NotFound' -import appCss from '~/styles/app.css?url' -import { seo } from '~/utils/seo' - -export const Route = createRootRoute({ - head: () => ({ - meta: [ - { - charSet: 'utf-8', - }, - { - name: 'viewport', - content: 'width=device-width, initial-scale=1', - }, - ...seo({ - title: - 'TanStack Start | Type-Safe, Client-First, Full-Stack React Framework', - description: `TanStack Start is a type-safe, client-first, full-stack React framework. `, - }), - ], - links: [ - { rel: 'stylesheet', href: appCss }, - { - rel: 'apple-touch-icon', - sizes: '180x180', - href: '/apple-touch-icon.png', - }, - { - rel: 'icon', - type: 'image/png', - sizes: '32x32', - href: '/favicon-32x32.png', - }, - { - rel: 'icon', - type: 'image/png', - sizes: '16x16', - href: '/favicon-16x16.png', - }, - { rel: 'manifest', href: '/site.webmanifest', color: '#fffff' }, - { rel: 'icon', href: '/favicon.ico' }, - ], - }), - errorComponent: (props) => { - return ( - - - - ) - }, - notFoundComponent: () => , - component: RootComponent, -}) - -function RootComponent() { - return ( - - - - ) -} - -function RootDocument({ children }: { children: React.ReactNode }) { - return ( - - - - - -
- - Home - {' '} - - Posts - {' '} - - Users - {' '} - - Pathless Layout - {' '} - - Deferred - {' '} - - This Route Does Not Exist - -
-
- {children} - - - - - ) -} diff --git a/examples/internal/playground/sites/tanstack-start/app/routes/_pathlessLayout.tsx b/examples/internal/playground/sites/tanstack-start/app/routes/_pathlessLayout.tsx deleted file mode 100644 index c3b12442b8..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/routes/_pathlessLayout.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Outlet, createFileRoute } from '@tanstack/react-router' - -export const Route = createFileRoute('/_pathlessLayout')({ - component: LayoutComponent, -}) - -function LayoutComponent() { - return ( -
-
I'm a layout
-
- -
-
- ) -} diff --git a/examples/internal/playground/sites/tanstack-start/app/routes/_pathlessLayout/_nested-layout.tsx b/examples/internal/playground/sites/tanstack-start/app/routes/_pathlessLayout/_nested-layout.tsx deleted file mode 100644 index 9a48b73a46..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/routes/_pathlessLayout/_nested-layout.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Link, Outlet, createFileRoute } from '@tanstack/react-router' - -export const Route = createFileRoute('/_pathlessLayout/_nested-layout')({ - component: LayoutComponent, -}) - -function LayoutComponent() { - return ( -
-
I'm a nested layout
-
- - Go to route A - - - Go to route B - -
-
- -
-
- ) -} diff --git a/examples/internal/playground/sites/tanstack-start/app/routes/_pathlessLayout/_nested-layout/route-a.tsx b/examples/internal/playground/sites/tanstack-start/app/routes/_pathlessLayout/_nested-layout/route-a.tsx deleted file mode 100644 index 426a8fe486..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/routes/_pathlessLayout/_nested-layout/route-a.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' - -export const Route = createFileRoute('/_pathlessLayout/_nested-layout/route-a')( - { - component: LayoutAComponent, - }, -) - -function LayoutAComponent() { - return
I'm A!
-} diff --git a/examples/internal/playground/sites/tanstack-start/app/routes/_pathlessLayout/_nested-layout/route-b.tsx b/examples/internal/playground/sites/tanstack-start/app/routes/_pathlessLayout/_nested-layout/route-b.tsx deleted file mode 100644 index 20facf2daf..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/routes/_pathlessLayout/_nested-layout/route-b.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' - -export const Route = createFileRoute('/_pathlessLayout/_nested-layout/route-b')( - { - component: LayoutBComponent, - }, -) - -function LayoutBComponent() { - return
I'm B!
-} diff --git a/examples/internal/playground/sites/tanstack-start/app/routes/api/users.$id.ts b/examples/internal/playground/sites/tanstack-start/app/routes/api/users.$id.ts deleted file mode 100644 index b1797e7917..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/routes/api/users.$id.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { json } from '@tanstack/react-start' -import { createAPIFileRoute } from '@tanstack/react-start/api' -import axios from 'redaxios' -import type { User } from '../../utils/users' - -export const APIRoute = createAPIFileRoute('/api/users/$id')({ - GET: async ({ request, params }) => { - console.info(`Fetching users by id=${params.id}... @`, request.url) - try { - const res = await axios.get( - 'https://jsonplaceholder.typicode.com/users/' + params.id, - ) - - return json({ - id: res.data.id, - name: res.data.name, - email: res.data.email, - }) - } catch (e) { - console.error(e) - return json({ error: 'User not found' }, { status: 404 }) - } - }, -}) diff --git a/examples/internal/playground/sites/tanstack-start/app/routes/api/users.ts b/examples/internal/playground/sites/tanstack-start/app/routes/api/users.ts deleted file mode 100644 index 679eb9e8ab..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/routes/api/users.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { json } from '@tanstack/react-start' -import { createAPIFileRoute } from '@tanstack/react-start/api' -import axios from 'redaxios' -import type { User } from '../../utils/users' - -export const APIRoute = createAPIFileRoute('/api/users')({ - GET: async ({ request }) => { - console.info('Fetching users... @', request.url) - const res = await axios.get>( - 'https://jsonplaceholder.typicode.com/users', - ) - - const list = res.data.slice(0, 10) - - return json(list.map((u) => ({ id: u.id, name: u.name, email: u.email }))) - }, -}) diff --git a/examples/internal/playground/sites/tanstack-start/app/routes/deferred.tsx b/examples/internal/playground/sites/tanstack-start/app/routes/deferred.tsx deleted file mode 100644 index f3e09d1d4e..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/routes/deferred.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Await, createFileRoute } from '@tanstack/react-router' -import { createServerFn } from '@tanstack/react-start' -import { Suspense, useState } from 'react' - -const personServerFn = createServerFn({ method: 'GET' }) - .validator((d: string) => d) - .handler(({ data: name }) => { - return { name, randomNumber: Math.floor(Math.random() * 100) } - }) - -const slowServerFn = createServerFn({ method: 'GET' }) - .validator((d: string) => d) - .handler(async ({ data: name }) => { - await new Promise((r) => setTimeout(r, 1000)) - return { name, randomNumber: Math.floor(Math.random() * 100) } - }) - -export const Route = createFileRoute('/deferred')({ - loader: async () => { - return { - deferredStuff: new Promise((r) => - setTimeout(() => r('Hello deferred!'), 2000), - ), - deferredPerson: slowServerFn({ data: 'Tanner Linsley' }), - person: await personServerFn({ data: 'John Doe' }), - } - }, - component: Deferred, -}) - -function Deferred() { - const [count, setCount] = useState(0) - const { deferredStuff, deferredPerson, person } = Route.useLoaderData() - - return ( -
-
- {person.name} - {person.randomNumber} -
- Loading person...
}> - ( -
- {data.name} - {data.randomNumber} -
- )} - /> - - Loading stuff...}> -

{data}

} - /> -
-
Count: {count}
-
- -
- - ) -} diff --git a/examples/internal/playground/sites/tanstack-start/app/routes/index.tsx b/examples/internal/playground/sites/tanstack-start/app/routes/index.tsx deleted file mode 100644 index 09a907cb18..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/routes/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' - -export const Route = createFileRoute('/')({ - component: Home, -}) - -function Home() { - return ( -
-

Welcome Home!!!

-
- ) -} diff --git a/examples/internal/playground/sites/tanstack-start/app/routes/posts.$postId.tsx b/examples/internal/playground/sites/tanstack-start/app/routes/posts.$postId.tsx deleted file mode 100644 index d6de7c9dc3..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/routes/posts.$postId.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Link, createFileRoute } from '@tanstack/react-router' -import { fetchPost } from '../utils/posts' -import { NotFound } from '~/components/NotFound' -import { PostErrorComponent } from '~/components/PostError' - -export const Route = createFileRoute('/posts/$postId')({ - loader: ({ params: { postId } }) => fetchPost({ data: postId }), - errorComponent: PostErrorComponent, - component: PostComponent, - notFoundComponent: () => { - return Post not found - }, -}) - -function PostComponent() { - const post = Route.useLoaderData() - - return ( -
-

{post.title}

-
{post.body}
- - Deep View - -
- ) -} diff --git a/examples/internal/playground/sites/tanstack-start/app/routes/posts.index.tsx b/examples/internal/playground/sites/tanstack-start/app/routes/posts.index.tsx deleted file mode 100644 index 5b5f08f95b..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/routes/posts.index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' - -export const Route = createFileRoute('/posts/')({ - component: PostsIndexComponent, -}) - -function PostsIndexComponent() { - return
Select a post.
-} diff --git a/examples/internal/playground/sites/tanstack-start/app/routes/posts.route.tsx b/examples/internal/playground/sites/tanstack-start/app/routes/posts.route.tsx deleted file mode 100644 index f29619363e..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/routes/posts.route.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Link, Outlet, createFileRoute } from '@tanstack/react-router' -import { fetchPosts } from '../utils/posts' - -export const Route = createFileRoute('/posts')({ - loader: async () => fetchPosts(), - component: PostsLayoutComponent, -}) - -function PostsLayoutComponent() { - const posts = Route.useLoaderData() - - return ( -
-
    - {[...posts, { id: 'i-do-not-exist', title: 'Non-existent Post' }].map( - (post) => { - return ( -
  • - -
    {post.title.substring(0, 20)}
    - -
  • - ) - }, - )} -
-
- -
- ) -} diff --git a/examples/internal/playground/sites/tanstack-start/app/routes/posts_.$postId.deep.tsx b/examples/internal/playground/sites/tanstack-start/app/routes/posts_.$postId.deep.tsx deleted file mode 100644 index 29e6c39b5a..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/routes/posts_.$postId.deep.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Link, createFileRoute } from '@tanstack/react-router' -import { fetchPost } from '../utils/posts' -import { PostErrorComponent } from '~/components/PostError' - -export const Route = createFileRoute('/posts_/$postId/deep')({ - loader: async ({ params: { postId } }) => - fetchPost({ - data: postId, - }), - errorComponent: PostErrorComponent, - component: PostDeepComponent, -}) - -function PostDeepComponent() { - const post = Route.useLoaderData() - - return ( -
- - ← All Posts - -

{post.title}

-
{post.body}
-
- ) -} diff --git a/examples/internal/playground/sites/tanstack-start/app/routes/redirect.tsx b/examples/internal/playground/sites/tanstack-start/app/routes/redirect.tsx deleted file mode 100644 index c9286de13d..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/routes/redirect.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { createFileRoute, redirect } from '@tanstack/react-router' - -export const Route = createFileRoute('/redirect')({ - beforeLoad: async () => { - throw redirect({ - to: '/posts', - }) - }, -}) diff --git a/examples/internal/playground/sites/tanstack-start/app/routes/users.$userId.tsx b/examples/internal/playground/sites/tanstack-start/app/routes/users.$userId.tsx deleted file mode 100644 index 623698bd35..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/routes/users.$userId.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' -import axios from 'redaxios' -import type { User } from '~/utils/users' -import { DEPLOY_URL } from '~/utils/users' -import { NotFound } from '~/components/NotFound' -import { UserErrorComponent } from '~/components/UserError' - -export const Route = createFileRoute('/users/$userId')({ - loader: async ({ params: { userId } }) => { - return await axios - .get(DEPLOY_URL + '/api/users/' + userId) - .then((r) => r.data) - .catch(() => { - throw new Error('Failed to fetch user') - }) - }, - errorComponent: UserErrorComponent, - component: UserComponent, - notFoundComponent: () => { - return User not found - }, -}) - -function UserComponent() { - const user = Route.useLoaderData() - - return ( -
-

{user.name}

-
{user.email}
-
- ) -} diff --git a/examples/internal/playground/sites/tanstack-start/app/routes/users.index.tsx b/examples/internal/playground/sites/tanstack-start/app/routes/users.index.tsx deleted file mode 100644 index b6b0ee67fb..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/routes/users.index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' - -export const Route = createFileRoute('/users/')({ - component: UsersIndexComponent, -}) - -function UsersIndexComponent() { - return
Select a user.
-} diff --git a/examples/internal/playground/sites/tanstack-start/app/routes/users.route.tsx b/examples/internal/playground/sites/tanstack-start/app/routes/users.route.tsx deleted file mode 100644 index 76cf588f6a..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/routes/users.route.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Link, Outlet, createFileRoute } from '@tanstack/react-router' -import axios from 'redaxios' -import { DEPLOY_URL } from '../utils/users' -import type { User } from '../utils/users' - -export const Route = createFileRoute('/users')({ - loader: async () => { - return await axios - .get>(DEPLOY_URL + '/api/users') - .then((r) => r.data) - .catch(() => { - throw new Error('Failed to fetch users') - }) - }, - component: UsersLayoutComponent, -}) - -function UsersLayoutComponent() { - const users = Route.useLoaderData() - - return ( -
-
    - {[ - ...users, - { id: 'i-do-not-exist', name: 'Non-existent User', email: '' }, - ].map((user) => { - return ( -
  • - -
    {user.name}
    - -
  • - ) - })} -
-
- -
- ) -} diff --git a/examples/internal/playground/sites/tanstack-start/app/ssr.tsx b/examples/internal/playground/sites/tanstack-start/app/ssr.tsx deleted file mode 100644 index 8981a9a338..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/ssr.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/// -import { - createStartHandler, - defaultStreamHandler, -} from '@tanstack/react-start/server' -import { getRouterManifest } from '@tanstack/react-start/router-manifest' - -import { createRouter } from './router' - -export default createStartHandler({ - createRouter, - getRouterManifest, -})(defaultStreamHandler) diff --git a/examples/internal/playground/sites/tanstack-start/app/styles/app.css b/examples/internal/playground/sites/tanstack-start/app/styles/app.css deleted file mode 100644 index c53c870665..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/styles/app.css +++ /dev/null @@ -1,22 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - html { - color-scheme: light dark; - } - - * { - @apply border-gray-200 dark:border-gray-800; - } - - html, - body { - @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; - } - - .using-mouse * { - outline: none !important; - } -} diff --git a/examples/internal/playground/sites/tanstack-start/app/utils/loggingMiddleware.tsx b/examples/internal/playground/sites/tanstack-start/app/utils/loggingMiddleware.tsx deleted file mode 100644 index 3944490725..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/utils/loggingMiddleware.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { createMiddleware } from '@tanstack/react-start' - -const preLogMiddleware = createMiddleware() - .client(async (ctx) => { - const clientTime = new Date() - - return ctx.next({ - context: { - clientTime, - }, - sendContext: { - clientTime, - }, - }) - }) - .server(async (ctx) => { - const serverTime = new Date() - - return ctx.next({ - sendContext: { - serverTime, - durationToServer: - serverTime.getTime() - ctx.context.clientTime.getTime(), - }, - }) - }) - -export const logMiddleware = createMiddleware() - .middleware([preLogMiddleware]) - .client(async (ctx) => { - const res = await ctx.next() - - const now = new Date() - console.log('Client Req/Res:', { - duration: res.context.clientTime.getTime() - now.getTime(), - durationToServer: res.context.durationToServer, - durationFromServer: now.getTime() - res.context.serverTime.getTime(), - }) - - return res - }) diff --git a/examples/internal/playground/sites/tanstack-start/app/utils/posts.tsx b/examples/internal/playground/sites/tanstack-start/app/utils/posts.tsx deleted file mode 100644 index 5a2ff0232b..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/utils/posts.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { notFound } from '@tanstack/react-router' -import { createServerFn } from '@tanstack/react-start' -import axios from 'redaxios' - -export type PostType = { - id: string - title: string - body: string -} - -export const fetchPost = createServerFn({ method: 'GET' }) - .validator((d: string) => d) - .handler(async ({ data }) => { - console.info(`Fetching post with id ${data}...`) - const post = await axios - .get(`https://jsonplaceholder.typicode.com/posts/${data}`) - .then((r) => r.data) - .catch((err) => { - console.error(err) - if (err.status === 404) { - throw notFound() - } - throw err - }) - - return post - }) - -export const fetchPosts = createServerFn({ method: 'GET' }).handler( - async () => { - console.info('Fetching posts...') - return axios - .get>('https://jsonplaceholder.typicode.com/posts') - .then((r) => r.data.slice(0, 10)) - }, -) diff --git a/examples/internal/playground/sites/tanstack-start/app/utils/seo.ts b/examples/internal/playground/sites/tanstack-start/app/utils/seo.ts deleted file mode 100644 index d18ad84b74..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/utils/seo.ts +++ /dev/null @@ -1,33 +0,0 @@ -export const seo = ({ - title, - description, - keywords, - image, -}: { - title: string - description?: string - image?: string - keywords?: string -}) => { - const tags = [ - { title }, - { name: 'description', content: description }, - { name: 'keywords', content: keywords }, - { name: 'twitter:title', content: title }, - { name: 'twitter:description', content: description }, - { name: 'twitter:creator', content: '@tannerlinsley' }, - { name: 'twitter:site', content: '@tannerlinsley' }, - { name: 'og:type', content: 'website' }, - { name: 'og:title', content: title }, - { name: 'og:description', content: description }, - ...(image - ? [ - { name: 'twitter:image', content: image }, - { name: 'twitter:card', content: 'summary_large_image' }, - { name: 'og:image', content: image }, - ] - : []), - ] - - return tags -} diff --git a/examples/internal/playground/sites/tanstack-start/app/utils/users.tsx b/examples/internal/playground/sites/tanstack-start/app/utils/users.tsx deleted file mode 100644 index c52eafbc01..0000000000 --- a/examples/internal/playground/sites/tanstack-start/app/utils/users.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export type User = { - id: number; - name: string; - email: string; -}; - -export const DEPLOY_URL = - process.env.NODE_ENV === "development" - ? "http://localhost:3000" - : "https://tanstack.playground.sst.sh"; diff --git a/examples/internal/playground/sites/tanstack-start/package.json b/examples/internal/playground/sites/tanstack-start/package.json deleted file mode 100644 index 4e9a877290..0000000000 --- a/examples/internal/playground/sites/tanstack-start/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "tanstack-start-example-basic", - "private": true, - "sideEffects": false, - "type": "module", - "scripts": { - "dev": "vinxi dev", - "build": "sed -i '' 's/basepath: .*/basepath: undefined,/' app/router.tsx && vinxi build", - "build-base": "sed -i '' 's/basepath: .*/basepath: \"\\/tan\",/' app/router.tsx && vinxi build", - "start": "vinxi start" - }, - "dependencies": { - "@tanstack/react-router": "^1.112.13", - "@tanstack/router-devtools": "^1.112.13", - "@tanstack/react-start": "^1.112.13", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "redaxios": "^0.5.1", - "tailwind-merge": "^2.6.0", - "vinxi": "0.5.3" - }, - "devDependencies": { - "@types/node": "^22.5.4", - "@types/react": "^19.0.8", - "@types/react-dom": "^19.0.3", - "postcss": "^8.5.1", - "autoprefixer": "^10.4.20", - "tailwindcss": "^3.4.17", - "typescript": "^5.7.2", - "vite-tsconfig-paths": "^5.1.4" - } -} diff --git a/examples/internal/playground/sites/tanstack-start/postcss.config.mjs b/examples/internal/playground/sites/tanstack-start/postcss.config.mjs deleted file mode 100644 index 2e7af2b7f1..0000000000 --- a/examples/internal/playground/sites/tanstack-start/postcss.config.mjs +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/examples/internal/playground/sites/tanstack-start/public/android-chrome-192x192.png b/examples/internal/playground/sites/tanstack-start/public/android-chrome-192x192.png deleted file mode 100644 index 09c8324f8c..0000000000 Binary files a/examples/internal/playground/sites/tanstack-start/public/android-chrome-192x192.png and /dev/null differ diff --git a/examples/internal/playground/sites/tanstack-start/public/android-chrome-512x512.png b/examples/internal/playground/sites/tanstack-start/public/android-chrome-512x512.png deleted file mode 100644 index 11d626ea3d..0000000000 Binary files a/examples/internal/playground/sites/tanstack-start/public/android-chrome-512x512.png and /dev/null differ diff --git a/examples/internal/playground/sites/tanstack-start/public/apple-touch-icon.png b/examples/internal/playground/sites/tanstack-start/public/apple-touch-icon.png deleted file mode 100644 index 5a9423cc02..0000000000 Binary files a/examples/internal/playground/sites/tanstack-start/public/apple-touch-icon.png and /dev/null differ diff --git a/examples/internal/playground/sites/tanstack-start/public/favicon-16x16.png b/examples/internal/playground/sites/tanstack-start/public/favicon-16x16.png deleted file mode 100644 index e3389b0044..0000000000 Binary files a/examples/internal/playground/sites/tanstack-start/public/favicon-16x16.png and /dev/null differ diff --git a/examples/internal/playground/sites/tanstack-start/public/favicon-32x32.png b/examples/internal/playground/sites/tanstack-start/public/favicon-32x32.png deleted file mode 100644 index 900c77d444..0000000000 Binary files a/examples/internal/playground/sites/tanstack-start/public/favicon-32x32.png and /dev/null differ diff --git a/examples/internal/playground/sites/tanstack-start/public/favicon.ico b/examples/internal/playground/sites/tanstack-start/public/favicon.ico deleted file mode 100644 index 1a1751676f..0000000000 Binary files a/examples/internal/playground/sites/tanstack-start/public/favicon.ico and /dev/null differ diff --git a/examples/internal/playground/sites/tanstack-start/public/favicon.png b/examples/internal/playground/sites/tanstack-start/public/favicon.png deleted file mode 100644 index 1e77bc0609..0000000000 Binary files a/examples/internal/playground/sites/tanstack-start/public/favicon.png and /dev/null differ diff --git a/examples/internal/playground/sites/tanstack-start/public/site.webmanifest b/examples/internal/playground/sites/tanstack-start/public/site.webmanifest deleted file mode 100644 index fa99de77db..0000000000 --- a/examples/internal/playground/sites/tanstack-start/public/site.webmanifest +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "", - "short_name": "", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/examples/internal/playground/sites/tanstack-start/sst-env.d.ts b/examples/internal/playground/sites/tanstack-start/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/internal/playground/sites/tanstack-start/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/playground/sites/tanstack-start/tailwind.config.mjs b/examples/internal/playground/sites/tanstack-start/tailwind.config.mjs deleted file mode 100644 index 07c3598bac..0000000000 --- a/examples/internal/playground/sites/tanstack-start/tailwind.config.mjs +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: ['./app/**/*.{js,jsx,ts,tsx}'], -} diff --git a/examples/internal/playground/sites/tanstack-start/tsconfig.json b/examples/internal/playground/sites/tanstack-start/tsconfig.json deleted file mode 100644 index d1b5b77660..0000000000 --- a/examples/internal/playground/sites/tanstack-start/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "include": ["**/*.ts", "**/*.tsx"], - "compilerOptions": { - "strict": true, - "esModuleInterop": true, - "jsx": "react-jsx", - "module": "ESNext", - "moduleResolution": "Bundler", - "lib": ["DOM", "DOM.Iterable", "ES2022"], - "isolatedModules": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "target": "ES2022", - "allowJs": true, - "forceConsistentCasingInFileNames": true, - "baseUrl": ".", - "paths": { - "~/*": ["./app/*"] - }, - "noEmit": true - } -} diff --git a/examples/internal/playground/sites/vite/.eslintrc.cjs b/examples/internal/playground/sites/vite/.eslintrc.cjs deleted file mode 100644 index d6c9537953..0000000000 --- a/examples/internal/playground/sites/vite/.eslintrc.cjs +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, es2020: true }, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:react-hooks/recommended', - ], - ignorePatterns: ['dist', '.eslintrc.cjs'], - parser: '@typescript-eslint/parser', - plugins: ['react-refresh'], - rules: { - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - }, -} diff --git a/examples/internal/playground/sites/vite/.gitignore b/examples/internal/playground/sites/vite/.gitignore deleted file mode 100644 index a547bf36d8..0000000000 --- a/examples/internal/playground/sites/vite/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/examples/internal/playground/sites/vite/README.md b/examples/internal/playground/sites/vite/README.md deleted file mode 100644 index 0d6babeddb..0000000000 --- a/examples/internal/playground/sites/vite/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - -- Configure the top-level `parserOptions` property like this: - -```js -export default { - // other rules... - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: ['./tsconfig.json', './tsconfig.node.json'], - tsconfigRootDir: __dirname, - }, -} -``` - -- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` -- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/examples/internal/playground/sites/vite/index.html b/examples/internal/playground/sites/vite/index.html deleted file mode 100644 index e4b78eae12..0000000000 --- a/examples/internal/playground/sites/vite/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/examples/internal/playground/sites/vite/package.json b/examples/internal/playground/sites/vite/package.json deleted file mode 100644 index 0053a05538..0000000000 --- a/examples/internal/playground/sites/vite/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "vite", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "sed -i '' 's/base: .*/base: undefined,/' vite.config.ts && tsc && vite build", - "build-base": "sed -i '' 's/base: .*/base: \"\\/vite\\/\",/' vite.config.ts && tsc && vite build", - "o": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" - }, - "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@types/react": "^18.2.56", - "@types/react-dom": "^18.2.19", - "@typescript-eslint/eslint-plugin": "^7.0.2", - "@typescript-eslint/parser": "^7.0.2", - "@vitejs/plugin-react": "^4.2.1", - "eslint": "^8.56.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.5", - "typescript": "^5.2.2", - "vite": "^5.1.4" - } -} diff --git a/examples/internal/playground/sites/vite/public/.well-known/apple-app-site-association b/examples/internal/playground/sites/vite/public/.well-known/apple-app-site-association deleted file mode 100644 index 8bfec6612f..0000000000 --- a/examples/internal/playground/sites/vite/public/.well-known/apple-app-site-association +++ /dev/null @@ -1 +0,0 @@ -{ "name": "apple-app-site-association" } \ No newline at end of file diff --git a/examples/internal/playground/sites/vite/public/.well-known/assetlinks.json b/examples/internal/playground/sites/vite/public/.well-known/assetlinks.json deleted file mode 100644 index c1e01cb5fa..0000000000 --- a/examples/internal/playground/sites/vite/public/.well-known/assetlinks.json +++ /dev/null @@ -1 +0,0 @@ -{ "name": "assetLinks.json" } diff --git a/examples/internal/playground/sites/vite/public/vite.svg b/examples/internal/playground/sites/vite/public/vite.svg deleted file mode 100644 index e7b8dfb1b2..0000000000 --- a/examples/internal/playground/sites/vite/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/internal/playground/sites/vite/src/App.css b/examples/internal/playground/sites/vite/src/App.css deleted file mode 100644 index b9d355df2a..0000000000 --- a/examples/internal/playground/sites/vite/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/examples/internal/playground/sites/vite/src/App.tsx b/examples/internal/playground/sites/vite/src/App.tsx deleted file mode 100644 index ebe9aea950..0000000000 --- a/examples/internal/playground/sites/vite/src/App.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useState } from "react"; -import reactLogo from "./assets/react.svg"; -import viteLogo from "/vite.svg"; -import "./App.css"; - -function App() { - const [count, setCount] = useState(0); - - return ( - <> - -

Test Links

- -

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ); -} - -export default App; diff --git a/examples/internal/playground/sites/vite/src/assets/react.svg b/examples/internal/playground/sites/vite/src/assets/react.svg deleted file mode 100644 index 6c87de9bb3..0000000000 --- a/examples/internal/playground/sites/vite/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/internal/playground/sites/vite/src/index.css b/examples/internal/playground/sites/vite/src/index.css deleted file mode 100644 index 6119ad9a8f..0000000000 --- a/examples/internal/playground/sites/vite/src/index.css +++ /dev/null @@ -1,68 +0,0 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} diff --git a/examples/internal/playground/sites/vite/src/main.tsx b/examples/internal/playground/sites/vite/src/main.tsx deleted file mode 100644 index 3d7150da80..0000000000 --- a/examples/internal/playground/sites/vite/src/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App.tsx' -import './index.css' - -ReactDOM.createRoot(document.getElementById('root')!).render( - - - , -) diff --git a/examples/internal/playground/sites/vite/src/sst-env.d.ts b/examples/internal/playground/sites/vite/src/sst-env.d.ts deleted file mode 100644 index 47a8fbec7b..0000000000 --- a/examples/internal/playground/sites/vite/src/sst-env.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/// -interface ImportMetaEnv { - -} -interface ImportMeta { - readonly env: ImportMetaEnv -} \ No newline at end of file diff --git a/examples/internal/playground/sites/vite/src/vite-env.d.ts b/examples/internal/playground/sites/vite/src/vite-env.d.ts deleted file mode 100644 index 11f02fe2a0..0000000000 --- a/examples/internal/playground/sites/vite/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/examples/internal/playground/sites/vite/sst-env.d.ts b/examples/internal/playground/sites/vite/sst-env.d.ts deleted file mode 100644 index b6a7e9066e..0000000000 --- a/examples/internal/playground/sites/vite/sst-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/playground/sites/vite/tsconfig.json b/examples/internal/playground/sites/vite/tsconfig.json deleted file mode 100644 index a7fc6fbf23..0000000000 --- a/examples/internal/playground/sites/vite/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} diff --git a/examples/internal/playground/sites/vite/tsconfig.node.json b/examples/internal/playground/sites/vite/tsconfig.node.json deleted file mode 100644 index 97ede7ee6f..0000000000 --- a/examples/internal/playground/sites/vite/tsconfig.node.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, - "strict": true - }, - "include": ["vite.config.ts"] -} diff --git a/examples/internal/playground/sites/vite/vite.config.ts b/examples/internal/playground/sites/vite/vite.config.ts deleted file mode 100644 index b8295a4ff0..0000000000 --- a/examples/internal/playground/sites/vite/vite.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [react()], - base: undefined, -}); diff --git a/examples/internal/playground/sst-env.d.ts b/examples/internal/playground/sst-env.d.ts deleted file mode 100644 index 92f2b89671..0000000000 --- a/examples/internal/playground/sst-env.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -declare module "sst" { - export interface Resource { - "GOOGLE_CLIENT_ID": { - "type": "sst.sst.Secret" - "value": string - } - "GOOGLE_CLIENT_SECRET": { - "type": "sst.sst.Secret" - "value": string - } - "MyAuth": { - "type": "sst.aws.Auth" - "url": string - } - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "MyNextjsSite": { - "type": "sst.aws.Nextjs" - "url": string - } - "MyRouter": { - "type": "sst.aws.Router" - "url": string - } - "MyRouterApp": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyRouterAstro": { - "type": "sst.aws.Astro" - "url": string - } - "MyStaticSite": { - "type": "sst.aws.StaticSite" - "url": string - } - "MyVpc": { - "type": "sst.aws.Vpc" - } - } -} -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/internal/playground/sst.config.ts b/examples/internal/playground/sst.config.ts deleted file mode 100644 index ae6377cf29..0000000000 --- a/examples/internal/playground/sst.config.ts +++ /dev/null @@ -1,625 +0,0 @@ -/// - -export default $config({ - app(input) { - return { - name: "playground", - removal: input?.stage === "production" ? "retain" : "remove", - home: "aws", - }; - }, - async run() { - const ret: Record> = {}; - - const vpc = addVpc(); - const bucket = addBucket(); - const auth = addAuth(); - const oc = addOpenControl(); - //const queue = addQueue(); - //const efs = addEfs(); - //const email = addEmail(); - //const apiv1 = addApiV1(); - //const apiv2 = addApiV2(); - //const apiws = addApiWebsocket(); - const ssrSite = addSsrSite(); - const staticSite = addStaticSite(); - const router = addRouter(); - //const app = addFunction(); - //const cluster = addCluster(); - //const service = addService(); - //const task = addTask(); - //const postgres = addAuroraPostgres(); - //const postgres = addPostgres(); - //const mysql = addMysql(); - //const redis = addRedis(); - //const cron = addCron(); - //const topic = addTopic(); - //const bus = addBus(); - //const dynamo = addDynamo(); - //addOpenSearch(); - //addStepFunction(); - - return ret; - - function addVpc() { - const vpc = new sst.aws.Vpc("MyVpc", { nat: "ec2" }); - return vpc; - } - - function addBucket() { - const bucket = new sst.aws.Bucket("MyBucket", { - access: "public", - }); - - //const queue = new sst.aws.Queue("MyQueue"); - //queue.subscribe("functions/bucket/index.handler"); - - //const topic = new sst.aws.SnsTopic("MyTopic"); - //topic.subscribe("MyTopicSubscriber", "functions/bucket/index.handler"); - - //bucket.notify({ - // notifications: [ - // { - // name: "LambdaSubscriber", - // function: "functions/bucket/index.handler", - // filterSuffix: ".json", - // events: ["s3:ObjectCreated:*"], - // }, - // { - // name: "QueueSubscriber", - // queue, - // filterSuffix: ".png", - // events: ["s3:ObjectCreated:*"], - // }, - // { - // name: "TopicSubscriber", - // topic, - // filterSuffix: ".csv", - // events: ["s3:ObjectCreated:*"], - // }, - // ], - //}); - ret.bucket = bucket.name; - return bucket; - } - - function addAuth() { - const GOOGLE_CLIENT_ID = new sst.Secret("GOOGLE_CLIENT_ID"); - const GOOGLE_CLIENT_SECRET = new sst.Secret("GOOGLE_CLIENT_SECRET"); - const auth = new sst.aws.Auth("MyAuth", { - domain: "auth.playground.sst.sh", - issuer: { - handler: "functions/auth/index.handler", - link: [GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET], - }, - }); - return auth; - } - - function addOpenControl() { - const oc = new sst.aws.OpenControl("MyOpenControl", { - server: { - handler: "functions/open-control/index.handler", - link: [bucket], - policies: ["arn:aws:iam::aws:policy/ReadOnlyAccess"], - }, - }); - return oc; - } - - function addQueue() { - const queue = new sst.aws.Queue("MyQueue"); - queue.subscribe("functions/queue/index.subscriber"); - - new sst.aws.Function("MyQueuePublisher", { - handler: "functions/queue/index.publisher", - link: [queue], - url: true, - }); - ret.queue = queue.url; - - return queue; - } - - function addEfs() { - const efs = new sst.aws.Efs("MyEfs", { vpc }); - ret.efs = efs.id; - ret.efsAccessPoint = efs.nodes.accessPoint.id; - - const app = new sst.aws.Function("MyEfsApp", { - handler: "functions/efs/index.handler", - volume: { efs }, - url: true, - vpc, - }); - ret.efsApp = app.url; - - return efs; - } - - function addEmail() { - const topic = new sst.aws.SnsTopic("MyTopic"); - topic.subscribe( - "MyTopicSubscriber", - "functions/email/index.notification" - ); - - const email = new sst.aws.Email("MyEmail", { - sender: "wangfanjie@gmail.com", - events: [ - { - name: "notif", - types: ["delivery"], - topic: topic.arn, - }, - ], - }); - - const sender = new sst.aws.Function("MyApi", { - handler: "functions/email/index.sender", - link: [email], - url: true, - }); - - ret.emailSend = sender.url; - ret.email = email.sender; - ret.emailConfig = email.configSet; - return ret; - } - - function addApiV1() { - const api = new sst.aws.ApiGatewayV1("MyApiV1"); - api.route( - "GET /", - { - handler: "functions/apiv2/index.handler", - link: [bucket], - }, - { - apiKey: true, - } - ); - api.deploy(); - const plan = api.addUsagePlan("MyUsagePlan", { - quota: { limit: 1000, period: "day" }, - }); - plan.addApiKey("MyApiKey", { - value: "1234567890123456789012345678901234567890", - }); - - return api; - } - - function addApiV2() { - const api = new sst.aws.ApiGatewayV2("MyApiV2", { - link: [bucket], - }); - const authorizer = api.addAuthorizer({ - name: "MyAuthorizer", - lambda: { - function: "functions/apiv2/index.authorizer", - identitySources: [], - }, - }); - api.route( - "GET /", - { - handler: "functions/apiv2/index.handler", - }, - { - auth: { lambda: authorizer.id }, - } - ); - return api; - } - - function addApiWebsocket() { - const api = new sst.aws.ApiGatewayWebSocket("MyApiWebsocket", {}); - const authorizer = api.addAuthorizer("MyAuthorizer", { - lambda: { - function: "functions/apiws/index.authorizer", - identitySources: ["route.request.querystring.Authorization"], - }, - }); - api.route("$connect", "functions/apiws/index.connect", { - auth: { lambda: authorizer.id }, - }); - api.route("$disconnect", "functions/apiws/index.disconnect"); - api.route("$default", { - handler: "functions/apiws/index.catchAll", - link: [api], - }); - api.route("sendmessage", "functions/apiws/index.sendMessage"); - - return { - managementEndpoint: api.managementEndpoint, - }; - return api; - } - - function addRouter() { - //const rr7 = new sst.aws.React("MyRouterSite", { - // path: "sites/react-router-7-ssr", - // cdn: false, - //}); - //const solid = new sst.aws.SolidStart("MyRouterSolidSite", { - // path: "sites/solid-start", - // link: [bucket], - // cdn: false, - //}); - //const nuxt = new sst.aws.Nuxt("MyRouterNuxtSite", { - // path: "sites/nuxt", - // link: [bucket], - // cdn: false, - //}); - //const svelte = new sst.aws.SvelteKit("MyRouterSvelteSite", { - // path: "sites/svelte-kit", - // link: [bucket], - // cdn: false, - //}); - //const analog = new sst.aws.Analog("MyRouterAnalogSite", { - // path: "sites/analog", - // link: [bucket], - // cdn: false, - //}); - //const remix = new sst.aws.Remix("MyRouterRemixSite", { - // path: "sites/remix", - // link: [bucket], - // cdn: false, - //}); - - const router = new sst.aws.Router("MyRouter", { - domain: { - name: "router.playground.sst.sh", - aliases: ["*.router.playground.sst.sh"], - }, - //routes: { - // "/*": app.url, - //}, - }); - //router.route("/api", app.url, { - //rewrite: { - // regex: "^/api/(.*)$", - // to: "/$1", - //}, - //connectionTimeout: "1 second", - //}); - //router.routeSite("/rr7", rr7); - //router.routeSite("/astro5", astro5); - //router.routeSite("/solid", solid); - //router.routeSite("/nuxt", nuxt); - //router.routeSite("/svelte", svelte); - //router.routeSite("/tan", tanstackStart); - //router.routeSite("/analog", analog); - //router.routeSite("/remix", remix); - //router.routeSite("/vite", vite); - //router.routeSite("/tan", staticSite); - - new sst.aws.Function("MyRouterApp", { - handler: "functions/router/index.handler", - url: { - router: { - instance: router, - domain: "api.router.playground.sst.sh", - }, - }, - }); - - //const vite = new sst.aws.StaticSite("MyRouterVite", { - // path: "sites/vite", - // route: { - // router, - // path: "/vite", - // }, - // build: { - // command: "npm run build", - // output: "dist", - // }, - //}); - - //new sst.aws.Nextjs("MyRouterNextjs", { - // route: { - // router, - // path: "/next", - // }, - // path: "sites/nextjs", - // link: [bucket], - // server: { - // timeout: "50 seconds", - // }, - //}); - - new sst.aws.Astro("MyRouterAstro", { - path: "sites/astro5", - router: { - instance: router, - path: "/astro5", - }, - }); - - //const tanstackStart = new sst.aws.TanStackStart("MyRouterTanStack", { - // path: "sites/tanstack-start", - // route: { - // router, - // }, - //}); - - return router; - } - - function addSsrSite() { - return new sst.aws.Nextjs("MyNextjsSite", { - domain: "ssr.playground.sst.sh", - path: "sites/nextjs", - //path: "sites/astro4", - //path: "sites/astro5", - //path: "sites/astro5-static", - //path: "sites/react-router-7-ssr", - //path: "sites/react-router-7-csr", - //path: "sites/tanstack-start", - - // multi-region - //regions: ["us-east-1", "us-west-1"], - link: [bucket], - //assets: { - // purge: true, - //}, - }); - } - - function addStaticSite() { - new sst.aws.StaticSite("MyStaticSite", { - domain: "static.playground.sst.sh", - path: "sites/vite", - build: { - command: "npm run build", - output: "dist", - }, - }); - } - - function addFunction() { - const app = new sst.aws.Function("MyApp", { - handler: "functions/handler-example/index.handler", - link: [bucket], - url: true, - }); - ret.app = app.url; - return app; - } - - function addCluster() { - return new sst.aws.Cluster("MyCluster", { - vpc, - }); - } - - function addService() { - const service = new sst.aws.Service("MyService", { - cluster, - loadBalancer: { - rules: [ - { - listen: "80/http", - //container: "app", - }, - //{ listen: "80/http", container: "web" }, - //{ listen: "8080/http", container: "sidecar" }, - ], - }, - image: { - context: "images/web", - }, - //containers: [ - // { - // name: "web", - // image: { - // context: "images/web", - // }, - // cpu: "0.125 vCPU", - // memory: "0.25 GB", - // }, - // { - // name: "sidecar", - // image: { - // context: "images/sidecar", - // }, - // cpu: "0.125 vCPU", - // memory: "0.25 GB", - // }, - //], - link: [bucket], - }); - ret.service = service.service; - return service; - } - - function addTask() { - const task = new sst.aws.Task("MyTask", { - cluster, - image: { - context: "images/task", - }, - link: [bucket], - }); - - new sst.aws.Function("MyTaskApp", { - handler: "functions/task/index.handler", - url: true, - vpc, - link: [task], - }); - - //new sst.aws.Cron("MyTaskCron", { - // schedule: "rate(1 minute)", - // task, - //}); - - return task; - } - - function addAuroraPostgres() { - const postgres = new sst.aws.Aurora("MyPostgres", { - engine: "postgres", - vpc, - }); - new sst.aws.Function("MyPostgresApp", { - handler: "functions/postgres/index.handler", - url: true, - link: [postgres], - vpc, - }); - ret.pgHost = postgres.host; - ret.pgPort = $interpolate`${postgres.port}`; - ret.pgUsername = postgres.username; - ret.pgPassword = postgres.password; - ret.pgDatabase = postgres.database; - return postgres; - } - - function addPostgres() { - const postgres = new sst.aws.Postgres("MyPostgres", { - vpc, - }); - new sst.aws.Function("MyPostgresApp", { - handler: "functions/postgres/index.handler", - url: true, - vpc, - link: [postgres], - }); - ret.pgHost = postgres.host; - ret.pgPort = $interpolate`${postgres.port}`; - ret.pgUsername = postgres.username; - ret.pgPassword = postgres.password; - return postgres; - } - - function addMysql() { - const mysql = new sst.aws.Mysql("MyMysql", { - vpc, - dev: { - username: "root", - password: "password", - database: "local", - port: 3306, - }, - }); - new sst.aws.Function("MyMysqlApp", { - handler: "functions/mysql/index.handler", - url: true, - vpc, - link: [mysql], - }); - ret.mysqlHost = mysql.host; - ret.mysqlPort = $interpolate`${mysql.port}`; - ret.mysqlUsername = mysql.username; - ret.mysqlPassword = mysql.password; - return mysql; - } - - function addRedis() { - const redis = new sst.aws.Redis("MyRedis", { - vpc, - parameters: { - "maxmemory-policy": "noeviction", - }, - //cluster: false, - }); - ret.redisHost = redis.host; - const app = new sst.aws.Function("MyRedisApp", { - handler: "functions/redis/cluster-index.handler", - //handler: "functions/redis/instance-index.handler", - url: true, - vpc, - link: [redis], - }); - return redis; - } - - function addCron() { - const cron = new sst.aws.Cron("MyCron", { - schedule: "rate(1 minute)", - function: { - handler: "functions/cron/index.handler", - link: [bucket], - }, - event: { foo: "bar" }, - }); - ret.cron = cron.nodes.function.name; - return cron; - } - - function addTopic() { - const topic = new sst.aws.SnsTopic("MyTopic"); - topic.subscribe("MyTopicSubscriber", "functions/topic/index.subscriber"); - - new sst.aws.Function("MyTopicPublisher", { - handler: "functions/topic/index.publisher", - link: [topic], - url: true, - }); - - return topic; - } - - function addBus() { - const bus = new sst.aws.Bus("MyBus"); - bus.subscribe("functions/bus/index.subscriber", { - pattern: { - source: ["app.myevent"], - }, - }); - bus.subscribeQueue("test", queue); - - new sst.aws.Function("MyBusPublisher", { - handler: "functions/bus/index.publisher", - link: [bus], - url: true, - }); - - return bus; - } - - function addDynamo() { - new sst.aws.Dynamo("MyTable", { - fields: { - userId: "string", - noteId: "string", - createdAt: "string", - }, - primaryIndex: { hashKey: "userId", rangeKey: "noteId" }, - globalIndexes: { - CreatedAtIndex: { hashKey: "userId", rangeKey: "createdAt" }, - CreatedAtIndex2: { hashKey: "userId", rangeKey: "createdAt" }, - }, - }); - } - - function addOpenSearch() { - const os = new sst.aws.OpenSearch("MyOpenSearch"); - new sst.aws.Function("MyOpenSearchApp", { - handler: "functions/open-search/index.handler", - url: true, - link: [os], - }); - ret.osUrl = os.url; - ret.osUsername = os.username; - ret.osPassword = os.password; - return os; - } - - function addStepFunction() { - const runTask = sst.aws.StepFunctions.ecsRunTask({ - name: "MyTask", - task, - environment: { - FOO: "hello", - }, - }); - const stepFunction = new sst.aws.StepFunctions("MyStepFunction", { - definition: runTask, - }); - return stepFunction; - } - }, -}); diff --git a/examples/internal/streaming-problem/.gitignore b/examples/internal/streaming-problem/.gitignore deleted file mode 100644 index fa2aea8208..0000000000 --- a/examples/internal/streaming-problem/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.sst diff --git a/examples/internal/streaming-problem/package.json b/examples/internal/streaming-problem/package.json deleted file mode 100644 index 8e95864c03..0000000000 --- a/examples/internal/streaming-problem/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "type": "module", - "dependencies": { - "sst": "^3.0.2" - } -} diff --git a/examples/internal/streaming-problem/src/streaming.ts b/examples/internal/streaming-problem/src/streaming.ts deleted file mode 100644 index 773fc1f8f5..0000000000 --- a/examples/internal/streaming-problem/src/streaming.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { APIGatewayProxyEventV2 } from "aws-lambda"; - -export const handler = awslambda.streamifyResponse( - async (evt: APIGatewayProxyEventV2, responseStream) => { - const httpResponseMetadata = { - statusCode: 200, - headers: { - "Transfer-Encoding": "chunked", - }, - }; - const chunkSize = parseInt(evt.queryStringParameters?.chunkSize || "1"); - const delay = parseInt(evt.queryStringParameters?.delay || "100"); - const writer = awslambda.HttpResponseStream.from( - responseStream, - httpResponseMetadata, - ); - const data = new Uint8Array(Buffer.from("0".repeat(chunkSize * 10))); - for (let i = 0; i < data.length; i += chunkSize) { - const chunk = data.subarray(i, i + chunkSize); - writer.write(chunk, "utf8"); - await new Promise((r) => setTimeout(r, delay)); - } - writer.end(); - }, -); diff --git a/examples/internal/streaming-problem/sst-env.d.ts b/examples/internal/streaming-problem/sst-env.d.ts deleted file mode 100644 index e556a36a76..0000000000 --- a/examples/internal/streaming-problem/sst-env.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - MyFunction: { - name: string - type: "sst.aws.Function" - url: string - } - } -} -export {} \ No newline at end of file diff --git a/examples/internal/streaming-problem/sst.config.ts b/examples/internal/streaming-problem/sst.config.ts deleted file mode 100644 index 83d21a0f86..0000000000 --- a/examples/internal/streaming-problem/sst.config.ts +++ /dev/null @@ -1,23 +0,0 @@ -/// - -export default $config({ - app(input) { - return { - name: "streaming-problem", - removal: input?.stage === "production" ? "retain" : "remove", - home: "aws", - }; - }, - async run() { - const fn = new sst.aws.Function("MyFunction", { - handler: "./src/streaming.handler", - streaming: true, - url: true, - timeout: "15 minutes", - }); - - return { - url: fn.url, - }; - }, -}); diff --git a/examples/internal/streaming-problem/tsconfig.json b/examples/internal/streaming-problem/tsconfig.json deleted file mode 100644 index 0967ef424b..0000000000 --- a/examples/internal/streaming-problem/tsconfig.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/examples/python-layouts/flat-layout/handler.py b/examples/python-layouts/flat-layout/handler.py new file mode 100644 index 0000000000..ac7b5092f8 --- /dev/null +++ b/examples/python-layouts/flat-layout/handler.py @@ -0,0 +1,18 @@ +from utils import json_response + + +def main(event, context): + return json_response( + { + "layout": "flat", + "message": "Hello from the project root", + } + ) + + +def worker(event, context): + return { + "layout": "flat", + "job": event.get("job", "send-email"), + "status": "completed", + } diff --git a/examples/python-layouts/flat-layout/pyproject.toml b/examples/python-layouts/flat-layout/pyproject.toml new file mode 100644 index 0000000000..95b54894b2 --- /dev/null +++ b/examples/python-layouts/flat-layout/pyproject.toml @@ -0,0 +1,29 @@ +[project] +name = "flat-example" +version = "0.1.0" +description = "SST Python flat layout example" +requires-python = ">=3.11" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +include = [ + "handler.py", + "utils.py", +] + +[tool.hatch.build.targets.sdist] +include = [ + "handler.py", + "utils.py", + "pyproject.toml", +] +exclude = [ + ".sst/", + "node_modules/", + "dist/", + ".venv/", + "__pycache__/", +] diff --git a/examples/python-layouts/flat-layout/sst.config.ts b/examples/python-layouts/flat-layout/sst.config.ts new file mode 100644 index 0000000000..c62bfa0402 --- /dev/null +++ b/examples/python-layouts/flat-layout/sst.config.ts @@ -0,0 +1,53 @@ +/// + +/** + * ## Python Flat Layout + * + * The smallest Python layout SST supports. The handler lives at the project + * root alongside `pyproject.toml`. + * + * ```txt + * β”œβ”€β”€ sst.config.ts + * β”œβ”€β”€ pyproject.toml + * β”œβ”€β”€ handler.py + * └── utils.py + * ``` + * + * Point the handler directly at a root file. + * + * ```ts title="sst.config.ts" + * new sst.aws.Function("ApiFunction", { + * handler: "handler.main", + * runtime: "python3.11", + * url: true, + * }); + * ``` + * + * Multiple functions can still share the same root files. + */ +export default $config({ + app(input) { + return { + name: "flat-example", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws" + }; + }, + async run() { + const api = new sst.aws.Function("ApiFunction", { + handler: "handler.main", + runtime: "python3.11", + url: true, + }); + + const worker = new sst.aws.Function("WorkerFunction", { + handler: "handler.worker", + runtime: "python3.11", + }); + + return { + api: api.url, + worker: worker.name, + }; + } +}); diff --git a/examples/python-layouts/flat-layout/utils.py b/examples/python-layouts/flat-layout/utils.py new file mode 100644 index 0000000000..ddb263854f --- /dev/null +++ b/examples/python-layouts/flat-layout/utils.py @@ -0,0 +1,9 @@ +import json + + +def json_response(body, status_code=200): + return { + "statusCode": status_code, + "headers": {"Content-Type": "application/json"}, + "body": json.dumps(body), + } diff --git a/examples/python-layouts/monorepo-layout/pyproject.toml b/examples/python-layouts/monorepo-layout/pyproject.toml new file mode 100644 index 0000000000..9a360d9d63 --- /dev/null +++ b/examples/python-layouts/monorepo-layout/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "monorepo-example" +version = "0.1.0" +description = "SST Python monorepo layout example" +requires-python = ">=3.12" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["services", "shared"] + +[tool.hatch.build.targets.sdist] +packages = ["services", "shared"] + +[tool.uv.workspace] +members = ["services/*", "shared"] diff --git a/examples/python-layouts/monorepo-layout/services/api/handler.py b/examples/python-layouts/monorepo-layout/services/api/handler.py new file mode 100644 index 0000000000..698937bc1f --- /dev/null +++ b/examples/python-layouts/monorepo-layout/services/api/handler.py @@ -0,0 +1,5 @@ +from shared.utils import api_response + + +def main(event, context): + return api_response("api") diff --git a/examples/python-layouts/monorepo-layout/services/api/pyproject.toml b/examples/python-layouts/monorepo-layout/services/api/pyproject.toml new file mode 100644 index 0000000000..db15c0ce14 --- /dev/null +++ b/examples/python-layouts/monorepo-layout/services/api/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "api-service" +version = "0.1.0" +description = "API service" +requires-python = ">=3.12" +dependencies = ["shared"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["."] + +[tool.uv.sources] +shared = { workspace = true } diff --git a/examples/python-layouts/monorepo-layout/services/worker/handler.py b/examples/python-layouts/monorepo-layout/services/worker/handler.py new file mode 100644 index 0000000000..51f4467503 --- /dev/null +++ b/examples/python-layouts/monorepo-layout/services/worker/handler.py @@ -0,0 +1,5 @@ +from shared.utils import worker_result + + +def main(event, context): + return worker_result(event.get("job", "daily-report")) diff --git a/examples/python-layouts/monorepo-layout/services/worker/pyproject.toml b/examples/python-layouts/monorepo-layout/services/worker/pyproject.toml new file mode 100644 index 0000000000..6b0c642a60 --- /dev/null +++ b/examples/python-layouts/monorepo-layout/services/worker/pyproject.toml @@ -0,0 +1,19 @@ +[project] +name = "worker-service" +version = "0.1.0" +description = "Worker service" +requires-python = ">=3.12" +dependencies = [ + "shared", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["."] + +# Use workspace = true to reference the shared package from the UV workspace +[tool.uv.sources] +shared = { workspace = true } diff --git a/examples/python-layouts/monorepo-layout/shared/pyproject.toml b/examples/python-layouts/monorepo-layout/shared/pyproject.toml new file mode 100644 index 0000000000..9281af9f16 --- /dev/null +++ b/examples/python-layouts/monorepo-layout/shared/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "shared" +version = "0.1.0" +description = "Shared utilities" +requires-python = ">=3.11" +dependencies = [] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["shared"] diff --git a/examples/python-layouts/monorepo-layout/shared/shared/__init__.py b/examples/python-layouts/monorepo-layout/shared/shared/__init__.py new file mode 100644 index 0000000000..d2cb68d898 --- /dev/null +++ b/examples/python-layouts/monorepo-layout/shared/shared/__init__.py @@ -0,0 +1 @@ +"""Shared utilities for the monorepo.""" \ No newline at end of file diff --git a/examples/python-layouts/monorepo-layout/shared/shared/utils.py b/examples/python-layouts/monorepo-layout/shared/shared/utils.py new file mode 100644 index 0000000000..7ff7ed5abb --- /dev/null +++ b/examples/python-layouts/monorepo-layout/shared/shared/utils.py @@ -0,0 +1,13 @@ +import json + + +def api_response(service): + return { + "statusCode": 200, + "headers": {"Content-Type": "application/json"}, + "body": json.dumps({"layout": "monorepo", "service": service}), + } + + +def worker_result(job): + return {"layout": "monorepo", "job": job, "status": "completed"} diff --git a/examples/python-layouts/monorepo-layout/sst.config.ts b/examples/python-layouts/monorepo-layout/sst.config.ts new file mode 100644 index 0000000000..c5e98f1bd2 --- /dev/null +++ b/examples/python-layouts/monorepo-layout/sst.config.ts @@ -0,0 +1,60 @@ +/// + +/** + * ## Python Monorepo Layout + * + * Put each service in its own workspace package and share code from a separate + * package. + * + * ```txt + * β”œβ”€β”€ sst.config.ts + * β”œβ”€β”€ pyproject.toml + * β”œβ”€β”€ shared + * β”‚ β”œβ”€β”€ pyproject.toml + * β”‚ └── shared + * β”‚ β”œβ”€β”€ __init__.py + * β”‚ └── utils.py + * └── services + * β”œβ”€β”€ api + * β”‚ β”œβ”€β”€ pyproject.toml + * β”‚ └── handler.py + * └── worker + * β”œβ”€β”€ pyproject.toml + * └── handler.py + * ``` + * + * Each service has its own `pyproject.toml` and points at the shared workspace + * package with `workspace = true`. + * + * ```toml title="services/api/pyproject.toml" + * [tool.uv.sources] + * shared = { workspace = true } + * ``` + */ +export default $config({ + app(input) { + return { + name: "monorepo-example", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + }; + }, + async run() { + const api = new sst.aws.Function("ApiService", { + handler: "services/api/handler.main", + runtime: "python3.12", + url: true, + }); + + const worker = new sst.aws.Function("WorkerService", { + handler: "services/worker/handler.main", + runtime: "python3.12", + timeout: "5 minutes", + }); + + return { + api: api.url, + worker: worker.name, + }; + }, +}); diff --git a/examples/python-layouts/nested-layout/app/data/config.json b/examples/python-layouts/nested-layout/app/data/config.json new file mode 100644 index 0000000000..842b8f6922 --- /dev/null +++ b/examples/python-layouts/nested-layout/app/data/config.json @@ -0,0 +1,4 @@ +{ + "app_name": "nested-example", + "feature_flag": true +} diff --git a/examples/python-layouts/nested-layout/app/functions/api/handler.py b/examples/python-layouts/nested-layout/app/functions/api/handler.py new file mode 100644 index 0000000000..450a67471d --- /dev/null +++ b/examples/python-layouts/nested-layout/app/functions/api/handler.py @@ -0,0 +1,18 @@ +import json +from pathlib import Path + +from shared.utils import json_response + + +CONFIG_PATH = Path(__file__).parents[2] / "data" / "config.json" + + +def main(event, context): + config = json.loads(CONFIG_PATH.read_text()) + return json_response( + { + "layout": "nested", + "app": config["app_name"], + "feature_flag": config["feature_flag"], + } + ) diff --git a/examples/python-layouts/nested-layout/app/functions/auth/handler.py b/examples/python-layouts/nested-layout/app/functions/auth/handler.py new file mode 100644 index 0000000000..be44ccd54a --- /dev/null +++ b/examples/python-layouts/nested-layout/app/functions/auth/handler.py @@ -0,0 +1,5 @@ +from shared.utils import json_response + + +def main(event, context): + return json_response({"layout": "nested", "service": "auth"}) diff --git a/examples/python-layouts/nested-layout/app/functions/worker/handler.py b/examples/python-layouts/nested-layout/app/functions/worker/handler.py new file mode 100644 index 0000000000..7585956098 --- /dev/null +++ b/examples/python-layouts/nested-layout/app/functions/worker/handler.py @@ -0,0 +1,6 @@ +def main(event, context): + return { + "layout": "nested", + "job": event.get("job", "sync-users"), + "status": "completed", + } diff --git a/examples/python-layouts/nested-layout/pyproject.toml b/examples/python-layouts/nested-layout/pyproject.toml new file mode 100644 index 0000000000..b39a5dcef8 --- /dev/null +++ b/examples/python-layouts/nested-layout/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "nested-example" +version = "0.1.0" +description = "SST Python nested layout example" +requires-python = ">=3.11" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["app", "shared"] + +[tool.hatch.build.targets.sdist] +packages = ["app", "shared"] +include = ["pyproject.toml"] diff --git a/examples/python-layouts/nested-layout/shared/utils.py b/examples/python-layouts/nested-layout/shared/utils.py new file mode 100644 index 0000000000..ddb263854f --- /dev/null +++ b/examples/python-layouts/nested-layout/shared/utils.py @@ -0,0 +1,9 @@ +import json + + +def json_response(body, status_code=200): + return { + "statusCode": status_code, + "headers": {"Content-Type": "application/json"}, + "body": json.dumps(body), + } diff --git a/examples/python-layouts/nested-layout/sst.config.ts b/examples/python-layouts/nested-layout/sst.config.ts new file mode 100644 index 0000000000..4d954e28f5 --- /dev/null +++ b/examples/python-layouts/nested-layout/sst.config.ts @@ -0,0 +1,76 @@ +/// + +/** + * ## Python Nested Layout + * + * Keep handlers in nested folders while sharing one `pyproject.toml` at the + * project root. + * + * ```txt + * β”œβ”€β”€ sst.config.ts + * β”œβ”€β”€ pyproject.toml + * β”œβ”€β”€ shared + * β”‚ └── utils.py + * └── app + * β”œβ”€β”€ data + * β”‚ └── config.json + * └── functions + * β”œβ”€β”€ api + * β”‚ └── handler.py + * β”œβ”€β”€ auth + * β”‚ └── handler.py + * └── worker + * └── handler.py + * ``` + * + * SST walks up from the handler path to find the nearest `pyproject.toml`. + * + * ```ts title="sst.config.ts" + * new sst.aws.Function("Api", { + * handler: "app/functions/api/handler.main", + * runtime: "python3.11", + * url: true, + * }); + * ``` + * + * Nested handlers can still read static files relative to `__file__`. + * + * ```py title="app/functions/api/handler.py" + * from pathlib import Path + * config = Path(__file__).parents[2] / "data" / "config.json" + * ``` + */ +export default $config({ + app(input) { + return { + name: "nested-example", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + }; + }, + async run() { + const api = new sst.aws.Function("Api", { + handler: "app/functions/api/handler.main", + runtime: "python3.11", + url: true, + }); + + const worker = new sst.aws.Function("Worker", { + handler: "app/functions/worker/handler.main", + runtime: "python3.11", + timeout: "5 minutes", + }); + + const auth = new sst.aws.Function("Auth", { + handler: "app/functions/auth/handler.main", + runtime: "python3.11", + url: true, + }); + + return { + api: api.url, + worker: worker.name, + auth: auth.url, + }; + }, +}); diff --git a/examples/python-layouts/workspace-layout/pyproject.toml b/examples/python-layouts/workspace-layout/pyproject.toml new file mode 100644 index 0000000000..e26536d898 --- /dev/null +++ b/examples/python-layouts/workspace-layout/pyproject.toml @@ -0,0 +1,19 @@ +[project] +name = "workspace-example" +version = "0.1.0" +description = "SST Python workspace layout example" +requires-python = ">=3.11" + +[tool.uv] +package = true + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/mypackage"] + +[tool.hatch.build.targets.sdist] +packages = ["src/mypackage"] +include = ["pyproject.toml"] diff --git a/examples/python-layouts/workspace-layout/src/mypackage/__init__.py b/examples/python-layouts/workspace-layout/src/mypackage/__init__.py new file mode 100644 index 0000000000..36b39b668a --- /dev/null +++ b/examples/python-layouts/workspace-layout/src/mypackage/__init__.py @@ -0,0 +1 @@ +"""Workspace layout example package.""" diff --git a/examples/python-layouts/workspace-layout/src/mypackage/handler.py b/examples/python-layouts/workspace-layout/src/mypackage/handler.py new file mode 100644 index 0000000000..b1c8fbbb4f --- /dev/null +++ b/examples/python-layouts/workspace-layout/src/mypackage/handler.py @@ -0,0 +1,18 @@ +from mypackage.utils import json_response + + +def api_handler(event, context): + return json_response( + { + "layout": "workspace", + "package": "mypackage", + } + ) + + +def worker_handler(event, context): + return { + "layout": "workspace", + "job": event.get("job", "resize-image"), + "status": "completed", + } diff --git a/examples/python-layouts/workspace-layout/src/mypackage/utils.py b/examples/python-layouts/workspace-layout/src/mypackage/utils.py new file mode 100644 index 0000000000..ddb263854f --- /dev/null +++ b/examples/python-layouts/workspace-layout/src/mypackage/utils.py @@ -0,0 +1,9 @@ +import json + + +def json_response(body, status_code=200): + return { + "statusCode": status_code, + "headers": {"Content-Type": "application/json"}, + "body": json.dumps(body), + } diff --git a/examples/python-layouts/workspace-layout/sst.config.ts b/examples/python-layouts/workspace-layout/sst.config.ts new file mode 100644 index 0000000000..b070ecc449 --- /dev/null +++ b/examples/python-layouts/workspace-layout/sst.config.ts @@ -0,0 +1,62 @@ +/// + +/** + * ## Python Workspace Layout + * + * Use the standard `src/` layout for a single Python package. + * + * ```txt + * β”œβ”€β”€ sst.config.ts + * β”œβ”€β”€ pyproject.toml + * └── src + * └── mypackage + * β”œβ”€β”€ __init__.py + * β”œβ”€β”€ handler.py + * └── utils.py + * ``` + * + * Different functions in the same module can still become separate Lambdas. + * + * ```ts title="sst.config.ts" + * new sst.aws.Function("Api", { + * handler: "src/mypackage/handler.api_handler", + * runtime: "python3.11", + * url: true, + * }); + * + * new sst.aws.Function("Worker", { + * handler: "src/mypackage/handler.worker_handler", + * runtime: "python3.11", + * }); + * ``` + * + * Both handlers share the same package and imports. + */ +export default $config({ + app(input) { + return { + name: "workspace-example", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + }; + }, + async run() { + const api = new sst.aws.Function("Api", { + handler: "src/mypackage/handler.api_handler", + runtime: "python3.11", + timeout: "30 seconds", + url: true, + }); + + const worker = new sst.aws.Function("Worker", { + handler: "src/mypackage/handler.worker_handler", + runtime: "python3.11", + timeout: "5 minutes", + }); + + return { + api: api.url, + worker: worker.name, + }; + }, +}); diff --git a/examples/python-modern-uv/handler.py b/examples/python-modern-uv/handler.py new file mode 100644 index 0000000000..7fd8471a1a --- /dev/null +++ b/examples/python-modern-uv/handler.py @@ -0,0 +1,8 @@ +from myapp import utils + + +def lambda_handler(event, context): + return { + "statusCode": 200, + "body": utils.greeting("root package"), + } diff --git a/examples/python-modern-uv/packages/api/pyproject.toml b/examples/python-modern-uv/packages/api/pyproject.toml new file mode 100644 index 0000000000..b1117ce057 --- /dev/null +++ b/examples/python-modern-uv/packages/api/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "api" +version = "0.1.0" +description = "API package with uv package mode" +dependencies = ["shared"] +requires-python = ">=3.11" + +[tool.uv] +package = true + +[tool.uv.sources] +shared = { workspace = true } + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/examples/python-modern-uv/packages/api/src/api/__init__.py b/examples/python-modern-uv/packages/api/src/api/__init__.py new file mode 100644 index 0000000000..b2b744fbac --- /dev/null +++ b/examples/python-modern-uv/packages/api/src/api/__init__.py @@ -0,0 +1,2 @@ +"""API package""" +__version__ = "0.1.0" diff --git a/examples/python-modern-uv/packages/api/src/api/handler.py b/examples/python-modern-uv/packages/api/src/api/handler.py new file mode 100644 index 0000000000..7bdc4b7461 --- /dev/null +++ b/examples/python-modern-uv/packages/api/src/api/handler.py @@ -0,0 +1,10 @@ +import json + +from shared import models + + +def lambda_handler(event, context): + return { + "statusCode": 200, + "body": json.dumps(models.response("api package")), + } diff --git a/examples/python-modern-uv/packages/shared/pyproject.toml b/examples/python-modern-uv/packages/shared/pyproject.toml new file mode 100644 index 0000000000..6b972de0e8 --- /dev/null +++ b/examples/python-modern-uv/packages/shared/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "shared" +version = "0.1.0" +description = "Shared models used by api and worker" +dependencies = [] +requires-python = ">=3.11" + +[tool.uv] +package = true + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/examples/python-modern-uv/packages/shared/src/shared/__init__.py b/examples/python-modern-uv/packages/shared/src/shared/__init__.py new file mode 100644 index 0000000000..c471f2571a --- /dev/null +++ b/examples/python-modern-uv/packages/shared/src/shared/__init__.py @@ -0,0 +1,2 @@ +"""Shared package""" +__version__ = "0.1.0" diff --git a/examples/python-modern-uv/packages/shared/src/shared/models.py b/examples/python-modern-uv/packages/shared/src/shared/models.py new file mode 100644 index 0000000000..d188887352 --- /dev/null +++ b/examples/python-modern-uv/packages/shared/src/shared/models.py @@ -0,0 +1,5 @@ +"""Shared helpers used by workspace members.""" + + +def response(source: str) -> dict: + return {"layout": "modern-uv", "source": source} diff --git a/examples/python-modern-uv/packages/worker/pyproject.toml b/examples/python-modern-uv/packages/worker/pyproject.toml new file mode 100644 index 0000000000..a83ef6a201 --- /dev/null +++ b/examples/python-modern-uv/packages/worker/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "worker" +version = "0.1.0" +description = "Worker package that uses shared models" +dependencies = ["shared", "arrow>=1.3.0"] +requires-python = ">=3.11" + +[tool.uv] +package = true + +[tool.uv.sources] +shared = { workspace = true } + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/examples/python-modern-uv/packages/worker/src/worker/__init__.py b/examples/python-modern-uv/packages/worker/src/worker/__init__.py new file mode 100644 index 0000000000..f2233b600a --- /dev/null +++ b/examples/python-modern-uv/packages/worker/src/worker/__init__.py @@ -0,0 +1,2 @@ +"""Worker package""" +__version__ = "0.1.0" diff --git a/examples/python-modern-uv/packages/worker/src/worker/handler.py b/examples/python-modern-uv/packages/worker/src/worker/handler.py new file mode 100644 index 0000000000..9dbef0a24b --- /dev/null +++ b/examples/python-modern-uv/packages/worker/src/worker/handler.py @@ -0,0 +1,14 @@ +import json + +import arrow +from shared import models + + +def lambda_handler(event, context): + body = models.response("worker package") + body["timestamp"] = arrow.utcnow().isoformat() + + return { + "statusCode": 200, + "body": json.dumps(body), + } diff --git a/examples/python-modern-uv/pyproject.toml b/examples/python-modern-uv/pyproject.toml new file mode 100644 index 0000000000..a67f8310fc --- /dev/null +++ b/examples/python-modern-uv/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "python-modern-uv" +version = "0.1.0" +description = "SST Python uv workspace example" +requires-python = ">=3.11" + +[tool.uv] +package = true + +[tool.uv.workspace] +members = ["packages/*"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/myapp"] diff --git a/examples/python-modern-uv/src/myapp/__init__.py b/examples/python-modern-uv/src/myapp/__init__.py new file mode 100644 index 0000000000..40cbc790ed --- /dev/null +++ b/examples/python-modern-uv/src/myapp/__init__.py @@ -0,0 +1,2 @@ +"""Main application package""" +__version__ = "0.1.0" diff --git a/examples/python-modern-uv/src/myapp/utils.py b/examples/python-modern-uv/src/myapp/utils.py new file mode 100644 index 0000000000..5c9bf4a8d6 --- /dev/null +++ b/examples/python-modern-uv/src/myapp/utils.py @@ -0,0 +1,5 @@ +"""Helpers for the root package.""" + + +def greeting(source: str) -> str: + return f"Hello from {source}" diff --git a/examples/python-modern-uv/sst.config.ts b/examples/python-modern-uv/sst.config.ts new file mode 100644 index 0000000000..3d7b1acd47 --- /dev/null +++ b/examples/python-modern-uv/sst.config.ts @@ -0,0 +1,94 @@ +/// + +/** + * ## AWS Python uv Workspaces + * + * Deploy Python Lambda functions from a + * [uv workspace](https://docs.astral.sh/uv/concepts/projects/workspaces/) that + * uses `package = true` and a `src/` layout. + * + * SST traverses up from the handler path to find the nearest `pyproject.toml`. + * + * ```txt + * β”œβ”€β”€ sst.config.ts + * β”œβ”€β”€ pyproject.toml + * β”œβ”€β”€ handler.py + * β”œβ”€β”€ src + * β”‚ └── myapp + * β”‚ β”œβ”€β”€ __init__.py + * β”‚ └── utils.py + * └── packages + * β”œβ”€β”€ api + * β”‚ β”œβ”€β”€ pyproject.toml + * β”‚ └── src/api + * β”‚ β”œβ”€β”€ __init__.py + * β”‚ └── handler.py + * β”œβ”€β”€ shared + * β”‚ β”œβ”€β”€ pyproject.toml + * β”‚ └── src/shared + * β”‚ β”œβ”€β”€ __init__.py + * β”‚ └── models.py + * └── worker + * β”œβ”€β”€ pyproject.toml + * └── src/worker + * β”œβ”€β”€ __init__.py + * └── handler.py + * ``` + * + * With `package = true`, the package is importable by name instead of `src.*`. + * + * ```toml title="pyproject.toml" + * [tool.hatch.build.targets.wheel] + * packages = ["src/myapp"] + * ``` + * + * Then import it normally in your handler. + * + * ```py title="handler.py" + * from myapp import utils + * ``` + * + * Each workspace member can become its own function. + * + * ```ts title="sst.config.ts" + * new sst.aws.Function("PackageHandler", { + * handler: "packages/api/src/api/handler.lambda_handler", + * runtime: "python3.11", + * url: true, + * }); + * ``` + */ +export default $config({ + app(input) { + return { + name: "python-modern-uv", + removal: input?.stage === "production" ? "retain" : "remove", + home: "aws", + }; + }, + async run() { + const rootHandler = new sst.aws.Function("RootHandler", { + handler: "handler.lambda_handler", + runtime: "python3.11", + url: true, + }); + + const packageHandler = new sst.aws.Function("PackageHandler", { + handler: "packages/api/src/api/handler.lambda_handler", + runtime: "python3.11", + url: true, + }); + + const workspaceHandler = new sst.aws.Function("WorkspaceHandler", { + handler: "packages/worker/src/worker/handler.lambda_handler", + runtime: "python3.11", + url: true, + }); + + return { + rootHandler: rootHandler.url, + packageHandler: packageHandler.url, + workspaceHandler: workspaceHandler.url, + }; + }, +}); diff --git a/examples/scrap/.gitignore b/examples/scrap/.gitignore deleted file mode 100644 index c84fd38455..0000000000 --- a/examples/scrap/.gitignore +++ /dev/null @@ -1,35 +0,0 @@ -# prod -dist/ -lambda.zip - -# dev -.yarn/ -!.yarn/releases -.vscode/* -!.vscode/launch.json -!.vscode/*.code-snippets -.idea/workspace.xml -.idea/usage.statistics.xml -.idea/shelf - -# deps -node_modules/ - -# env -.env -.env.production - -# logs -logs/ -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -# misc -.DS_Store - -# sst -.sst diff --git a/examples/scrap/README.md b/examples/scrap/README.md deleted file mode 100644 index 5bb2accb27..0000000000 --- a/examples/scrap/README.md +++ /dev/null @@ -1,4 +0,0 @@ -``` -npm install -npm run deploy -``` diff --git a/examples/scrap/package.json b/examples/scrap/package.json deleted file mode 100644 index 8f59212bd3..0000000000 --- a/examples/scrap/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "scrap", - "type": "module", - "scripts": { - "build": "esbuild --bundle --outfile=./dist/index.js --platform=node --target=node20 ./src/index.ts", - "deploy": "run-s build zip update", - "update": "aws lambda update-function-code --zip-file fileb://lambda.zip --function-name hello", - "zip": "zip -j lambda.zip dist/index.js" - }, - "devDependencies": { - "@types/aws-lambda": "8.10.146", - "esbuild": "^0.21.4", - "npm-run-all2": "^6.2.0" - }, - "dependencies": { - "@aws-sdk/client-s3": "^3.701.0", - "@aws-sdk/s3-request-presigner": "^3.701.0", - "hono": "^4.6.12", - "sst": "^3" - } -} diff --git a/examples/scrap/sst-env.d.ts b/examples/scrap/sst-env.d.ts deleted file mode 100644 index 4c22b567e3..0000000000 --- a/examples/scrap/sst-env.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* This file is auto-generated by SST. Do not edit. */ -/* tslint:disable */ -/* eslint-disable */ -/* deno-fmt-ignore-file */ - -declare module "sst" { - export interface Resource { - "Hono": { - "name": string - "type": "sst.aws.Function" - "url": string - } - "MyBucket": { - "name": string - "type": "sst.aws.Bucket" - } - "Router": { - "type": "sst.aws.Router" - "url": string - } - } -} -/// - -import "sst" -export {} \ No newline at end of file diff --git a/examples/scrap/sst.config.ts b/examples/scrap/sst.config.ts deleted file mode 100644 index fbd0251b25..0000000000 --- a/examples/scrap/sst.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -/// -export default $config({ - app(input) { - return { - name: "scrap", - removal: input?.stage === "production" ? "retain" : "remove", - home: "aws", - providers: { command: "1.0.2" }, - }; - }, - async run() { - const bucket = new sst.aws.Bucket("MyBucket"); - new sst.aws.Function("Hono", { - url: true, - link: [bucket], - handler: "src/index.handler", - }); - // new sst.aws.Router("Router"); - }, -}); diff --git a/examples/scrap/tsconfig.json b/examples/scrap/tsconfig.json deleted file mode 100644 index 667b7e7e6d..0000000000 --- a/examples/scrap/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "strict": true, - "skipLibCheck": true, - "types": [ - "node" - ], - "jsx": "react-jsx", - "jsxImportSource": "hono/jsx", - } -} \ No newline at end of file diff --git a/examples/secret-link-all/package.json b/examples/secret-link-all/package.json index fac7dd2b04..753111e951 100644 --- a/examples/secret-link-all/package.json +++ b/examples/secret-link-all/package.json @@ -10,7 +10,7 @@ "author": "", "license": "ISC", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" }, "devDependencies": { "@types/aws-lambda": "8.10.137" diff --git a/examples/secret-link-all/sst-env.d.ts b/examples/secret-link-all/sst-env.d.ts deleted file mode 100644 index 4ec010d045..0000000000 --- a/examples/secret-link-all/sst-env.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -import "sst" -declare module "sst" { - export interface Resource { - MyBucket: { - name: string - type: "sst.aws.Bucket" - } - Secret1: { - type: "sst.sst.Secret" - value: string - } - Secret2: { - type: "sst.sst.Secret" - value: string - } - } -} -export {} \ No newline at end of file diff --git a/examples/sst-transform/package.json b/examples/sst-transform/package.json index cc80971ea7..ec03e9cbc4 100644 --- a/examples/sst-transform/package.json +++ b/examples/sst-transform/package.json @@ -2,6 +2,6 @@ "name": "sst-transform", "version": "0.0.0", "dependencies": { - "sst": "^3" + "sst": "file:../../sdk/js" } } diff --git a/examples/start-nextjs/.gitignore b/examples/start-nextjs/.gitignore deleted file mode 100644 index 047dc61852..0000000000 --- a/examples/start-nextjs/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# nextjs -.next - -# open-next -.open-next \ No newline at end of file diff --git a/examples/start-nextjs/next-env.d.ts b/examples/start-nextjs/next-env.d.ts deleted file mode 100644 index 4f11a03dc6..0000000000 --- a/examples/start-nextjs/next-env.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/start-remix/.gitignore b/examples/start-remix/.gitignore deleted file mode 100644 index e01f468792..0000000000 --- a/examples/start-remix/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# remix -build \ No newline at end of file diff --git a/examples/vercel-domain/package.json b/examples/vercel-domain/package.json new file mode 100644 index 0000000000..3813ef1141 --- /dev/null +++ b/examples/vercel-domain/package.json @@ -0,0 +1,7 @@ +{ + "name": "vercel-domain", + "version": "0.0.0", + "dependencies": { + "sst": "file:../../sdk/js" + } +} diff --git a/go.mod b/go.mod index f8f20c0815..208e8f405a 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( github.com/BurntSushi/toml v1.2.1 github.com/Masterminds/semver/v3 v3.2.1 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 - github.com/aws/aws-sdk-go v1.50.36 github.com/aws/aws-sdk-go-v2 v1.36.2 github.com/aws/aws-sdk-go-v2/config v1.27.11 github.com/aws/aws-sdk-go-v2/credentials v1.17.11 @@ -25,14 +24,12 @@ require ( github.com/aws/smithy-go v1.22.2 github.com/briandowns/spinner v1.23.0 github.com/charmbracelet/bubbles v0.17.2-0.20240108170749-ec883029c8e6 - github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2 github.com/charmbracelet/huh v0.3.0 github.com/charmbracelet/lipgloss v0.9.1 github.com/charmbracelet/x/ansi v0.4.3 github.com/cloudflare/cloudflare-go v0.115.0 github.com/creack/pty v1.1.21 - github.com/eclipse/paho.mqtt.golang v1.4.3 - github.com/evanw/esbuild v0.21.5 + github.com/evanw/esbuild v0.27.2 github.com/fatih/color v1.17.0 github.com/fsnotify/fsnotify v1.7.0 github.com/gdamore/tcell/v2 v2.7.4 @@ -40,16 +37,14 @@ require ( github.com/joho/godotenv v1.5.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/klauspost/cpuid/v2 v2.2.9 - github.com/libp2p/go-libp2p v0.38.2 github.com/manifoldco/promptui v0.9.0 github.com/mattn/go-runewidth v0.0.15 github.com/posthog/posthog-go v0.0.0-20240221135834-4944045455b4 - github.com/pulumi/pulumi/pkg/v3 v3.210.0 - github.com/pulumi/pulumi/sdk/v3 v3.210.0 - github.com/spf13/pflag v1.0.9 + github.com/pulumi/pulumi/pkg/v3 v3.215.0 + github.com/pulumi/pulumi/sdk/v3 v3.215.0 + github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.10.0 github.com/tailscale/hujson v0.0.0-20241010212012-29efb4a0184b - github.com/twitchtv/twirp v8.1.3+incompatible github.com/xjasonlyu/tun2socks/v2 v2.5.3-0.20241012195127-b65d23180cc5 github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 github.com/zeebo/xxh3 v1.0.2 @@ -57,7 +52,7 @@ require ( golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 golang.org/x/sync v0.18.0 golang.org/x/term v0.37.0 - google.golang.org/protobuf v1.36.6 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -83,35 +78,20 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/benbjohnson/clock v1.3.5 // indirect - github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver v3.5.1+incompatible // indirect github.com/catppuccin/go v0.2.0 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/bubbletea v0.25.0 // indirect - github.com/charmbracelet/colorprofile v0.1.6 // indirect - github.com/charmbracelet/x/cellbuf v0.0.3 // indirect - github.com/charmbracelet/x/term v0.2.0 // indirect - github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91 // indirect - github.com/charmbracelet/x/windows v0.2.0 // indirect github.com/cheggaaa/pb v1.0.29 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect - github.com/containerd/cgroups v1.1.0 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/cyphar/filepath-securejoin v0.3.6 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect github.com/deckarep/golang-set/v2 v2.5.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/djherbis/times v1.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect - github.com/elastic/gosigar v0.14.3 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/flynn/noise v1.1.0 // indirect - github.com/francoispqt/gojay v1.2.13 // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/go-chi/chi/v5 v5.1.0 // indirect github.com/go-chi/cors v1.2.1 // indirect @@ -120,10 +100,8 @@ require ( github.com/go-git/go-billy/v5 v5.6.1 // indirect github.com/go-git/go-git/v5 v5.13.1 // indirect github.com/go-gost/relay v0.5.0 // indirect - github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-test/deep v1.1.1 // indirect github.com/goccy/go-json v0.10.5 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gofrs/uuid v4.2.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.2.4 // indirect @@ -131,8 +109,6 @@ require ( github.com/google/btree v1.1.3 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/gopacket v1.1.19 // indirect - github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/schema v1.4.1 // indirect @@ -140,94 +116,35 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/hcl/v2 v2.22.0 // indirect - github.com/huin/goupnp v1.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/ipfs/go-cid v0.4.1 // indirect - github.com/ipfs/go-log/v2 v2.5.1 // indirect - github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/klauspost/compress v1.17.11 // indirect - github.com/koron/go-ssdp v0.0.4 // indirect - github.com/libp2p/go-buffer-pool v0.1.0 // indirect - github.com/libp2p/go-flow-metrics v0.2.0 // indirect - github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-msgio v0.3.0 // indirect - github.com/libp2p/go-nat v0.2.0 // indirect - github.com/libp2p/go-netroute v0.2.2 // indirect - github.com/libp2p/go-reuseport v0.4.0 // indirect - github.com/libp2p/go-yamux/v4 v4.0.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/miekg/dns v1.1.62 // indirect - github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect - github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect - github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/mr-tron/base58 v1.2.0 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect - github.com/multiformats/go-base32 v0.1.0 // indirect - github.com/multiformats/go-base36 v0.2.0 // indirect - github.com/multiformats/go-multiaddr v0.14.0 // indirect - github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect - github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect - github.com/multiformats/go-multibase v0.2.0 // indirect - github.com/multiformats/go-multicodec v0.9.0 // indirect - github.com/multiformats/go-multihash v0.2.3 // indirect - github.com/multiformats/go-multistream v0.6.0 // indirect - github.com/multiformats/go-varint v0.0.7 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/natefinch/atomic v1.0.1 // indirect - github.com/onsi/ginkgo/v2 v2.22.0 // indirect - github.com/opencontainers/runtime-spec v1.2.0 // indirect + github.com/onsi/gomega v1.34.2 // indirect github.com/opentracing/basictracer-go v1.1.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect - github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pgavlin/fx v0.1.6 // indirect github.com/pgavlin/fx/v2 v2.0.10 // indirect github.com/pgavlin/goldmark v1.1.33-0.20200616210433-b5eb04559386 // indirect - github.com/pion/datachannel v1.5.10 // indirect - github.com/pion/dtls/v2 v2.2.12 // indirect - github.com/pion/ice/v2 v2.3.37 // indirect - github.com/pion/interceptor v0.1.37 // indirect - github.com/pion/logging v0.2.2 // indirect - github.com/pion/mdns v0.0.12 // indirect - github.com/pion/randutil v0.1.0 // indirect - github.com/pion/rtcp v1.2.15 // indirect - github.com/pion/rtp v1.8.10 // indirect - github.com/pion/sctp v1.8.35 // indirect - github.com/pion/sdp/v3 v3.0.9 // indirect - github.com/pion/srtp/v2 v2.0.20 // indirect - github.com/pion/stun v0.6.1 // indirect - github.com/pion/transport/v2 v2.2.10 // indirect - github.com/pion/transport/v3 v3.0.7 // indirect - github.com/pion/turn/v2 v2.1.6 // indirect - github.com/pion/webrtc/v3 v3.3.5 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkg/term v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.20.5 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.61.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 // indirect github.com/pulumi/esc v0.17.0 // indirect - github.com/quic-go/qpack v0.5.1 // indirect - github.com/quic-go/quic-go v0.48.2 // indirect - github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect - github.com/raulk/go-watchdog v1.3.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect @@ -236,28 +153,21 @@ require ( github.com/segmentio/encoding v0.3.5 // indirect github.com/sergi/go-diff v1.4.0 // indirect github.com/skeema/knownhosts v1.3.0 // indirect - github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/cobra v1.10.1 // indirect github.com/texttheater/golang-levenshtein v1.0.1 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect - github.com/wlynxg/anet v0.0.5 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/zclconf/go-cty v1.13.2 // indirect go.uber.org/atomic v1.11.0 // indirect - go.uber.org/dig v1.18.0 // indirect - go.uber.org/fx v1.23.0 // indirect - go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/mod v0.29.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sys v0.38.0 // indirect - golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.38.0 // indirect @@ -265,9 +175,8 @@ require ( golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect google.golang.org/grpc v1.72.1 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect gvisor.dev/gvisor v0.0.0-20240928194204-917bbae826a0 // indirect - lukechampine.com/blake3 v1.3.0 // indirect lukechampine.com/frand v1.4.2 // indirect ) diff --git a/go.sum b/go.sum index db08c504ea..e99aa24b82 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,5 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= -dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= -dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= -dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= -git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= @@ -27,7 +17,6 @@ github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7l github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= @@ -38,8 +27,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aws/aws-sdk-go v1.50.36 h1:PjWXHwZPuTLMR1NIb8nEjLucZBMzmf84TLoLbD8BZqk= -github.com/aws/aws-sdk-go v1.50.36/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/aws/aws-sdk-go-v2 v1.36.2 h1:Ub6I4lq/71+tPb/atswvToaLGVMxKZvjYDVOWEExOcU= github.com/aws/aws-sdk-go-v2 v1.36.2/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg= @@ -98,45 +85,22 @@ github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= -github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A= github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE= -github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbles v0.17.2-0.20240108170749-ec883029c8e6 h1:6nVCV8pqGaeyxetur3gpX3AAaiyKgzjIoCPV3NXKZBE= github.com/charmbracelet/bubbles v0.17.2-0.20240108170749-ec883029c8e6/go.mod h1:9HxZWlkCqz2PRwsCbYl7a3KXvGzFaDHpYbSYMJ+nE3o= github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= -github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2 h1:NkQFWhCii9NtL7Q0L/4mNKtZFgrDpfPSVZAzTwEJdGg= -github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2/go.mod h1:24niqT9RbtXhWg8zLRU/v/xTixlo1+DUsHQZ3+kez5Y= -github.com/charmbracelet/colorprofile v0.1.6 h1:nMMqCns0c0DfCwNGdagBh6SxutFqkltSxxKk5S9kt+Y= -github.com/charmbracelet/colorprofile v0.1.6/go.mod h1:3EMXDxwRDJl0c17eJ1jX99MhtlP9OxE/9Qw0C5lvyUg= github.com/charmbracelet/huh v0.3.0 h1:CxPplWkgW2yUTDDG0Z4S5HH8SJOosWHd4LxCvi0XsKE= github.com/charmbracelet/huh v0.3.0/go.mod h1:fujUdKX8tC45CCSaRQdw789O6uaCRwx8l2NDyKfC4jA= github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= github.com/charmbracelet/x/ansi v0.4.3 h1:wcdDrW0ejaaZGJxCyxVNzzmctqV+oARIudaFGQvsRkA= github.com/charmbracelet/x/ansi v0.4.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/cellbuf v0.0.3 h1:HapUUjlo0pZ7iGijrTer1f4X8Uvq17l0zR+80Oh+iJg= -github.com/charmbracelet/x/cellbuf v0.0.3/go.mod h1:SF8R3AqchNzYKKJCFT7co8wt1HgQDfAitQ+SBoxWLNc= -github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= -github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= -github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91 h1:D5OO0lVavz7A+Swdhp62F9gbkibxmz9B2hZ/jVdMPf0= -github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91/go.mod h1:Ey8PFmYwH+/td9bpiEx07Fdx9ZVkxfIjWXxBluxF4Nw= -github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw= -github.com/charmbracelet/x/windows v0.2.0/go.mod h1:ZibNFR49ZFqCXgP76sYanisxRyC+EYrBE7TTknD8s1s= github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo= github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -148,75 +112,43 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM= github.com/cloudflare/cloudflare-go v0.115.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU= -github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= -github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= -github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= -github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= -github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= github.com/deckarep/golang-set/v2 v2.5.0 h1:hn6cEZtQ0h3J8kFrHR/NrzyOoTnjgW1+FmNJzQ7y/sA= github.com/deckarep/golang-set/v2 v2.5.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= -github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= -github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/djherbis/times v1.5.0 h1:79myA211VwPhFTqUk8xehWrsEO+zcIZj0zT8mXPVARU= github.com/djherbis/times v1.5.0/go.mod h1:5q7FDLvbNg1L/KaBmPcWlVR9NmoKo3+ucqUA3ijQhA0= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= -github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= -github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= -github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo= -github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ= github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/evanw/esbuild v0.21.5 h1:oShm8TT5QUhf6vM7teg0nmd14eHu64dPmVluC2f4DMg= -github.com/evanw/esbuild v0.21.5/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +github.com/evanw/esbuild v0.27.2 h1:3xBEws9y/JosfewXMM2qIyHAi+xRo8hVx475hVkJfNg= +github.com/evanw/esbuild v0.27.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= -github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= -github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= -github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU= github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= @@ -225,7 +157,6 @@ github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA= @@ -240,65 +171,36 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc= github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= -github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= -github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -308,29 +210,16 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= -github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= -github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= -github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= -github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= -github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= -github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= -github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= -github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= -github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= @@ -338,55 +227,24 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= -github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= -github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= -github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= -github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= -github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= -github.com/libp2p/go-libp2p v0.38.2 h1:9SZQDOCi82A25An4kx30lEtr6kGTxrtoaDkbs5xrK5k= -github.com/libp2p/go-libp2p v0.38.2/go.mod h1:QWV4zGL3O9nXKdHirIC59DoRcZ446dfkjbOJ55NEWFo= -github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= -github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= -github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= -github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= -github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= -github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= -github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk= -github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk= -github.com/libp2p/go-netroute v0.2.2 h1:Dejd8cQ47Qx2kRABg6lPwknU7+nBnFRpko45/fFPuZ8= -github.com/libp2p/go-netroute v0.2.2/go.mod h1:Rntq6jUAH0l9Gg17w5bFGhcC9a+vk4KNXs6s7IljKYE= -github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= -github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= -github.com/libp2p/go-yamux/v4 v4.0.1 h1:FfDR4S1wj6Bw2Pqbc8Uz7pCxeRBPbwsBbEdfwiCypkQ= -github.com/libp2p/go-yamux/v4 v4.0.1/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= -github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= @@ -396,20 +254,6 @@ github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= -github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= -github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= -github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= -github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= -github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= -github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= -github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= -github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= -github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= -github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= -github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= -github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= @@ -418,11 +262,6 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= -github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -431,102 +270,25 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= -github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= -github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= -github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= -github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= -github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= -github.com/multiformats/go-multiaddr v0.14.0 h1:bfrHrJhrRuh/NXH5mCnemjpbGjzRw/b+tJFOD41g2tU= -github.com/multiformats/go-multiaddr v0.14.0/go.mod h1:6EkVAxtznq2yC3QT5CM1UTAwG0GTP3EWAIcjHuzQ+r4= -github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M= -github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc= -github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= -github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= -github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= -github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= -github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= -github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= -github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= -github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= -github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= -github.com/multiformats/go-multistream v0.6.0 h1:ZaHKbsL404720283o4c/IHQXiS6gb8qAN5EIJ4PN5EA= -github.com/multiformats/go-multistream v0.6.0/go.mod h1:MOyoG5otO24cHIg8kf9QW2/NozURlkP/rvi2FQJyCPg= -github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= -github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxschmitt/golang-combinations v1.0.0 h1:NFoO7CSP8MUcFlHpe1YdewKwMa15dgDbaqkVLC5DUPI= github.com/mxschmitt/golang-combinations v1.0.0/go.mod h1:RbMhWvfCelHR6WROvT2bVfxJvZHoEvBj71SKe+H0MYU= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= -github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= -github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= -github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= -github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= -github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= -github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opentracing/basictracer-go v1.1.0 h1:Oa1fTSBvAl8pa3U+IJYqrKm0NALwH9OsgwOqDv4xJW0= github.com/opentracing/basictracer-go v1.1.0/go.mod h1:V2HZueSJEp879yv285Aap1BS69fQMD+MNP1mRs6mBQc= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= -github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= -github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= -github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pgavlin/fx v0.1.6 h1:r9jEg69DhNoCd3Xh0+5mIbdbS3PqWrVWujkY76MFRTU= github.com/pgavlin/fx v0.1.6/go.mod h1:KWZJ6fqBBSh8GxHYqwYCf3rYE7Gp2p0N8tJp8xv9u9M= github.com/pgavlin/fx/v2 v2.0.10 h1:ggyQ6pB+lEQEbEae48Wh/X221eLOamMD7i01ISe88u4= github.com/pgavlin/fx/v2 v2.0.10/go.mod h1:M/nF/ooAOy+NUBooYYXl2REARzJ/giPJxfMs8fINfKc= github.com/pgavlin/goldmark v1.1.33-0.20200616210433-b5eb04559386 h1:LoCV5cscNVWyK5ChN/uCoIFJz8jZD63VQiGJIRgr6uo= github.com/pgavlin/goldmark v1.1.33-0.20200616210433-b5eb04559386/go.mod h1:MRxHTJrf9FhdfNQ8Hdeh9gmHevC9RJE/fu8M3JIGjoE= -github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= -github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= -github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= -github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= -github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/ice/v2 v2.3.37 h1:ObIdaNDu1rCo7hObhs34YSBcO7fjslJMZV0ux+uZWh0= -github.com/pion/ice/v2 v2.3.37/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ= -github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= -github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= -github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= -github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= -github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= -github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= -github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= -github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= -github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= -github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= -github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/rtp v1.8.10 h1:puphjdbjPB+L+NFaVuZ5h6bt1g5q4kFIoI+r5q/g0CU= -github.com/pion/rtp v1.8.10/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4= -github.com/pion/sctp v1.8.35 h1:qwtKvNK1Wc5tHMIYgTDJhfZk7vATGVHhXbUDfHbYwzA= -github.com/pion/sctp v1.8.35/go.mod h1:EcXP8zCYVTRy3W9xtOF7wJm1L1aXfKRQzaM33SjQlzg= -github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= -github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= -github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk= -github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA= -github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= -github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= -github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= -github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= -github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= -github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= -github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= -github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= -github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= -github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= -github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= -github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc= -github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= -github.com/pion/webrtc/v3 v3.3.5 h1:ZsSzaMz/i9nblPdiAkZoP+E6Kmjw+jnyq3bEmU3EtRg= -github.com/pion/webrtc/v3 v3.3.5/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk= @@ -535,34 +297,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posthog/posthog-go v0.0.0-20240221135834-4944045455b4 h1:p+8kZn9P90aX6vPLDHPGP2WZeW2Q1/SAQd935eAWCRI= github.com/posthog/posthog-go v0.0.0-20240221135834-4944045455b4/go.mod h1:QjlpryJtfYLrZF2GUkAhejH4E7WlDbdKkvOi5hLmkdg= -github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= -github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= -github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= -github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 h1:vkHw5I/plNdTr435cARxCW6q9gc0S/Yxz7Mkd38pOb0= github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231/go.mod h1:murToZ2N9hNJzewjHBgfFdXhZKjY3z5cYC1VXk+lbFE= github.com/pulumi/esc v0.17.0 h1:oaVOIyFTENlYDuqc3pW75lQT9jb2cd6ie/4/Twxn66w= github.com/pulumi/esc v0.17.0/go.mod h1:XnSxlt5NkmuAj304l/gK4pRErFbtqq6XpfX1tYT9Jbc= -github.com/pulumi/pulumi/pkg/v3 v3.210.0 h1:4Azta7RY+wls+5f+3+vxBgGpZ1yj4bgbpq2GXE0HNPY= -github.com/pulumi/pulumi/pkg/v3 v3.210.0/go.mod h1:RtgEywVUxAGuL5P0rV870nnml9e2JUK87xO1EK5espM= -github.com/pulumi/pulumi/sdk/v3 v3.210.0 h1:QMNdfQfB7jCa/ZoY8aIfwOwApuvhnOoIH8s4umNvR3U= -github.com/pulumi/pulumi/sdk/v3 v3.210.0/go.mod h1:0qnUzUV5ypAcdoPNOX426wV4ePMnkDvGlPBZqlizHmU= -github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= -github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= -github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= -github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg= -github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw= -github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= -github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= +github.com/pulumi/pulumi/pkg/v3 v3.215.0 h1:/3HEWgLEa8QDqwQd6qv1A66c5+9azGaVtpCUXTkFnr0= +github.com/pulumi/pulumi/pkg/v3 v3.215.0/go.mod h1:5kx6//mZ6u6KtG3zPyvxgGnc17gZqUFQ5n+GeYY1gT8= +github.com/pulumi/pulumi/sdk/v3 v3.215.0 h1:XZMiv9aSE1pD3kW4JpA53cy7mFflbJYn1fFKH1NMIAY= +github.com/pulumi/pulumi/sdk/v3 v3.215.0/go.mod h1:Bn5Z9Rzp1lPqdAccaB+F2ivUBiamEl2TNR3Gg/h7iLs= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -570,8 +312,6 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= @@ -581,46 +321,17 @@ github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= github.com/segmentio/encoding v0.3.5 h1:UZEiaZ55nlXGDL92scoVuw00RmiRCazIEmvPSbSvt8Y= github.com/segmentio/encoding v0.3.5/go.mod h1:n0JeuIqEQrQoPDGsjo8UNd1iA0U8d8+oHAA4E3G3OxM= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= -github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= -github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= -github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= -github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= -github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= -github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= -github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= -github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= -github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= -github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= -github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= -github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= -github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= -github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= -github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= -github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= -github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= -github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= -github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= -github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= -github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= -github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= -github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -628,31 +339,16 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tailscale/hujson v0.0.0-20241010212012-29efb4a0184b h1:MNaGusDfB1qxEsl6iVb33Gbe777IKzPP5PDta0xGC8M= github.com/tailscale/hujson v0.0.0-20241010212012-29efb4a0184b/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo= -github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U= github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8= -github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU= -github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= -github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= -github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= -github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= -github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= -github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= @@ -663,13 +359,10 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xjasonlyu/tun2socks/v2 v2.5.3-0.20241012195127-b65d23180cc5 h1:hb6JF00DUHLKgIWktdufsy57TQxwK4LHsVS49aFJ/PM= github.com/xjasonlyu/tun2socks/v2 v2.5.3-0.20241012195127-b65d23180cc5/go.mod h1:cdgCv2eLil+9COT6VP+HqnHZSCLBKUofYJvEA0WWitQ= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= @@ -677,7 +370,6 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= -go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= @@ -690,123 +382,62 @@ go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= -go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= -go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= -go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= -go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= -golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= -golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= -golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -816,53 +447,31 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU= -golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= @@ -875,56 +484,28 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= -google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= -google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= gvisor.dev/gvisor v0.0.0-20240928194204-917bbae826a0 h1:ky55xcxFZFrn43Ryc+QZg3mW/HrqRqtUSO+Vsr63a+E= gvisor.dev/gvisor v0.0.0-20240928194204-917bbae826a0/go.mod h1:QtjD9K86CsntTIYXKeD346t7JkYoytFlRUErM64p6uY= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= -lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= lukechampine.com/frand v1.4.2 h1:RzFIpOvkMXuPMBb9maa4ND4wjBn71E1Jpf8BzJHMaVw= lukechampine.com/frand v1.4.2/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s= pgregory.net/rapid v0.6.1 h1:4eyrDxyht86tT4Ztm+kvlyNBLIk071gR+ZQdhphc9dQ= pgregory.net/rapid v0.6.1/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= -sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= -sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/internal/fs/fs.go b/internal/fs/fs.go index 583fe7d1e3..a87b99083b 100644 --- a/internal/fs/fs.go +++ b/internal/fs/fs.go @@ -36,8 +36,29 @@ func Exists(path string) bool { return err == nil } +func IsGitSubmodule(dir string) bool { + gitPath := filepath.Join(dir, ".git") + info, err := os.Stat(gitPath) + return err == nil && !info.IsDir() +} + func FindDown(dir, filename string) []string { + return FindDownWithIgnore(dir, filename, nil) +} + +func FindDownWithIgnore(dir, filename string, ignore []string) []string { var result []string + ignored := make([]string, 0, len(ignore)) + for _, item := range ignore { + if item == "" { + continue + } + if filepath.IsAbs(item) { + ignored = append(ignored, filepath.Clean(item)) + continue + } + ignored = append(ignored, filepath.Clean(filepath.Join(dir, item))) + } filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if err != nil { @@ -48,6 +69,14 @@ func FindDown(dir, filename string) []string { if name == "node_modules" || strings.HasPrefix(name, ".") { return filepath.SkipDir } + if path != dir && IsGitSubmodule(path) { + return filepath.SkipDir + } + for _, ignoredPath := range ignored { + if isWithin(ignoredPath, path) { + return filepath.SkipDir + } + } } if !info.IsDir() && info.Name() == filename { result = append(result, path) @@ -57,3 +86,14 @@ func FindDown(dir, filename string) []string { return result } + +func isWithin(parent, path string) bool { + rel, err := filepath.Rel(parent, path) + if err != nil { + return false + } + if rel == "." { + return true + } + return rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) +} diff --git a/internal/fs/fs_test.go b/internal/fs/fs_test.go new file mode 100644 index 0000000000..328f1cc9f7 --- /dev/null +++ b/internal/fs/fs_test.go @@ -0,0 +1,161 @@ +package fs_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/sst/sst/v3/internal/fs" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFindUp(t *testing.T) { + t.Run("file in current dir", func(t *testing.T) { + dir := t.TempDir() + target := filepath.Join(dir, "target.txt") + os.WriteFile(target, []byte("x"), 0644) + + found, err := fs.FindUp(dir, "target.txt") + require.NoError(t, err) + assert.Equal(t, target, found) + }) + + t.Run("file in parent dir", func(t *testing.T) { + dir := t.TempDir() + target := filepath.Join(dir, "target.txt") + os.WriteFile(target, []byte("x"), 0644) + child := filepath.Join(dir, "sub") + os.Mkdir(child, 0755) + + found, err := fs.FindUp(child, "target.txt") + require.NoError(t, err) + assert.Equal(t, target, found) + }) + + t.Run("file not found", func(t *testing.T) { + dir := t.TempDir() + _, err := fs.FindUp(dir, "nonexistent.txt") + assert.Error(t, err) + }) +} + +func TestFindDown(t *testing.T) { + t.Run("finds nested file", func(t *testing.T) { + dir := t.TempDir() + nested := filepath.Join(dir, "a", "b") + os.MkdirAll(nested, 0755) + target := filepath.Join(nested, "config.json") + os.WriteFile(target, []byte("{}"), 0644) + + results := fs.FindDown(dir, "config.json") + assert.Equal(t, []string{target}, results) + }) + + t.Run("skips node_modules", func(t *testing.T) { + dir := t.TempDir() + nm := filepath.Join(dir, "node_modules") + os.MkdirAll(nm, 0755) + os.WriteFile(filepath.Join(nm, "pkg.json"), []byte("{}"), 0644) + + results := fs.FindDown(dir, "pkg.json") + assert.Empty(t, results) + }) + + t.Run("skips dot dirs", func(t *testing.T) { + dir := t.TempDir() + dotDir := filepath.Join(dir, ".hidden") + os.MkdirAll(dotDir, 0755) + os.WriteFile(filepath.Join(dotDir, "f.txt"), []byte("x"), 0644) + + results := fs.FindDown(dir, "f.txt") + assert.Empty(t, results) + }) + + t.Run("skips git submodules", func(t *testing.T) { + dir := t.TempDir() + + // Normal nested dir β€” should be found + normal := filepath.Join(dir, "app") + os.MkdirAll(normal, 0755) + os.WriteFile(filepath.Join(normal, "package.json"), []byte("{}"), 0644) + + // Submodule dir (has .git file) β€” should be skipped + sub := filepath.Join(dir, "submod") + os.MkdirAll(sub, 0755) + os.WriteFile(filepath.Join(sub, ".git"), []byte("gitdir: ../../.git/modules/submod"), 0644) + os.WriteFile(filepath.Join(sub, "package.json"), []byte("{}"), 0644) + + results := fs.FindDown(dir, "package.json") + assert.Equal(t, []string{filepath.Join(normal, "package.json")}, results) + }) + + t.Run("skips ignored directories", func(t *testing.T) { + dir := t.TempDir() + ignored := filepath.Join(dir, "packages", "docs") + included := filepath.Join(dir, "packages", "web") + os.MkdirAll(ignored, 0755) + os.MkdirAll(included, 0755) + os.WriteFile(filepath.Join(ignored, "package.json"), []byte("{}"), 0644) + includedFile := filepath.Join(included, "package.json") + os.WriteFile(includedFile, []byte("{}"), 0644) + + results := fs.FindDownWithIgnore(dir, "package.json", []string{"packages/docs"}) + assert.Equal(t, []string{includedFile}, results) + }) + + t.Run("does not skip dirs with .git directory", func(t *testing.T) { + dir := t.TempDir() + + // Nested repo clone (has .git directory, not file) β€” should be walked + nested := filepath.Join(dir, "nested-repo") + os.MkdirAll(filepath.Join(nested, ".git"), 0755) + os.WriteFile(filepath.Join(nested, "package.json"), []byte("{}"), 0644) + + results := fs.FindDown(dir, "package.json") + assert.Equal(t, []string{filepath.Join(nested, "package.json")}, results) + }) + + t.Run("not found returns empty", func(t *testing.T) { + dir := t.TempDir() + results := fs.FindDown(dir, "nope.txt") + assert.Empty(t, results) + }) +} + +func TestIsGitSubmodule(t *testing.T) { + t.Run("git file returns true", func(t *testing.T) { + dir := t.TempDir() + os.WriteFile(filepath.Join(dir, ".git"), []byte("gitdir: ../../.git/modules/foo"), 0644) + assert.True(t, fs.IsGitSubmodule(dir)) + }) + + t.Run("git directory returns false", func(t *testing.T) { + dir := t.TempDir() + os.Mkdir(filepath.Join(dir, ".git"), 0755) + assert.False(t, fs.IsGitSubmodule(dir)) + }) + + t.Run("no git entry returns false", func(t *testing.T) { + dir := t.TempDir() + assert.False(t, fs.IsGitSubmodule(dir)) + }) +} + +func TestExists(t *testing.T) { + t.Run("existing file", func(t *testing.T) { + dir := t.TempDir() + f := filepath.Join(dir, "file.txt") + os.WriteFile(f, []byte("x"), 0644) + assert.True(t, fs.Exists(f)) + }) + + t.Run("non-existing", func(t *testing.T) { + assert.False(t, fs.Exists("/tmp/nonexistent_sst_test_file")) + }) + + t.Run("existing dir", func(t *testing.T) { + dir := t.TempDir() + assert.True(t, fs.Exists(dir)) + }) +} diff --git a/internal/util/util_test.go b/internal/util/util_test.go new file mode 100644 index 0000000000..88f5400487 --- /dev/null +++ b/internal/util/util_test.go @@ -0,0 +1,157 @@ +package util_test + +import ( + "errors" + "sync" + "testing" + + "github.com/sst/sst/v3/internal/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRandomString(t *testing.T) { + t.Run("correct length", func(t *testing.T) { + assert.Len(t, util.RandomString(10), 10) + assert.Len(t, util.RandomString(0), 0) + assert.Len(t, util.RandomString(50), 50) + }) + + t.Run("valid charset", func(t *testing.T) { + const charset = "abcdefhkmnorstuvwxz" + s := util.RandomString(100) + for _, c := range s { + assert.Contains(t, charset, string(c)) + } + }) + + t.Run("different outputs", func(t *testing.T) { + a := util.RandomString(20) + b := util.RandomString(20) + assert.NotEqual(t, a, b) + }) +} + +func TestReadableError(t *testing.T) { + inner := errors.New("inner") + + t.Run("NewReadableError", func(t *testing.T) { + err := util.NewReadableError(inner, "readable msg") + assert.Equal(t, "readable msg", err.Error()) + assert.Equal(t, inner, err.Unwrap()) + assert.False(t, err.IsHinted()) + }) + + t.Run("NewHintedError", func(t *testing.T) { + err := util.NewHintedError(inner, "hint msg") + assert.Equal(t, "hint msg", err.Error()) + assert.Equal(t, inner, err.Unwrap()) + assert.True(t, err.IsHinted()) + }) +} + +func TestKeyLock(t *testing.T) { + t.Run("lock unlock basic", func(t *testing.T) { + kl := util.NewKeyLock() + kl.Lock("a") + kl.Unlock("a") + // should not deadlock + kl.Lock("a") + kl.Unlock("a") + }) + + t.Run("concurrent lock blocks", func(t *testing.T) { + kl := util.NewKeyLock() + kl.Lock("key") + + started := make(chan struct{}) + done := make(chan struct{}) + go func() { + close(started) + kl.Lock("key") + close(done) + kl.Unlock("key") + }() + + <-started + select { + case <-done: + t.Fatal("second lock should block") + default: + } + + kl.Unlock("key") + <-done // now it should complete + }) + + t.Run("different keys independent", func(t *testing.T) { + kl := util.NewKeyLock() + kl.Lock("a") + kl.Lock("b") // should not block + kl.Unlock("a") + kl.Unlock("b") + }) +} + +func TestSyncMap(t *testing.T) { + t.Run("store and load", func(t *testing.T) { + var m util.SyncMap[string, int] + m.Store("x", 42) + v, ok := m.Load("x") + require.True(t, ok) + assert.Equal(t, 42, v) + }) + + t.Run("load missing", func(t *testing.T) { + var m util.SyncMap[string, int] + _, ok := m.Load("missing") + assert.False(t, ok) + }) + + t.Run("delete", func(t *testing.T) { + var m util.SyncMap[string, int] + m.Store("x", 1) + m.Delete("x") + _, ok := m.Load("x") + assert.False(t, ok) + }) + + t.Run("load or store", func(t *testing.T) { + var m util.SyncMap[string, int] + m.Store("x", 1) + v, loaded := m.LoadOrStore("x", 99) + assert.True(t, loaded) + assert.Equal(t, 1, v) + + v, loaded = m.LoadOrStore("y", 99) + assert.False(t, loaded) + assert.Equal(t, 99, v) + }) + + t.Run("load and delete", func(t *testing.T) { + var m util.SyncMap[string, int] + m.Store("x", 5) + v, loaded := m.LoadAndDelete("x") + assert.True(t, loaded) + assert.Equal(t, 5, v) + + _, ok := m.Load("x") + assert.False(t, ok) + }) + + t.Run("range", func(t *testing.T) { + var m util.SyncMap[string, int] + m.Store("a", 1) + m.Store("b", 2) + + collected := map[string]int{} + var mu sync.Mutex + m.Range(func(k string, v int) bool { + mu.Lock() + collected[k] = v + mu.Unlock() + return true + }) + assert.Equal(t, map[string]int{"a": 1, "b": 2}, collected) + }) +} diff --git a/package.json b/package.json index fd53e8dc8c..ccaa07fb72 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,18 @@ "platform", "sdk/js" ], + "scripts": { + "setup": "bun install && go mod tidy && bun run build:platform && bun run build:sdk", + "build:platform": "bun run --filter @sst/platform build", + "build:cli": "go build ./cmd/sst", + "build:sdk": "bun run --cwd ./sdk/js build", + "test:cli": "go test ./...", + "typecheck": "bun run typecheck:platform && bun run typecheck:sdk", + "typecheck:platform": "tsc --noEmit -p platform/tsconfig.json", + "typecheck:sdk": "tsc --noEmit -p sdk/js/tsconfig.json", + "docs:generate": "bun run --filter www generate", + "docs:dev": "bun run --filter www dev" + }, "overrides": { "google-protobuf": "3.21.2" } diff --git a/pkg/bus/bus_test.go b/pkg/bus/bus_test.go new file mode 100644 index 0000000000..4c157b7ca4 --- /dev/null +++ b/pkg/bus/bus_test.go @@ -0,0 +1,68 @@ +package bus_test + +import ( + "testing" + + "github.com/sst/sst/v3/pkg/bus" + "github.com/stretchr/testify/assert" +) + +type testEventA struct{ Value string } +type testEventB struct{ Value int } + +func TestBus(t *testing.T) { + t.Run("subscribe and publish", func(t *testing.T) { + ch := bus.Subscribe(testEventA{}) + bus.Publish(testEventA{Value: "hello"}) + evt := <-ch + assert.Equal(t, testEventA{Value: "hello"}, evt) + }) + + t.Run("wrong type not received", func(t *testing.T) { + ch := bus.Subscribe(testEventB{}) + bus.Publish(testEventA{Value: "nope"}) + + select { + case <-ch: + t.Fatal("should not receive wrong type") + default: + } + }) + + t.Run("subscribe all", func(t *testing.T) { + ch := bus.SubscribeAll() + defer bus.Unsubscribe(ch) + + bus.Publish(testEventA{Value: "a"}) + bus.Publish(testEventB{Value: 1}) + + evt1 := <-ch + evt2 := <-ch + assert.Equal(t, testEventA{Value: "a"}, evt1) + assert.Equal(t, testEventB{Value: 1}, evt2) + }) + + t.Run("unsubscribe stops receiving", func(t *testing.T) { + ch := bus.SubscribeAll() + bus.Unsubscribe(ch) + bus.Publish(testEventA{Value: "after unsub"}) + + select { + case <-ch: + t.Fatal("should not receive after unsubscribe") + default: + } + }) + + t.Run("multiple subscribers", func(t *testing.T) { + ch1 := bus.Subscribe(testEventA{}) + ch2 := bus.Subscribe(testEventA{}) + + bus.Publish(testEventA{Value: "multi"}) + + evt1 := <-ch1 + evt2 := <-ch2 + assert.Equal(t, testEventA{Value: "multi"}, evt1) + assert.Equal(t, testEventA{Value: "multi"}, evt2) + }) +} diff --git a/pkg/flag/flag.go b/pkg/flag/flag.go index 36a6a53a07..a0dfb2424e 100644 --- a/pkg/flag/flag.go +++ b/pkg/flag/flag.go @@ -10,6 +10,7 @@ var SST_PRINT_LOGS = isTrue("SST_PRINT_LOGS") var SST_NO_CLEANUP = isTrue("SST_NO_CLEANUP") var SST_PASSPHRASE = os.Getenv("SST_PASSPHRASE") var SST_PULUMI_PATH = os.Getenv("SST_PULUMI_PATH") +var SST_BUN_PATH = os.Getenv("SST_BUN_PATH") // SST_BUILD_CONCURRENCY is deprecated, use SST_FUNCTION_BUILD_CONCURRENCY instead var SST_BUILD_CONCURRENCY = os.Getenv("SST_BUILD_CONCURRENCY") diff --git a/pkg/global/bun.go b/pkg/global/bun.go index 3f46511a3e..84398cb410 100644 --- a/pkg/global/bun.go +++ b/pkg/global/bun.go @@ -4,6 +4,7 @@ import ( "archive/zip" "bytes" "context" + "debug/elf" "fmt" "io" "log/slog" @@ -22,6 +23,9 @@ import ( ) func BunPath() string { + if flag.SST_BUN_PATH != "" { + return flag.SST_BUN_PATH + } path := filepath.Join(BinPath(), "bun") if runtime.GOOS == "windows" { path += ".exe" @@ -33,6 +37,9 @@ func NeedsBun() bool { if flag.SST_NO_BUN { return false } + if flag.SST_BUN_PATH != "" { + return false + } path := BunPath() slog.Info("checking for bun", "path", path) if _, err := os.Stat(path); err != nil { @@ -54,16 +61,9 @@ func InstallBun(ctx context.Context) error { goos := runtime.GOOS arch := runtime.GOARCH - // Check for MUSL on Linux isMusl := false if goos == "linux" { - if _, err := os.Stat("/lib/ld-musl-x86_64.so.1"); err == nil { - isMusl = true - } else { - cmd := exec.Command("ldd", "--version") - output, _ := cmd.CombinedOutput() - isMusl = strings.Contains(strings.ToLower(string(output)), "musl") - } + isMusl = linuxUsesMusl() } var filename string @@ -163,3 +163,102 @@ func InstallBun(ctx context.Context) error { }) return err } + +func linuxUsesMusl() bool { + isMusl, ok := muslFromLdd() + if ok { + return isMusl + } + + for _, path := range []string{"/bin/sh", "/usr/bin/env", "/bin/busybox"} { + isMusl, ok := muslFromELF(path) + if ok { + return isMusl + } + } + + isMusl, ok = muslFromAlpineRelease() + if ok { + return isMusl + } + + isMusl, ok = muslFromLoaderPath() + if ok { + return isMusl + } + + return false +} + +func muslFromLdd() (bool, bool) { + lddPath, err := exec.LookPath("ldd") + if err != nil { + return false, false + } + + output, err := exec.Command(lddPath, "--version").CombinedOutput() + if err != nil && len(output) == 0 { + return false, false + } + + return strings.Contains(strings.ToLower(string(output)), "musl"), true +} + +func muslFromELF(path string) (bool, bool) { + interpreter, err := elfInterpreter(path) + if err != nil || interpreter == "" { + return false, false + } + + return strings.Contains(strings.ToLower(interpreter), "musl"), true +} + +func muslFromAlpineRelease() (bool, bool) { + _, err := os.Stat("/etc/alpine-release") + if err == nil { + return true, true + } + + return false, false +} + +func muslFromLoaderPath() (bool, bool) { + var path string + switch runtime.GOARCH { + case "amd64": + path = "/lib/ld-musl-x86_64.so.1" + case "arm64": + path = "/lib/ld-musl-aarch64.so.1" + default: + return false, false + } + + _, err := os.Stat(path) + if err == nil { + return true, true + } + + return false, false +} + +func elfInterpreter(path string) (string, error) { + file, err := elf.Open(path) + if err != nil { + return "", err + } + defer file.Close() + + for _, prog := range file.Progs { + if prog.Type != elf.PT_INTERP { + continue + } + + interpreter, err := io.ReadAll(prog.Open()) + if err != nil { + return "", err + } + return strings.TrimRight(string(interpreter), "\x00"), nil + } + + return "", fmt.Errorf("no ELF interpreter found for %s", path) +} diff --git a/pkg/global/upgrade.go b/pkg/global/upgrade.go index b88bff2fec..b6c8699baf 100644 --- a/pkg/global/upgrade.go +++ b/pkg/global/upgrade.go @@ -110,7 +110,8 @@ func Upgrade(existingVersion string, nextVersion string) (string, error) { func UpgradeNode(existingVersion string, nextVersion string) (map[string]string, error) { result := make(map[string]string) if nextVersion == "" { - pkg, err := npm.Get("sst", "latest") + registry := npm.LoadRegistry() + pkg, err := npm.Get(registry, "sst", "latest") if err != nil { return result, err } diff --git a/pkg/js/js_test.go b/pkg/js/js_test.go new file mode 100644 index 0000000000..d93d9a79c1 --- /dev/null +++ b/pkg/js/js_test.go @@ -0,0 +1,36 @@ +package js_test + +import ( + "testing" + + esbuild "github.com/evanw/esbuild/pkg/api" + "github.com/sst/sst/v3/pkg/js" + "github.com/stretchr/testify/assert" +) + +func TestFormatError(t *testing.T) { + t.Run("empty slice", func(t *testing.T) { + assert.Equal(t, "", js.FormatError([]esbuild.Message{})) + }) + + t.Run("no location", func(t *testing.T) { + msgs := []esbuild.Message{{Text: "something broke"}} + assert.Equal(t, "something broke", js.FormatError(msgs)) + }) + + t.Run("with location", func(t *testing.T) { + msgs := []esbuild.Message{{ + Text: "unexpected token", + Location: &esbuild.Location{File: "app.ts", Line: 10, Column: 5}, + }} + assert.Equal(t, "app.ts:10:5: unexpected token", js.FormatError(msgs)) + }) + + t.Run("multiple errors", func(t *testing.T) { + msgs := []esbuild.Message{ + {Text: "err1"}, + {Text: "err2", Location: &esbuild.Location{File: "b.ts", Line: 2, Column: 3}}, + } + assert.Equal(t, "err1\nb.ts:2:3: err2", js.FormatError(msgs)) + }) +} diff --git a/pkg/npm/npm.go b/pkg/npm/npm.go index 3899e789df..5709cfee1c 100644 --- a/pkg/npm/npm.go +++ b/pkg/npm/npm.go @@ -1,32 +1,213 @@ package npm import ( + "bufio" + "encoding/base64" "encoding/json" "fmt" "log/slog" "net/http" + "net/url" "os" + "path/filepath" + "regexp" + "strings" "github.com/sst/sst/v3/internal/fs" ) +var envVarRegex = regexp.MustCompile(`\$\{([^}]+)\}`) +var authTokenRegex = regexp.MustCompile(`^(//.+?):_authToken\s*=\s*(.+)$`) +var authRegex = regexp.MustCompile(`^(//.+?):_auth\s*=\s*(.+)$`) +var usernameRegex = regexp.MustCompile(`^(//.+?):username\s*=\s*(.+)$`) +var passwordRegex = regexp.MustCompile(`^(//.+?):_password\s*=\s*(.+)$`) +var registryRegex = regexp.MustCompile(`^registry\s*=\s*(.+)$`) + +type Auth struct { + token string + scheme string // "Bearer" or "Basic" +} + +type npmrc struct { + registry string + auths map[string]Auth +} + +func expandEnvVars(s string) string { + return envVarRegex.ReplaceAllStringFunc(s, func(match string) string { + return os.Getenv(envVarRegex.FindStringSubmatch(match)[1]) + }) +} + +func parseNpmrc(content string) npmrc { + result := npmrc{auths: make(map[string]Auth)} + usernames := make(map[string]string) + passwords := make(map[string]string) + + scanner := bufio.NewScanner(strings.NewReader(content)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + if matches := authTokenRegex.FindStringSubmatch(line); matches != nil { + result.auths[matches[1]] = Auth{ + token: strings.TrimSpace(expandEnvVars(matches[2])), + scheme: "Bearer", + } + continue + } + if matches := authRegex.FindStringSubmatch(line); matches != nil { + result.auths[matches[1]] = Auth{ + token: strings.TrimSpace(expandEnvVars(matches[2])), + scheme: "Basic", + } + continue + } + if matches := usernameRegex.FindStringSubmatch(line); matches != nil { + usernames[matches[1]] = strings.TrimSpace(expandEnvVars(matches[2])) + continue + } + if matches := passwordRegex.FindStringSubmatch(line); matches != nil { + passwords[matches[1]] = strings.TrimSpace(expandEnvVars(matches[2])) + continue + } + if matches := registryRegex.FindStringSubmatch(line); matches != nil { + result.registry = strings.TrimSpace(matches[1]) + } + } + + // username + _password produces Basic auth (password is base64-encoded in .npmrc) + for host, username := range usernames { + if _, exists := result.auths[host]; exists { + continue + } + if password, ok := passwords[host]; ok { + decoded, err := base64.StdEncoding.DecodeString(password) + if err != nil { + continue + } + result.auths[host] = Auth{ + token: base64.StdEncoding.EncodeToString([]byte(username + ":" + string(decoded))), + scheme: "Basic", + } + } + } + + return result +} + +type Registry struct { + url string + auth Auth +} + +func LoadRegistry() Registry { + var merged npmrc + merged.auths = make(map[string]Auth) + + merge := func(path string) { + data, err := os.ReadFile(path) + if err != nil { + return + } + rc := parseNpmrc(string(data)) + if rc.registry != "" { + merged.registry = rc.registry + } + for k, v := range rc.auths { + merged.auths[k] = v + } + } + + // Load from ~/.npmrc + if home, err := os.UserHomeDir(); err == nil { + merge(filepath.Join(home, ".npmrc")) + } + + // Load from project .npmrc (overrides home) + if npmrcPath, err := fs.FindUp(".", ".npmrc"); err == nil { + merge(npmrcPath) + } + + // NPM_REGISTRY env var takes priority + registry := os.Getenv("NPM_REGISTRY") + if registry == "" { + registry = merged.registry + } + if registry == "" { + registry = "https://registry.npmjs.org" + } + + auth := findAuth(registry, merged.auths) + + return Registry{url: registry, auth: auth} +} + +// findAuth walks up the registry URL path to find the longest matching +// auth key, mirroring npm's regFromURI behavior. +// e.g. for https://registry.example.com/some/path it checks: +// +// //registry.example.com/some/path +// //registry.example.com/some/ +// //registry.example.com/some +// //registry.example.com/ +// //registry.example.com +func findAuth(registryURL string, auths map[string]Auth) Auth { + parsed, err := url.Parse(registryURL) + if err != nil { + return Auth{} + } + path := parsed.Path + if !strings.HasSuffix(path, "/") { + path += "/" + } + regKey := "//" + parsed.Host + path + for len(regKey) > len("//") { + if auth, ok := auths[regKey]; ok { + return auth + } + if regKey[len(regKey)-1] == '/' { + // remove trailing slash: //host/path/ -> //host/path + regKey = regKey[:len(regKey)-1] + } else { + // remove last segment: //host/path -> //host/ + i := strings.LastIndex(regKey, "/") + if i <= 1 { + break + } + regKey = regKey[:i+1] + } + } + return Auth{} +} + type Package struct { Name string `json:"name"` Version string `json:"version"` Pulumi *struct { - Name string `json:"name"` - Version string `json:"version"` + Name string `json:"name"` + Version string `json:"version"` + Parameterization *struct { + Name string `json:"name"` + } `json:"parameterization"` } } -func Get(name string, version string) (*Package, error) { +func Get(registry Registry, name string, version string) (*Package, error) { slog.Info("getting package", "name", name, "version", version) - baseUrl := os.Getenv("NPM_REGISTRY") - if baseUrl == "" { - baseUrl = "https://registry.npmjs.org" + pkgURL := fmt.Sprintf("%s/%s/%s", registry.url, name, version) + + req, err := http.NewRequest("GET", pkgURL, nil) + if err != nil { + return nil, err } - url := fmt.Sprintf("%s/%s/%s", baseUrl, name, version) - resp, err := http.Get(url) + + if registry.auth.token != "" { + req.Header.Set("Authorization", registry.auth.scheme+" "+registry.auth.token) + } + + resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } diff --git a/pkg/npm/npm_test.go b/pkg/npm/npm_test.go new file mode 100644 index 0000000000..538b84f40a --- /dev/null +++ b/pkg/npm/npm_test.go @@ -0,0 +1,269 @@ +package npm + +import ( + "encoding/base64" + "os" + "path/filepath" + "testing" +) + +func TestParseNpmrc(t *testing.T) { + t.Setenv("MY_TOKEN", "env-token") + + input := `# comment +//registry.npmjs.org/:_authToken=hardcoded-token +//custom.registry.com/:_authToken=${MY_TOKEN} + +registry=https://custom.registry.com +not-a-token-line=value +//another.host/:_authToken=plain +` + rc := parseNpmrc(input) + + if rc.registry != "https://custom.registry.com" { + t.Errorf("registry = %q, want %q", rc.registry, "https://custom.registry.com") + } + + wantAuths := map[string]Auth{ + "//registry.npmjs.org/": {token: "hardcoded-token", scheme: "Bearer"}, + "//custom.registry.com/": {token: "env-token", scheme: "Bearer"}, + "//another.host/": {token: "plain", scheme: "Bearer"}, + } + for key, want := range wantAuths { + got := rc.auths[key] + if got.token != want.token || got.scheme != want.scheme { + t.Errorf("auths[%q] = %+v, want %+v", key, got, want) + } + } + if len(rc.auths) != len(wantAuths) { + t.Errorf("len(auths) = %d, want %d", len(rc.auths), len(wantAuths)) + } +} + +func TestParseNpmrcBasicAuth(t *testing.T) { + input := `//private.registry.com/:_auth=dXNlcjpwYXNz +//token.registry.com/:_authToken=my-bearer-token +` + rc := parseNpmrc(input) + + if got := rc.auths["//private.registry.com/"]; got.token != "dXNlcjpwYXNz" || got.scheme != "Basic" { + t.Errorf("basic auth = %+v, want {token: dXNlcjpwYXNz, scheme: Basic}", got) + } + if got := rc.auths["//token.registry.com/"]; got.token != "my-bearer-token" || got.scheme != "Bearer" { + t.Errorf("bearer auth = %+v, want {token: my-bearer-token, scheme: Bearer}", got) + } +} + +func TestParseNpmrcWhitespaceAroundEquals(t *testing.T) { + input := `//registry.example.com/:_authToken = spaced-token +//basic.example.com/:_auth = base64value +` + rc := parseNpmrc(input) + + if got := rc.auths["//registry.example.com/"]; got.token != "spaced-token" || got.scheme != "Bearer" { + t.Errorf("spaced bearer = %+v, want {token: spaced-token, scheme: Bearer}", got) + } + if got := rc.auths["//basic.example.com/"]; got.token != "base64value" || got.scheme != "Basic" { + t.Errorf("spaced basic = %+v, want {token: base64value, scheme: Basic}", got) + } +} + +func TestParseNpmrcEmpty(t *testing.T) { + rc := parseNpmrc("") + if len(rc.auths) != 0 { + t.Errorf("expected no auths, got %v", rc.auths) + } + if rc.registry != "" { + t.Errorf("expected empty registry, got %q", rc.registry) + } +} + +func TestParseNpmrcUnsetEnvVar(t *testing.T) { + rc := parseNpmrc("//host/:_authToken=${UNSET_VAR_99999}") + if got := rc.auths["//host/"]; got.token != "" { + t.Errorf("unset env var token = %q, want empty", got.token) + } +} + +func TestParseNpmrcUsernamePassword(t *testing.T) { + b64Pass := base64.StdEncoding.EncodeToString([]byte("s3cret")) + input := "//private.registry.com/:username=myuser\n//private.registry.com/:_password=" + b64Pass + "\n" + rc := parseNpmrc(input) + + got := rc.auths["//private.registry.com/"] + wantToken := base64.StdEncoding.EncodeToString([]byte("myuser:s3cret")) + if got.token != wantToken || got.scheme != "Basic" { + t.Errorf("username/password auth = %+v, want {token: %s, scheme: Basic}", got, wantToken) + } +} + +func TestParseNpmrcUsernamePasswordSkippedWhenAuthTokenExists(t *testing.T) { + b64Pass := base64.StdEncoding.EncodeToString([]byte("pass")) + input := "//host.com/:_authToken=my-token\n//host.com/:username=user\n//host.com/:_password=" + b64Pass + "\n" + rc := parseNpmrc(input) + + got := rc.auths["//host.com/"] + if got.token != "my-token" || got.scheme != "Bearer" { + t.Errorf("should prefer _authToken, got %+v", got) + } +} + +func TestParseNpmrcRegistryOnly(t *testing.T) { + rc := parseNpmrc("registry=https://my.registry.io") + if rc.registry != "https://my.registry.io" { + t.Errorf("registry = %q, want %q", rc.registry, "https://my.registry.io") + } + if len(rc.auths) != 0 { + t.Errorf("expected no auths, got %v", rc.auths) + } +} + +func TestParseNpmrcUsernameWithoutPassword(t *testing.T) { + rc := parseNpmrc("//host.com/:username=lonely-user\n") + if len(rc.auths) != 0 { + t.Errorf("username without password should produce no auth, got %v", rc.auths) + } +} + +func TestParseNpmrcPasswordWithoutUsername(t *testing.T) { + b64Pass := base64.StdEncoding.EncodeToString([]byte("orphan")) + rc := parseNpmrc("//host.com/:_password=" + b64Pass + "\n") + if len(rc.auths) != 0 { + t.Errorf("password without username should produce no auth, got %v", rc.auths) + } +} + +func TestParseNpmrcInvalidBase64Password(t *testing.T) { + rc := parseNpmrc("//host.com/:username=user\n//host.com/:_password=%%%not-base64\n") + if len(rc.auths) != 0 { + t.Errorf("invalid base64 password should be skipped, got %v", rc.auths) + } +} + +func TestParseNpmrcBasicAuthOverridesUsernamePassword(t *testing.T) { + b64Pass := base64.StdEncoding.EncodeToString([]byte("pass")) + input := "//host.com/:_auth=direct-basic\n//host.com/:username=user\n//host.com/:_password=" + b64Pass + "\n" + rc := parseNpmrc(input) + + got := rc.auths["//host.com/"] + if got.token != "direct-basic" || got.scheme != "Basic" { + t.Errorf("should prefer _auth, got %+v", got) + } +} + +func TestParseNpmrcRegistryWithTrailingSlash(t *testing.T) { + rc := parseNpmrc("registry=https://my.registry.io/") + if rc.registry != "https://my.registry.io/" { + t.Errorf("registry = %q, want %q", rc.registry, "https://my.registry.io/") + } +} + +func TestParseNpmrcRegistryWithWhitespace(t *testing.T) { + rc := parseNpmrc("registry = https://my.registry.io") + if rc.registry != "https://my.registry.io" { + t.Errorf("registry = %q, want %q", rc.registry, "https://my.registry.io") + } +} + +func TestFindAuth(t *testing.T) { + auths := map[string]Auth{ + "//registry.example.com/": {token: "host-token", scheme: "Bearer"}, + } + got := findAuth("https://registry.example.com", auths) + if got.token != "host-token" || got.scheme != "Bearer" { + t.Errorf("host match = %+v, want {token: host-token, scheme: Bearer}", got) + } +} + +func TestFindAuthPathMatch(t *testing.T) { + auths := map[string]Auth{ + "//registry.example.com/custom/path/": {token: "path-token", scheme: "Bearer"}, + "//registry.example.com/": {token: "host-token", scheme: "Bearer"}, + } + // should match the longer path first + got := findAuth("https://registry.example.com/custom/path", auths) + if got.token != "path-token" { + t.Errorf("path match = %+v, want path-token", got) + } + // different path should fall back to host + got = findAuth("https://registry.example.com/other", auths) + if got.token != "host-token" { + t.Errorf("fallback match = %+v, want host-token", got) + } +} + +func TestFindAuthNoMatch(t *testing.T) { + auths := map[string]Auth{ + "//other.host/": {token: "other-token", scheme: "Bearer"}, + } + got := findAuth("https://registry.example.com", auths) + if got.token != "" { + t.Errorf("no match should return empty, got %+v", got) + } +} + +func TestFindAuthWithPort(t *testing.T) { + auths := map[string]Auth{ + "//registry.example.com:8080/": {token: "port-token", scheme: "Bearer"}, + } + got := findAuth("https://registry.example.com:8080", auths) + if got.token != "port-token" { + t.Errorf("port match = %+v, want port-token", got) + } +} + +func TestFindAuthRegistryWithTrailingSlash(t *testing.T) { + auths := map[string]Auth{ + "//registry.example.com/": {token: "slash-token", scheme: "Bearer"}, + } + got := findAuth("https://registry.example.com/", auths) + if got.token != "slash-token" { + t.Errorf("trailing slash = %+v, want slash-token", got) + } +} + +func TestFindAuthEmptyAuths(t *testing.T) { + got := findAuth("https://registry.example.com", map[string]Auth{}) + if got.token != "" { + t.Errorf("empty auths should return empty, got %+v", got) + } +} + +func TestLoadRegistryDefaults(t *testing.T) { + tmp := t.TempDir() + t.Setenv("HOME", tmp) + t.Setenv("NPM_REGISTRY", "") + + origDir, _ := os.Getwd() + os.Chdir(tmp) + defer os.Chdir(origDir) + + reg := LoadRegistry() + if reg.url != "https://registry.npmjs.org" { + t.Errorf("default registry = %q, want https://registry.npmjs.org", reg.url) + } + if reg.auth.token != "" { + t.Errorf("default auth should be empty, got %+v", reg.auth) + } +} + +func TestLoadRegistryNpmRegistryEnvVar(t *testing.T) { + tmp := t.TempDir() + t.Setenv("HOME", tmp) + t.Setenv("NPM_REGISTRY", "https://env.registry.io") + + // write an .npmrc with a different registry to confirm env wins + os.WriteFile(filepath.Join(tmp, ".npmrc"), []byte("registry=https://file.registry.io\n//env.registry.io/:_authToken=env-tok\n"), 0644) + + origDir, _ := os.Getwd() + os.Chdir(tmp) + defer os.Chdir(origDir) + + reg := LoadRegistry() + if reg.url != "https://env.registry.io" { + t.Errorf("env registry = %q, want https://env.registry.io", reg.url) + } + if reg.auth.token != "env-tok" { + t.Errorf("env auth token = %q, want env-tok", reg.auth.token) + } +} diff --git a/pkg/process/detach_unix.go b/pkg/process/detach_unix.go index 7b16c5dd61..7736124d7d 100644 --- a/pkg/process/detach_unix.go +++ b/pkg/process/detach_unix.go @@ -16,3 +16,10 @@ func Detach(cmd *exec.Cmd) { // clobbering any other existing SysProcAttr fields set by the caller. cmd.SysProcAttr.Setpgid = true } + +func DetachSession(cmd *exec.Cmd) { + if cmd.SysProcAttr == nil { + cmd.SysProcAttr = &syscall.SysProcAttr{} + } + cmd.SysProcAttr.Setsid = true +} diff --git a/pkg/process/detach_windows.go b/pkg/process/detach_windows.go index 973194f206..4ae340be55 100644 --- a/pkg/process/detach_windows.go +++ b/pkg/process/detach_windows.go @@ -7,3 +7,6 @@ import "os/exec" func Detach(cmd *exec.Cmd) { } + +func DetachSession(cmd *exec.Cmd) { +} diff --git a/pkg/process/kill_unix_test.go b/pkg/process/kill_unix_test.go index f2c3ddcbe2..1b008cd2db 100644 --- a/pkg/process/kill_unix_test.go +++ b/pkg/process/kill_unix_test.go @@ -42,6 +42,35 @@ func TestKillTerminatesProcessGroup(t *testing.T) { } } +func TestKillTerminatesSessionGroup(t *testing.T) { + reset() + cmd := Command("sh", "-c", "sleep 60 & wait") + DetachSession(cmd) + if err := cmd.Start(); err != nil { + t.Fatalf("failed to start: %v", err) + } + parentPid := cmd.Process.Pid + + time.Sleep(100 * time.Millisecond) + + // with Setsid the pgid equals the parent pid, same as Setpgid + childPid := findChildInGroup(t, parentPid) + + if err := Kill(cmd.Process); err != nil { + t.Fatalf("Kill returned error: %v", err) + } + + time.Sleep(100 * time.Millisecond) + if err := syscall.Kill(parentPid, 0); err == nil { + t.Error("parent still alive after Kill") + } + if childPid != 0 { + if err := syscall.Kill(childPid, 0); err == nil { + t.Error("child still alive after group Kill") + } + } +} + func TestSendSignalInvalidPid(t *testing.T) { p := &os.Process{} // Pid 0 should be rejected diff --git a/pkg/project/add.go b/pkg/project/add.go index 10a84556eb..fb447c2d66 100644 --- a/pkg/project/add.go +++ b/pkg/project/add.go @@ -9,12 +9,15 @@ import ( "strings" ) -func (p *Project) Add(pkg string, version string) error { +func (p *Project) Add(provider string, version string, pkg string) error { var stderr bytes.Buffer - cmd := process.Command("node", filepath.Join(p.PathPlatformDir(), "src/ast/add.mjs"), + cmd := process.Command("node", + "--no-warnings", + filepath.Join(p.PathPlatformDir(), "src/ast/add.mjs"), p.PathConfig(), - pkg, + provider, version, + pkg, ) cmd.Stdout = os.Stdout cmd.Stderr = &stderr diff --git a/pkg/project/completed.go b/pkg/project/completed.go index 7a7d25871b..380c5940c2 100644 --- a/pkg/project/completed.go +++ b/pkg/project/completed.go @@ -13,7 +13,7 @@ import ( ) func (p *Project) GetCompleted(ctx context.Context) (*CompleteEvent, error) { - passphrase, err := provider.Passphrase(p.home, p.app.Name, p.app.Stage) + passphrase, err := provider.GetPassphrase(p.home, p.app.Name, p.app.Stage) if err != nil { return nil, err } diff --git a/pkg/project/create.go b/pkg/project/create.go index 06c86c610d..6fd05e8266 100644 --- a/pkg/project/create.go +++ b/pkg/project/create.go @@ -146,7 +146,8 @@ func Create(templateName string, home string) ([]string, error) { version := npmStep.Version if version == "" { slog.Info("fetching latest version", "package", npmStep.Package) - data, err := npm.Get(npmStep.Package, "latest") + registry := npm.LoadRegistry() + data, err := npm.Get(registry, npmStep.Package, "latest") if err != nil { return nil, err } diff --git a/pkg/project/env.go b/pkg/project/env.go index d24546c1f8..cc18c38704 100644 --- a/pkg/project/env.go +++ b/pkg/project/env.go @@ -5,6 +5,8 @@ import ( "encoding/json" "fmt" "log/slog" + "os" + "runtime" awssdk "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/sts" @@ -38,14 +40,39 @@ func (p *Project) EnvFor(ctx context.Context, complete *CompleteEvent, name stri } } log.Info("dev", "links", dev.Links) - for _, resource := range dev.Links { - value := complete.Links[resource].Properties - jsonValue, _ := json.Marshal(value) - env["SST_RESOURCE_"+resource] = string(jsonValue) + // On Windows, always use a consolidated environment variable because + // Windows uppercases all environment variable names, breaking + // case-sensitive resource lookups. + if runtime.GOOS == "windows" { + allResources := make(map[string]any) + for _, resource := range dev.Links { + allResources[resource] = complete.Links[resource].Properties + } + allResources["App"] = map[string]string{ + "name": p.App().Name, + "stage": p.App().Stage, + } + jsonData, _ := json.Marshal(allResources) + env["SST_RESOURCES_JSON"] = string(jsonData) + } else { + for _, resource := range dev.Links { + value := complete.Links[resource].Properties + jsonValue, _ := json.Marshal(value) + env["SST_RESOURCE_"+resource] = string(jsonValue) + } + env["SST_RESOURCE_App"] = fmt.Sprintf(`{"name": "%s", "stage": "%s" }`, p.App().Name, p.App().Stage) } - env["SST_RESOURCE_App"] = fmt.Sprintf(`{"name": "%s", "stage": "%s" }`, p.App().Name, p.App().Stage) for key, value := range dev.Environment { env[key] = value } + if dev.Cloudflare != nil && dev.Cloudflare.Path != "" { + env["SST_WRANGLER_PATH"] = dev.Cloudflare.Path + } + // Pass Cloudflare credentials to the child process for remote bindings + for _, key := range []string{"CLOUDFLARE_API_TOKEN", "CLOUDFLARE_API_KEY", "CLOUDFLARE_EMAIL", "CLOUDFLARE_DEFAULT_ACCOUNT_ID"} { + if val := os.Getenv(key); val != "" { + env[key] = val + } + } return env, nil } diff --git a/pkg/project/env_test.go b/pkg/project/env_test.go new file mode 100644 index 0000000000..376e069f28 --- /dev/null +++ b/pkg/project/env_test.go @@ -0,0 +1,84 @@ +package project + +import ( + "context" + "encoding/json" + "testing" + + "github.com/sst/sst/v3/pkg/project/common" +) + +func TestEnvForIncludesWranglerPath(t *testing.T) { + project := &Project{ + app: &App{ + Name: "my-app", + Stage: "test", + }, + } + + complete := &CompleteEvent{ + Devs: Devs{ + "MyWeb": { + Name: "MyWeb", + Environment: map[string]string{"FOO": "bar"}, + Links: []string{"MyKv"}, + Cloudflare: &DevCloudflare{ + Path: "/tmp/MyWeb.jsonc", + }, + }, + }, + Links: common.Links{ + "MyKv": { + Properties: map[string]interface{}{ + "namespaceId": "1234", + }, + }, + }, + } + + env, err := project.EnvFor(context.Background(), complete, "MyWeb") + if err != nil { + t.Fatal(err) + } + + if got := env["SST_WRANGLER_PATH"]; got != "/tmp/MyWeb.jsonc" { + t.Fatalf("expected SST_WRANGLER_PATH to be set, got %q", got) + } + if got := env["FOO"]; got != "bar" { + t.Fatalf("expected FOO to be preserved, got %q", got) + } + + var resource map[string]interface{} + if err := json.Unmarshal([]byte(env["SST_RESOURCE_MyKv"]), &resource); err != nil { + t.Fatal(err) + } + if got := resource["namespaceId"]; got != "1234" { + t.Fatalf("expected linked resource data, got %#v", resource) + } +} + +func TestEnvForOmitsWranglerPathWhenMissing(t *testing.T) { + project := &Project{ + app: &App{ + Name: "my-app", + Stage: "test", + }, + } + + complete := &CompleteEvent{ + Devs: Devs{ + "MyWeb": { + Name: "MyWeb", + }, + }, + } + + env, err := project.EnvFor(context.Background(), complete, "MyWeb") + if err != nil { + t.Fatal(err) + } + + if _, ok := env["SST_WRANGLER_PATH"]; ok { + t.Fatalf("expected SST_WRANGLER_PATH to be omitted") + } +} diff --git a/pkg/project/install.go b/pkg/project/install.go index 07f1073692..33ed05970c 100644 --- a/pkg/project/install.go +++ b/pkg/project/install.go @@ -2,7 +2,6 @@ package project import ( "encoding/json" - "errors" "fmt" "io" "log/slog" @@ -31,6 +30,9 @@ func (err *ErrProviderVersionTooLow) Error() string { } func (p *Project) NeedsInstall() bool { + if _, err := os.Stat(filepath.Join(p.PathPlatformDir(), "node_modules")); err != nil { + return true + } if len(p.app.Providers) != len(p.lock) { return true } @@ -40,6 +42,10 @@ func (p *Project) NeedsInstall() bool { return false } config := match.(map[string]interface{}) + pkg := config["package"] + if pkg != nil && pkg != "" && pkg != entry.Package { + return true + } version := config["version"] if version == nil || version == "" { continue @@ -156,7 +162,11 @@ func (p *Project) writeTypes() error { file.WriteString(` interface Providers {` + "\n") file.WriteString(` providers?: {` + "\n") for _, entry := range p.lock { - file.WriteString(` "` + entry.Name + `"?: (_` + entry.Alias + `.ProviderArgs & { version?: string }) | boolean | string;` + "\n") + versionField := "version: string" + if entry.Name == "aws" || entry.Name == "cloudflare" { + versionField = "version?: string" + } + file.WriteString(` "` + entry.Name + `"?: (_` + entry.Alias + `.ProviderArgs & { package?: string, ` + versionField + ` }) | string;` + "\n") } file.WriteString(` }` + "\n") file.WriteString(` }` + "\n") @@ -180,7 +190,7 @@ func (p *Project) fetchDeps() error { cmd.Dir = p.PathPlatformDir() output, err := cmd.CombinedOutput() if err != nil { - return errors.New("failed to run bun install " + string(output)) + return fmt.Errorf("failed to run %s install: %w\n%s", manager, err, output) } return nil } @@ -218,12 +228,17 @@ func (p *Project) generateProviderLock() error { } for name, config := range p.app.Providers { n := name + // Use the explicit package name if set, otherwise default to the provider name + pkgName := config.(map[string]interface{})["package"] + if pkgName == nil || pkgName == "" { + pkgName = n + } version := config.(map[string]interface{})["version"] if version == nil || version == "" { version = "latest" } wg.Go(func() error { - result, err := FindProvider(n, version.(string)) + result, err := FindProvider(n, version.(string), pkgName.(string)) if err != nil { return err } @@ -257,32 +272,37 @@ func (p *Project) generateProviderLock() error { return nil } -func FindProvider(name string, version string) (*ProviderLockEntry, error) { +func FindProvider(provider string, version string, pkg string) (*ProviderLockEntry, error) { + registry := npm.LoadRegistry() for _, prefix := range []string{"@sst-provider/", "@pulumi/", "@pulumiverse/", "pulumi-", "@", ""} { - pkg, err := npm.Get(prefix+name, version) + npmPkg, err := npm.Get(registry, prefix+pkg, version) if err != nil { continue } - if pkg.Pulumi == nil { + if npmPkg.Pulumi == nil { continue } - alias := pkg.Pulumi.Name + alias := npmPkg.Pulumi.Name if alias == "" || alias == "terraform-provider" { - alias = pkg.Name - alias = strings.ReplaceAll(alias, "@sst-provider", "") - alias = strings.ReplaceAll(alias, "/", "") - alias = strings.ReplaceAll(alias, "@", "") - alias = strings.ReplaceAll(alias, "pulumi", "") + if npmPkg.Pulumi.Parameterization != nil && npmPkg.Pulumi.Parameterization.Name != "" { + alias = npmPkg.Pulumi.Parameterization.Name + } else { + alias = npmPkg.Name + alias = strings.ReplaceAll(alias, "@sst-provider", "") + alias = strings.ReplaceAll(alias, "/", "") + alias = strings.ReplaceAll(alias, "@", "") + alias = strings.ReplaceAll(alias, "pulumi", "") + } } alias = strings.ReplaceAll(alias, "-", "") return &ProviderLockEntry{ - Name: name, - Package: pkg.Name, - Version: pkg.Version, + Name: provider, + Package: npmPkg.Name, + Version: npmPkg.Version, Alias: alias, }, nil } - return nil, fmt.Errorf("provider %s not found", name) + return nil, fmt.Errorf("provider %s not found", provider) } func (p *Project) writeProviderLock() error { diff --git a/pkg/project/path/path_test.go b/pkg/project/path/path_test.go new file mode 100644 index 0000000000..524b84aa43 --- /dev/null +++ b/pkg/project/path/path_test.go @@ -0,0 +1,33 @@ +package path_test + +import ( + "path/filepath" + "testing" + + "github.com/sst/sst/v3/pkg/project/path" + "github.com/stretchr/testify/assert" +) + +func TestPathResolvers(t *testing.T) { + cfgPath := filepath.Join("/foo", "bar", "sst.config.ts") + root := filepath.Join("/foo", "bar") + working := filepath.Join(root, ".sst") + + tests := []struct { + name string + fn func(string) string + expected string + }{ + {"ResolveRootDir", path.ResolveRootDir, root}, + {"ResolveWorkingDir", path.ResolveWorkingDir, working}, + {"ResolvePlatformDir", path.ResolvePlatformDir, filepath.Join(working, "platform")}, + {"ResolveLogDir", path.ResolveLogDir, filepath.Join(working, "log")}, + {"ResolveProviderLock", path.ResolveProviderLock, filepath.Join(working, "provider-lock.json")}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.fn(cfgPath)) + }) + } +} diff --git a/pkg/project/preflight.go b/pkg/project/preflight.go new file mode 100644 index 0000000000..f4a7ab26d8 --- /dev/null +++ b/pkg/project/preflight.go @@ -0,0 +1,95 @@ +package project + +import ( + "strings" + + "github.com/Masterminds/semver/v3" + "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" +) + +type migrationNotice struct { + providerName string + fromVersion string + toVersion string + message string +} + +var migrationNotices = []migrationNotice{ + { + providerName: "aws", + fromVersion: "6.0.0", + toVersion: "7.0.0", + message: "Detected AWS provider upgrade from v6 to v7\n\nA one-time state migration is required before you can deploy.\nSST components are already updated β€” you may be affected if you\nuse transforms or the AWS provider directly.\n\n1. Run `sst diff` to preview changes\n2. Run `sst refresh` to migrate state (repeat for each stage)\n3. Run `sst deploy`\n\nIf you share resources across stages, refresh the stage where the\nresource is created first, not the one referencing it via get().\n\nMigration guide: https://sst.dev/docs/migrate-from-v3", + }, +} + +func (p *Project) checkProviderUpgrade(resources []apitype.ResourceV3) []string { + providerVersions := make(map[string]string) + + // We iterate backwards over the slice b/c when multiple provider versions are present (i.e. just after refreshing but before deploying) + // the old provider version appears at the end of the array, and we want to override the version checkpoint with the new version. + for i := len(resources) - 1; i >= 0; i-- { + v := resources[i] + name := strings.TrimPrefix(string(v.Type), "pulumi:providers:") + if name == string(v.Type) { + continue + } + versionOutput, ok := v.Outputs["version"].(string) + if !ok { + continue + } + providerVersions[name] = versionOutput + } + + var messages []string + + for _, entry := range p.lock { + currentVersionStr, ok := providerVersions[entry.Name] + if !ok { + continue + } + + currentVersion, err := semver.NewVersion(currentVersionStr) + if err != nil { + continue + } + + targetVersion, err := semver.NewVersion(entry.Version) + if err != nil { + continue + } + + // Check if this is an upgrade + if !currentVersion.LessThan(targetVersion) { + continue + } + + // Check against all applicable upgrade rules + for _, rule := range migrationNotices { + if rule.providerName != entry.Name { + continue + } + + ruleFrom, err := semver.NewVersion(rule.fromVersion) + if err != nil { + continue + } + + ruleTo, err := semver.NewVersion(rule.toVersion) + if err != nil { + continue + } + + // Check if the upgrade path crosses this rule's version range + // Current version must be >= fromVersion AND < toVersion + // Target version must be >= toVersion + if currentVersion.Compare(ruleFrom) >= 0 && + currentVersion.Compare(ruleTo) < 0 && + targetVersion.Compare(ruleTo) >= 0 { + messages = append(messages, rule.message) + } + } + } + + return messages +} diff --git a/pkg/project/preflight_test.go b/pkg/project/preflight_test.go new file mode 100644 index 0000000000..ed4a20459f --- /dev/null +++ b/pkg/project/preflight_test.go @@ -0,0 +1,72 @@ +package project + +import ( + "strings" + "testing" + + "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" + "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" +) + +func makeProviderResource(provider string, version string) apitype.ResourceV3 { + return apitype.ResourceV3{ + Type: tokens.Type("pulumi:providers:" + provider), + Outputs: map[string]interface{}{ + "version": version, + }, + } +} + +func TestCheckProviderUpgrade_AWSV6ToV7(t *testing.T) { + p := &Project{ + lock: ProviderLock{ + {Name: "aws", Package: "@pulumi/aws", Alias: "aws", Version: "7.0.0"}, + }, + } + + resources := []apitype.ResourceV3{ + makeProviderResource("aws", "6.52.0"), + } + + messages := p.checkProviderUpgrade(resources) + if len(messages) != 1 { + t.Fatalf("expected 1 message, got %d", len(messages)) + } + if !strings.Contains(messages[0], "AWS") { + t.Fatalf("expected message to mention AWS, got: %s", messages[0]) + } +} + +func TestCheckProviderUpgrade_AWSAlreadyOnV7(t *testing.T) { + p := &Project{ + lock: ProviderLock{ + {Name: "aws", Package: "@pulumi/aws", Alias: "aws", Version: "7.0.0"}, + }, + } + + resources := []apitype.ResourceV3{ + makeProviderResource("aws", "7.0.0"), + } + + messages := p.checkProviderUpgrade(resources) + if len(messages) != 0 { + t.Fatalf("expected 0 messages, got %d", len(messages)) + } +} + +func TestCheckProviderUpgrade_NoMatchingProvider(t *testing.T) { + p := &Project{ + lock: ProviderLock{ + {Name: "aws", Package: "@pulumi/aws", Alias: "aws", Version: "7.0.0"}, + }, + } + + resources := []apitype.ResourceV3{ + makeProviderResource("vercel", "1.11.0"), + } + + messages := p.checkProviderUpgrade(resources) + if len(messages) != 0 { + t.Fatalf("expected 0 messages, got %d", len(messages)) + } +} diff --git a/pkg/project/project.go b/pkg/project/project.go index 06b5601d06..684882ffc7 100644 --- a/pkg/project/project.go +++ b/pkg/project/project.go @@ -35,13 +35,63 @@ type App struct { Home string `json:"home"` Version string `json:"version"` Protect bool `json:"protect"` - Watch []string `json:"watch"` + Watch Watch `json:"watch"` + State *State `json:"state"` + Types struct { + Ignore []string `json:"ignore"` + } `json:"types"` // Deprecated: Backend is now Home Backend string `json:"backend"` // Deprecated: RemovalPolicy is now Removal RemovalPolicy string `json:"removalPolicy"` } +type State struct { + Purge bool `json:"purge"` + Compress bool `json:"compress"` +} + +type Watch struct { + Paths []string `json:"paths"` + Ignore []string `json:"ignore"` + legacyArray bool +} + +func (w Watch) UsesLegacyArray() bool { + return w.legacyArray +} + +func (w *Watch) UnmarshalJSON(data []byte) error { + var paths []string + if err := json.Unmarshal(data, &paths); err == nil { + w.Paths = paths + w.Ignore = nil + w.legacyArray = true + return nil + } + + type value struct { + Paths []string `json:"paths"` + Ignore []string `json:"ignore"` + } + + var decoded value + if err := json.Unmarshal(data, &decoded); err != nil { + return err + } + + for _, path := range decoded.Paths { + if strings.ContainsAny(path, "*?[") { + return util.NewReadableError(nil, fmt.Sprintf("Watch path globs are only supported in the legacy array form: %q\nUse explicit directories or ignore patterns instead: https://sst.dev/docs/reference/config/#watch", path)) + } + } + + w.Paths = decoded.Paths + w.Ignore = decoded.Ignore + w.legacyArray = false + return nil +} + type Project struct { version string lock ProviderLock @@ -121,22 +171,25 @@ func New(input *ProjectConfig) (*Project, error) { } rootPath := filepath.Dir(input.Config) + tmp := filepath.Join(rootPath, ".sst") + + pythonRuntime := python.New() proj := &Project{ version: input.Version, root: rootPath, config: input.Config, env: map[string]string{}, - Runtime: runtime.NewCollection( - input.Config, - node.New(input.Version), - worker.New(), - python.New(), - golang.New(), - rust.New(), - ), } - tmp := proj.PathWorkingDir() + + proj.Runtime = runtime.NewCollection( + input.Config, + node.New(input.Version), + worker.New(), + pythonRuntime, + golang.New(), + rust.New(), + ) _, err := os.Stat(tmp) if err != nil { @@ -206,8 +259,10 @@ console.log("~j" + JSON.stringify(await mod.app({ } for name, args := range proj.app.Providers { - if argsBool, ok := args.(bool); ok && argsBool { - proj.app.Providers[name] = make(map[string]interface{}) + if _, ok := args.(bool); ok { + return nil, util.NewReadableError(nil, + fmt.Sprintf(`Setting providers.%s to true is deprecated. Specify the version explicitly instead.`, name), + ) } if argsString, ok := args.(string); ok { @@ -215,6 +270,14 @@ console.log("~j" + JSON.stringify(await mod.app({ "version": argsString, } } + + if argsMap, ok := args.(map[string]interface{}); ok { + if _, hasVersion := argsMap["version"]; !hasVersion && name != "aws" && name != "cloudflare" { + return nil, util.NewReadableError(nil, + fmt.Sprintf(`Provider %s is missing a version. Specify the version explicitly instead.`, name), + ) + } + } } if proj.app.Name == "" { @@ -321,13 +384,15 @@ func (proj *Project) LoadHome() error { var home provider.Home + compress := proj.app.State != nil && proj.app.State.Compress + switch proj.app.Home { case "local": home = provider.NewLocalHome() case "aws": - home = provider.NewAwsHome(loadedProviders["aws"].(*provider.AwsProvider)) + home = provider.NewAwsHome(loadedProviders["aws"].(*provider.AwsProvider), compress) case "cloudflare": - home = provider.NewCloudflareHome(loadedProviders["cloudflare"].(*provider.CloudflareProvider)) + home = provider.NewCloudflareHome(loadedProviders["cloudflare"].(*provider.CloudflareProvider), compress) default: return fmt.Errorf("Home provider %s is invalid", proj.app.Home) } diff --git a/pkg/project/provider/aws.go b/pkg/project/provider/aws.go index 6b0a0ebacd..d4843ccaf3 100644 --- a/pkg/project/provider/aws.go +++ b/pkg/project/provider/aws.go @@ -92,13 +92,17 @@ func (a *AwsProvider) Init(app string, stage string, args map[string]interface{} if err != nil { return err } - if assumeRole, ok := args["assumeRole"].(map[string]interface{}); ok { - stsclient := sts.NewFromConfig(cfg) - cfg.Credentials = stscreds.NewAssumeRoleProvider(stsclient, assumeRole["roleArn"].(string), func(aro *stscreds.AssumeRoleOptions) { - if sessionName, ok := assumeRole["sessionName"].(string); ok { - aro.RoleSessionName = sessionName + if assumeRoles, ok := args["assumeRoles"].([]interface{}); ok { + for _, role := range assumeRoles { + if roleMap, ok := role.(map[string]interface{}); ok { + stsclient := sts.NewFromConfig(cfg) + cfg.Credentials = stscreds.NewAssumeRoleProvider(stsclient, roleMap["roleArn"].(string), func(aro *stscreds.AssumeRoleOptions) { + if sessionName, ok := roleMap["sessionName"].(string); ok { + aro.RoleSessionName = sessionName + } + }) } - }) + } } _, err = cfg.Credentials.Retrieve(ctx) if err != nil { @@ -530,11 +534,13 @@ var steps = []bootstrapStep{ type AwsHome struct { provider *AwsProvider + compress bool } -func NewAwsHome(provider *AwsProvider) *AwsHome { +func NewAwsHome(provider *AwsProvider, compress bool) *AwsHome { return &AwsHome{ provider: provider, + compress: compress, } } @@ -570,7 +576,7 @@ func (a *AwsHome) getData(key, app, stage string) (io.Reader, error) { } return nil, err } - return result.Body, nil + return gzipDecode(result.Body) } func (a *AwsHome) putData(key, app, stage string, data io.Reader) error { @@ -580,11 +586,21 @@ func (a *AwsHome) putData(key, app, stage string, data io.Reader) error { } s3Client := s3.NewFromConfig(a.provider.config) + var contentEncoding *string + if a.compress { + data, err = gzipEncode(data) + if err != nil { + return err + } + contentEncoding = aws.String("gzip") + } + _, err = s3Client.PutObject(context.TODO(), &s3.PutObjectInput{ - Bucket: aws.String(bootstrap.State), - Key: aws.String(a.pathForData(key, app, stage)), - Body: data, - ContentType: aws.String("application/json"), + Bucket: aws.String(bootstrap.State), + Key: aws.String(a.pathForData(key, app, stage)), + Body: data, + ContentType: aws.String("application/json"), + ContentEncoding: contentEncoding, }) if err != nil { return err @@ -669,6 +685,79 @@ func (a *AwsHome) cleanup(key, app, stage string) error { return nil } +func (a *AwsHome) purge(app, stage string) error { + bootstrap, err := a.provider.Bootstrap(a.provider.config.Region) + if err != nil { + return err + } + s3Client := s3.NewFromConfig(a.provider.config) + + prefixes := []string{ + a.pathForData("app", app, stage), + a.pathForData("secret", app, stage), + path.Join("update", app, stage) + "/", + path.Join("summary", app, stage) + "/", + path.Join("eventlog", app, stage) + "/", + path.Join("snapshot", app, stage) + "/", + } + for _, prefix := range prefixes { + if err := a.purgePrefix(s3Client, bootstrap.State, prefix); err != nil { + return err + } + } + return nil +} + +func (a *AwsHome) purgePrefix(s3Client *s3.Client, bucket, prefix string) error { + slog.Info("purging prefix", "bucket", bucket, "prefix", prefix) + + var keyMarker, versionMarker *string + for { + out, err := s3Client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{ + Bucket: aws.String(bucket), + Prefix: aws.String(prefix), + KeyMarker: keyMarker, + VersionIdMarker: versionMarker, + }) + if err != nil { + var apiErr smithy.APIError + if errors.As(err, &apiErr) && apiErr.ErrorCode() == "NoSuchBucket" { + return nil + } + return err + } + + ids := make([]s3types.ObjectIdentifier, 0, len(out.Versions)+len(out.DeleteMarkers)) + for _, v := range out.Versions { + ids = append(ids, s3types.ObjectIdentifier{Key: v.Key, VersionId: v.VersionId}) + } + for _, dm := range out.DeleteMarkers { + ids = append(ids, s3types.ObjectIdentifier{Key: dm.Key, VersionId: dm.VersionId}) + } + + // DeleteObjects accepts at most 1000 keys per call. + for i := 0; i < len(ids); i += 1000 { + end := i + 1000 + if end > len(ids) { + end = len(ids) + } + if _, err := s3Client.DeleteObjects(context.TODO(), &s3.DeleteObjectsInput{ + Bucket: aws.String(bucket), + Delete: &s3types.Delete{Objects: ids[i:end], Quiet: aws.Bool(true)}, + }); err != nil { + return err + } + } + + if out.IsTruncated == nil || !*out.IsTruncated { + break + } + keyMarker = out.NextKeyMarker + versionMarker = out.NextVersionIdMarker + } + return nil +} + func (a *AwsHome) getPassphrase(app string, stage string) (string, error) { ssmClient := ssm.NewFromConfig(a.provider.config) @@ -687,6 +776,22 @@ func (a *AwsHome) getPassphrase(app string, stage string) (string, error) { return *result.Parameter.Value, nil } +func (a *AwsHome) removePassphrase(app, stage string) error { + ssmClient := ssm.NewFromConfig(a.provider.config) + + _, err := ssmClient.DeleteParameter(context.TODO(), &ssm.DeleteParameterInput{ + Name: aws.String(a.pathForPassphrase(app, stage)), + }) + if err != nil { + var pnf *ssmTypes.ParameterNotFound + if errors.As(err, &pnf) { + return nil + } + return err + } + return nil +} + func (a *AwsHome) setPassphrase(app, stage, passphrase string) error { ssmClient := ssm.NewFromConfig(a.provider.config) @@ -722,7 +827,9 @@ func (a *AwsHome) listStages(app string) ([]string, error) { filename := path.Base(*obj.Key) if strings.HasSuffix(filename, ".json") { stageName := strings.TrimSuffix(filename, ".json") - stages = append(stages, stageName) + if hasResources(a, app, stageName) { + stages = append(stages, stageName) + } } } diff --git a/pkg/project/provider/cloudflare.go b/pkg/project/provider/cloudflare.go index ff45bd660c..8f538c8617 100644 --- a/pkg/project/provider/cloudflare.go +++ b/pkg/project/provider/cloudflare.go @@ -81,11 +81,13 @@ type CloudflareHome struct { sync.Mutex provider *CloudflareProvider bootstrap *bootstrap + compress bool } -func NewCloudflareHome(provider *CloudflareProvider) *CloudflareHome { +func NewCloudflareHome(provider *CloudflareProvider, compress bool) *CloudflareHome { return &CloudflareHome{ provider: provider, + compress: compress, } } @@ -137,6 +139,13 @@ func (c *CloudflareHome) putData(kind, app, stage string, data io.Reader) error c.Lock() defer c.Unlock() path := filepath.Join(kind, app, stage) + if c.compress { + encoded, err := gzipEncode(data) + if err != nil { + return err + } + data = encoded + } _, err := makeRequestContext(c.provider.api, context.Background(), http.MethodPut, "/accounts/"+c.provider.identifier.Identifier+"/r2/buckets/"+c.bootstrap.State+"/objects/"+path, data) if err != nil { return err @@ -155,7 +164,7 @@ func (c *CloudflareHome) getData(kind, app, stage string) (io.Reader, error) { } return nil, err } - return bytes.NewReader(data), nil + return gzipDecode(bytes.NewReader(data)) } func (c *CloudflareHome) removeData(kind, app, stage string) error { @@ -169,6 +178,55 @@ func (c *CloudflareHome) removeData(kind, app, stage string) error { return nil } +func (c *CloudflareHome) purge(app, stage string) error { + // Single-file keys. + for _, key := range []string{"app", "secret"} { + if err := c.removeData(key, app, stage); err != nil { + return err + } + } + // Folder-style keys: list under the prefix and delete each object. + for _, key := range []string{"update", "summary", "eventlog", "snapshot"} { + if err := c.purgePrefix(filepath.Join(key, app, stage) + "/"); err != nil { + return err + } + } + return nil +} + +func (c *CloudflareHome) purgePrefix(prefix string) error { + type r2Object struct { + Key string `json:"key"` + } + type r2Response struct { + Result []r2Object `json:"result"` + } + + c.Lock() + defer c.Unlock() + + listPath := "/accounts/" + c.provider.identifier.Identifier + "/r2/buckets/" + c.bootstrap.State + "/objects?prefix=" + prefix + data, err := makeRequestContext(c.provider.api, context.Background(), http.MethodGet, listPath, nil) + if err != nil { + return err + } + var response r2Response + if err := json.Unmarshal(data, &response); err != nil { + return err + } + for _, obj := range response.Result { + _, err := makeRequestContext(c.provider.api, context.Background(), http.MethodDelete, "/accounts/"+c.provider.identifier.Identifier+"/r2/buckets/"+c.bootstrap.State+"/objects/"+obj.Key, nil) + if err != nil { + return err + } + } + return nil +} + +func (c *CloudflareHome) removePassphrase(app, stage string) error { + return c.removeData("passphrase", app, stage) +} + // these should go into secrets manager once it's out of beta func (c *CloudflareHome) setPassphrase(app, stage string, passphrase string) error { return c.putData("passphrase", app, stage, bytes.NewReader([]byte(passphrase))) @@ -219,7 +277,10 @@ func (c *CloudflareHome) listStages(app string) ([]string, error) { for _, obj := range response.Result { segments := strings.Split(obj.Key, "/") - stages = append(stages, segments[len(segments)-1]) + stageName := segments[len(segments)-1] + if hasResources(c, app, stageName) { + stages = append(stages, stageName) + } } return stages, nil diff --git a/pkg/project/provider/compress.go b/pkg/project/provider/compress.go new file mode 100644 index 0000000000..fa95b3cdbf --- /dev/null +++ b/pkg/project/provider/compress.go @@ -0,0 +1,33 @@ +package provider + +import ( + "bufio" + "bytes" + "compress/gzip" + "io" +) + +func gzipEncode(r io.Reader) (io.Reader, error) { + var buf bytes.Buffer + gw := gzip.NewWriter(&buf) + if _, err := io.Copy(gw, r); err != nil { + gw.Close() + return nil, err + } + if err := gw.Close(); err != nil { + return nil, err + } + return &buf, nil +} + +func gzipDecode(r io.Reader) (io.Reader, error) { + br := bufio.NewReader(r) + head, err := br.Peek(2) + if err != nil && err != io.EOF { + return nil, err + } + if len(head) == 2 && head[0] == 0x1f && head[1] == 0x8b { + return gzip.NewReader(br) + } + return br, nil +} diff --git a/pkg/project/provider/compress_test.go b/pkg/project/provider/compress_test.go new file mode 100644 index 0000000000..1f140ecd21 --- /dev/null +++ b/pkg/project/provider/compress_test.go @@ -0,0 +1,105 @@ +package provider + +import ( + "bytes" + "io" + "testing" +) + +func TestGzipEncodeRoundTrip(t *testing.T) { + t.Parallel() + + input := bytes.Repeat([]byte(`{"resource":"value","nested":{"enabled":true}}`), 1024) + encoded, err := gzipEncode(bytes.NewReader(input)) + if err != nil { + t.Fatalf("gzipEncode returned error: %v", err) + } + encodedBytes, err := io.ReadAll(encoded) + if err != nil { + t.Fatalf("reading encoded data failed: %v", err) + } + if len(encodedBytes) >= len(input) { + t.Fatalf("expected gzip output to be smaller than input, got raw=%d encoded=%d", len(input), len(encodedBytes)) + } + + decoded, err := gzipDecode(bytes.NewReader(encodedBytes)) + if err != nil { + t.Fatalf("gzipDecode returned error: %v", err) + } + got, err := io.ReadAll(decoded) + if err != nil { + t.Fatalf("reading decoded data failed: %v", err) + } + if !bytes.Equal(got, input) { + t.Fatal("round-tripped payload did not match original input") + } +} + +func TestGzipEncodeProducesGzipMagic(t *testing.T) { + t.Parallel() + + encoded, err := gzipEncode(bytes.NewReader([]byte("hello"))) + if err != nil { + t.Fatalf("gzipEncode returned error: %v", err) + } + encodedBytes, err := io.ReadAll(encoded) + if err != nil { + t.Fatalf("reading encoded data failed: %v", err) + } + if len(encodedBytes) < 2 || encodedBytes[0] != 0x1f || encodedBytes[1] != 0x8b { + t.Fatalf("expected output to start with gzip magic bytes, got % x", encodedBytes) + } +} + +func TestGzipEncodeEmpty(t *testing.T) { + t.Parallel() + + encoded, err := gzipEncode(bytes.NewReader(nil)) + if err != nil { + t.Fatalf("gzipEncode returned error: %v", err) + } + encodedBytes, err := io.ReadAll(encoded) + if err != nil { + t.Fatalf("reading encoded data failed: %v", err) + } + + decoded, err := gzipDecode(bytes.NewReader(encodedBytes)) + if err != nil { + t.Fatalf("gzipDecode returned error: %v", err) + } + got, err := io.ReadAll(decoded) + if err != nil { + t.Fatalf("reading decoded data failed: %v", err) + } + if len(got) != 0 { + t.Fatalf("expected empty payload, got %d bytes", len(got)) + } +} + +func TestGzipDecodePlainPassthrough(t *testing.T) { + t.Parallel() + + plain := []byte(`{"legacy":"state","compressed":false}`) + decoded, err := gzipDecode(bytes.NewReader(plain)) + if err != nil { + t.Fatalf("gzipDecode returned error: %v", err) + } + got, err := io.ReadAll(decoded) + if err != nil { + t.Fatalf("reading decoded data failed: %v", err) + } + if !bytes.Equal(got, plain) { + t.Fatal("plain payload did not pass through unchanged") + } +} + +func TestGzipDecodeInvalidGzip(t *testing.T) { + t.Parallel() + + // Starts with the gzip magic bytes but is not a valid gzip stream. + invalid := []byte{0x1f, 0x8b, 0x00, 0x00} + _, err := gzipDecode(bytes.NewReader(invalid)) + if err == nil { + t.Fatal("expected invalid gzip payload to return an error") + } +} diff --git a/pkg/project/provider/local.go b/pkg/project/provider/local.go index 8beca05a9b..9f97ebb1bb 100644 --- a/pkg/project/provider/local.go +++ b/pkg/project/provider/local.go @@ -65,6 +65,28 @@ func (l *LocalHome) removeData(key, app, stage string) error { return os.Remove(p) } +func (l *LocalHome) purge(app, stage string) error { + // Single-file keys. + for _, key := range []string{"app", "secret"} { + p := l.pathForData(key, app, stage) + if err := os.Remove(p); err != nil && !os.IsNotExist(err) { + return err + } + } + // Folder-style keys. + for _, key := range []string{"update", "summary", "eventlog", "snapshot"} { + dir := filepath.Join(global.ConfigDir(), "state", key, app, stage) + if err := os.RemoveAll(dir); err != nil { + return err + } + } + return nil +} + +func (c *LocalHome) removePassphrase(app, stage string) error { + return c.removeData("passphrase", app, stage) +} + // these should go into secrets manager once it's out of beta func (c *LocalHome) setPassphrase(app, stage string, passphrase string) error { return c.putData("passphrase", app, stage, bytes.NewReader([]byte(passphrase))) @@ -99,10 +121,9 @@ func (a *LocalHome) listStages(app string) ([]string, error) { var stages []string for _, entry := range entries { - if !entry.IsDir() { - filename := entry.Name() - if strings.HasSuffix(filename, ".json") { - stageName := strings.TrimSuffix(filename, ".json") + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".json") { + stageName := strings.TrimSuffix(entry.Name(), ".json") + if hasResources(a, app, stageName) { stages = append(stages, stageName) } } diff --git a/pkg/project/provider/provider.go b/pkg/project/provider/provider.go index 16d23254d6..eace496be7 100644 --- a/pkg/project/provider/provider.go +++ b/pkg/project/provider/provider.go @@ -12,6 +12,8 @@ import ( "os" "time" + "github.com/pulumi/pulumi/pkg/v3/resource/stack" + "github.com/pulumi/pulumi/sdk/v3/go/common/encoding" "github.com/sst/sst/v3/internal/util" "github.com/sst/sst/v3/pkg/flag" "github.com/sst/sst/v3/pkg/id" @@ -27,6 +29,8 @@ type Home interface { getPassphrase(app, stage string) (string, error) listStages(app string) ([]string, error) cleanup(key, app, stage string) error + purge(app, stage string) error + removePassphrase(app, stage string) error info() (util.KeyValuePairs[string], error) } @@ -81,7 +85,7 @@ func Copy(from Home, to Home, app, stage string) error { return nil } -func Passphrase(backend Home, app, stage string) (string, error) { +func GetPassphrase(backend Home, app, stage string) (string, error) { slog.Info("getting passphrase", "app", app, "stage", stage) cache, ok := passphraseCache[backend] @@ -100,6 +104,18 @@ func Passphrase(backend Home, app, stage string) (string, error) { return "", err } + if passphrase != "" { + cache[app+stage] = passphrase + } + return passphrase, nil +} + +func GetOrCreatePassphrase(backend Home, app, stage string) (string, error) { + passphrase, err := GetPassphrase(backend, app, stage) + if err != nil { + return "", err + } + if passphrase == "" { slog.Info("passphrase not found, setting passphrase", "app", app, "stage", stage) passphrase = flag.SST_PASSPHRASE @@ -115,9 +131,10 @@ func Passphrase(backend Home, app, stage string) (string, error) { if err != nil { return "", err } + + passphraseCache[backend][app+stage] = passphrase } - existingPassphrase, ok = cache[app+stage] return passphrase, nil } @@ -166,6 +183,20 @@ func Cleanup(backend Home, app, stage string) error { return nil } +func Purge(backend Home, app, stage string) error { + slog.Info("purging stage", "app", app, "stage", stage) + if err := backend.purge(app, stage); err != nil { + return err + } + if err := backend.removePassphrase(app, stage); err != nil { + return err + } + if cache, ok := passphraseCache[backend]; ok { + delete(cache, app+stage) + } + return nil +} + func GetSecrets(backend Home, app, stage string) (map[string]string, error) { if stage == "" { stage = "_fallback" @@ -321,13 +352,32 @@ func Info(backend Home) (util.KeyValuePairs[string], error) { return backend.info() } +func hasResources(backend Home, app, stage string) bool { + data, err := backend.getData("app", app, stage) + if err != nil { + return false + } + + bytes, err := io.ReadAll(data) + if err != nil { + return false + } + + checkpoint, _, _, err := stack.UnmarshalVersionedCheckpointToLatestCheckpoint(encoding.JSON, bytes) + if err != nil { + return false + } + + return checkpoint != nil && checkpoint.Latest != nil && len(checkpoint.Latest.Resources) > 0 +} + func putData(backend Home, key, app, stage string, encrypt bool, data interface{}) error { jsonBytes, err := json.Marshal(data) if err != nil { return err } if encrypt { - passphrase, err := Passphrase(backend, app, stage) + passphrase, err := GetOrCreatePassphrase(backend, app, stage) if err != nil { return err } @@ -368,7 +418,7 @@ func getData(backend Home, key, app, stage string, encrypted bool, out interface } if encrypted { - passphrase, err := Passphrase(backend, app, stage) + passphrase, err := GetPassphrase(backend, app, stage) if err != nil { return err } diff --git a/pkg/project/run.go b/pkg/project/run.go index caf41e24f1..eb9816a9bf 100644 --- a/pkg/project/run.go +++ b/pkg/project/run.go @@ -33,14 +33,6 @@ import ( ) func (p *Project) Run(ctx context.Context, input *StackInput) error { - // if flag.SST_EXPERIMENTAL { - // slog.Info("using next run system") - // } - // return p.RunOld(ctx, input) - return p.RunNext(ctx, input) -} - -func (p *Project) RunNext(ctx context.Context, input *StackInput) error { log := slog.Default().With("service", "project.run") log.Info("running stack command", "cmd", input.Command) @@ -78,7 +70,12 @@ func (p *Project) RunNext(ctx context.Context, input *StackInput) error { } defer workdir.Cleanup() - passphrase, err := provider.Passphrase(p.home, p.app.Name, p.app.Stage) + var passphrase string + if input.Command == "deploy" || input.Command == "diff" || input.Dev { + passphrase, err = provider.GetOrCreatePassphrase(p.home, p.app.Name, p.app.Stage) + } else { + passphrase, err = provider.GetPassphrase(p.home, p.app.Name, p.app.Stage) + } if err != nil { return err } @@ -102,7 +99,7 @@ func (p *Project) RunNext(ctx context.Context, input *StackInput) error { _, err = workdir.Pull() if err != nil { if errors.Is(err, provider.ErrStateNotFound) { - if input.Command != "deploy" { + if input.Command != "deploy" && input.Command != "diff" { return ErrStageNotFound } cmd := process.Command(global.PulumiPath(), "stack", "init", "organization/"+p.app.Name+"/"+p.app.Stage) @@ -266,7 +263,7 @@ func (p *Project) RunNext(ctx context.Context, input *StackInput) error { "PULUMI_IGNORE_AMBIENT_PLUGINS=true", // "PULUMI_DISABLE_AUTOMATIC_PLUGIN_ACQUISITION=true", "NODE_OPTIONS="+func() string { - nodeOptions := "--enable-source-maps --no-deprecation" + nodeOptions := "--enable-source-maps --no-deprecation --no-warnings" if existing := os.Getenv("NODE_OPTIONS"); existing != "" { nodeOptions = existing + " " + nodeOptions } @@ -295,9 +292,20 @@ func (p *Project) RunNext(ctx context.Context, input *StackInput) error { "--event-log", eventlogPath, } - if input.Command == "deploy" || input.Command == "diff" { + if input.Command == "deploy" { + upgradeMsgs := p.checkProviderUpgrade(completed.Resources) + if len(upgradeMsgs) > 0 { + return util.NewReadableError(nil, strings.Join(upgradeMsgs, "\n\n")) + } + } + + if input.Command == "deploy" || input.Command == "diff" || input.Command == "refresh" { for provider, opts := range p.app.Providers { for key, value := range opts.(map[string]interface{}) { + // Skip SST-only fields that Pulumi doesn't understand + if key == "package" { + continue + } switch v := value.(type) { case map[string]interface{}: bytes, err := json.Marshal(v) @@ -322,7 +330,7 @@ func (p *Project) RunNext(ctx context.Context, input *StackInput) error { case "diff": args = append([]string{"preview"}, args...) case "refresh": - args = append([]string{"refresh", "--yes"}, args...) + args = append([]string{"refresh", "--yes", "--run-program"}, args...) case "deploy": args = append([]string{"up", "--yes", "-f"}, args...) case "remove": @@ -410,7 +418,7 @@ func (p *Project) RunNext(ctx context.Context, input *StackInput) error { } exited := make(chan struct{}) go func() { - cmd.Wait() + err := cmd.Wait() log.Info("pulumi exited", "err", err) close(exited) }() @@ -509,18 +517,10 @@ loop: } if event.DiagnosticEvent != nil && event.DiagnosticEvent.Severity == "error" { - if strings.HasPrefix(event.DiagnosticEvent.Message, "update failed") || strings.Contains(event.DiagnosticEvent.Message, "failed to register new resource") { + if strings.HasPrefix(event.DiagnosticEvent.Message, "update failed") || strings.HasPrefix(event.DiagnosticEvent.Message, "update cancelled") || strings.Contains(event.DiagnosticEvent.Message, "failed to register new resource") { continue } - // check if the error is a common error - help := []string{} - for _, commonError := range CommonErrors { - if strings.Contains(event.DiagnosticEvent.Message, commonError.Message) { - help = append(help, commonError.Short...) - } - } - exists := false if event.DiagnosticEvent.URN != "" { for _, item := range errors { @@ -539,7 +539,6 @@ loop: errors = append(errors, Error{ Message: strings.TrimSpace(event.DiagnosticEvent.Message), URN: event.DiagnosticEvent.URN, - Help: help, }) log.Info("telemetry tracking error") telemetry.Track("cli.resource.error", map[string]interface{}{ @@ -581,6 +580,47 @@ loop: } } + // fallback: re-read the event log if the tailing loop missed SummaryEvent + if !finished { + if data, err := os.ReadFile(eventlogPath); err == nil { + for _, line := range strings.Split(string(data), "\n") { + if line == "" { + continue + } + var ev events.EngineEvent + if json.Unmarshal([]byte(line), &ev) == nil && ev.SummaryEvent != nil { + finished = true + break + } + } + } + } + + // if pulumi exited without completing and no resource errors were captured, + // check stderr for the actual error (e.g. snapshot integrity failures) + if !finished && len(errors) == 0 { + if data, err := os.ReadFile(pulumiStderr.Name()); err == nil { + stderr := strings.TrimSpace(string(data)) + if stderr != "" { + if strings.Contains(stderr, "snapshot integrity") { + errors = append(errors, Error{ + Message: "Your app state is corrupted.", + Help: []string{ + "Run `sst state repair` to fix state integrity issues", + "Learn more: https://sst.dev/docs/reference/cli/#state-repair", + }, + }) + } else { + msg := strings.SplitN(stderr, "\n", 2)[0] + msg = strings.TrimPrefix(msg, "error: ") + errors = append(errors, Error{ + Message: msg, + }) + } + } + } + } + log.Info("parsing state") complete, err := getCompletedEvent(context.Background(), passphrase, workdir) if err != nil { @@ -590,7 +630,7 @@ loop: complete.Finished = finished complete.Errors = errors complete.ImportDiffs = importDiffs - types.Generate(p.PathConfig(), complete.Links) + types.Generate(p.PathConfig(), complete.Links, p.App().Types.Ignore) defer bus.Publish(complete) if input.Command != "diff" { @@ -625,7 +665,15 @@ loop: } if input.Command == "remove" && len(complete.Resources) == 0 { - provider.Cleanup(p.home, p.app.Name, p.app.Stage) + if p.app.State != nil && p.app.State.Purge { + if err := provider.Purge(p.home, p.app.Name, p.app.Stage); err != nil { + return err + } + } else { + if err := provider.Cleanup(p.home, p.app.Name, p.app.Stage); err != nil { + return err + } + } } log.Info("done running stack command", "resources", len(complete.Resources)) diff --git a/pkg/project/stack.go b/pkg/project/stack.go index 0dd0100548..62dd5c5595 100644 --- a/pkg/project/stack.go +++ b/pkg/project/stack.go @@ -56,9 +56,14 @@ type Dev struct { Aws *struct { Role string `json:"role"` } `json:"aws"` + Cloudflare *DevCloudflare `json:"cloudflare"` } type Devs map[string]Dev +type DevCloudflare struct { + Path string `json:"path"` +} + type Task struct { Name string `json:"-"` Command *string `json:"command"` @@ -109,52 +114,11 @@ type Error struct { Help []string `json:"help"` } -type CommonError struct { - Code string `json:"code"` - Message string `json:"message"` - Short []string `json:"short"` - Long []string `json:"long"` -} - -var CommonErrors = []CommonError{ - { - Code: "TooManyCacheBehaviors", - Message: "TooManyCacheBehaviors: Your request contains more CacheBehaviors than are allowed per distribution", - Short: []string{ - "There are too many top-level files and directories inside your app's public asset directory. Move some of them inside subdirectories.", - "Learn more about this https://sst.dev/docs/common-errors#toomanycachebehaviors", - }, - Long: []string{ - "This error usually happens to `SvelteKit`, `SolidStart`, `Nuxt`, and `Analog` components.", - "", - "CloudFront distributions have a **limit of 25 cache behaviors** per distribution. Each top-level file or directory in your frontend app's asset directory creates a cache behavior.", - "", - "For example, in the case of SvelteKit, the static assets are in the `static/` directory. If you have a file and a directory in it, it'll create 2 cache behaviors.", - "", - "```bash frame=\"none\"", - "static/", - "β”œβ”€β”€ icons/ # Cache behavior for /icons/*", - "└── logo.png # Cache behavior for /logo.png", - "```", - "So if you have many of these at the top-level, you'll hit the limit. You can request a limit increase through the AWS Support.", - "", - "Alternatively, you can move some of these into subdirectories. For example, moving them to an `images/` directory, will only create 1 cache behavior.", - "", - "```bash frame=\"none\"", - "static/", - "└── images/ # Cache behavior for /images/*", - " β”œβ”€β”€ icons/", - " └── logo.png", - "```", - "Learn more about these [CloudFront limits](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html#limits-web-distributions).", - }, - }, -} - var ErrStackRunFailed = fmt.Errorf("stack run had errors") var ErrStageNotFound = fmt.Errorf("stage not found") var ErrPassphraseInvalid = fmt.Errorf("passphrase invalid") var ErrProtectedStage = fmt.Errorf("cannot remove protected stage") +var ErrProtectedDevStage = fmt.Errorf("cannot run sst dev on protected stage") var ErrPolicyViolation = fmt.Errorf("policy violations detected") var ErrPolicyConfigError = fmt.Errorf("policy configuration error") diff --git a/pkg/proto/test.pb.go b/pkg/proto/test.pb.go deleted file mode 100644 index 2ae2d97b28..0000000000 --- a/pkg/proto/test.pb.go +++ /dev/null @@ -1,214 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.31.0 -// protoc v4.25.1 -// source: proto/test.proto - -package proto - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type EchoRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` -} - -func (x *EchoRequest) Reset() { - *x = EchoRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_proto_test_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *EchoRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*EchoRequest) ProtoMessage() {} - -func (x *EchoRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_test_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use EchoRequest.ProtoReflect.Descriptor instead. -func (*EchoRequest) Descriptor() ([]byte, []int) { - return file_proto_test_proto_rawDescGZIP(), []int{0} -} - -func (x *EchoRequest) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -type EchoResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` -} - -func (x *EchoResponse) Reset() { - *x = EchoResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_proto_test_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *EchoResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*EchoResponse) ProtoMessage() {} - -func (x *EchoResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_test_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use EchoResponse.ProtoReflect.Descriptor instead. -func (*EchoResponse) Descriptor() ([]byte, []int) { - return file_proto_test_proto_rawDescGZIP(), []int{1} -} - -func (x *EchoResponse) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -var File_proto_test_proto protoreflect.FileDescriptor - -var file_proto_test_proto_rawDesc = []byte{ - 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x12, 0x17, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x73, - 0x73, 0x74, 0x2e, 0x69, 0x6f, 0x6e, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x22, 0x27, 0x0a, 0x0b, 0x45, - 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x22, 0x28, 0x0a, 0x0c, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x5d, - 0x0a, 0x04, 0x54, 0x65, 0x73, 0x74, 0x12, 0x55, 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x12, 0x24, - 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x73, 0x73, 0x74, 0x2e, - 0x69, 0x6f, 0x6e, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2e, 0x73, 0x73, 0x74, 0x2e, 0x69, 0x6f, 0x6e, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x45, - 0x63, 0x68, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x0b, 0x5a, - 0x09, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, -} - -var ( - file_proto_test_proto_rawDescOnce sync.Once - file_proto_test_proto_rawDescData = file_proto_test_proto_rawDesc -) - -func file_proto_test_proto_rawDescGZIP() []byte { - file_proto_test_proto_rawDescOnce.Do(func() { - file_proto_test_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_test_proto_rawDescData) - }) - return file_proto_test_proto_rawDescData -} - -var file_proto_test_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_proto_test_proto_goTypes = []interface{}{ - (*EchoRequest)(nil), // 0: github.com.sst.ion.test.EchoRequest - (*EchoResponse)(nil), // 1: github.com.sst.ion.test.EchoResponse -} -var file_proto_test_proto_depIdxs = []int32{ - 0, // 0: github.com.sst.ion.test.Test.Echo:input_type -> github.com.sst.ion.test.EchoRequest - 1, // 1: github.com.sst.ion.test.Test.Echo:output_type -> github.com.sst.ion.test.EchoResponse - 1, // [1:2] is the sub-list for method output_type - 0, // [0:1] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_proto_test_proto_init() } -func file_proto_test_proto_init() { - if File_proto_test_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_proto_test_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*EchoRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_proto_test_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*EchoResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_proto_test_proto_rawDesc, - NumEnums: 0, - NumMessages: 2, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_proto_test_proto_goTypes, - DependencyIndexes: file_proto_test_proto_depIdxs, - MessageInfos: file_proto_test_proto_msgTypes, - }.Build() - File_proto_test_proto = out.File - file_proto_test_proto_rawDesc = nil - file_proto_test_proto_goTypes = nil - file_proto_test_proto_depIdxs = nil -} diff --git a/pkg/proto/test.twirp.go b/pkg/proto/test.twirp.go deleted file mode 100644 index 452d0b89d7..0000000000 --- a/pkg/proto/test.twirp.go +++ /dev/null @@ -1,1101 +0,0 @@ -// Code generated by protoc-gen-twirp v8.1.3, DO NOT EDIT. -// source: proto/test.proto - -package proto - -import context "context" -import fmt "fmt" -import http "net/http" -import io "io" -import json "encoding/json" -import strconv "strconv" -import strings "strings" - -import protojson "google.golang.org/protobuf/encoding/protojson" -import proto "google.golang.org/protobuf/proto" -import twirp "github.com/twitchtv/twirp" -import ctxsetters "github.com/twitchtv/twirp/ctxsetters" - -import bytes "bytes" -import errors "errors" -import path "path" -import url "net/url" - -// Version compatibility assertion. -// If the constant is not defined in the package, that likely means -// the package needs to be updated to work with this generated code. -// See https://twitchtv.github.io/twirp/docs/version_matrix.html -const _ = twirp.TwirpPackageMinVersion_8_1_0 - -// ============== -// Test Interface -// ============== - -type Test interface { - Echo(context.Context, *EchoRequest) (*EchoResponse, error) -} - -// ==================== -// Test Protobuf Client -// ==================== - -type testProtobufClient struct { - client HTTPClient - urls [1]string - interceptor twirp.Interceptor - opts twirp.ClientOptions -} - -// NewTestProtobufClient creates a Protobuf client that implements the Test interface. -// It communicates using Protobuf and can be configured with a custom HTTPClient. -func NewTestProtobufClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) Test { - if c, ok := client.(*http.Client); ok { - client = withoutRedirects(c) - } - - clientOpts := twirp.ClientOptions{} - for _, o := range opts { - o(&clientOpts) - } - - // Using ReadOpt allows backwards and forwards compatibility with new options in the future - literalURLs := false - _ = clientOpts.ReadOpt("literalURLs", &literalURLs) - var pathPrefix string - if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { - pathPrefix = "/twirp" // default prefix - } - - // Build method URLs: []/./ - serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(pathPrefix, "github.com.sst.ion.test", "Test") - urls := [1]string{ - serviceURL + "Echo", - } - - return &testProtobufClient{ - client: client, - urls: urls, - interceptor: twirp.ChainInterceptors(clientOpts.Interceptors...), - opts: clientOpts, - } -} - -func (c *testProtobufClient) Echo(ctx context.Context, in *EchoRequest) (*EchoResponse, error) { - ctx = ctxsetters.WithPackageName(ctx, "github.com.sst.ion.test") - ctx = ctxsetters.WithServiceName(ctx, "Test") - ctx = ctxsetters.WithMethodName(ctx, "Echo") - caller := c.callEcho - if c.interceptor != nil { - caller = func(ctx context.Context, req *EchoRequest) (*EchoResponse, error) { - resp, err := c.interceptor( - func(ctx context.Context, req interface{}) (interface{}, error) { - typedReq, ok := req.(*EchoRequest) - if !ok { - return nil, twirp.InternalError("failed type assertion req.(*EchoRequest) when calling interceptor") - } - return c.callEcho(ctx, typedReq) - }, - )(ctx, req) - if resp != nil { - typedResp, ok := resp.(*EchoResponse) - if !ok { - return nil, twirp.InternalError("failed type assertion resp.(*EchoResponse) when calling interceptor") - } - return typedResp, err - } - return nil, err - } - } - return caller(ctx, in) -} - -func (c *testProtobufClient) callEcho(ctx context.Context, in *EchoRequest) (*EchoResponse, error) { - out := new(EchoResponse) - ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out) - if err != nil { - twerr, ok := err.(twirp.Error) - if !ok { - twerr = twirp.InternalErrorWith(err) - } - callClientError(ctx, c.opts.Hooks, twerr) - return nil, err - } - - callClientResponseReceived(ctx, c.opts.Hooks) - - return out, nil -} - -// ================ -// Test JSON Client -// ================ - -type testJSONClient struct { - client HTTPClient - urls [1]string - interceptor twirp.Interceptor - opts twirp.ClientOptions -} - -// NewTestJSONClient creates a JSON client that implements the Test interface. -// It communicates using JSON and can be configured with a custom HTTPClient. -func NewTestJSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) Test { - if c, ok := client.(*http.Client); ok { - client = withoutRedirects(c) - } - - clientOpts := twirp.ClientOptions{} - for _, o := range opts { - o(&clientOpts) - } - - // Using ReadOpt allows backwards and forwards compatibility with new options in the future - literalURLs := false - _ = clientOpts.ReadOpt("literalURLs", &literalURLs) - var pathPrefix string - if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { - pathPrefix = "/twirp" // default prefix - } - - // Build method URLs: []/./ - serviceURL := sanitizeBaseURL(baseURL) - serviceURL += baseServicePath(pathPrefix, "github.com.sst.ion.test", "Test") - urls := [1]string{ - serviceURL + "Echo", - } - - return &testJSONClient{ - client: client, - urls: urls, - interceptor: twirp.ChainInterceptors(clientOpts.Interceptors...), - opts: clientOpts, - } -} - -func (c *testJSONClient) Echo(ctx context.Context, in *EchoRequest) (*EchoResponse, error) { - ctx = ctxsetters.WithPackageName(ctx, "github.com.sst.ion.test") - ctx = ctxsetters.WithServiceName(ctx, "Test") - ctx = ctxsetters.WithMethodName(ctx, "Echo") - caller := c.callEcho - if c.interceptor != nil { - caller = func(ctx context.Context, req *EchoRequest) (*EchoResponse, error) { - resp, err := c.interceptor( - func(ctx context.Context, req interface{}) (interface{}, error) { - typedReq, ok := req.(*EchoRequest) - if !ok { - return nil, twirp.InternalError("failed type assertion req.(*EchoRequest) when calling interceptor") - } - return c.callEcho(ctx, typedReq) - }, - )(ctx, req) - if resp != nil { - typedResp, ok := resp.(*EchoResponse) - if !ok { - return nil, twirp.InternalError("failed type assertion resp.(*EchoResponse) when calling interceptor") - } - return typedResp, err - } - return nil, err - } - } - return caller(ctx, in) -} - -func (c *testJSONClient) callEcho(ctx context.Context, in *EchoRequest) (*EchoResponse, error) { - out := new(EchoResponse) - ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out) - if err != nil { - twerr, ok := err.(twirp.Error) - if !ok { - twerr = twirp.InternalErrorWith(err) - } - callClientError(ctx, c.opts.Hooks, twerr) - return nil, err - } - - callClientResponseReceived(ctx, c.opts.Hooks) - - return out, nil -} - -// =================== -// Test Server Handler -// =================== - -type testServer struct { - Test - interceptor twirp.Interceptor - hooks *twirp.ServerHooks - pathPrefix string // prefix for routing - jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response - jsonCamelCase bool // JSON fields are serialized as lowerCamelCase rather than keeping the original proto names -} - -// NewTestServer builds a TwirpServer that can be used as an http.Handler to handle -// HTTP requests that are routed to the right method in the provided svc implementation. -// The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). -func NewTestServer(svc Test, opts ...interface{}) TwirpServer { - serverOpts := newServerOpts(opts) - - // Using ReadOpt allows backwards and forwards compatibility with new options in the future - jsonSkipDefaults := false - _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) - jsonCamelCase := false - _ = serverOpts.ReadOpt("jsonCamelCase", &jsonCamelCase) - var pathPrefix string - if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { - pathPrefix = "/twirp" // default prefix - } - - return &testServer{ - Test: svc, - hooks: serverOpts.Hooks, - interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), - pathPrefix: pathPrefix, - jsonSkipDefaults: jsonSkipDefaults, - jsonCamelCase: jsonCamelCase, - } -} - -// writeError writes an HTTP response with a valid Twirp error format, and triggers hooks. -// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) -func (s *testServer) writeError(ctx context.Context, resp http.ResponseWriter, err error) { - writeError(ctx, resp, err, s.hooks) -} - -// handleRequestBodyError is used to handle error when the twirp server cannot read request -func (s *testServer) handleRequestBodyError(ctx context.Context, resp http.ResponseWriter, msg string, err error) { - if context.Canceled == ctx.Err() { - s.writeError(ctx, resp, twirp.NewError(twirp.Canceled, "failed to read request: context canceled")) - return - } - if context.DeadlineExceeded == ctx.Err() { - s.writeError(ctx, resp, twirp.NewError(twirp.DeadlineExceeded, "failed to read request: deadline exceeded")) - return - } - s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) -} - -// TestPathPrefix is a convenience constant that may identify URL paths. -// Should be used with caution, it only matches routes generated by Twirp Go clients, -// with the default "/twirp" prefix and default CamelCase service and method names. -// More info: https://twitchtv.github.io/twirp/docs/routing.html -const TestPathPrefix = "/twirp/github.com.sst.ion.test.Test/" - -func (s *testServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { - ctx := req.Context() - ctx = ctxsetters.WithPackageName(ctx, "github.com.sst.ion.test") - ctx = ctxsetters.WithServiceName(ctx, "Test") - ctx = ctxsetters.WithResponseWriter(ctx, resp) - - var err error - ctx, err = callRequestReceived(ctx, s.hooks) - if err != nil { - s.writeError(ctx, resp, err) - return - } - - if req.Method != "POST" { - msg := fmt.Sprintf("unsupported method %q (only POST is allowed)", req.Method) - s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) - return - } - - // Verify path format: []/./ - prefix, pkgService, method := parseTwirpPath(req.URL.Path) - if pkgService != "github.com.sst.ion.test.Test" { - msg := fmt.Sprintf("no handler for path %q", req.URL.Path) - s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) - return - } - if prefix != s.pathPrefix { - msg := fmt.Sprintf("invalid path prefix %q, expected %q, on path %q", prefix, s.pathPrefix, req.URL.Path) - s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) - return - } - - switch method { - case "Echo": - s.serveEcho(ctx, resp, req) - return - default: - msg := fmt.Sprintf("no handler for path %q", req.URL.Path) - s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) - return - } -} - -func (s *testServer) serveEcho(ctx context.Context, resp http.ResponseWriter, req *http.Request) { - header := req.Header.Get("Content-Type") - i := strings.Index(header, ";") - if i == -1 { - i = len(header) - } - switch strings.TrimSpace(strings.ToLower(header[:i])) { - case "application/json": - s.serveEchoJSON(ctx, resp, req) - case "application/protobuf": - s.serveEchoProtobuf(ctx, resp, req) - default: - msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type")) - twerr := badRouteError(msg, req.Method, req.URL.Path) - s.writeError(ctx, resp, twerr) - } -} - -func (s *testServer) serveEchoJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { - var err error - ctx = ctxsetters.WithMethodName(ctx, "Echo") - ctx, err = callRequestRouted(ctx, s.hooks) - if err != nil { - s.writeError(ctx, resp, err) - return - } - - d := json.NewDecoder(req.Body) - rawReqBody := json.RawMessage{} - if err := d.Decode(&rawReqBody); err != nil { - s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) - return - } - reqContent := new(EchoRequest) - unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} - if err = unmarshaler.Unmarshal(rawReqBody, reqContent); err != nil { - s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) - return - } - - handler := s.Test.Echo - if s.interceptor != nil { - handler = func(ctx context.Context, req *EchoRequest) (*EchoResponse, error) { - resp, err := s.interceptor( - func(ctx context.Context, req interface{}) (interface{}, error) { - typedReq, ok := req.(*EchoRequest) - if !ok { - return nil, twirp.InternalError("failed type assertion req.(*EchoRequest) when calling interceptor") - } - return s.Test.Echo(ctx, typedReq) - }, - )(ctx, req) - if resp != nil { - typedResp, ok := resp.(*EchoResponse) - if !ok { - return nil, twirp.InternalError("failed type assertion resp.(*EchoResponse) when calling interceptor") - } - return typedResp, err - } - return nil, err - } - } - - // Call service method - var respContent *EchoResponse - func() { - defer ensurePanicResponses(ctx, resp, s.hooks) - respContent, err = handler(ctx, reqContent) - }() - - if err != nil { - s.writeError(ctx, resp, err) - return - } - if respContent == nil { - s.writeError(ctx, resp, twirp.InternalError("received a nil *EchoResponse and nil error while calling Echo. nil responses are not supported")) - return - } - - ctx = callResponsePrepared(ctx, s.hooks) - - marshaler := &protojson.MarshalOptions{UseProtoNames: !s.jsonCamelCase, EmitUnpopulated: !s.jsonSkipDefaults} - respBytes, err := marshaler.Marshal(respContent) - if err != nil { - s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) - return - } - - ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) - resp.Header().Set("Content-Type", "application/json") - resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) - resp.WriteHeader(http.StatusOK) - - if n, err := resp.Write(respBytes); err != nil { - msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) - twerr := twirp.NewError(twirp.Unknown, msg) - ctx = callError(ctx, s.hooks, twerr) - } - callResponseSent(ctx, s.hooks) -} - -func (s *testServer) serveEchoProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) { - var err error - ctx = ctxsetters.WithMethodName(ctx, "Echo") - ctx, err = callRequestRouted(ctx, s.hooks) - if err != nil { - s.writeError(ctx, resp, err) - return - } - - buf, err := io.ReadAll(req.Body) - if err != nil { - s.handleRequestBodyError(ctx, resp, "failed to read request body", err) - return - } - reqContent := new(EchoRequest) - if err = proto.Unmarshal(buf, reqContent); err != nil { - s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded")) - return - } - - handler := s.Test.Echo - if s.interceptor != nil { - handler = func(ctx context.Context, req *EchoRequest) (*EchoResponse, error) { - resp, err := s.interceptor( - func(ctx context.Context, req interface{}) (interface{}, error) { - typedReq, ok := req.(*EchoRequest) - if !ok { - return nil, twirp.InternalError("failed type assertion req.(*EchoRequest) when calling interceptor") - } - return s.Test.Echo(ctx, typedReq) - }, - )(ctx, req) - if resp != nil { - typedResp, ok := resp.(*EchoResponse) - if !ok { - return nil, twirp.InternalError("failed type assertion resp.(*EchoResponse) when calling interceptor") - } - return typedResp, err - } - return nil, err - } - } - - // Call service method - var respContent *EchoResponse - func() { - defer ensurePanicResponses(ctx, resp, s.hooks) - respContent, err = handler(ctx, reqContent) - }() - - if err != nil { - s.writeError(ctx, resp, err) - return - } - if respContent == nil { - s.writeError(ctx, resp, twirp.InternalError("received a nil *EchoResponse and nil error while calling Echo. nil responses are not supported")) - return - } - - ctx = callResponsePrepared(ctx, s.hooks) - - respBytes, err := proto.Marshal(respContent) - if err != nil { - s.writeError(ctx, resp, wrapInternal(err, "failed to marshal proto response")) - return - } - - ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) - resp.Header().Set("Content-Type", "application/protobuf") - resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) - resp.WriteHeader(http.StatusOK) - if n, err := resp.Write(respBytes); err != nil { - msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) - twerr := twirp.NewError(twirp.Unknown, msg) - ctx = callError(ctx, s.hooks, twerr) - } - callResponseSent(ctx, s.hooks) -} - -func (s *testServer) ServiceDescriptor() ([]byte, int) { - return twirpFileDescriptor0, 0 -} - -func (s *testServer) ProtocGenTwirpVersion() string { - return "v8.1.3" -} - -// PathPrefix returns the base service path, in the form: "//./" -// that is everything in a Twirp route except for the . This can be used for routing, -// for example to identify the requests that are targeted to this service in a mux. -func (s *testServer) PathPrefix() string { - return baseServicePath(s.pathPrefix, "github.com.sst.ion.test", "Test") -} - -// ===== -// Utils -// ===== - -// HTTPClient is the interface used by generated clients to send HTTP requests. -// It is fulfilled by *(net/http).Client, which is sufficient for most users. -// Users can provide their own implementation for special retry policies. -// -// HTTPClient implementations should not follow redirects. Redirects are -// automatically disabled if *(net/http).Client is passed to client -// constructors. See the withoutRedirects function in this file for more -// details. -type HTTPClient interface { - Do(req *http.Request) (*http.Response, error) -} - -// TwirpServer is the interface generated server structs will support: they're -// HTTP handlers with additional methods for accessing metadata about the -// service. Those accessors are a low-level API for building reflection tools. -// Most people can think of TwirpServers as just http.Handlers. -type TwirpServer interface { - http.Handler - - // ServiceDescriptor returns gzipped bytes describing the .proto file that - // this service was generated from. Once unzipped, the bytes can be - // unmarshalled as a - // google.golang.org/protobuf/types/descriptorpb.FileDescriptorProto. - // - // The returned integer is the index of this particular service within that - // FileDescriptorProto's 'Service' slice of ServiceDescriptorProtos. This is a - // low-level field, expected to be used for reflection. - ServiceDescriptor() ([]byte, int) - - // ProtocGenTwirpVersion is the semantic version string of the version of - // twirp used to generate this file. - ProtocGenTwirpVersion() string - - // PathPrefix returns the HTTP URL path prefix for all methods handled by this - // service. This can be used with an HTTP mux to route Twirp requests. - // The path prefix is in the form: "//./" - // that is, everything in a Twirp route except for the at the end. - PathPrefix() string -} - -func newServerOpts(opts []interface{}) *twirp.ServerOptions { - serverOpts := &twirp.ServerOptions{} - for _, opt := range opts { - switch o := opt.(type) { - case twirp.ServerOption: - o(serverOpts) - case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument - twirp.WithServerHooks(o)(serverOpts) - case nil: // backwards compatibility, allow nil value for the argument - continue - default: - panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o)) - } - } - return serverOpts -} - -// WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). -// Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. -// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) -func WriteError(resp http.ResponseWriter, err error) { - writeError(context.Background(), resp, err, nil) -} - -// writeError writes Twirp errors in the response and triggers hooks. -func writeError(ctx context.Context, resp http.ResponseWriter, err error, hooks *twirp.ServerHooks) { - // Convert to a twirp.Error. Non-twirp errors are converted to internal errors. - var twerr twirp.Error - if !errors.As(err, &twerr) { - twerr = twirp.InternalErrorWith(err) - } - - statusCode := twirp.ServerHTTPStatusFromErrorCode(twerr.Code()) - ctx = ctxsetters.WithStatusCode(ctx, statusCode) - ctx = callError(ctx, hooks, twerr) - - respBody := marshalErrorToJSON(twerr) - - resp.Header().Set("Content-Type", "application/json") // Error responses are always JSON - resp.Header().Set("Content-Length", strconv.Itoa(len(respBody))) - resp.WriteHeader(statusCode) // set HTTP status code and send response - - _, writeErr := resp.Write(respBody) - if writeErr != nil { - // We have three options here. We could log the error, call the Error - // hook, or just silently ignore the error. - // - // Logging is unacceptable because we don't have a user-controlled - // logger; writing out to stderr without permission is too rude. - // - // Calling the Error hook would confuse users: it would mean the Error - // hook got called twice for one request, which is likely to lead to - // duplicated log messages and metrics, no matter how well we document - // the behavior. - // - // Silently ignoring the error is our least-bad option. It's highly - // likely that the connection is broken and the original 'err' says - // so anyway. - _ = writeErr - } - - callResponseSent(ctx, hooks) -} - -// sanitizeBaseURL parses the the baseURL, and adds the "http" scheme if needed. -// If the URL is unparsable, the baseURL is returned unchanged. -func sanitizeBaseURL(baseURL string) string { - u, err := url.Parse(baseURL) - if err != nil { - return baseURL // invalid URL will fail later when making requests - } - if u.Scheme == "" { - u.Scheme = "http" - } - return u.String() -} - -// baseServicePath composes the path prefix for the service (without ). -// e.g.: baseServicePath("/twirp", "my.pkg", "MyService") -// -// returns => "/twirp/my.pkg.MyService/" -// -// e.g.: baseServicePath("", "", "MyService") -// -// returns => "/MyService/" -func baseServicePath(prefix, pkg, service string) string { - fullServiceName := service - if pkg != "" { - fullServiceName = pkg + "." + service - } - return path.Join("/", prefix, fullServiceName) + "/" -} - -// parseTwirpPath extracts path components form a valid Twirp route. -// Expected format: "[]/./" -// e.g.: prefix, pkgService, method := parseTwirpPath("/twirp/pkg.Svc/MakeHat") -func parseTwirpPath(path string) (string, string, string) { - parts := strings.Split(path, "/") - if len(parts) < 2 { - return "", "", "" - } - method := parts[len(parts)-1] - pkgService := parts[len(parts)-2] - prefix := strings.Join(parts[0:len(parts)-2], "/") - return prefix, pkgService, method -} - -// getCustomHTTPReqHeaders retrieves a copy of any headers that are set in -// a context through the twirp.WithHTTPRequestHeaders function. -// If there are no headers set, or if they have the wrong type, nil is returned. -func getCustomHTTPReqHeaders(ctx context.Context) http.Header { - header, ok := twirp.HTTPRequestHeaders(ctx) - if !ok || header == nil { - return nil - } - copied := make(http.Header) - for k, vv := range header { - if vv == nil { - copied[k] = nil - continue - } - copied[k] = make([]string, len(vv)) - copy(copied[k], vv) - } - return copied -} - -// newRequest makes an http.Request from a client, adding common headers. -func newRequest(ctx context.Context, url string, reqBody io.Reader, contentType string) (*http.Request, error) { - req, err := http.NewRequest("POST", url, reqBody) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - if customHeader := getCustomHTTPReqHeaders(ctx); customHeader != nil { - req.Header = customHeader - } - req.Header.Set("Accept", contentType) - req.Header.Set("Content-Type", contentType) - req.Header.Set("Twirp-Version", "v8.1.3") - return req, nil -} - -// JSON serialization for errors -type twerrJSON struct { - Code string `json:"code"` - Msg string `json:"msg"` - Meta map[string]string `json:"meta,omitempty"` -} - -// marshalErrorToJSON returns JSON from a twirp.Error, that can be used as HTTP error response body. -// If serialization fails, it will use a descriptive Internal error instead. -func marshalErrorToJSON(twerr twirp.Error) []byte { - // make sure that msg is not too large - msg := twerr.Msg() - if len(msg) > 1e6 { - msg = msg[:1e6] - } - - tj := twerrJSON{ - Code: string(twerr.Code()), - Msg: msg, - Meta: twerr.MetaMap(), - } - - buf, err := json.Marshal(&tj) - if err != nil { - buf = []byte("{\"type\": \"" + twirp.Internal + "\", \"msg\": \"There was an error but it could not be serialized into JSON\"}") // fallback - } - - return buf -} - -// errorFromResponse builds a twirp.Error from a non-200 HTTP response. -// If the response has a valid serialized Twirp error, then it's returned. -// If not, the response status code is used to generate a similar twirp -// error. See twirpErrorFromIntermediary for more info on intermediary errors. -func errorFromResponse(resp *http.Response) twirp.Error { - statusCode := resp.StatusCode - statusText := http.StatusText(statusCode) - - if isHTTPRedirect(statusCode) { - // Unexpected redirect: it must be an error from an intermediary. - // Twirp clients don't follow redirects automatically, Twirp only handles - // POST requests, redirects should only happen on GET and HEAD requests. - location := resp.Header.Get("Location") - msg := fmt.Sprintf("unexpected HTTP status code %d %q received, Location=%q", statusCode, statusText, location) - return twirpErrorFromIntermediary(statusCode, msg, location) - } - - respBodyBytes, err := io.ReadAll(resp.Body) - if err != nil { - return wrapInternal(err, "failed to read server error response body") - } - - var tj twerrJSON - dec := json.NewDecoder(bytes.NewReader(respBodyBytes)) - dec.DisallowUnknownFields() - if err := dec.Decode(&tj); err != nil || tj.Code == "" { - // Invalid JSON response; it must be an error from an intermediary. - msg := fmt.Sprintf("Error from intermediary with HTTP status code %d %q", statusCode, statusText) - return twirpErrorFromIntermediary(statusCode, msg, string(respBodyBytes)) - } - - errorCode := twirp.ErrorCode(tj.Code) - if !twirp.IsValidErrorCode(errorCode) { - msg := "invalid type returned from server error response: " + tj.Code - return twirp.InternalError(msg).WithMeta("body", string(respBodyBytes)) - } - - twerr := twirp.NewError(errorCode, tj.Msg) - for k, v := range tj.Meta { - twerr = twerr.WithMeta(k, v) - } - return twerr -} - -// twirpErrorFromIntermediary maps HTTP errors from non-twirp sources to twirp errors. -// The mapping is similar to gRPC: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md. -// Returned twirp Errors have some additional metadata for inspection. -func twirpErrorFromIntermediary(status int, msg string, bodyOrLocation string) twirp.Error { - var code twirp.ErrorCode - if isHTTPRedirect(status) { // 3xx - code = twirp.Internal - } else { - switch status { - case 400: // Bad Request - code = twirp.Internal - case 401: // Unauthorized - code = twirp.Unauthenticated - case 403: // Forbidden - code = twirp.PermissionDenied - case 404: // Not Found - code = twirp.BadRoute - case 429: // Too Many Requests - code = twirp.ResourceExhausted - case 502, 503, 504: // Bad Gateway, Service Unavailable, Gateway Timeout - code = twirp.Unavailable - default: // All other codes - code = twirp.Unknown - } - } - - twerr := twirp.NewError(code, msg) - twerr = twerr.WithMeta("http_error_from_intermediary", "true") // to easily know if this error was from intermediary - twerr = twerr.WithMeta("status_code", strconv.Itoa(status)) - if isHTTPRedirect(status) { - twerr = twerr.WithMeta("location", bodyOrLocation) - } else { - twerr = twerr.WithMeta("body", bodyOrLocation) - } - return twerr -} - -func isHTTPRedirect(status int) bool { - return status >= 300 && status <= 399 -} - -// wrapInternal wraps an error with a prefix as an Internal error. -// The original error cause is accessible by github.com/pkg/errors.Cause. -func wrapInternal(err error, prefix string) twirp.Error { - return twirp.InternalErrorWith(&wrappedError{prefix: prefix, cause: err}) -} - -type wrappedError struct { - prefix string - cause error -} - -func (e *wrappedError) Error() string { return e.prefix + ": " + e.cause.Error() } -func (e *wrappedError) Unwrap() error { return e.cause } // for go1.13 + errors.Is/As -func (e *wrappedError) Cause() error { return e.cause } // for github.com/pkg/errors - -// ensurePanicResponses makes sure that rpc methods causing a panic still result in a Twirp Internal -// error response (status 500), and error hooks are properly called with the panic wrapped as an error. -// The panic is re-raised so it can be handled normally with middleware. -func ensurePanicResponses(ctx context.Context, resp http.ResponseWriter, hooks *twirp.ServerHooks) { - if r := recover(); r != nil { - // Wrap the panic as an error so it can be passed to error hooks. - // The original error is accessible from error hooks, but not visible in the response. - err := errFromPanic(r) - twerr := &internalWithCause{msg: "Internal service panic", cause: err} - // Actually write the error - writeError(ctx, resp, twerr, hooks) - // If possible, flush the error to the wire. - f, ok := resp.(http.Flusher) - if ok { - f.Flush() - } - - panic(r) - } -} - -// errFromPanic returns the typed error if the recovered panic is an error, otherwise formats as error. -func errFromPanic(p interface{}) error { - if err, ok := p.(error); ok { - return err - } - return fmt.Errorf("panic: %v", p) -} - -// internalWithCause is a Twirp Internal error wrapping an original error cause, -// but the original error message is not exposed on Msg(). The original error -// can be checked with go1.13+ errors.Is/As, and also by (github.com/pkg/errors).Unwrap -type internalWithCause struct { - msg string - cause error -} - -func (e *internalWithCause) Unwrap() error { return e.cause } // for go1.13 + errors.Is/As -func (e *internalWithCause) Cause() error { return e.cause } // for github.com/pkg/errors -func (e *internalWithCause) Error() string { return e.msg + ": " + e.cause.Error() } -func (e *internalWithCause) Code() twirp.ErrorCode { return twirp.Internal } -func (e *internalWithCause) Msg() string { return e.msg } -func (e *internalWithCause) Meta(key string) string { return "" } -func (e *internalWithCause) MetaMap() map[string]string { return nil } -func (e *internalWithCause) WithMeta(key string, val string) twirp.Error { return e } - -// malformedRequestError is used when the twirp server cannot unmarshal a request -func malformedRequestError(msg string) twirp.Error { - return twirp.NewError(twirp.Malformed, msg) -} - -// badRouteError is used when the twirp server cannot route a request -func badRouteError(msg string, method, url string) twirp.Error { - err := twirp.NewError(twirp.BadRoute, msg) - err = err.WithMeta("twirp_invalid_route", method+" "+url) - return err -} - -// withoutRedirects makes sure that the POST request can not be redirected. -// The standard library will, by default, redirect requests (including POSTs) if it gets a 302 or -// 303 response, and also 301s in go1.8. It redirects by making a second request, changing the -// method to GET and removing the body. This produces very confusing error messages, so instead we -// set a redirect policy that always errors. This stops Go from executing the redirect. -// -// We have to be a little careful in case the user-provided http.Client has its own CheckRedirect -// policy - if so, we'll run through that policy first. -// -// Because this requires modifying the http.Client, we make a new copy of the client and return it. -func withoutRedirects(in *http.Client) *http.Client { - copy := *in - copy.CheckRedirect = func(req *http.Request, via []*http.Request) error { - if in.CheckRedirect != nil { - // Run the input's redirect if it exists, in case it has side effects, but ignore any error it - // returns, since we want to use ErrUseLastResponse. - err := in.CheckRedirect(req, via) - _ = err // Silly, but this makes sure generated code passes errcheck -blank, which some people use. - } - return http.ErrUseLastResponse - } - return © -} - -// doProtobufRequest makes a Protobuf request to the remote Twirp service. -func doProtobufRequest(ctx context.Context, client HTTPClient, hooks *twirp.ClientHooks, url string, in, out proto.Message) (_ context.Context, err error) { - reqBodyBytes, err := proto.Marshal(in) - if err != nil { - return ctx, wrapInternal(err, "failed to marshal proto request") - } - reqBody := bytes.NewBuffer(reqBodyBytes) - if err = ctx.Err(); err != nil { - return ctx, wrapInternal(err, "aborted because context was done") - } - - req, err := newRequest(ctx, url, reqBody, "application/protobuf") - if err != nil { - return ctx, wrapInternal(err, "could not build request") - } - ctx, err = callClientRequestPrepared(ctx, hooks, req) - if err != nil { - return ctx, err - } - - req = req.WithContext(ctx) - resp, err := client.Do(req) - if err != nil { - return ctx, wrapInternal(err, "failed to do request") - } - defer func() { _ = resp.Body.Close() }() - - if err = ctx.Err(); err != nil { - return ctx, wrapInternal(err, "aborted because context was done") - } - - if resp.StatusCode != 200 { - return ctx, errorFromResponse(resp) - } - - respBodyBytes, err := io.ReadAll(resp.Body) - if err != nil { - return ctx, wrapInternal(err, "failed to read response body") - } - if err = ctx.Err(); err != nil { - return ctx, wrapInternal(err, "aborted because context was done") - } - - if err = proto.Unmarshal(respBodyBytes, out); err != nil { - return ctx, wrapInternal(err, "failed to unmarshal proto response") - } - return ctx, nil -} - -// doJSONRequest makes a JSON request to the remote Twirp service. -func doJSONRequest(ctx context.Context, client HTTPClient, hooks *twirp.ClientHooks, url string, in, out proto.Message) (_ context.Context, err error) { - marshaler := &protojson.MarshalOptions{UseProtoNames: true} - reqBytes, err := marshaler.Marshal(in) - if err != nil { - return ctx, wrapInternal(err, "failed to marshal json request") - } - if err = ctx.Err(); err != nil { - return ctx, wrapInternal(err, "aborted because context was done") - } - - req, err := newRequest(ctx, url, bytes.NewReader(reqBytes), "application/json") - if err != nil { - return ctx, wrapInternal(err, "could not build request") - } - ctx, err = callClientRequestPrepared(ctx, hooks, req) - if err != nil { - return ctx, err - } - - req = req.WithContext(ctx) - resp, err := client.Do(req) - if err != nil { - return ctx, wrapInternal(err, "failed to do request") - } - - defer func() { - cerr := resp.Body.Close() - if err == nil && cerr != nil { - err = wrapInternal(cerr, "failed to close response body") - } - }() - - if err = ctx.Err(); err != nil { - return ctx, wrapInternal(err, "aborted because context was done") - } - - if resp.StatusCode != 200 { - return ctx, errorFromResponse(resp) - } - - d := json.NewDecoder(resp.Body) - rawRespBody := json.RawMessage{} - if err := d.Decode(&rawRespBody); err != nil { - return ctx, wrapInternal(err, "failed to unmarshal json response") - } - unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} - if err = unmarshaler.Unmarshal(rawRespBody, out); err != nil { - return ctx, wrapInternal(err, "failed to unmarshal json response") - } - if err = ctx.Err(); err != nil { - return ctx, wrapInternal(err, "aborted because context was done") - } - return ctx, nil -} - -// Call twirp.ServerHooks.RequestReceived if the hook is available -func callRequestReceived(ctx context.Context, h *twirp.ServerHooks) (context.Context, error) { - if h == nil || h.RequestReceived == nil { - return ctx, nil - } - return h.RequestReceived(ctx) -} - -// Call twirp.ServerHooks.RequestRouted if the hook is available -func callRequestRouted(ctx context.Context, h *twirp.ServerHooks) (context.Context, error) { - if h == nil || h.RequestRouted == nil { - return ctx, nil - } - return h.RequestRouted(ctx) -} - -// Call twirp.ServerHooks.ResponsePrepared if the hook is available -func callResponsePrepared(ctx context.Context, h *twirp.ServerHooks) context.Context { - if h == nil || h.ResponsePrepared == nil { - return ctx - } - return h.ResponsePrepared(ctx) -} - -// Call twirp.ServerHooks.ResponseSent if the hook is available -func callResponseSent(ctx context.Context, h *twirp.ServerHooks) { - if h == nil || h.ResponseSent == nil { - return - } - h.ResponseSent(ctx) -} - -// Call twirp.ServerHooks.Error if the hook is available -func callError(ctx context.Context, h *twirp.ServerHooks, err twirp.Error) context.Context { - if h == nil || h.Error == nil { - return ctx - } - return h.Error(ctx, err) -} - -func callClientResponseReceived(ctx context.Context, h *twirp.ClientHooks) { - if h == nil || h.ResponseReceived == nil { - return - } - h.ResponseReceived(ctx) -} - -func callClientRequestPrepared(ctx context.Context, h *twirp.ClientHooks, req *http.Request) (context.Context, error) { - if h == nil || h.RequestPrepared == nil { - return ctx, nil - } - return h.RequestPrepared(ctx, req) -} - -func callClientError(ctx context.Context, h *twirp.ClientHooks, err twirp.Error) { - if h == nil || h.Error == nil { - return - } - h.Error(ctx, err) -} - -var twirpFileDescriptor0 = []byte{ - // 155 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x28, 0x28, 0xca, 0x2f, - 0xc9, 0xd7, 0x2f, 0x49, 0x2d, 0x2e, 0xd1, 0x03, 0x33, 0x85, 0xc4, 0xd3, 0x33, 0x4b, 0x32, 0x4a, - 0x93, 0xf4, 0x92, 0xf3, 0x73, 0xf5, 0x8a, 0x8b, 0x4b, 0xf4, 0x32, 0xf3, 0xf3, 0xf4, 0x40, 0xd2, - 0x4a, 0xea, 0x5c, 0xdc, 0xae, 0xc9, 0x19, 0xf9, 0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, 0x42, - 0x12, 0x5c, 0xec, 0xb9, 0xa9, 0xc5, 0xc5, 0x89, 0xe9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, - 0x41, 0x30, 0xae, 0x92, 0x06, 0x17, 0x0f, 0x44, 0x61, 0x71, 0x41, 0x7e, 0x5e, 0x71, 0x2a, 0x6e, - 0x95, 0x46, 0xb1, 0x5c, 0x2c, 0x21, 0x20, 0xb3, 0x42, 0xb9, 0x58, 0x40, 0x3a, 0x84, 0x54, 0xf4, - 0x70, 0x58, 0xae, 0x87, 0x64, 0xb3, 0x94, 0x2a, 0x01, 0x55, 0x10, 0x6b, 0x95, 0x18, 0x9c, 0xb8, - 0xa3, 0x38, 0x0b, 0xb2, 0xd3, 0xf5, 0xc1, 0xfe, 0x4a, 0x62, 0x03, 0x53, 0xc6, 0x80, 0x00, 0x00, - 0x00, 0xff, 0xff, 0x2a, 0x25, 0xff, 0x1b, 0xf2, 0x00, 0x00, 0x00, -} diff --git a/pkg/runtime/golang/golang.go b/pkg/runtime/golang/golang.go index 6059faef34..4bbd1db4e2 100644 --- a/pkg/runtime/golang/golang.go +++ b/pkg/runtime/golang/golang.go @@ -145,3 +145,10 @@ func (r *Runtime) ShouldRebuild(functionID string, file string) bool { } return !strings.HasPrefix(rel, "..") } + +// ShouldRunEagerly returns true for Go - workers restart immediately after rebuild. +// Go uses directory-based dependency tracking, so only functions in the changed +// directory will rebuild. +func (r *Runtime) ShouldRunEagerly() bool { + return true +} diff --git a/pkg/runtime/node/build.go b/pkg/runtime/node/build.go index 8f713b602c..006b40ecaa 100644 --- a/pkg/runtime/node/build.go +++ b/pkg/runtime/node/build.go @@ -15,6 +15,7 @@ import ( "github.com/sst/sst/v3/pkg/js" "github.com/sst/sst/v3/pkg/process" "github.com/sst/sst/v3/pkg/runtime" + "gopkg.in/yaml.v3" ) var forceExternal = []string{ @@ -22,6 +23,7 @@ var forceExternal = []string{ } var targetMap = map[string]esbuild.Target{ + "nodejs24.x": esbuild.ES2024, "nodejs22.x": esbuild.ES2023, "nodejs20.x": esbuild.ES2023, "nodejs18.x": esbuild.ES2022, @@ -109,7 +111,7 @@ func (r *Runtime) Build(ctx context.Context, input *runtime.BuildInput) (*runtim if properties.Plugins != "" { plugins = append(plugins, plugin(properties.Plugins)) } - external := append(forceExternal, properties.Install...) + external := append(forceExternal, resolveInstallPackages(properties.Install)...) external = append(external, properties.ESBuild.External...) slog.Debug("esbuild options", "target", properties.ESBuild.Target, @@ -164,6 +166,7 @@ func (r *Runtime) Build(ctx context.Context, input *runtime.BuildInput) (*runtim ".js": ".mjs", } options.Outfile = "" + options.EntryNames = "bundle" } if !input.Dev { @@ -239,7 +242,7 @@ func (r *Runtime) Build(ctx context.Context, input *runtime.BuildInput) (*runtim var metafile js.Metafile json.Unmarshal([]byte(result.Metafile), &metafile) - installPackages := properties.Install + installPackages := resolveInstallPackages(properties.Install) for _, pkg := range forceExternal { if slices.Contains(properties.ESBuild.External, pkg) { continue @@ -255,7 +258,7 @@ func (r *Runtime) Build(ctx context.Context, input *runtime.BuildInput) (*runtim if len(installPackages) > 0 { log.Info("installing", "packages", installPackages) - src, err := fs.FindUp(filepath.Dir(target), "package.json") + src, err := fs.FindUp(filepath.Dir(file), "package.json") if err != nil { return nil, err } @@ -271,10 +274,11 @@ func (r *Runtime) Build(ctx context.Context, input *runtime.BuildInput) (*runtim } dependencies := map[string]string{} for _, pkg := range installPackages { - dependencies[pkg] = "*" - if parsed.Dependencies[pkg] != "" { - dependencies[pkg] = parsed.Dependencies[pkg] + version, err := resolveInstallVersion(pkg, properties.Install, filepath.Dir(src), parsed) + if err != nil { + return nil, err } + dependencies[pkg] = version } outPkg := filepath.Join(input.Out(), "package.json") outFile, err := os.Create(outPkg) @@ -320,3 +324,175 @@ func (r *Runtime) Build(ctx context.Context, input *runtime.BuildInput) (*runtim Sourcemaps: sourcemaps, }, nil } + +type catalogSource struct { + Catalog map[string]string `json:"catalog" yaml:"catalog"` + Catalogs map[string]map[string]string `json:"catalogs" yaml:"catalogs"` +} + +type bunPackageJSON struct { + Catalog map[string]string `json:"catalog"` + Catalogs map[string]map[string]string `json:"catalogs"` + Workspaces json.RawMessage `json:"workspaces"` +} + +type bunWorkspaces struct { + Catalog map[string]string `json:"catalog"` + Catalogs map[string]map[string]string `json:"catalogs"` +} + +func resolveInstallPackages(install map[string]string) []string { + result := make([]string, 0, len(install)) + for pkg := range install { + result = append(result, pkg) + } + return result +} + +func resolveInstallVersion(pkg string, install map[string]string, dir string, packageJSON js.PackageJson) (string, error) { + version := install[pkg] + if version == "" || version == "*" { + version = packageJSON.Dependencies[pkg] + } + if version == "" { + return "*", nil + } + if strings.HasPrefix(version, "catalog:") { + var err error + version, err = resolveCatalogVersion(dir, pkg, version) + if err != nil { + return "", err + } + } + for _, prefix := range []string{"catalog:", "workspace:", "file:", "link:", "portal:", "patch:"} { + if strings.HasPrefix(version, prefix) { + return "", fmt.Errorf("could not determine an npm-compatible version for %q in nodejs.install: found %q using %q; pin the version explicitly", pkg, version, prefix) + } + } + return version, nil +} + +func resolveCatalogVersion(dir string, pkg string, version string) (string, error) { + workspacePath, err := fs.FindUp(dir, "pnpm-workspace.yaml") + if err == nil { + return resolvePnpmCatalogVersion(workspacePath, pkg, version) + } + resolved, found, err := resolveBunCatalogVersion(dir, pkg, version) + if err != nil { + return "", err + } + if found { + return resolved, nil + } + return "", fmt.Errorf("could not determine an npm-compatible version for %q in nodejs.install: found %q but pnpm-workspace.yaml was not found and no Bun catalog was found in an ancestor package.json; pin the version explicitly", pkg, version) +} + +func resolvePnpmCatalogVersion(workspacePath string, pkg string, version string) (string, error) { + data, err := os.ReadFile(workspacePath) + if err != nil { + return "", err + } + var workspace catalogSource + if err := yaml.Unmarshal(data, &workspace); err != nil { + return "", err + } + resolved, ok := resolveCatalogEntry(pkg, version, workspace) + if !ok { + return "", fmt.Errorf("could not determine an npm-compatible version for %q in nodejs.install: found %q but no matching catalog entry exists in pnpm-workspace.yaml; pin the version explicitly", pkg, version) + } + return resolved, nil +} + +func resolveBunCatalogVersion(dir string, pkg string, version string) (string, bool, error) { + currentDir := dir + for { + packagePath := filepath.Join(currentDir, "package.json") + data, err := os.ReadFile(packagePath) + if err == nil { + source, found, err := parseBunCatalogSource(data) + if err != nil { + return "", false, err + } + if found { + resolved, ok := resolveCatalogEntry(pkg, version, source) + if !ok { + return "", true, fmt.Errorf("could not determine an npm-compatible version for %q in nodejs.install: found %q but no matching catalog entry exists in %s; pin the version explicitly", pkg, version, packagePath) + } + return resolved, true, nil + } + } else if !os.IsNotExist(err) { + return "", false, err + } + + if currentDir == filepath.Dir(currentDir) { + break + } + currentDir = filepath.Dir(currentDir) + } + return "", false, nil +} + +func parseBunCatalogSource(data []byte) (catalogSource, bool, error) { + var manifest bunPackageJSON + if err := json.Unmarshal(data, &manifest); err != nil { + return catalogSource{}, false, err + } + source := catalogSource{ + Catalog: manifest.Catalog, + Catalogs: manifest.Catalogs, + } + workspaceSource, found, err := parseBunWorkspacesCatalogSource(manifest.Workspaces) + if err != nil { + return catalogSource{}, false, err + } + if found { + if workspaceSource.Catalog != nil { + source.Catalog = workspaceSource.Catalog + } + if workspaceSource.Catalogs != nil { + if source.Catalogs == nil { + source.Catalogs = map[string]map[string]string{} + } + for name, catalog := range workspaceSource.Catalogs { + source.Catalogs[name] = catalog + } + } + } + if source.Catalog == nil && len(source.Catalogs) == 0 { + return catalogSource{}, false, nil + } + return source, true, nil +} + +func parseBunWorkspacesCatalogSource(raw json.RawMessage) (catalogSource, bool, error) { + trimmed := strings.TrimSpace(string(raw)) + if trimmed == "" || trimmed[0] != '{' { + return catalogSource{}, false, nil + } + var workspaces bunWorkspaces + if err := json.Unmarshal(raw, &workspaces); err != nil { + return catalogSource{}, false, err + } + if workspaces.Catalog == nil && len(workspaces.Catalogs) == 0 { + return catalogSource{}, false, nil + } + return catalogSource{ + Catalog: workspaces.Catalog, + Catalogs: workspaces.Catalogs, + }, true, nil +} + +func resolveCatalogEntry(pkg string, version string, source catalogSource) (string, bool) { + catalogName := strings.TrimSpace(strings.TrimPrefix(version, "catalog:")) + var catalog map[string]string + if catalogName == "" || catalogName == "default" { + catalog = source.Catalog + if catalog == nil { + catalog = source.Catalogs["default"] + } + } else { + catalog = source.Catalogs[catalogName] + } + resolved := catalog[pkg] + return resolved, resolved != "" +} diff --git a/pkg/runtime/node/build_test.go b/pkg/runtime/node/build_test.go new file mode 100644 index 0000000000..f22efcdb91 --- /dev/null +++ b/pkg/runtime/node/build_test.go @@ -0,0 +1,257 @@ +package node + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/sst/sst/v3/pkg/js" +) + +func TestResolveInstallVersion(t *testing.T) { + tests := []struct { + name string + pkg string + install map[string]string + setup func(t *testing.T) (string, js.PackageJson) + want string + wantErr string + }{ + { + name: "explicit version overrides package json", + pkg: "sharp", + install: map[string]string{"sharp": "0.33.5"}, + setup: func(t *testing.T) (string, js.PackageJson) { + return "", js.PackageJson{Dependencies: map[string]string{"sharp": "0.32.6"}} + }, + want: "0.33.5", + }, + { + name: "wildcard falls back to package json", + pkg: "sharp", + install: map[string]string{"sharp": "*"}, + setup: func(t *testing.T) (string, js.PackageJson) { + return "", js.PackageJson{Dependencies: map[string]string{"sharp": "0.32.6"}} + }, + want: "0.32.6", + }, + { + name: "missing package falls back to wildcard", + pkg: "sharp", + install: map[string]string{"sharp": "*"}, + setup: func(t *testing.T) (string, js.PackageJson) { + return "", js.PackageJson{} + }, + want: "*", + }, + { + name: "catalog spec resolves from pnpm workspace", + pkg: "sharp", + install: map[string]string{"sharp": "*"}, + setup: func(t *testing.T) (string, js.PackageJson) { + dir := t.TempDir() + pkgDir := filepath.Join(dir, "packages", "functions") + mustMkdirAll(t, pkgDir) + mustWriteFile(t, filepath.Join(dir, "pnpm-workspace.yaml"), "catalogs:\n shared:\n sharp: 0.32.6\n") + return pkgDir, js.PackageJson{Dependencies: map[string]string{"sharp": "catalog:shared"}} + }, + want: "0.32.6", + }, + { + name: "explicit catalog spec resolves from pnpm workspace", + pkg: "sharp", + install: map[string]string{"sharp": "catalog:shared"}, + setup: func(t *testing.T) (string, js.PackageJson) { + dir := t.TempDir() + pkgDir := filepath.Join(dir, "packages", "functions") + mustMkdirAll(t, pkgDir) + mustWriteFile(t, filepath.Join(dir, "pnpm-workspace.yaml"), "catalogs:\n shared:\n sharp: 0.32.6\n") + return pkgDir, js.PackageJson{} + }, + want: "0.32.6", + }, + { + name: "missing catalog entry returns error", + pkg: "sharp", + install: map[string]string{"sharp": "*"}, + setup: func(t *testing.T) (string, js.PackageJson) { + dir := t.TempDir() + pkgDir := filepath.Join(dir, "packages", "functions") + mustMkdirAll(t, pkgDir) + mustWriteFile(t, filepath.Join(dir, "pnpm-workspace.yaml"), "catalogs:\n shared:\n other: 1.0.0\n") + return pkgDir, js.PackageJson{Dependencies: map[string]string{"sharp": "catalog:shared"}} + }, + wantErr: "no matching catalog entry exists", + }, + { + name: "catalog spec resolves from bun top level catalog", + pkg: "sharp", + install: map[string]string{"sharp": "*"}, + setup: func(t *testing.T) (string, js.PackageJson) { + dir := t.TempDir() + pkgDir := filepath.Join(dir, "packages", "functions") + mustMkdirAll(t, pkgDir) + mustWriteFile(t, filepath.Join(dir, "package.json"), `{ + "catalog": { + "sharp": "0.32.6" + } +}`) + return pkgDir, js.PackageJson{Dependencies: map[string]string{"sharp": "catalog:"}} + }, + want: "0.32.6", + }, + { + name: "explicit catalog spec resolves from bun top level catalogs", + pkg: "sharp", + install: map[string]string{"sharp": "catalog:shared"}, + setup: func(t *testing.T) (string, js.PackageJson) { + dir := t.TempDir() + pkgDir := filepath.Join(dir, "packages", "functions") + mustMkdirAll(t, pkgDir) + mustWriteFile(t, filepath.Join(dir, "package.json"), `{ + "catalogs": { + "shared": { + "sharp": "0.32.6" + } + } +}`) + return pkgDir, js.PackageJson{} + }, + want: "0.32.6", + }, + { + name: "catalog spec resolves from bun workspaces catalog", + pkg: "sharp", + install: map[string]string{"sharp": "*"}, + setup: func(t *testing.T) (string, js.PackageJson) { + dir := t.TempDir() + pkgDir := filepath.Join(dir, "packages", "functions") + mustMkdirAll(t, pkgDir) + mustWriteFile(t, filepath.Join(dir, "package.json"), `{ + "workspaces": { + "packages": ["packages/*"], + "catalog": { + "sharp": "0.32.6" + } + } +}`) + return pkgDir, js.PackageJson{Dependencies: map[string]string{"sharp": "catalog:"}} + }, + want: "0.32.6", + }, + { + name: "explicit catalog spec resolves from bun workspaces catalogs", + pkg: "sharp", + install: map[string]string{"sharp": "catalog:shared"}, + setup: func(t *testing.T) (string, js.PackageJson) { + dir := t.TempDir() + pkgDir := filepath.Join(dir, "packages", "functions") + mustMkdirAll(t, pkgDir) + mustWriteFile(t, filepath.Join(dir, "package.json"), `{ + "workspaces": { + "packages": ["packages/*"], + "catalogs": { + "shared": { + "sharp": "0.32.6" + } + } + } +}`) + return pkgDir, js.PackageJson{} + }, + want: "0.32.6", + }, + { + name: "catalog spec resolves from bun top level catalog with workspaces array", + pkg: "sharp", + install: map[string]string{"sharp": "*"}, + setup: func(t *testing.T) (string, js.PackageJson) { + dir := t.TempDir() + pkgDir := filepath.Join(dir, "packages", "functions") + mustMkdirAll(t, pkgDir) + mustWriteFile(t, filepath.Join(dir, "package.json"), `{ + "workspaces": ["packages/*"], + "catalog": { + "sharp": "0.32.6" + } +}`) + return pkgDir, js.PackageJson{Dependencies: map[string]string{"sharp": "catalog:"}} + }, + want: "0.32.6", + }, + { + name: "missing bun catalog entry returns error", + pkg: "sharp", + install: map[string]string{"sharp": "*"}, + setup: func(t *testing.T) (string, js.PackageJson) { + dir := t.TempDir() + pkgDir := filepath.Join(dir, "packages", "functions") + mustMkdirAll(t, pkgDir) + mustWriteFile(t, filepath.Join(dir, "package.json"), `{ + "catalogs": { + "shared": { + "other": "1.0.0" + } + } +}`) + return pkgDir, js.PackageJson{Dependencies: map[string]string{"sharp": "catalog:shared"}} + }, + wantErr: "no matching catalog entry exists in", + }, + { + name: "workspace spec returns error", + pkg: "sharp", + install: map[string]string{"sharp": "*"}, + setup: func(t *testing.T) (string, js.PackageJson) { + return "", js.PackageJson{Dependencies: map[string]string{"sharp": "workspace:^"}} + }, + wantErr: "found \"workspace:^\" using \"workspace:\"", + }, + { + name: "explicit workspace spec returns error", + pkg: "sharp", + install: map[string]string{"sharp": "workspace:^"}, + setup: func(t *testing.T) (string, js.PackageJson) { + return "", js.PackageJson{} + }, + wantErr: "found \"workspace:^\" using \"workspace:\"", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir, packageJSON := tt.setup(t) + got, err := resolveInstallVersion(tt.pkg, tt.install, dir, packageJSON) + if tt.wantErr != "" { + if err == nil { + t.Fatalf("resolveInstallVersion() error = nil, want %q", tt.wantErr) + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("resolveInstallVersion() error = %q, want substring %q", err.Error(), tt.wantErr) + } + return + } + if err != nil { + t.Fatalf("resolveInstallVersion() error = %v", err) + } + if got != tt.want { + t.Fatalf("resolveInstallVersion() = %q, want %q", got, tt.want) + } + }) + } +} + +func mustMkdirAll(t *testing.T, dir string) { + t.Helper() + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } +} + +func mustWriteFile(t *testing.T, path string, contents string) { + t.Helper() + if err := os.WriteFile(path, []byte(contents), 0o644); err != nil { + t.Fatal(err) + } +} diff --git a/pkg/runtime/node/node.go b/pkg/runtime/node/node.go index 99cb08038b..0a8d42471f 100644 --- a/pkg/runtime/node/node.go +++ b/pkg/runtime/node/node.go @@ -113,7 +113,7 @@ func (w *Worker) Logs() io.ReadCloser { type NodeProperties struct { Loader map[string]string `json:"loader"` - Install []string `json:"install"` + Install map[string]string `json:"install"` Banner string `json:"banner"` ESBuild ESBuildOptions `json:"esbuild"` Minify bool `json:"minify"` @@ -210,10 +210,23 @@ var esSourcemapMap = map[string]esbuild.SourceMap{ var NODE_EXTENSIONS = []string{".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"} +var EDITOR_ENV = []string{ + "VSCODE_INSPECTOR_OPTIONS", + "JB_IDE_HOST", + "JB_IDE_PORT", + "JB_INTERPRETER", + "JB_NODE_DEBUG_CONNECTION_GATEWAY_HOST", + "JB_NODE_DEBUG_CONNECTION_GATEWAY_PORT", + "JETBRAINS_NODE_BIND_HOST", + "JETBRAINS_NODE_DEBUGGER_ATTACH_TO_HELPERS", + "JETBRAINS_NODE_DEBUGGER_VERBOSE_LOGGING", +} + func (r *Runtime) Run(ctx context.Context, input *runtime.RunInput) (runtime.Worker, error) { cmd := process.Command( "node", "--enable-source-maps", + "--no-warnings", filepath.Join( path.ResolvePlatformDir(input.CfgPath), "/dist/nodejs-runtime/index.js", @@ -223,7 +236,11 @@ func (r *Runtime) Run(ctx context.Context, input *runtime.RunInput) (runtime.Wor ) cmd.Env = input.Env cmd.Env = append(cmd.Env, "NODE_OPTIONS="+os.Getenv("NODE_OPTIONS")) - cmd.Env = append(cmd.Env, "VSCODE_INSPECTOR_OPTIONS="+os.Getenv("VSCODE_INSPECTOR_OPTIONS")) + for _, key := range EDITOR_ENV { + if value := os.Getenv(key); value != "" { + cmd.Env = append(cmd.Env, key+"="+value) + } + } cmd.Env = append(cmd.Env, "AWS_LAMBDA_RUNTIME_API="+input.Server) slog.Info("starting worker", "server", input.Server) cmd.Dir = input.Build.Out @@ -241,6 +258,13 @@ func (r *Runtime) Match(runtime string) bool { return strings.HasPrefix(runtime, "node") } +// ShouldRunEagerly returns true for Node.js - workers restart immediately after rebuild. +// Node.js uses esbuild's metafile for precise per-function dependency tracking, +// so only functions that actually import a changed file will rebuild. +func (r *Runtime) ShouldRunEagerly() bool { + return true +} + func (r *Runtime) getFile(input *runtime.BuildInput) (string, bool) { dir := filepath.Dir(input.Handler) fileSplit := strings.Split(filepath.Base(input.Handler), ".") diff --git a/pkg/runtime/node/node_test.go b/pkg/runtime/node/node_test.go index db2fe969c5..e6cca7510b 100644 --- a/pkg/runtime/node/node_test.go +++ b/pkg/runtime/node/node_test.go @@ -10,7 +10,7 @@ import ( func TestNodePropertiesUnmarshal(t *testing.T) { payload := `{ "loader": {".png": "file"}, - "install": ["sharp"], + "install": {"sharp": "0.33.5"}, "minify": true, "splitting": false, "esbuild": { @@ -34,6 +34,9 @@ func TestNodePropertiesUnmarshal(t *testing.T) { if got := props.ESBuild.ResolveTarget(esbuild.ESNext); got != esbuild.ES2022 { t.Errorf("Target = %v, want ES2022", got) } + if got := props.Install["sharp"]; got != "0.33.5" { + t.Errorf("Install = %v, want 0.33.5", got) + } if got := props.ESBuild.ResolveSourcemap(esbuild.SourceMapNone); got != esbuild.SourceMapLinked { t.Errorf("Sourcemap = %v, want SourceMapLinked", got) } diff --git a/pkg/runtime/node/plugin.go b/pkg/runtime/node/plugin.go index d4741860f6..0c82eedb2a 100644 --- a/pkg/runtime/node/plugin.go +++ b/pkg/runtime/node/plugin.go @@ -32,7 +32,7 @@ func plugin(path string) api.Plugin { Name: "nodejs-plugin", Setup: func(build api.PluginBuild) { slog.Info("nodejs plugin", "path", path) - cmd := process.Command("node", ".sst/platform/functions/nodejs-runtime/plugin.mjs", path) + cmd := process.Command("node", "--no-warnings", ".sst/platform/functions/nodejs-runtime/plugin.mjs", path) var wg errgroup.Group // cmd.Stderr = os.Stderr stdin, err := cmd.StdinPipe() diff --git a/pkg/runtime/python/build.go b/pkg/runtime/python/build.go new file mode 100644 index 0000000000..7cbd1556cb --- /dev/null +++ b/pkg/runtime/python/build.go @@ -0,0 +1,1677 @@ +package python + +import ( + "archive/tar" + "archive/zip" + "compress/gzip" + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "os" + "os/exec" + "path/filepath" + goruntime "runtime" + "strconv" + "strings" + "sync" + "time" + + "github.com/sst/sst/v3/pkg/process" + "github.com/sst/sst/v3/pkg/project/path" + "github.com/sst/sst/v3/pkg/runtime" +) + +// findWorkspaceRoot walks up from PyprojectPath to find the UV workspace root +// ([tool.uv.workspace]). Falls back to the pyproject dir, or SourceRoot if unset. +// +// Intentionally walks above the SST project root (sst.config.ts directory) because +// UV workspaces can legitimately live above it β€” e.g. a monorepo where sst.config.ts +// is nested inside a larger Python workspace. The [tool.uv.workspace] declaration +// acts as the natural stop condition for well-configured projects. +func findWorkspaceRoot(projectInfo *projectInfo) string { + if projectInfo.PyprojectPath == "" { + return projectInfo.SourceRoot + } + + pyprojectDir := filepath.Dir(projectInfo.PyprojectPath) + best := pyprojectDir + + const maxDepth = 5 + currentDir := filepath.Dir(pyprojectDir) + for i := 0; i < maxDepth && currentDir != filepath.Dir(currentDir) && currentDir != "."; i++ { + parentPyproject := filepath.Join(currentDir, "pyproject.toml") + if _, err := os.Stat(parentPyproject); err == nil { + best = currentDir + if config, parseErr := parsePyprojectToml(parentPyproject); parseErr == nil { + if len(config.Tool.UV.Workspace.Members) > 0 { + return currentDir + } + } + } + currentDir = filepath.Dir(currentDir) + } + + return best +} + +func buildDeploy(ctx context.Context, input *runtime.BuildInput, cacheDir string, projectRoot string) (*runtime.BuildOutput, error) { + if cacheDir == "" { + return nil, fmt.Errorf("cache directory is required") + } + if projectRoot == "" { + return nil, fmt.Errorf("project root is required") + } + + if err := os.MkdirAll(cacheDir, 0755); err != nil { + return nil, fmt.Errorf("failed to create cache directory: %w", err) + } + + projectInfo, err := resolveHandler(projectRoot, input.Handler) + if err != nil { + return nil, fmt.Errorf("project resolution: %w", err) + } + + localPackages, err := discoverBuildablePackages(projectInfo) + if err != nil { + return nil, fmt.Errorf("package discovery: %w", err) + } + + var packagesBuilt []string + for _, pkg := range localPackages { + if err := buildPackage(ctx, input, pkg); err != nil { + return nil, fmt.Errorf("build %s: %w", pkg.Name, err) + } + packagesBuilt = append(packagesBuilt, pkg.Name) + } + + if err := installDependenciesForBuild(ctx, input, projectInfo); err != nil { + return nil, fmt.Errorf("dependency installation: %w", err) + } + + // Precompile the function's own source files (deps are already compiled in the cache). + if !input.IsContainer { + if err := precompilePythonFiles(ctx, input, input.Out()); err != nil { + slog.Warn("failed to precompile Python source files", "error", err) + } + } + + output, err := createFinalBuildOutput(input, projectInfo) + if err != nil { + return nil, fmt.Errorf("build output: %w", err) + } + + return output, nil +} + +func buildPackage(ctx context.Context, input *runtime.BuildInput, pkg *localPackageInfo) error { + buildType := "sdist" + if input.Dev { + buildType = "wheel" + } + + buildCmd := &uvBuildCommand{ + PackageName: pkg.Name, + PackageDir: pkg.Path, + OutputDir: input.Out(), + BuildType: buildType, + } + + if err := runUvBuild(ctx, buildCmd); err != nil { + return fmt.Errorf("failed to build package %s: %w", pkg.Name, err) + } + + if err := extractAndProcessPackageArchive(input.Out(), pkg); err != nil { + return fmt.Errorf("package post-processing: %w", err) + } + + return nil +} + +func createFinalBuildOutput(input *runtime.BuildInput, projectInfo *projectInfo) (*runtime.BuildOutput, error) { + adjustedHandler, err := adjustHandlerPath(input, projectInfo) + if err != nil { + return nil, fmt.Errorf("failed to adjust handler path: %w", err) + } + + if input.IsContainer { + if err := ensureDockerfile(input, projectInfo); err != nil { + return nil, fmt.Errorf("failed to ensure Dockerfile: %w", err) + } + } + + return &runtime.BuildOutput{ + Out: input.Out(), + Handler: adjustedHandler, + Errors: []string{}, + Sourcemaps: []string{}, + }, nil +} + +// precompilePythonFiles runs `python -m compileall` on the given directory to generate +// .pyc bytecode files. Uses -s/-p flags to rewrite paths so bytecode matches the Lambda +// runtime path (/var/task/) rather than the local build path. Uses checked-hash invalidation +// so Python validates by source hash (not mtime) β€” avoids stale detection after zip extraction. +// Uses --python flag to match the target Lambda runtime version. +func precompilePythonFiles(ctx context.Context, input *runtime.BuildInput, dir string) error { + // Determine the target Python version from the runtime (e.g., "python3.12" -> "3.12") + pythonVersion := "" + if input.Runtime != "" { + pythonVersion = strings.TrimPrefix(input.Runtime, "python") + } + if pythonVersion == "" || pythonVersion == input.Runtime { + pythonVersion = "3.13" // default + } + + args := []string{"run", "--python", pythonVersion, "python", "-m", "compileall", + "-q", + "--invalidation-mode", "checked-hash", + "-s", dir, + "-p", "/var/task", + dir} + + cmd := process.CommandContext(ctx, "uv", args...) + cmd.Dir = dir + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("compileall failed: %v\n%s", err, string(output)) + } + slog.Info("precompiled Python bytecode", "dir", dir, "pythonVersion", pythonVersion) + return nil +} + +// ensureDockerfile ensures a Dockerfile exists in the output directory for container builds. +func ensureDockerfile(input *runtime.BuildInput, projectInfo *projectInfo) error { + outputDockerfile := filepath.Join(input.Out(), "Dockerfile") + + // Ensure pyproject.toml is in the build context for `pip install .` + outputPyproject := filepath.Join(input.Out(), "pyproject.toml") + if _, err := os.Stat(outputPyproject); err != nil && projectInfo.PyprojectPath != "" { + if _, err := os.Stat(projectInfo.PyprojectPath); err == nil { + _ = copyFile(projectInfo.PyprojectPath, outputPyproject) + } + } + + if _, err := os.Stat(outputDockerfile); err == nil { + return nil + } + + projectRoot := projectInfo.ProjectRoot + if projectRoot == "" { + projectRoot = path.ResolveRootDir(input.CfgPath) + } + + customDockerfile := filepath.Join(projectRoot, "Dockerfile") + if _, err := os.Stat(customDockerfile); err == nil { + return copyFile(customDockerfile, outputDockerfile) + } + + if projectInfo.PyprojectPath != "" { + handlerPkgDir := filepath.Dir(projectInfo.PyprojectPath) + if handlerPkgDir != projectRoot { + handlerDockerfile := filepath.Join(handlerPkgDir, "Dockerfile") + if _, err := os.Stat(handlerDockerfile); err == nil { + return copyFile(handlerDockerfile, outputDockerfile) + } + } + } + + defaultDockerfile := filepath.Join(path.ResolvePlatformDir(input.CfgPath), "dist", "dockerfiles", "python.Dockerfile") + if _, err := os.Stat(defaultDockerfile); err != nil { + return fmt.Errorf("default Python Dockerfile not found at %s: %w", defaultDockerfile, err) + } + + return copyFile(defaultDockerfile, outputDockerfile) +} + +// adjustHandlerPath adjusts the handler path for the artifact structure. +// Strips workspace prefixes for containers and flattens src/ layouts. +func adjustHandlerPath(input *runtime.BuildInput, projectInfo *projectInfo) (string, error) { + handler := strings.TrimPrefix(input.Handler, "./") + + // For container builds, strip the workspace prefix since source files + // are copied to the root of the build context. + if input.IsContainer && projectInfo.ProjectRoot != "" && projectInfo.PyprojectPath != "" { + workspaceDir := filepath.Dir(projectInfo.PyprojectPath) + if workspaceDir != projectInfo.ProjectRoot { + relWorkspacePath, err := filepath.Rel(projectInfo.ProjectRoot, workspaceDir) + if err == nil && relWorkspacePath != "." { + prefix := filepath.ToSlash(relWorkspacePath) + "/" + handler = strings.TrimPrefix(handler, prefix) + } + } + } + + lastDot := strings.LastIndex(handler, ".") + if lastDot == -1 { + return handler, nil + } + filePath := handler[:lastDot] + funcName := handler[lastDot+1:] + + // Flatten src/ layout (PEP 517): pkg/src/pkg -> pkg + adjustedPath := flattenSrcLayout(filePath) + if adjustedPath != filePath { + adjustedHandler := adjustedPath + "." + funcName + if input.IsContainer { + return adjustedHandler, nil + } + adjustedFile := filepath.Join(input.Out(), adjustedPath+".py") + if _, err := os.Stat(adjustedFile); err == nil { + return adjustedHandler, nil + } + } + + return handler, nil +} + +func extractAndProcessPackageArchive(outputDir string, pkg *localPackageInfo) error { + // Python normalizes package names: dashes become underscores + normalizedName := strings.ReplaceAll(pkg.Name, "-", "_") + + // Try wheel files first + patterns := []string{ + filepath.Join(outputDir, normalizedName+"-*.whl"), + filepath.Join(outputDir, normalizedName+"-*.tar.gz"), + filepath.Join(outputDir, pkg.Name+"-*.whl"), + filepath.Join(outputDir, pkg.Name+"-*.tar.gz"), + } + + var files []string + var err error + + for _, pattern := range patterns { + files, err = filepath.Glob(pattern) + if err != nil { + return fmt.Errorf("failed to find package archive: %w", err) + } + if len(files) > 0 { + break + } + } + + if len(files) == 0 { + return fmt.Errorf("no package archive found for %s (tried patterns: %s-*.whl, %s-*.tar.gz, %s-*.whl, %s-*.tar.gz)", + pkg.Name, normalizedName, normalizedName, pkg.Name, pkg.Name) + } + + // Process each archive file + for _, archiveFile := range files { + if err := processPackageArchive(archiveFile, outputDir); err != nil { + return fmt.Errorf("failed to process archive %s: %w", archiveFile, err) + } + } + + return nil +} + +// processPackageArchive extracts and cleans up a single package archive +func processPackageArchive(archiveFile, outputDir string) error { + if strings.HasSuffix(archiveFile, ".whl") { + if err := extractZip(archiveFile, outputDir); err != nil { + return fmt.Errorf("failed to extract wheel: %w", err) + } + + os.Remove(archiveFile) + + return nil + } + + // Extract tar.gz + if err := extractTarGz(archiveFile, outputDir); err != nil { + return fmt.Errorf("failed to extract archive: %w", err) + } + + // Get the directory name without version number + archiveBaseName := filepath.Base(archiveFile) + dirName := strings.TrimSuffix(archiveBaseName, ".tar.gz") + lastHyphen := strings.LastIndex(dirName, "-") + if lastHyphen == -1 { + return fmt.Errorf("invalid archive name format: %s", archiveBaseName) + } + + baseName := dirName[:lastHyphen] + extractedDir := filepath.Join(outputDir, dirName) + targetDir := filepath.Join(outputDir, baseName) + + // Move extracted directory to target + if err := moveExtractedPackage(extractedDir, targetDir, baseName); err != nil { + return fmt.Errorf("failed to move extracted package: %w", err) + } + + // Remove the original archive + os.Remove(archiveFile) + + return nil +} + +// extractZip extracts a zip archive (used for .whl files) to the destination directory. +func extractZip(archiveFile, destDir string) error { + r, err := zip.OpenReader(archiveFile) + if err != nil { + return fmt.Errorf("failed to open zip: %w", err) + } + defer r.Close() + + for _, f := range r.File { + // Guard against zip slip + target := filepath.Join(destDir, f.Name) + if !strings.HasPrefix(filepath.Clean(target), filepath.Clean(destDir)+string(os.PathSeparator)) { + return fmt.Errorf("illegal file path in zip: %s", f.Name) + } + + if f.FileInfo().IsDir() { + if err := os.MkdirAll(target, 0755); err != nil { + return err + } + continue + } + + if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { + return err + } + + rc, err := f.Open() + if err != nil { + return err + } + + out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + rc.Close() + return err + } + + if _, err := io.Copy(out, rc); err != nil { + out.Close() + rc.Close() + return err + } + out.Close() + rc.Close() + } + return nil +} + +// extractTarGz extracts a .tar.gz archive to the destination directory. +func extractTarGz(archiveFile, destDir string) error { + f, err := os.Open(archiveFile) + if err != nil { + return fmt.Errorf("failed to open archive: %w", err) + } + defer f.Close() + + gz, err := gzip.NewReader(f) + if err != nil { + return fmt.Errorf("failed to create gzip reader: %w", err) + } + defer gz.Close() + + tr := tar.NewReader(gz) + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("failed to read tar entry: %w", err) + } + + target := filepath.Join(destDir, hdr.Name) + // Guard against tar slip + if !strings.HasPrefix(filepath.Clean(target), filepath.Clean(destDir)+string(os.PathSeparator)) { + return fmt.Errorf("illegal file path in tar: %s", hdr.Name) + } + + switch hdr.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(target, 0755); err != nil { + return err + } + case tar.TypeReg: + if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { + return err + } + out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(hdr.Mode)) + if err != nil { + return err + } + if _, err := io.Copy(out, tr); err != nil { + out.Close() + return err + } + out.Close() + } + } + return nil +} + +// moveExtractedPackage moves the extracted package to the correct location +func moveExtractedPackage(extractedDir, targetDir, baseName string) error { + // For src layout, flatten src/{package_name} to {package_name} + srcPath := filepath.Join(extractedDir, "src", baseName) + if _, err := os.Stat(srcPath); err == nil { + if err := os.RemoveAll(targetDir); err != nil { + return fmt.Errorf("failed to remove old directory: %w", err) + } + + // Move src/{package_name} to target + if err := os.Rename(srcPath, targetDir); err != nil { + return fmt.Errorf("failed to move src directory contents: %w", err) + } + + // Clean up extracted directory + if err := os.RemoveAll(extractedDir); err != nil { + return fmt.Errorf("failed to clean up extracted directory: %w", err) + } + } else { + // No src directory β€” check if package needs flattening + if shouldFlattenPackage(extractedDir) { + return flattenPackageToRoot(extractedDir, targetDir) + } + + // Standard case: rename directory + if err := os.RemoveAll(targetDir); err != nil { + return fmt.Errorf("failed to remove old directory: %w", err) + } + + if err := os.Rename(extractedDir, targetDir); err != nil { + return fmt.Errorf("failed to rename directory: %w", err) + } + } + + return nil +} + +// shouldFlattenPackage checks if the directory contains root-level Python files +func shouldFlattenPackage(extractedDir string) bool { + entries, err := os.ReadDir(extractedDir) + if err != nil { + return false + } + + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".py") { + return true + } + } + return false +} + +// flattenPackageToRoot moves Python files from a package directory to the root level +func flattenPackageToRoot(extractedDir, outputDir string) error { + var pythonFiles []string + err := filepath.Walk(extractedDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Skip directories + if info.IsDir() { + return nil + } + + ext := filepath.Ext(path) + if ext == ".py" || ext == ".pyi" || info.Name() == "py.typed" { + pythonFiles = append(pythonFiles, path) + } + + return nil + }) + + if err != nil { + return fmt.Errorf("failed to walk extracted directory: %w", err) + } + + // Create output directory + if err := os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + + for _, srcFile := range pythonFiles { + relPath, _ := filepath.Rel(extractedDir, srcFile) + destFile := filepath.Join(outputDir, relPath) + + if err := copyFile(srcFile, destFile); err != nil { + return fmt.Errorf("failed to copy %s to %s: %w", srcFile, destFile, err) + } + } + + // Clean up the extracted directory + os.RemoveAll(extractedDir) + + return nil +} + +func installDependenciesForBuild(ctx context.Context, input *runtime.BuildInput, projectInfo *projectInfo) error { + if err := os.MkdirAll(input.Out(), 0755); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + + requirementsFile := filepath.Join(input.Out(), "requirements.txt") + if err := generateOrCopyRequirementsFile(ctx, projectInfo, requirementsFile); err != nil { + return fmt.Errorf("failed to generate requirements file: %w", err) + } + + // Determine architecture for Lambda + architecture := "x86_64" + if props, err := parseInputProperties(input); err == nil && props.Architecture != "" { + architecture = props.Architecture + } + + // Install dependencies for the target platform (Linux) + if err := installDependenciesForLambda(ctx, input, projectInfo, architecture); err != nil { + return fmt.Errorf("failed to install dependencies: %w", err) + } + + return nil +} + +// generateOrCopyRequirementsFile generates requirements.txt once per workspace, +// then copies it to each function's output directory. +func generateOrCopyRequirementsFile(ctx context.Context, projectInfo *projectInfo, outputFile string) error { + // Include dev dependencies for projects without a build system (source-only projects). + // If the project has no [build-system], runtime deps may be in the dev group. + noDev := true + if projectInfo.PyprojectPath != "" { + if !hasBuildConfig(filepath.Dir(projectInfo.PyprojectPath)) { + noDev = false + } + } + + // Determine if this is a workspace member (has its own pyproject.toml in a subdirectory) + packageName := "" + useAllPackages := true + workspaceRoot := findWorkspaceRoot(projectInfo) + + if projectInfo.PyprojectPath != "" { + if config, err := parsePyprojectToml(projectInfo.PyprojectPath); err == nil { + if config.Project.Name != "" { + pyprojectDir := filepath.Dir(projectInfo.PyprojectPath) + if workspaceRoot != pyprojectDir { + packageName = config.Project.Name + useAllPackages = false + } + } + } else { + slog.Warn("failed to parse pyproject.toml", "path", projectInfo.PyprojectPath, "error", err) + } + } + + exportCmd := &uvExportCommand{ + WorkspaceDir: workspaceRoot, + PackageName: packageName, + OutputFile: outputFile, + NoEmitWorkspace: false, + NoDev: noDev, + AllPackages: useAllPackages, + NoEmitProject: !useAllPackages, + NoEditable: true, + } + + // uv export is fast (~300ms, no network/installs) so we run it per function + // rather than caching. The .deps disk cache handles the expensive uv pip install. + if err := runUvExport(ctx, exportCmd); err != nil { + return err + } + + return nil +} + +// inputProperties represents the input properties structure +type inputProperties struct { + Architecture string `json:"architecture"` +} + +// parseInputProperties parses the input properties JSON +func parseInputProperties(input *runtime.BuildInput) (*inputProperties, error) { + var props inputProperties + if err := json.Unmarshal(input.Properties, &props); err != nil { + return nil, fmt.Errorf("failed to parse properties: %w", err) + } + + return &props, nil +} + +func installDependenciesForLambda(ctx context.Context, input *runtime.BuildInput, projectInfo *projectInfo, architecture string) error { + if err := copySourceFilesSimple(input, projectInfo); err != nil { + return fmt.Errorf("failed to copy source files: %w", err) + } + + // Container builds: Dockerfile handles deps; zip builds: install here + if input.IsContainer { + if err := copyWorkspacePackagesForContainer(input, projectInfo); err != nil { + return fmt.Errorf("failed to copy workspace packages for container: %w", err) + } + } else { + if err := copySyncedDependencies(ctx, input, projectInfo, architecture); err != nil { + return fmt.Errorf("failed to copy synced dependencies: %w", err) + } + } + + return nil +} + +// copyWorkspacePackagesForContainer copies workspace package directories into the artifact +// so the Dockerfile's `uv pip install -r requirements.txt` can resolve relative paths. +func copyWorkspacePackagesForContainer(input *runtime.BuildInput, projectInfo *projectInfo) error { + workspaceRoot := findWorkspaceRoot(projectInfo) + + requirementsPath := filepath.Join(input.Out(), "requirements.txt") + content, err := os.ReadFile(requirementsPath) + if err != nil { + return nil + } + + lines := strings.Split(string(content), "\n") + + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "-") { + continue + } + + if !strings.HasPrefix(line, "./") && !strings.HasPrefix(line, "../") { + continue + } + + // Strip extras or markers (e.g., "./core[extra] ; python_version >= '3.11'") + pkgPath := line + for _, sep := range []string{" ", "[", ";"} { + if idx := strings.Index(pkgPath, sep); idx > 0 { + pkgPath = pkgPath[:idx] + } + } + + // Resolve full path relative to workspace root + fullPath := filepath.Join(workspaceRoot, pkgPath) + if _, err := os.Stat(fullPath); err != nil { + slog.Warn("workspace package directory not found", "path", fullPath, "line", line) + continue + } + + // Copy to artifact at the same relative path + destPath := filepath.Join(input.Out(), pkgPath) + if _, err := os.Stat(destPath); err == nil { + // Already exists β€” just ensure pyproject.toml is present for uv pip install + srcPyproject := filepath.Join(fullPath, "pyproject.toml") + destPyproject := filepath.Join(destPath, "pyproject.toml") + if _, err := os.Stat(srcPyproject); err == nil { + if _, err := os.Stat(destPyproject); err != nil { + data, readErr := os.ReadFile(srcPyproject) + if readErr != nil { + return fmt.Errorf("failed to read pyproject.toml for workspace package %s: %w", pkgPath, readErr) + } + if err := os.WriteFile(destPyproject, data, 0644); err != nil { + return fmt.Errorf("failed to copy pyproject.toml for workspace package %s: %w", pkgPath, err) + } + } + } + continue + } + + if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { + return fmt.Errorf("failed to create directory for workspace package %s: %w", pkgPath, err) + } + + // Preserve pyproject.toml and metadata for uv pip install + if err := copyDir(fullPath, destPath, skipBuildArtifacts); err != nil { + return fmt.Errorf("failed to copy workspace package %s: %w", pkgPath, err) + } + + } + + return nil +} + +// copySourceFilesSimple copies handler source files to the build output. +// Workspace packages are installed via uv pip install separately. +func copySourceFilesSimple(input *runtime.BuildInput, projectInfo *projectInfo) error { + workspaceDir := projectInfo.SourceRoot + if projectInfo.PyprojectPath != "" { + workspaceDir = filepath.Dir(projectInfo.PyprojectPath) + } + + handlerPath := input.Handler + + // Strip workspace prefix from handler path + var outputPrefix string + if projectInfo.ProjectRoot != "" && workspaceDir != projectInfo.ProjectRoot { + relWorkspacePath, err := filepath.Rel(projectInfo.ProjectRoot, workspaceDir) + if err == nil && relWorkspacePath != "." { + relWorkspacePath = filepath.ToSlash(relWorkspacePath) + prefix := relWorkspacePath + "/" + if strings.HasPrefix(handlerPath, prefix) { + handlerPath = strings.TrimPrefix(handlerPath, prefix) + outputPrefix = relWorkspacePath + } + } + } + + outputBase := input.Out() + // For container builds, source files go at root of build context (Dockerfile expects it). + // For zip builds, preserve the nested directory structure via outputPrefix. + if outputPrefix != "" && !input.IsContainer { + outputBase = filepath.Join(input.Out(), outputPrefix) + } + + if strings.Contains(handlerPath, "/") { + // Find the top-level directory that exists in the workspace + parts := strings.Split(handlerPath, "/") + copied := false + for i := 0; i < len(parts)-1; i++ { + candidate := parts[i] + candidatePath := filepath.Join(workspaceDir, candidate) + if info, err := os.Stat(candidatePath); err == nil && info.IsDir() { + if err := copyDir(candidatePath, filepath.Join(outputBase, candidate), skipContent); err != nil { + return fmt.Errorf("failed to copy directory %s: %w", candidate, err) + } + copied = true + break + } + } + if !copied { + // Handler path fully resolved by workspaceDir β€” root .py files will be copied below + } + } + + // Also copy root-level .py files + entries, err := os.ReadDir(workspaceDir) + if err != nil { + return fmt.Errorf("failed to read workspace directory: %w", err) + } + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".py") { + if err := copyFile(filepath.Join(workspaceDir, entry.Name()), filepath.Join(outputBase, entry.Name())); err != nil { + return fmt.Errorf("failed to copy file %s: %w", entry.Name(), err) + } + } + } + + return nil +} + +// copySyncedDependencies installs dependencies with correct platform targeting +func copySyncedDependencies(ctx context.Context, input *runtime.BuildInput, projectInfo *projectInfo, architecture string) error { + requirementsPath := filepath.Join(input.Out(), "requirements.txt") + + if _, err := os.Stat(requirementsPath); os.IsNotExist(err) { + slog.Warn("requirements.txt not found, skipping dependency installation", "path", requirementsPath) + return nil + } + + workspaceRoot := findWorkspaceRoot(projectInfo) + + // Filter editable installs from requirements + filteredRequirementsPath := filepath.Join(input.Out(), "requirements-filtered.txt") + err := filterEditableInstalls(requirementsPath, filteredRequirementsPath) + if err != nil { + return fmt.Errorf("failed to filter requirements: %w", err) + } + requirementsPath = filteredRequirementsPath + + // Cache key from requirements hash + architecture + requirementsHash, err := hashFileContents(requirementsPath) + var cacheKey string + var depsCacheDir string + + if err == nil { + cacheKey = fmt.Sprintf("%s-%s", requirementsHash, architecture) + depsCacheDir = filepath.Join(filepath.Dir(input.Out()), ".deps", cacheKey) + + // Acquire lock to prevent concurrent installs for the same cache key + globalDependencyInstallLocksMutex.Lock() + cacheLock, exists := globalDependencyInstallLocks[cacheKey] + if !exists { + cacheLock = &sync.Mutex{} + globalDependencyInstallLocks[cacheKey] = cacheLock + } + globalDependencyInstallLocksMutex.Unlock() + + // Acquire lock with timeout (5 minutes) + lockAcquired := make(chan struct{}) + go func() { + cacheLock.Lock() + close(lockAcquired) + }() + select { + case <-lockAcquired: + // got the lock + case <-ctx.Done(): + return fmt.Errorf("context cancelled while waiting for dependency install lock") + case <-time.After(5 * time.Minute): + return fmt.Errorf("timed out waiting for dependency install lock after 5 minutes") + } + defer cacheLock.Unlock() + + // Check disk cache + if entries, err := os.ReadDir(depsCacheDir); err == nil && len(entries) > 0 { + if err := copyDependencyPackages(depsCacheDir, input.Out()); err != nil { + slog.Warn("failed to copy from disk cache, will reinstall", "error", err) + // Remove bad cache and continue to reinstall + os.RemoveAll(depsCacheDir) + } else { + return nil + } + } + + // Cache miss - create the cache directory + if err := os.MkdirAll(depsCacheDir, 0755); err != nil { + return fmt.Errorf("failed to create deps cache directory: %w", err) + } + } else { + depsCacheDir = input.Out() + } + + // Use --reinstall-package for workspace packages to bypass uv's stale cache + workspacePackages := getWorkspacePackageNames(projectInfo) + + // We use --reinstall-package (not --reinstall) to avoid re-fetching slow git dependencies + args := []string{"pip", "install", "-r", requirementsPath, "--target", depsCacheDir} + + for _, pkg := range workspacePackages { + args = append(args, "--reinstall-package", pkg) + } + + // Platform targeting for Lambda deployments (skip in dev mode and containers) + // Skip if already on the target platform to use native cached wheels + if !input.Dev && !input.IsContainer { + needsCrossPlatform := false + currentArch := goruntime.GOARCH + currentOS := goruntime.GOOS + + // Cross-platform needed if not on Linux or architecture mismatch + targetIsArm := architecture == "arm64" + currentIsArm := currentArch == "arm64" + + if currentOS != "linux" || targetIsArm != currentIsArm { + needsCrossPlatform = true + } + + if needsCrossPlatform { + // Manylinux tags for GLIBC compatibility: + // python3.11 and below β†’ AL2 (manylinux2014, GLIBC 2.17) + // python3.12 and above β†’ AL2023 (manylinux_2_28, GLIBC 2.28) + pythonVersion := strings.TrimPrefix(input.Runtime, "python") + if pythonVersion == "" || pythonVersion == input.Runtime { + pythonVersion = "3.13" + } + + manylinuxTag := "manylinux_2_28" + if parts := strings.SplitN(pythonVersion, ".", 2); len(parts) == 2 { + if minor, err := strconv.Atoi(parts[1]); err == nil && minor <= 11 { + manylinuxTag = "manylinux2014" + } + } + + archPrefix := "x86_64" + if architecture == "arm64" { + archPrefix = "aarch64" + } + pythonPlatform := archPrefix + "-" + manylinuxTag + + args = append(args, "--python-platform", pythonPlatform, "--python-version", pythonVersion) + } + } + + // Run from workspace root (requirements.txt has relative paths like ./vendored_sst) + installWorkspaceDir := workspaceRoot + + installCtx, installCancel := context.WithTimeout(ctx, 15*time.Minute) + defer installCancel() + + installCmd := process.CommandContext(installCtx, "uv", args...) + installCmd.Dir = installWorkspaceDir + + // Use a channel for timeout handling + type cmdResult struct { + output []byte + err error + } + resultChan := make(chan cmdResult, 1) + + // Progress ticker to diagnose hangs + installStartTime := time.Now() + progressDone := make(chan struct{}) + go func() { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + for { + select { + case <-progressDone: + return + case <-ticker.C: + elapsed := time.Since(installStartTime) + slog.Warn("uv pip install still running", "elapsed", elapsed) + } + } + }() + + go func() { + output, err := installCmd.CombinedOutput() + resultChan <- cmdResult{output, err} + }() + + var installOutput []byte + select { + case result := <-resultChan: + close(progressDone) + installOutput = result.output + err = result.err + case <-installCtx.Done(): + close(progressDone) + if installCmd.Process != nil { + installCmd.Process.Kill() + } + // Remove partial cache on timeout + if cacheKey != "" { + os.RemoveAll(depsCacheDir) + } + return fmt.Errorf("uv pip install timed out after 15 minutes - check network connectivity and try again") + } + + if err != nil { + slog.Error("uv pip install failed", + "command", strings.Join(installCmd.Args, " "), + "error", err, + "output", string(installOutput), + "functionID", input.FunctionID, + "handler", input.Handler, + "workingDir", installWorkspaceDir, + "pyprojectPath", projectInfo.PyprojectPath) + if cacheKey != "" { + os.RemoveAll(depsCacheDir) + } + return fmt.Errorf("failed to run uv pip install: %v\n%s\n\nFunction: %s\nHandler: %s\nWorking directory: %s\nPyproject path: %s", + err, string(installOutput), input.FunctionID, input.Handler, installWorkspaceDir, projectInfo.PyprojectPath) + } + + if err := cleanupInstalledDependencies(depsCacheDir); err != nil { + slog.Warn("failed to clean up installed dependencies", "error", err) + } + + // Precompile bytecode in the cache so all functions sharing these deps benefit + if err := precompilePythonFiles(ctx, input, depsCacheDir); err != nil { + slog.Warn("failed to precompile dependencies in cache", "error", err) + } + + if err := copyDependencyPackages(depsCacheDir, input.Out()); err != nil { + return fmt.Errorf("failed to copy dependencies to artifact: %w", err) + } + + os.Remove(filteredRequirementsPath) + + return nil +} + +// filterEditableInstalls removes editable (-e) local path installs from requirements.txt. +// Editable installs create symlinks which won't work in Lambda. +func filterEditableInstalls(inputPath, outputPath string) error { + content, err := os.ReadFile(inputPath) + if err != nil { + return fmt.Errorf("failed to read requirements file: %w", err) + } + + lines := strings.Split(string(content), "\n") + var filteredLines []string + + for _, line := range lines { + originalLine := line + line = strings.TrimSpace(line) + + // Skip empty lines and comments + if line == "" || strings.HasPrefix(line, "#") { + filteredLines = append(filteredLines, originalLine) + continue + } + + if strings.HasPrefix(line, "-e ") { + editablePath := strings.TrimSpace(strings.TrimPrefix(line, "-e ")) + + if strings.HasPrefix(editablePath, "./") || strings.HasPrefix(editablePath, "../") || + strings.HasPrefix(editablePath, "/") && !strings.Contains(editablePath, "://") { + continue + } + } + + // NON-editable local paths and boto3/botocore are handled downstream + filteredLines = append(filteredLines, originalLine) + } + + // Write filtered requirements + filteredContent := strings.Join(filteredLines, "\n") + if err := os.WriteFile(outputPath, []byte(filteredContent), 0644); err != nil { + return fmt.Errorf("failed to write filtered requirements file: %w", err) + } + + return nil +} + +// cleanupInstalledDependencies removes __pycache__, .pyc files, .dist-info, test dirs, +// and boto3/botocore (Lambda provides them, saves ~22MB) unless user opts in. +func cleanupInstalledDependencies(targetDir string) error { + err := filepath.Walk(targetDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil // Continue walking despite errors + } + + if path == targetDir { + return nil + } + + if !info.IsDir() { + ext := filepath.Ext(info.Name()) + fileName := info.Name() + if ext == ".pyo" || fileName == ".DS_Store" { + os.Remove(path) + } + } + + // Remove test directories + if info.IsDir() { + dirName := info.Name() + if dirName == "SelfTest" || dirName == "tests" || dirName == "test" { + os.RemoveAll(path) + return filepath.SkipDir + } + } + + return nil + }) + + if err != nil { + return fmt.Errorf("error walking target directory during cleanup: %w", err) + } + + return nil +} + +// getWorkspacePackageNames returns workspace package names from pyproject.toml +func getWorkspacePackageNames(projectInfo *projectInfo) []string { + var packages []string + + // Add the main package name + if projectInfo.PyprojectPath != "" { + if config, err := parsePyprojectToml(projectInfo.PyprojectPath); err == nil { + if config.Project.Name != "" { + packages = append(packages, config.Project.Name) + } else if config.Tool.Poetry.Name != "" { + packages = append(packages, config.Tool.Poetry.Name) + } + + // Add workspace packages referenced via { workspace = true } + for name, source := range config.Tool.UV.Sources { + if source.Workspace { + packages = append(packages, name) + } + } + } + } + + return packages +} + +// copyDependencyPackages copies installed dependency packages (not requirements.txt, etc.) +func copyDependencyPackages(srcDir, destDir string) error { + slog.Debug("copying dependency packages", "src", srcDir) + + entries, err := os.ReadDir(srcDir) + if err != nil { + return fmt.Errorf("failed to read source directory: %w", err) + } + + copiedCount := 0 + copiedFiles := 0 + copiedPthPackages := 0 + for _, entry := range entries { + name := entry.Name() + + // Skip special directories and non-package files + if strings.HasPrefix(name, ".") { + continue + } + + // Skip non-package files + if name == "requirements.txt" || name == "requirements-filtered.txt" || name == "resource.enc" { + continue + } + + srcPath := filepath.Join(srcDir, name) + destPath := filepath.Join(destDir, name) + + if entry.IsDir() { + if err := copyDir(srcPath, destPath, skipContent); err != nil { + slog.Warn("failed to copy package", "package", name, "error", err) + continue + } + copiedCount++ + } else if strings.HasSuffix(name, ".pth") { + // .pth files are UV path config files pointing to workspace package sources + pthContent, err := os.ReadFile(srcPath) + if err != nil { + slog.Warn("failed to read .pth file", "file", name, "error", err) + continue + } + + packageSourcePath := strings.TrimSpace(string(pthContent)) + if packageSourcePath == "" { + slog.Warn(".pth file is empty", "file", name) + continue + } + + // Resolve symlinked .pth files + if info, err := os.Lstat(srcPath); err == nil && info.Mode()&os.ModeSymlink != 0 { + realPath, err := filepath.EvalSymlinks(srcPath) + if err != nil { + slog.Warn("failed to resolve .pth symlink", "file", name, "error", err) + continue + } + pthContent, err = os.ReadFile(realPath) + if err != nil { + slog.Warn("failed to read resolved .pth file", "file", name, "realPath", realPath, "error", err) + continue + } + packageSourcePath = strings.TrimSpace(string(pthContent)) + } + + // Extract package name from .pth filename (e.g., "_sst.pth" -> "sst") + pthBaseName := strings.TrimSuffix(name, ".pth") + packageName := strings.TrimPrefix(pthBaseName, "_") + + // Find the actual package directory + var packageDir string + + candidatePath := filepath.Join(packageSourcePath, packageName) + if info, err := os.Stat(candidatePath); err == nil && info.IsDir() { + packageDir = candidatePath + } else { + // Try pyproject.toml for hatch build targets + pyprojectPath := filepath.Join(packageSourcePath, "pyproject.toml") + if _, err := os.Stat(pyprojectPath); err == nil { + if config, err := parsePyprojectToml(pyprojectPath); err == nil { + // Check hatch build targets + if len(config.Tool.Hatch.Build.Targets.Wheel.Packages) > 0 { + pkgName := config.Tool.Hatch.Build.Targets.Wheel.Packages[0] + candidatePath = filepath.Join(packageSourcePath, pkgName) + if info, err := os.Stat(candidatePath); err == nil && info.IsDir() { + packageDir = candidatePath + packageName = pkgName // Use the actual package name from config + } + } + } + } + } + + if packageDir == "" { + slog.Warn("could not find package directory for .pth file", + "pthFile", name, + "packageSourcePath", packageSourcePath, + "packageName", packageName) + continue + } + + // Copy the package + packageDestPath := filepath.Join(destDir, packageName) + slog.Debug("copying package from .pth reference", + "packageName", packageName, + "source", packageDir) + + if err := copyDir(packageDir, packageDestPath, skipContent); err != nil { + slog.Warn("failed to copy package from .pth", "package", packageName, "error", err) + continue + } + copiedPthPackages++ + } else if strings.HasSuffix(name, ".so") || strings.HasSuffix(name, ".py") { + if err := copyFile(srcPath, destPath); err != nil { + slog.Warn("failed to copy root file", "file", name, "error", err) + continue + } + copiedFiles++ + } + } + + slog.Debug("copied dependency packages", "directories", copiedCount, "rootFiles", copiedFiles, "pthPackages", copiedPthPackages) + return nil +} + +// localPackageInfo contains information about a local package +type localPackageInfo struct { + Name string + Path string +} + +// discoverBuildablePackages finds local packages that need building. +// Checks pyproject.toml workspace members first, then falls back to directory scanning. +func discoverBuildablePackages(projectInfo *projectInfo) ([]*localPackageInfo, error) { + workspaceDir := projectInfo.SourceRoot + if projectInfo.PyprojectPath != "" { + workspaceDir = filepath.Dir(projectInfo.PyprojectPath) + } + + // Try workspace members from pyproject.toml first + pyprojectPath := filepath.Join(workspaceDir, "pyproject.toml") + if _, err := os.Stat(pyprojectPath); err == nil { + if config, err := parsePyprojectToml(pyprojectPath); err == nil { + paths := workspacePackagePaths(config, workspaceDir) + if len(paths) > 0 { + return buildableFromPaths(paths) + } + } + } + + // Fallback: scan common directories for buildable packages + return buildableFromScan(workspaceDir) +} + +// workspacePackagePaths extracts package paths from a parsed pyproject.toml +func workspacePackagePaths(config *pyprojectConfig, workspaceDir string) []string { + var paths []string + + // UV workspace members + for _, member := range config.Tool.UV.Workspace.Members { + paths = append(paths, filepath.Join(workspaceDir, member)) + } + + // UV sources with local paths + for _, source := range config.Tool.UV.Sources { + if source.Path != "" { + paths = append(paths, filepath.Join(workspaceDir, source.Path)) + } + } + + // Setuptools packages.find.where + for _, where := range config.Tool.Setuptools.Packages.Find.Where { + paths = append(paths, filepath.Join(workspaceDir, where)) + } + + // Hatch build targets + for _, pkg := range config.Tool.Hatch.Build.Targets.Wheel.Packages { + paths = append(paths, filepath.Join(workspaceDir, pkg)) + } + + return paths +} + +// buildableFromPaths filters a list of paths to only those with build configuration +func buildableFromPaths(paths []string) ([]*localPackageInfo, error) { + var packages []*localPackageInfo + for _, p := range paths { + if !hasBuildConfig(p) { + continue + } + name := filepath.Base(p) + pyprojectPath := filepath.Join(p, "pyproject.toml") + if config, err := parsePyprojectToml(pyprojectPath); err == nil && config.Project.Name != "" { + name = config.Project.Name + } + packages = append(packages, &localPackageInfo{Name: name, Path: p}) + } + return packages, nil +} + +// buildableFromScan scans common directories for buildable packages +func buildableFromScan(workspaceDir string) ([]*localPackageInfo, error) { + var packages []*localPackageInfo + + // Check workspace root + if hasBuildConfig(workspaceDir) { + name := filepath.Base(workspaceDir) + if config, err := parsePyprojectToml(filepath.Join(workspaceDir, "pyproject.toml")); err == nil && config.Project.Name != "" { + name = config.Project.Name + } + packages = append(packages, &localPackageInfo{Name: name, Path: workspaceDir}) + } + + // Check common package locations one level deep + for _, dir := range []string{"src", "packages", "libs"} { + searchDir := filepath.Join(workspaceDir, dir) + entries, err := os.ReadDir(searchDir) + if err != nil { + continue + } + for _, entry := range entries { + if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") { + continue + } + candidatePath := filepath.Join(searchDir, entry.Name()) + if !hasBuildConfig(candidatePath) { + continue + } + name := entry.Name() + if config, err := parsePyprojectToml(filepath.Join(candidatePath, "pyproject.toml")); err == nil && config.Project.Name != "" { + name = config.Project.Name + } + packages = append(packages, &localPackageInfo{Name: name, Path: candidatePath}) + } + } + + // Check immediate subdirectories of workspace root + entries, err := os.ReadDir(workspaceDir) + if err == nil { + for _, entry := range entries { + if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") { + continue + } + candidatePath := filepath.Join(workspaceDir, entry.Name()) + if !hasBuildConfig(candidatePath) { + continue + } + // Skip if already found + found := false + for _, p := range packages { + if p.Path == candidatePath { + found = true + break + } + } + if found { + continue + } + name := entry.Name() + if config, err := parsePyprojectToml(filepath.Join(candidatePath, "pyproject.toml")); err == nil && config.Project.Name != "" { + name = config.Project.Name + } + packages = append(packages, &localPackageInfo{Name: name, Path: candidatePath}) + } + } + + return packages, nil +} + +// hasBuildConfig checks if a directory has build configuration (pyproject.toml with [build-system], setup.py, etc.) +func hasBuildConfig(dir string) bool { + // setup.py or setup.cfg β†’ has build config + for _, f := range []string{"setup.py", "setup.cfg"} { + if _, err := os.Stat(filepath.Join(dir, f)); err == nil { + return true + } + } + + // pyproject.toml with build configuration + pyprojectPath := filepath.Join(dir, "pyproject.toml") + content, err := os.ReadFile(pyprojectPath) + if err != nil { + return false + } + + contentStr := string(content) + + // Check for build system or tool-specific build config + buildIndicators := []string{ + "[build-system]", + "[tool.setuptools]", + "[tool.poetry]", + "[tool.hatch]", + "[tool.flit]", + "[tool.pdm]", + } + for _, indicator := range buildIndicators { + if strings.Contains(contentStr, indicator) { + return true + } + } + + return false +} + +// --- UV commands --- + +const uvCommandTimeout = 1 * time.Minute + +// commandResult represents the result of a UV command execution +type commandResult struct { + ExitCode int + Stderr string + Success bool +} + +// uvBuildCommand represents a UV build command +type uvBuildCommand struct { + PackageName string + PackageDir string + OutputDir string + BuildType string +} + +// uvExportCommand represents a UV export command +type uvExportCommand struct { + WorkspaceDir string + PackageName string + OutputFile string + NoEmitWorkspace bool + NoDev bool + NoEditable bool + NoEmitProject bool + AllPackages bool +} + +// runUvBuild executes a UV build command for a single package +func runUvBuild(ctx context.Context, cmd *uvBuildCommand) error { + args := []string{"build"} + + if cmd.PackageName != "" { + args = append(args, "--package="+cmd.PackageName) + } + + if cmd.BuildType == "wheel" { + args = append(args, "--wheel") + } else { + args = append(args, "--sdist") + } + + if cmd.OutputDir != "" { + args = append(args, "--out-dir="+cmd.OutputDir) + } + + args = append(args, "--no-sources") + + workingDir := cmd.PackageDir + if workingDir == "" { + workingDir = "." + } + + result, err := runUvCommand(ctx, "uv", args, workingDir) + if err != nil { + slog.Error("UV build command failed", + "package", cmd.PackageName, + "command", "uv "+strings.Join(args, " "), + "error", err) + return fmt.Errorf("uv build failed: %w", err) + } + + if !result.Success { + return fmt.Errorf("uv build failed (exit code %d): %s", result.ExitCode, result.Stderr) + } + + return nil +} + +// runUvExport executes a UV export command +func runUvExport(ctx context.Context, cmd *uvExportCommand) error { + args := []string{"export"} + + if cmd.AllPackages { + args = append(args, "--all-packages") + } else if cmd.PackageName != "" { + args = append(args, "--package="+cmd.PackageName) + } + + if cmd.OutputFile != "" { + args = append(args, "--output-file="+cmd.OutputFile) + } + if cmd.NoEmitWorkspace { + args = append(args, "--no-emit-workspace") + } + if cmd.NoEditable { + args = append(args, "--no-editable") + } + if cmd.NoEmitProject { + args = append(args, "--no-emit-project") + } + if cmd.NoDev { + args = append(args, "--no-dev") + } + + result, err := runUvCommand(ctx, "uv", args, cmd.WorkspaceDir) + if err != nil { + return fmt.Errorf("UV export failed: %w", err) + } + + if !result.Success { + return detailedExportError(result, cmd) + } + + return nil +} + +// detailedExportError creates a detailed error message for export failures +func detailedExportError(result *commandResult, cmd *uvExportCommand) error { + errorMsg := fmt.Sprintf("UV export failed with exit code %d", result.ExitCode) + + if cmd.PackageName != "" { + errorMsg += fmt.Sprintf(" (exporting package: %s)", cmd.PackageName) + } + if cmd.OutputFile != "" { + errorMsg += fmt.Sprintf(" to file: %s", cmd.OutputFile) + } + errorMsg += fmt.Sprintf(" in workspace: %s", cmd.WorkspaceDir) + + if result.Stderr != "" { + errorMsg += fmt.Sprintf("\nError output: %s", result.Stderr) + } + + if strings.Contains(result.Stderr, "package") && strings.Contains(result.Stderr, "not found") { + errorMsg += "\nSuggestion: Check if the package name is correct and exists in the workspace" + } else if strings.Contains(result.Stderr, "lock") { + errorMsg += "\nSuggestion: Run 'uv sync' first to ensure dependencies are resolved" + } + + return fmt.Errorf("%s", errorMsg) +} + +// runUvCommand executes a command with timeout and progress logging +func runUvCommand(ctx context.Context, command string, args []string, workingDir string) (*commandResult, error) { + startTime := time.Now() + + cmdCtx, cancel := context.WithTimeout(ctx, uvCommandTimeout) + defer cancel() + + cmd := process.CommandContext(cmdCtx, command, args...) + if workingDir != "" { + cmd.Dir = workingDir + } + cmd.Env = os.Environ() + + done := make(chan bool) + go func() { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + for { + select { + case <-done: + return + case <-ticker.C: + slog.Warn("UV command still running", + "command", command, + "elapsed", time.Since(startTime), + "workingDir", workingDir) + } + } + }() + + output, err := cmd.CombinedOutput() + close(done) + + result := &commandResult{} + + if err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + result.ExitCode = exitError.ExitCode() + } else { + result.ExitCode = -1 + } + result.Success = false + result.Stderr = string(output) + slog.Error("command error output", "stderr", result.Stderr) + } else { + result.ExitCode = 0 + result.Success = true + } + + return result, nil +} + +// --- Content filter (merged from content_filter.go) --- + +// defaultExcludePatterns lists patterns to exclude from deployment artifacts. +// Directory names (e.g. ".git") match all files underneath them. +var defaultExcludePatterns = []string{ + ".sst", ".git", ".gitignore", ".gitattributes", + + "*.pyo", "*.pyd", + ".pytest_cache", "*.egg-info", ".coverage", "htmlcov", + + ".venv", "venv", ".env", "env", + + ".vscode", ".idea", "*.swp", "*.swo", "*~", ".DS_Store", + + "node_modules", "package-lock.json", "yarn.lock", "bun.lockb", + + "README.md", "README.rst", "README.txt", + "CHANGELOG.md", "CHANGELOG.rst", "CHANGELOG.txt", + "MANIFEST.in", + "setup.cfg", "tox.ini", "Makefile", + + "tests", "test", + + "requirements-dev.txt", "requirements.dev.txt", "dev-requirements.txt", + ".python-version", ".pre-commit-config.yaml", + + "*.log", "*.tmp", "tmp", "temp", +} + +// isIgnored checks if a file or directory should be excluded from deployment artifacts. +func isIgnored(path string) bool { + normalizedPath := filepath.ToSlash(path) + + for _, pattern := range defaultExcludePatterns { + if matchesPattern(normalizedPath, pattern) { + return true + } + } + return false +} + +// matchesPattern checks if a path matches a pattern. +// Supports wildcards (*.pyc), directory names (.git matches .git/anything), +// and ** glob patterns. +func matchesPattern(path, pattern string) bool { + if dir, ok := strings.CutSuffix(pattern, "/"); ok { + return strings.HasPrefix(path, dir+"/") || path == dir + } + + if strings.Contains(pattern, "**") { + pattern = strings.ReplaceAll(pattern, "**/", "") + pattern = strings.ReplaceAll(pattern, "/**", "") + pattern = strings.ReplaceAll(pattern, "**", "") + if pattern == "" { + return true + } + } + + for _, part := range strings.Split(path, "/") { + if part == pattern { + return true + } + if matched, err := filepath.Match(pattern, part); err == nil && matched { + return true + } + } + + if matched, err := filepath.Match(pattern, path); err == nil && matched { + return true + } + + return false +} diff --git a/pkg/runtime/python/build_test.go b/pkg/runtime/python/build_test.go new file mode 100644 index 0000000000..851831ab54 --- /dev/null +++ b/pkg/runtime/python/build_test.go @@ -0,0 +1,305 @@ +package python + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/sst/sst/v3/pkg/runtime" +) + +func TestDeployBuilder_CleanupInstalledDependencies(t *testing.T) { + tempDir := t.TempDir() + + testFiles := map[string]string{ + "requests/__init__.py": "# requests", + "requests/api.py": "# api", + "boto3/__init__.py": "# boto3", + "botocore/__init__.py": "# botocore", + "requests/__pycache__/__init__.cpython-312.pyc": "compiled", + "boto3/__pycache__/__init__.cpython-312.pyc": "compiled", + "some_module.pyc": "compiled", + } + + for filePath, content := range testFiles { + fullPath := filepath.Join(tempDir, filePath) + os.MkdirAll(filepath.Dir(fullPath), 0755) + os.WriteFile(fullPath, []byte(content), 0644) + } + + if err := cleanupInstalledDependencies(tempDir); err != nil { + t.Fatalf("cleanupInstalledDependencies failed: %v", err) + } + + // All packages should be kept (no special stripping) + for _, pkg := range []string{"boto3", "botocore", "requests"} { + if _, err := os.Stat(filepath.Join(tempDir, pkg, "__init__.py")); err != nil { + t.Errorf("package %s should have been kept", pkg) + } + } + + // __pycache__ and .pyc files should be preserved (needed for cold start performance) + if _, err := os.Stat(filepath.Join(tempDir, "requests", "__pycache__")); err != nil { + t.Error("__pycache__ should have been preserved for bytecode caching") + } + + if _, err := os.Stat(filepath.Join(tempDir, "some_module.pyc")); err != nil { + t.Error(".pyc files should have been preserved for bytecode caching") + } +} + +func TestLegacyStructureRegressionFixes(t *testing.T) { + // Test 1: Path duplication fix for legacy functions/src/functions structure + t.Run("Legacy path duplication regression", func(t *testing.T) { + tempDir, err := os.MkdirTemp("", "sst-legacy-path-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create legacy structure: functions/src/functions/user/get_user_session.py + functionsDir := filepath.Join(tempDir, "functions") + srcDir := filepath.Join(functionsDir, "src") + innerFunctionsDir := filepath.Join(srcDir, "functions") + userDir := filepath.Join(innerFunctionsDir, "user") + + if err := os.MkdirAll(userDir, 0755); err != nil { + t.Fatalf("Failed to create directory structure: %v", err) + } + + handlerFile := filepath.Join(userDir, "get_user_session.py") + if err := os.WriteFile(handlerFile, []byte("def handler(event, context): pass"), 0644); err != nil { + t.Fatalf("Failed to create handler file: %v", err) + } + + projectInfo := &projectInfo{ + SourceRoot: functionsDir, // This used to cause path duplication + } + + input := &runtime.BuildInput{ + CfgPath: tempDir, + FunctionID: "legacy-test", + Handler: "functions/src/functions/user/get_user_session.handler", + } + + actualOutputDir := input.Out() + if err := os.MkdirAll(actualOutputDir, 0755); err != nil { + t.Fatalf("Failed to create output dir: %v", err) + } + + err = copySourceFilesSimple(input, projectInfo) + if err != nil { + t.Fatalf("copySourceFilesSimple failed: %v", err) + } + + // Verify the file was copied correctly + copiedFile := filepath.Join(actualOutputDir, "src", "functions", "user", "get_user_session.py") + if _, err := os.Stat(copiedFile); err != nil { + t.Errorf("Expected file not found: %s", copiedFile) + } + }) + + // Test 2: filterEditableInstalls keeps non-editable requirements unchanged + t.Run("filterEditableInstalls preserves standard requirements", func(t *testing.T) { + tempDir, err := os.MkdirTemp("", "sst-requirements-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + localPkgDir := filepath.Join(tempDir, "local-package") + if err := os.MkdirAll(localPkgDir, 0755); err != nil { + t.Fatalf("Failed to create local package dir: %v", err) + } + + requirementsContent := `requests==2.31.0 +boto3>=1.34.0` + + inputPath := filepath.Join(tempDir, "requirements.txt") + outputPath := filepath.Join(tempDir, "requirements-filtered.txt") + + if err := os.WriteFile(inputPath, []byte(requirementsContent), 0644); err != nil { + t.Fatalf("Failed to write requirements.txt: %v", err) + } + + err = filterEditableInstalls(inputPath, outputPath) + if err != nil { + t.Fatalf("filterEditableInstalls failed: %v", err) + } + + filteredContent, err := os.ReadFile(outputPath) + if err != nil { + t.Fatalf("Failed to read filtered requirements: %v", err) + } + + filteredStr := string(filteredContent) + + if !strings.Contains(filteredStr, "requests==2.31.0") { + t.Errorf("Valid package requests was filtered out") + } + + if !strings.Contains(filteredStr, "boto3") { + t.Errorf("boto3 should be kept in requirements (cleanup handles removal)") + } + }) +} + +// --- Content filter tests (merged from content_filter_test.go) --- + +func TestIsIgnored(t *testing.T) { + tests := []struct { + name string + testPaths map[string]bool // path -> should be excluded + }{ + { + name: "default exclude patterns", + testPaths: map[string]bool{ + "functions/handler.py": false, + "core/models.py": false, + ".sst/cache/build.json": true, + ".git/config": true, + "functions/__pycache__/test.pyc": false, // preserved for bytecode caching + ".pytest_cache/v/cache": true, + "node_modules/package/index.js": true, + ".DS_Store": true, + "test.pyc": false, // preserved for bytecode caching + "module.pyo": true, + ".coverage": true, + "htmlcov/index.html": true, + ".venv/bin/python": true, + "venv/lib/python3.9": true, + ".env": true, + "requirements.txt": false, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for testPath, shouldExclude := range tt.testPaths { + result := isIgnored(testPath) + if result != shouldExclude { + t.Errorf("Path %s: expected exclude=%v, got exclude=%v", testPath, shouldExclude, result) + } + } + }) + } +} + +func TestMatchesPattern(t *testing.T) { + tests := []struct { + name string + pattern string + paths map[string]bool // path -> should match + }{ + { + name: "exact match", + pattern: ".sst", + paths: map[string]bool{ + ".sst": true, + ".sst/cache/build.json": true, + "functions/.sst": true, + "sst": false, + "functions/sst_config.py": false, + }, + }, + { + name: "wildcard match", + pattern: "*.pyc", + paths: map[string]bool{ + "test.pyc": true, + "functions/__pycache__/test.pyc": true, + "module.py": false, + "test.pyo": false, + }, + }, + { + name: "directory pattern", + pattern: "__pycache__", + paths: map[string]bool{ + "__pycache__": true, + "__pycache__/test.pyc": true, + "functions/__pycache__": true, + "functions/__pycache__/test.pyc": true, + "pycache": false, + "my__pycache__": false, + }, + }, + { + name: "prefix pattern", + pattern: "temp*", + paths: map[string]bool{ + "temp": true, + "temp.txt": true, + "temporary": true, + "temp_file.json": true, + "my_temp.txt": false, + "not_temp": false, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for path, shouldMatch := range tt.paths { + result := matchesPattern(path, tt.pattern) + if result != shouldMatch { + t.Errorf("Pattern %s, Path %s: expected match=%v, got match=%v", tt.pattern, path, shouldMatch, result) + } + } + }) + } +} + +func TestHasBuildConfig(t *testing.T) { + tests := []struct { + name string + files map[string]string // filename -> content + expected bool + }{ + { + name: "setup.py makes it buildable", + files: map[string]string{ + "setup.py": "from setuptools import setup\nsetup()", + }, + expected: true, + }, + { + name: "pyproject.toml with build-system is buildable", + files: map[string]string{ + "pyproject.toml": "[project]\nname = \"my-pkg\"\n\n[build-system]\nrequires = [\"hatchling\"]\n", + }, + expected: true, + }, + { + name: "pyproject.toml without build-system is not buildable", + files: map[string]string{ + "pyproject.toml": "[project]\nname = \"my-app\"\ndependencies = [\"requests\"]\n", + }, + expected: false, + }, + { + name: "empty directory is not buildable", + files: map[string]string{}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + for filename, content := range tt.files { + path := filepath.Join(dir, filename) + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatalf("failed to write %s: %v", filename, err) + } + } + + got := hasBuildConfig(dir) + if got != tt.expected { + t.Errorf("hasBuildConfig() = %v, want %v", got, tt.expected) + } + }) + } +} diff --git a/pkg/runtime/python/project.go b/pkg/runtime/python/project.go new file mode 100644 index 0000000000..9ee3063535 --- /dev/null +++ b/pkg/runtime/python/project.go @@ -0,0 +1,184 @@ +package python + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/BurntSushi/toml" +) + +// uvSource represents a UV source configuration +type uvSource struct { + Path string `toml:"path"` + Workspace bool `toml:"workspace"` +} + +// projectInfo contains resolved project information +type projectInfo struct { + ProjectRoot string + SourceRoot string + PyprojectPath string +} + +// pyprojectConfig represents the structure of a pyproject.toml file. +type pyprojectConfig struct { + Project struct { + Name string `toml:"name"` + } `toml:"project"` + + Tool struct { + UV struct { + Sources map[string]uvSource `toml:"sources"` + Workspace struct { + Members []string `toml:"members"` + } `toml:"workspace"` + } `toml:"uv"` + + Poetry struct { + Name string `toml:"name"` + } `toml:"poetry"` + + Hatch struct { + Build struct { + Targets struct { + Wheel struct { + Packages []string `toml:"packages"` + } `toml:"wheel"` + } `toml:"targets"` + } `toml:"build"` + } `toml:"hatch"` + + Setuptools struct { + Packages struct { + Find struct { + Where []string `toml:"where"` + } `toml:"find"` + } `toml:"packages"` + } `toml:"setuptools"` + } `toml:"tool"` +} + +// resolveHandler finds and resolves a Python handler. +func resolveHandler(projectRoot, handlerPath string) (*projectInfo, error) { + handlerFile, err := findPythonFile(projectRoot, handlerPath) + if err != nil { + return nil, fmt.Errorf("failed to find Python file for handler %s: %w", handlerPath, err) + } + + pyprojectPath, _ := findPyprojectToml(projectRoot, handlerFile) + + info := &projectInfo{ + ProjectRoot: projectRoot, + PyprojectPath: pyprojectPath, + } + + info.SourceRoot = resolveSourceRoot(projectRoot, pyprojectPath) + + return info, nil +} + +// findPythonFile locates the Python file for the given handler path. +func findPythonFile(projectRoot, handlerPath string) (string, error) { + filePath := extractFilePath(handlerPath) + candidates := generateCandidatePaths(projectRoot, filePath) + + for _, candidate := range candidates { + if info, err := os.Stat(candidate); err == nil && info.Mode().IsRegular() && strings.HasSuffix(candidate, ".py") { + absPath, err := filepath.Abs(candidate) + if err != nil { + return "", fmt.Errorf("failed to get absolute path for %s: %w", candidate, err) + } + return absPath, nil + } + } + + return "", fmt.Errorf("handler not found: %s (searched %d candidate paths)", handlerPath, len(candidates)) +} + +// extractFilePath extracts the file path from a handler path. +func extractFilePath(handlerPath string) string { + if lastDot := strings.LastIndex(handlerPath, "."); lastDot != -1 { + return handlerPath[:lastDot] + } + return handlerPath +} + +// generateCandidatePaths creates a list of potential file locations. +func generateCandidatePaths(projectRoot, handlerPath string) []string { + var candidates []string + + // Direct path and with .py extension + candidates = append(candidates, filepath.Join(projectRoot, handlerPath)) + if !strings.HasSuffix(handlerPath, ".py") { + candidates = append(candidates, filepath.Join(projectRoot, handlerPath+".py")) + } + + // Common Python project directories + for _, dir := range []string{"src", "app", "functions", "lambda", "handlers", "lib"} { + candidates = append(candidates, filepath.Join(projectRoot, dir, handlerPath)) + if !strings.HasSuffix(handlerPath, ".py") { + candidates = append(candidates, filepath.Join(projectRoot, dir, handlerPath+".py")) + } + } + + // Nested paths with common source directories + if strings.Contains(handlerPath, "/") { + dir := filepath.Dir(handlerPath) + base := filepath.Base(handlerPath) + for _, commonDir := range []string{"src", "app", "functions", "lambda", "handlers", "lib"} { + candidates = append(candidates, filepath.Join(projectRoot, commonDir, dir, base)) + if !strings.HasSuffix(base, ".py") { + candidates = append(candidates, filepath.Join(projectRoot, commonDir, dir, base+".py")) + } + } + } + + return candidates +} + +// findPyprojectToml searches for pyproject.toml starting from the handler file directory. +func findPyprojectToml(projectRoot, handlerFile string) (string, error) { + currentDir := filepath.Dir(handlerFile) + for { + pyprojectPath := filepath.Join(currentDir, "pyproject.toml") + if _, err := os.Stat(pyprojectPath); err == nil { + return pyprojectPath, nil + } + parentDir := filepath.Dir(currentDir) + if parentDir == currentDir || !strings.HasPrefix(currentDir, projectRoot) { + break + } + currentDir = parentDir + } + return "", fmt.Errorf("no pyproject.toml found") +} + +// resolveSourceRoot determines the source root directory. +func resolveSourceRoot(projectRoot, pyprojectPath string) string { + if pyprojectPath != "" { + pyprojectDir := filepath.Dir(pyprojectPath) + srcDir := filepath.Join(pyprojectDir, "src") + if _, err := os.Stat(srcDir); err == nil { + return srcDir + } + return pyprojectDir + } + return projectRoot +} + +// parsePyprojectToml reads and parses a pyproject.toml file. +func parsePyprojectToml(path string) (*pyprojectConfig, error) { + content, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read pyproject.toml at %s: %w", path, err) + } + + var config pyprojectConfig + if err := toml.Unmarshal(content, &config); err != nil { + return nil, fmt.Errorf("TOML parsing error in %s: %w", path, err) + } + + return &config, nil +} diff --git a/pkg/runtime/python/project_test.go b/pkg/runtime/python/project_test.go new file mode 100644 index 0000000000..638f232094 --- /dev/null +++ b/pkg/runtime/python/project_test.go @@ -0,0 +1,130 @@ +package python + +import ( + "os" + "path/filepath" + "testing" +) + +func TestSetupSourceRoot_MonorepoStructure(t *testing.T) { + // Create a temp directory structure that mimics the GTF monorepo: + // /tmp/gtfd/ <- root with workspace pyproject.toml + // /tmp/gtfd/apps/main/ <- SST project root + // /tmp/gtfd/apps/main/packages/api/ <- workspace member with its own pyproject.toml + // /tmp/gtfd/apps/main/packages/api/auth/ <- handler files + // + // In the real GTF layout, each workspace member package has its own pyproject.toml. + // The resolver walks up from the handler file and finds the package-level one first, + // which is within the project root boundary. It never needs to go above apps/main/. + + tmpDir, err := os.MkdirTemp("", "sst-test-monorepo") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create directory structure + appsMainDir := filepath.Join(tmpDir, "apps", "main") + packagesApiDir := filepath.Join(appsMainDir, "packages", "api") + authDir := filepath.Join(packagesApiDir, "auth") + + if err := os.MkdirAll(authDir, 0755); err != nil { + t.Fatalf("Failed to create auth dir: %v", err) + } + + // Create root pyproject.toml (workspace root, above SST project) + rootPyproject := filepath.Join(tmpDir, "pyproject.toml") + if err := os.WriteFile(rootPyproject, []byte(`[project] +name = "gtf" +version = "1.0.0" + +[tool.uv.workspace] +members = ["apps/main/packages/api"] +`), 0644); err != nil { + t.Fatalf("Failed to write root pyproject.toml: %v", err) + } + + // Create package-level pyproject.toml (this is what the resolver actually finds) + packagePyproject := filepath.Join(packagesApiDir, "pyproject.toml") + if err := os.WriteFile(packagePyproject, []byte(`[project] +name = "gtf-api" +version = "0.1.0" +requires-python = ">=3.13" +`), 0644); err != nil { + t.Fatalf("Failed to write package pyproject.toml: %v", err) + } + + // Create handler file + handlerPath := filepath.Join(authDir, "login.py") + if err := os.WriteFile(handlerPath, []byte(`def handler(event, context): + return {"statusCode": 200} +`), 0644); err != nil { + t.Fatalf("Failed to write handler: %v", err) + } + + // Resolve the handler + info, err := resolveHandler(appsMainDir, "packages/api/auth/login.handler") + if err != nil { + t.Fatalf("Failed to resolve handler: %v", err) + } + + // Key assertions: + // 1. PyprojectPath should be the package-level one (within project root) + if info.PyprojectPath != packagePyproject { + t.Errorf("Expected PyprojectPath=%s, got %s", packagePyproject, info.PyprojectPath) + } + + // 2. SourceRoot should be the package directory (where pyproject.toml was found) + if info.SourceRoot != packagesApiDir { + t.Errorf("Expected SourceRoot=%s, got %s", packagesApiDir, info.SourceRoot) + } + + // 3. ProjectRoot should be apps/main + if info.ProjectRoot != appsMainDir { + t.Errorf("Expected ProjectRoot=%s, got %s", appsMainDir, info.ProjectRoot) + } +} + +func TestSetupSourceRoot_PyprojectInProjectRoot(t *testing.T) { + // Standard case: pyproject.toml is in the SST project root + tmpDir, err := os.MkdirTemp("", "sst-test-standard") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + packagesDir := filepath.Join(tmpDir, "packages", "api") + if err := os.MkdirAll(packagesDir, 0755); err != nil { + t.Fatalf("Failed to create packages dir: %v", err) + } + + // Create pyproject.toml in project root + pyprojectPath := filepath.Join(tmpDir, "pyproject.toml") + if err := os.WriteFile(pyprojectPath, []byte(`[project] +name = "test" +version = "1.0.0" +`), 0644); err != nil { + t.Fatalf("Failed to write pyproject.toml: %v", err) + } + + // Create handler + handlerPath := filepath.Join(packagesDir, "handler.py") + if err := os.WriteFile(handlerPath, []byte(`def handler(event, context): pass`), 0644); err != nil { + t.Fatalf("Failed to write handler: %v", err) + } + + info, err := resolveHandler(tmpDir, "packages/api/handler.handler") + if err != nil { + t.Fatalf("Failed to resolve handler: %v", err) + } + + // In standard case, SourceRoot should equal ProjectRoot + if info.SourceRoot != tmpDir { + t.Errorf("Expected SourceRoot=%s, got %s", tmpDir, info.SourceRoot) + } + + // PyprojectPath should be found at the project root + if info.PyprojectPath != pyprojectPath { + t.Errorf("Expected PyprojectPath=%s, got %s", pyprojectPath, info.PyprojectPath) + } +} diff --git a/pkg/runtime/python/python.go b/pkg/runtime/python/python.go index eadf2c3368..30580a6156 100644 --- a/pkg/runtime/python/python.go +++ b/pkg/runtime/python/python.go @@ -2,66 +2,58 @@ package python import ( "context" - "encoding/json" + "crypto/sha256" + "encoding/hex" "fmt" "io" "log/slog" "os" "os/exec" "path/filepath" + "strconv" "strings" "sync" - "github.com/BurntSushi/toml" + "github.com/sst/sst/v3/pkg/flag" "github.com/sst/sst/v3/pkg/process" "github.com/sst/sst/v3/pkg/project/path" "github.com/sst/sst/v3/pkg/runtime" + "golang.org/x/sync/semaphore" ) -type Worker struct { +var ( + // Per-cache-key locks β€” only one function installs per cache key at a time + globalDependencyInstallLocks = make(map[string]*sync.Mutex) + globalDependencyInstallLocksMutex sync.Mutex + + // Clear .deps/ once per SST run so workspace package changes are picked up + globalDepsCacheClearOnce sync.Once +) + +type worker struct { stdout io.ReadCloser stderr io.ReadCloser cmd *exec.Cmd } -func (w *Worker) Stop() { +func (w *worker) Stop() { // Terminate the whole process group process.Kill(w.cmd.Process) } -func (w *Worker) Logs() io.ReadCloser { +func (w *worker) Logs() io.ReadCloser { reader, writer := io.Pipe() go func() { defer writer.Close() - var wg sync.WaitGroup - wg.Add(2) - - copyStream := func(dst io.Writer, src io.Reader, name string) { - defer wg.Done() - buf := make([]byte, 1024) - for { - n, err := src.Read(buf) - if n > 0 { - _, werr := dst.Write(buf[:n]) - if werr != nil { - slog.Error("error writing to pipe", "stream", name, "err", werr) - return - } - } - if err != nil { - if err != io.EOF { - slog.Error("error reading from stream", "stream", name, "err", err) - } - return - } - } + for _, src := range []io.Reader{w.stdout, w.stderr} { + wg.Add(1) + go func(src io.Reader) { + defer wg.Done() + io.Copy(writer, src) + }(src) } - - go copyStream(writer, w.stdout, "stdout") - go copyStream(writer, w.stderr, "stderr") - wg.Wait() }() @@ -69,92 +61,142 @@ func (w *Worker) Logs() io.ReadCloser { } type PythonRuntime struct { - lastBuiltHandler map[string]string + // concurrency limits total parallel builds across all functions. + // This is separate from the per-cache-key mutex in build.go which deduplicates + // concurrent installs for the same dependency set. The two are complementary: + // the semaphore caps throughput, the mutex prevents redundant work. + concurrency *semaphore.Weighted } func New() *PythonRuntime { + weight := int64(4) + if flag.SST_BUILD_CONCURRENCY_FUNCTION != "" { + weight, _ = strconv.ParseInt(flag.SST_BUILD_CONCURRENCY_FUNCTION, 10, 64) + } else if flag.SST_BUILD_CONCURRENCY != "" { + weight, _ = strconv.ParseInt(flag.SST_BUILD_CONCURRENCY, 10, 64) + } + return &PythonRuntime{ - lastBuiltHandler: map[string]string{}, + concurrency: semaphore.NewWeighted(weight), } } func (r *PythonRuntime) Build(ctx context.Context, input *runtime.BuildInput) (*runtime.BuildOutput, error) { - /// Workspaces are the most challenging part of the build process - /// UV currently does not support --include-workspace-deps for builds - /// See: https://github.com/astral-sh/uv/issues/6935 hopefully this lands soon - - /// As a result, we have to manually construct the dependency tree - /// So we need to: - /// - /// 1. Build all packages (future tree shaking would be nice) - /// 2. Ensure local packages are built for lambdaric acccess (remove src/ nesting) - /// To future readers: we need to do this because of the way python packages are resolved - /// if you have a package called "mypackage" and it contains a sub-package called "src/mypackage" - /// then within the package you can resolve code via "import mypackage" but not "import mypackage.src.mypackage" - /// this means that builds get a little strange for aws lambda which does module level imports via lambdaric - /// so we need to ensure that the package is built such that lambdaric can resolve paths in the output bundle - /// but the full package is available for local development - /// 3. Export the uv package index to requirements.txt - /// 4. Install the dependencies into the artifact directory as a target (local for zip and delegate to the dockerfile for containers) - - file, err := r.getFile(input) + if input.Dev { + // Dev mode: no building needed, just return metadata. + // Run() handles everything at invocation time. + return &runtime.BuildOutput{ + Handler: input.Handler, + Sourcemaps: []string{}, + Errors: []string{}, + Out: input.Out(), + }, nil + } + + // Clear deps cache once per SST run + globalDepsCacheClearOnce.Do(func() { + artifactsDir := filepath.Dir(input.Out()) + depsDir := filepath.Join(artifactsDir, ".deps") + if _, err := os.Stat(depsDir); err == nil { + if err := os.RemoveAll(depsDir); err != nil { + slog.Warn("failed to clear deps cache", "error", err) + } + } + }) + + r.concurrency.Acquire(ctx, 1) + defer r.concurrency.Release(1) + + _, err := resolveHandler(path.ResolveRootDir(input.CfgPath), input.Handler) if err != nil { - return nil, fmt.Errorf("handler not found: %v", err) + return nil, fmt.Errorf("Handler not found: %v", input.Handler) } - build, err := r.CreateBuildAsset(ctx, input) + result, err := r.CreateBuildAsset(ctx, input) if err != nil { return nil, err } - r.lastBuiltHandler[input.FunctionID] = file - - return build, nil + return result, nil } func (r *PythonRuntime) Match(runtime string) bool { return strings.HasPrefix(runtime, "python") } -type Source struct { - URL string `toml:"url,omitempty"` - Git string `toml:"git,omitempty"` - Subdirectory *string `toml:"subdirectory,omitempty"` - Branch string `toml:"branch,omitempty"` -} - -type PyProject struct { - Project struct { - Name string `toml:"name"` - } `toml:"project"` +// ShouldRunEagerly returns false to enable lazy worker startup. +// Python lacks static import analysis, so any file change triggers ShouldRebuild() +// for ALL functions. Lazy startup avoids a startup storm of 50+ processes. +func (r *PythonRuntime) ShouldRunEagerly() bool { + return false } func (r *PythonRuntime) Run(ctx context.Context, input *runtime.RunInput) (runtime.Worker, error) { - // We need the lambda bridge in the artifact directory so that we can run the handler - // without having to manually isolate the runtime, So if it is not present then we need to copy it from - // the platform directory into the artifact directory + isLegacyLayout, err := r.syncArtifactsIfNeeded(input) + if err != nil { + slog.Error("failed to sync artifacts", + "functionID", input.FunctionID, + "error", err) + return nil, fmt.Errorf("failed to sync artifacts: %v", err) + } - // Check if the lambda bridge is present + // Copy lambda bridge to artifact directory if missing or outdated lambdaBridgePath := filepath.Join(input.Build.Out, "lambdaric_python_bridge.py") - if _, err := os.Stat(lambdaBridgePath); os.IsNotExist(err) { - // Copy the lambda bridge from the platform directory into the artifact directory - err := copyFile(filepath.Join(path.ResolvePlatformDir(input.CfgPath), "/dist/python-runtime/index.py"), lambdaBridgePath) - if err != nil { + sourceBridgePath := filepath.Join(path.ResolvePlatformDir(input.CfgPath), "/dist/python-runtime/index.py") + + dstInfo, dstErr := os.Stat(lambdaBridgePath) + srcInfo, srcErr := os.Stat(sourceBridgePath) + if dstErr != nil || (srcErr == nil && srcInfo.ModTime().After(dstInfo.ModTime())) { + if err := copyFile(sourceBridgePath, lambdaBridgePath); err != nil { return nil, fmt.Errorf("failed to copy lambda bridge: %v", err) } } + projectRoot := path.ResolveRootDir(input.CfgPath) + + var handlerPath string + var workingDir string + + if isLegacyLayout { + // Use relative handler since workingDir is the artifact directory + handlerPath = r.adjustHandlerForFlattenedLayout(input.Build.Handler) + workingDir = input.Build.Out + } else { + // Modern layout: run from source with PYTHONPATH + handlerPath = input.Build.Handler + workingDir = projectRoot + } + cmd := process.CommandContext( ctx, "uv", "run", - "--with=requests", lambdaBridgePath, - filepath.Join(input.Build.Out, input.Build.Handler), - input.WorkerID, + handlerPath, ) - cmd.Env = append(input.Env, "AWS_LAMBDA_RUNTIME_API="+input.Server) - cmd.Dir = input.Build.Out + + // Set up environment + env := append(input.Env, "AWS_LAMBDA_RUNTIME_API="+input.Server) + + if !isLegacyLayout { + pythonPaths := []string{projectRoot} + + // Add src/ if it exists + srcDir := filepath.Join(projectRoot, "src") + if _, err := os.Stat(srcDir); err == nil { + pythonPaths = append(pythonPaths, srcDir) + } + + // Join paths + pythonPath := strings.Join(pythonPaths, string(os.PathListSeparator)) + env = append(env, "PYTHONPATH="+pythonPath) + + resourceEncPath := filepath.Join(input.Build.Out, "resource.enc") + env = append(env, "SST_KEY_FILE="+resourceEncPath) + } + + cmd.Env = env + cmd.Dir = workingDir stdout, err := cmd.StdoutPipe() if err != nil { return nil, fmt.Errorf("failed to create stdout pipe: %v", err) @@ -164,10 +206,11 @@ func (r *PythonRuntime) Run(ctx context.Context, input *runtime.RunInput) (runti return nil, fmt.Errorf("failed to create stderr pipe: %v", err) } - slog.Info("starting worker", "env", cmd.Env, "args", cmd.Args) - cmd.Start() + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("failed to start worker process: %v", err) + } - return &Worker{ + return &worker{ stdout, stderr, cmd, @@ -176,339 +219,358 @@ func (r *PythonRuntime) Run(ctx context.Context, input *runtime.RunInput) (runti } func (r *PythonRuntime) ShouldRebuild(functionID string, file string) bool { - // Assume that the build is always stale. We could do a better job here but bc of how the build - // process actually works its not a slowdown as the real slow part is starting the python interpreter - // This is neglible for now and will get faster when we can move to uv's native build system. - // We could also pre-warm the runtime - custom watcher paths would be useful here. - return true -} - -func (r *PythonRuntime) CreateBuildAsset(ctx context.Context, input *runtime.BuildInput) (*runtime.BuildOutput, error) { - // Get the architecture from the input.properties.architecture json field - slog.Info("input properties", "json", string(input.Properties)) - - type Properties struct { - Architecture string `json:"architecture"` - Container bool `json:"container"` - } - var props Properties - if err := json.Unmarshal(input.Properties, &props); err != nil { - return nil, fmt.Errorf("failed to parse properties: %v", err) + // Skip paths inside build artifacts, caches, or virtual envs to avoid feedback loops + normalized := filepath.ToSlash(file) + for _, dir := range []string{".sst", ".venv", "venv", "__pycache__", ".git", "node_modules", ".pytest_cache", ".mypy_cache", ".tox"} { + if strings.Contains(normalized, dir+"/") { + return false + } } - arch := props.Architecture - if arch == "" { - arch = "x86_64" // Default to x86_64 + if filepath.Base(file) == "requirements.txt" { + return true } - - if arch != "x86_64" && arch != "arm64" { - return nil, fmt.Errorf("invalid architecture %q - must be x86_64 or arm64 - %v", arch, string(input.Properties)) + switch filepath.Ext(file) { + case ".py", ".toml", ".lock", ".cfg": + return true } - workingDir := path.ResolveRootDir(input.CfgPath) - - // 1. Generate non-local package index - syncCmd := process.CommandContext(ctx, "uv", "sync", "--all-packages") - syncCmd.Dir = workingDir - slog.Info("running uv sync in dir", "dir", syncCmd.Dir) + return false +} - // capture the output of the sync command - syncOutput, err := syncCmd.CombinedOutput() - if err != nil { - slog.Error("failed to run uv sync", "error", err, "output", string(syncOutput)) - return nil, fmt.Errorf("failed to run uv sync: %v\n%s", err, string(syncOutput)) - } - slog.Error("uv sync output", "output", string(syncOutput)) +func (r *PythonRuntime) syncArtifactsIfNeeded(input *runtime.RunInput) (bool, error) { + projectRoot := path.ResolveRootDir(input.CfgPath) + artifactDir := input.Build.Out - outputRequirementsFile := filepath.Join(input.Out(), "requirements.txt") - packageName, err := r.getPackageName(input) - if err != nil { - return nil, fmt.Errorf("failed to get package name: %v", err) - } - exportCmd := process.CommandContext(ctx, "uv", "export", "--package="+packageName, "--output-file="+outputRequirementsFile, "--no-emit-workspace", "--no-dev") - exportCmd.Dir = workingDir - err = exportCmd.Run() - if err != nil { - return nil, fmt.Errorf("failed to run uv export: %v", err) - } + // Only sync for legacy workspace layouts that need flattening + if r.hasWorkspaceLayoutPatterns(projectRoot) { + if err := r.syncPythonFiles(projectRoot, artifactDir); err != nil { + return true, err + } - // 2. Build the entire workspace - this should cache and be fast thank you astral - buildCmd := process.CommandContext(ctx, "uv", "build", "--all", "--sdist", "--out-dir="+input.Out()) - buildCmd.Dir = workingDir - buildOutput, err := buildCmd.CombinedOutput() - if err != nil { - return nil, fmt.Errorf("failed to run uv build: %v\n%s", err, string(buildOutput)) + return true, r.flattenWorkspaceLayouts(artifactDir) } - slog.Error("uv build output", "output", string(buildOutput)) - // 3. Decode each tar.gz file in the dist directory and remove the trailing "-{version}" - files, err := filepath.Glob(filepath.Join(input.Out(), "*.tar.gz")) - if err != nil { - return nil, fmt.Errorf("failed to glob tar.gz files: %v", err) - } + return false, nil +} - for _, file := range files { - // Extract the tar.gz file - cmd := process.CommandContext(ctx, "tar", "-xzf", file, "-C", input.Out()) - cmd.Dir = input.Out() - err = cmd.Run() +// syncPythonFiles syncs Python files from source to artifacts (add, update, delete) +func (r *PythonRuntime) syncPythonFiles(srcDir, destDir string) error { + sourceFiles := make(map[string]os.FileInfo) + err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { if err != nil { - return nil, fmt.Errorf("failed to extract tar.gz file: %v", err) + return err } - // Get the directory name without version number - dirName := strings.TrimSuffix(filepath.Base(file), ".tar.gz") - lastHyphen := strings.LastIndex(dirName, "-") - baseName := dirName[:lastHyphen] - - extractedDir := filepath.Join(input.Out(), dirName) - targetDir := filepath.Join(input.Out(), baseName) + relPath, err := filepath.Rel(srcDir, path) + if err != nil { + return err + } - // Check if the package has a src/{package_name} structure - srcPath := filepath.Join(extractedDir, "src", baseName) - if _, err := os.Stat(srcPath); err == nil { - // Remove old directory if it exists - if err := os.RemoveAll(targetDir); err != nil { - return nil, fmt.Errorf("failed to remove old directory: %v", err) - } - // Move the contents from src/{package_name} directly to the target - if err := os.Rename(srcPath, targetDir); err != nil { - return nil, fmt.Errorf("failed to move src directory contents: %v", err) - } - // Clean up the original extracted directory - if err := os.RemoveAll(extractedDir); err != nil { - return nil, fmt.Errorf("failed to clean up extracted directory: %v", err) - } - } else { - // Handle the regular case (no src directory) - if err := os.RemoveAll(targetDir); err != nil { - return nil, fmt.Errorf("failed to remove old directory: %v", err) - } - if err := os.Rename(extractedDir, targetDir); err != nil { - return nil, fmt.Errorf("failed to rename directory: %v", err) + // Skip hidden directories and common non-Python directories + if info.IsDir() { + if strings.HasPrefix(filepath.Base(path), ".") || + strings.Contains(relPath, "node_modules") || + strings.Contains(relPath, "__pycache__") { + return filepath.SkipDir } + return nil } - } - // 4. Remove the tar.gz files (non-recursive) - for _, file := range files { - err = os.Remove(file) - if err != nil { - return nil, fmt.Errorf("failed to remove tar.gz file: %v", err) + if strings.HasSuffix(path, ".py") { + sourceFiles[relPath] = info } - } - // If making a zip build or a local build then we need to install the dependencies and adjust the handler path - if !input.IsContainer || input.Dev { + return nil + }) + if err != nil { + return fmt.Errorf("failed to scan source files: %v", err) + } - // 5. Install the dependencies as a target - args := []string{"pip", "install", "-r", outputRequirementsFile, "--target", input.Out()} - if !input.Dev { - // If we are not in dev mode then we need to install the dependencies for the target platform - // which is amazon linux for the correct architecture - pythonPlatform := "x86_64-unknown-linux-gnu" - if arch == "arm64" { - pythonPlatform = "aarch64-unknown-linux-gnu" + // Collect Python files in artifacts + artifactFiles := make(map[string]os.FileInfo) + if _, err := os.Stat(destDir); err == nil { + err = filepath.Walk(destDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err } - args = append(args, "--python-platform", pythonPlatform) - } - installCmd := process.CommandContext(ctx, "uv", args...) - installCmd.Dir = input.Out() - installOutput, err := installCmd.CombinedOutput() - if err != nil { - return nil, fmt.Errorf("failed to run uv pip install: %v\n%s", err, string(installOutput)) - } - slog.Error("uv pip install output", "output", string(installOutput), "error", err) - // Adjust handler path if it contains the pattern {package_name}/src/{package_name} - adjustedHandler, err := r.adjustHandlerPath(input) - if err != nil { - return nil, fmt.Errorf("failed to adjust handler path: %v", err) - } - slog.Info("built python function", "handler", adjustedHandler, "out", input.Out()) + relPath, err := filepath.Rel(destDir, path) + if err != nil { + return err + } - errors := []string{} - sourcemaps := []string{} + if !info.IsDir() && strings.HasSuffix(path, ".py") { + artifactFiles[relPath] = info + } - return &runtime.BuildOutput{ - Handler: adjustedHandler, - Errors: errors, - Sourcemaps: sourcemaps, - }, nil - } else { - // 5. Check if there is a Dockerfile in the handler directory - // If not then copy over the default one from the platform directory - workspaceDir, err := r.getWorkspaceDirectory(input) + return nil + }) if err != nil { - return nil, fmt.Errorf("failed to get workspace directory: %v", err) + return fmt.Errorf("failed to scan artifact files: %v", err) } + } - slog.Info("checking for Dockerfile in workspace directory", "dir", workspaceDir) - _, err = os.Stat(filepath.Join(workspaceDir, "Dockerfile")) - if err != nil { - slog.Error("workspace directory does not contain Dockerfile", "dir", workspaceDir) - // Check if the Dockerfile exists in the platform directory - defaultDockerfilePath := filepath.Join(path.ResolvePlatformDir(input.CfgPath), "/dist/dockerfiles/python.Dockerfile") - _, err = os.Stat(defaultDockerfilePath) - if err != nil { - slog.Error("failed to check for Dockerfile in platform directory", "error", err) - return nil, fmt.Errorf("failed to check for Dockerfile in platform directory: %v", err) - } else { - slog.Info("dockerfile exists in platform directory", "dir", path.ResolvePlatformDir(input.CfgPath)) + // Delete files in artifacts that no longer exist in source + for relPath := range artifactFiles { + if _, exists := sourceFiles[relPath]; !exists { + artifactPath := filepath.Join(destDir, relPath) + if err := os.Remove(artifactPath); err != nil { + return fmt.Errorf("failed to delete %s: %v", artifactPath, err) } + } + } - slog.Info("copying default Dockerfile from platform directory to output directory", "dir", path.ResolvePlatformDir(input.CfgPath)) + // Copy/update changed files + for relPath, sourceInfo := range sourceFiles { + sourcePath := filepath.Join(srcDir, relPath) + artifactPath := filepath.Join(destDir, relPath) - // Copy over the default Dockerfile from the platform directory - copyFile(defaultDockerfilePath, filepath.Join(input.Out(), "Dockerfile")) - slog.Info("copied default Dockerfile to output directory", "dir", input.Out()) - } else { - slog.Info("Dockerfile already exists in workspace directory", "dir", workspaceDir) - copyFile(filepath.Join(workspaceDir, "Dockerfile"), filepath.Join(input.Out(), "Dockerfile")) + needsCopy := true + if artifactInfo, exists := artifactFiles[relPath]; exists { + if !sourceInfo.ModTime().After(artifactInfo.ModTime()) { + needsCopy = false + } } - adjustedHandler, err := r.adjustHandlerPath(input) - if err != nil { - return nil, fmt.Errorf("failed to adjust handler path: %v", err) + if needsCopy { + if err := os.MkdirAll(filepath.Dir(artifactPath), 0755); err != nil { + return fmt.Errorf("failed to create directory for %s: %v", artifactPath, err) + } + + if err := copyFile(sourcePath, artifactPath); err != nil { + return fmt.Errorf("failed to copy %s to %s: %w", sourcePath, artifactPath, err) + } } + } - errors := []string{} - sourcemaps := []string{} + return nil +} - return &runtime.BuildOutput{ - Handler: adjustedHandler, - Errors: errors, - Sourcemaps: sourcemaps, - }, nil +// flattenSrcLayout removes the "src/pkg" segment from paths that follow the +// PEP 517 src-layout convention (e.g., "pkg/src/pkg/module" -> "pkg/module"). +// Only flattens when the directory after "src" matches the directory before it. +func flattenSrcLayout(filePath string) string { + parts := strings.Split(filePath, "/") + if len(parts) < 3 { + return filePath } + for i := 0; i < len(parts)-1; i++ { + if parts[i] == "src" && i > 0 && i+1 < len(parts) && parts[i-1] == parts[i+1] { + flattened := append([]string{}, parts[:i]...) + flattened = append(flattened, parts[i+2:]...) + return strings.Join(flattened, "/") + } + } + return filePath } -func (r *PythonRuntime) getFile(input *runtime.BuildInput) (string, error) { - slog.Info("looking for python handler file", "handler", input.Handler) +// adjustHandlerForFlattenedLayout adjusts a handler path to account for flattened workspace layouts +// For example: functions/src/functions/user/handler.lambda_handler -> functions/user/handler.lambda_handler +func (r *PythonRuntime) adjustHandlerForFlattenedLayout(handlerPath string) string { + lastDot := strings.LastIndex(handlerPath, ".") + if lastDot == -1 { + return flattenSrcLayout(handlerPath) + } + filePath := handlerPath[:lastDot] + functionName := handlerPath[lastDot+1:] + return flattenSrcLayout(filePath) + "." + functionName +} - dir := filepath.Dir(input.Handler) - base := strings.TrimSuffix(filepath.Base(input.Handler), filepath.Ext(input.Handler)) - rootDir := path.ResolveRootDir(input.CfgPath) +func (r *PythonRuntime) CreateBuildAsset(ctx context.Context, input *runtime.BuildInput) (*runtime.BuildOutput, error) { + workingDir := path.ResolveRootDir(input.CfgPath) - // Look for .py file - pythonFile := filepath.Join(rootDir, dir, base+".py") - if _, err := os.Stat(pythonFile); err == nil { - return pythonFile, nil + result, err := buildDeploy(ctx, input, filepath.Join(workingDir, ".sst/cache/deploy"), workingDir) + if err != nil { + slog.Error("build failed", "functionID", input.FunctionID, "error", err) + return nil, err } - // No Python file found for the handler - return "", fmt.Errorf("could not find Python file '%s.py' in directory '%s'", - base, - filepath.Join(rootDir, dir)) + slog.Info("function built", "functionID", input.FunctionID) + return result, nil } +// copyFile copies a single file from src to dst, creating parent directories as needed. func copyFile(src, dst string) error { + if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return fmt.Errorf("failed to create directory for %s: %w", dst, err) + } + srcFile, err := os.Open(src) if err != nil { - return fmt.Errorf("failed to open source file: %v", err) + return fmt.Errorf("failed to open source file: %w", err) } defer srcFile.Close() dstFile, err := os.Create(dst) if err != nil { - return fmt.Errorf("failed to create destination file: %v", err) + return fmt.Errorf("failed to create destination file: %w", err) } defer dstFile.Close() - _, err = io.Copy(dstFile, srcFile) - if err != nil { - return fmt.Errorf("failed to copy file contents: %v", err) + if _, err := io.Copy(dstFile, srcFile); err != nil { + return fmt.Errorf("failed to copy file contents: %w", err) } return nil } -func (r *PythonRuntime) getWorkspaceDirectory(input *runtime.BuildInput) (string, error) { - file, err := r.getFile(input) - if err != nil { - return "", err - } - - projectRoot := path.ResolveRootDir(input.CfgPath) - currentDir := filepath.Dir(file) +// skipContent skips __pycache__, .pyc files, and anything matching isIgnored. +func skipContent(relPath string, info os.FileInfo) bool { + return isIgnored(relPath) +} - // First verify that the current directory is within the project root - if !strings.HasPrefix(currentDir, projectRoot) { - return "", fmt.Errorf("handler file %s is not within the project root %s", file, projectRoot) +// skipBuildArtifacts skips only dirs that would break workspace package builds. +// Preserves metadata files (pyproject.toml, etc.) needed by uv pip install. +func skipBuildArtifacts(_ string, info os.FileInfo) bool { + if !info.IsDir() { + return false } + name := info.Name() + return name == "__pycache__" || name == ".venv" || name == "node_modules" || name == ".git" +} - // Traverse up the file tree to find the pyproject.toml file - // If we reach the project root then return an error - for { - pyprojectPath := filepath.Join(currentDir, "pyproject.toml") - if _, err := os.Stat(pyprojectPath); err == nil { - // We found the pyproject.toml file - return currentDir, nil +// copyDir recursively copies src to dst, calling skip on every entry to decide +// whether to include it. Dirs that return true are pruned entirely. +func copyDir(src, dst string, skip func(relPath string, info os.FileInfo) bool) error { + return filepath.Walk(src, func(p string, info os.FileInfo, err error) error { + if err != nil { + return err } - - // Move up the directory tree - parentDir := filepath.Dir(currentDir) - - // Check if we have reached the project root or cannot move up anymore - if parentDir == currentDir || currentDir == projectRoot { - return "", fmt.Errorf("no pyproject.toml found in directory tree from %s up to project root %s", filepath.Dir(file), projectRoot) + relPath, _ := filepath.Rel(src, p) + if skip(relPath, info) { + if info.IsDir() { + return filepath.SkipDir + } + return nil } - - currentDir = parentDir - } + dstPath := filepath.Join(dst, relPath) + if info.IsDir() { + return os.MkdirAll(dstPath, info.Mode()) + } + return copyFile(p, dstPath) + }) } -func (r *PythonRuntime) getPackageName(input *runtime.BuildInput) (string, error) { - workspaceDir, err := r.getWorkspaceDirectory(input) +// hashFileContents computes a SHA256 hash of a file's contents. +func hashFileContents(filePath string) (string, error) { + file, err := os.Open(filePath) if err != nil { - return "", err + return "", fmt.Errorf("failed to open file for hashing: %w", err) } + defer file.Close() - // Read the pyproject.toml file - pyproject, err := os.ReadFile(filepath.Join(workspaceDir, "pyproject.toml")) - if err != nil { - return "", fmt.Errorf("failed to read pyproject.toml file: %v", err) + hasher := sha256.New() + if _, err := io.Copy(hasher, file); err != nil { + return "", fmt.Errorf("failed to hash file: %w", err) } - // Parse the pyproject.toml file - pyprojectData := PyProject{} - err = toml.Unmarshal(pyproject, &pyprojectData) - if err != nil { - return "", fmt.Errorf("failed to parse pyproject.toml file: %v", err) + return hex.EncodeToString(hasher.Sum(nil)), nil +} + +// hasWorkspaceLayoutPatterns checks for package/src/package patterns that need flattening. +// Scans up to one level below projectRoot to cover both single-package and monorepo layouts. +func (r *PythonRuntime) hasWorkspaceLayoutPatterns(projectRoot string) bool { + var scan func(dir string, depth int) bool + scan = func(dir string, depth int) bool { + entries, err := os.ReadDir(dir) + if err != nil { + return false + } + for _, entry := range entries { + name := entry.Name() + if !entry.IsDir() || strings.HasPrefix(name, ".") || name == "__pycache__" || name == "node_modules" { + continue + } + pkgDir := filepath.Join(dir, name) + if _, err := os.Stat(filepath.Join(pkgDir, "src", name)); err == nil { + return true + } + if depth > 0 && scan(pkgDir, depth-1) { + return true + } + } + return false } + return scan(projectRoot, 1) +} - return pyprojectData.Project.Name, nil +// flattenWorkspaceLayouts detects and flattens package/src/package structures for all legacy projects +func (r *PythonRuntime) flattenWorkspaceLayouts(artifactDir string) error { -} + // flattenDir checks all immediate subdirectories of dir for the package/src/package pattern + flattenDir := func(dir string) error { + entries, err := os.ReadDir(dir) + if err != nil { + return err + } -func (r *PythonRuntime) adjustHandlerPath(input *runtime.BuildInput) (string, error) { - handlerParts := strings.Split(input.Handler, "/") - adjustedHandler := input.Handler - if len(handlerParts) >= 3 { - // Start from the back, using a sliding window of 3 - for i := len(handlerParts) - 3; i >= 0; i-- { - // Check if we have enough parts left to match the pattern - if i+2 >= len(handlerParts) { + for _, entry := range entries { + if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") || entry.Name() == "__pycache__" || entry.Name() == "node_modules" { continue } - pkgName := handlerParts[i] - if handlerParts[i+1] == "src" && handlerParts[i+2] == pkgName { - // Found the pattern, now remove the middle two parts (src/{package_name}) - newParts := append( - handlerParts[:i+1], - handlerParts[i+3:]..., - ) - adjustedHandler = strings.Join(newParts, "/") - slog.Info("adjusted handler path", "original", input.Handler, "adjusted", adjustedHandler) - break - } + packageName := entry.Name() + packageDir := filepath.Join(dir, packageName) + srcDir := filepath.Join(packageDir, "src") + innerPackageDir := filepath.Join(srcDir, packageName) + + // Check if this follows the package/src/package pattern + if _, err := os.Stat(innerPackageDir); err == nil { + // Copy contents of package/src/package to package/ + innerEntries, err := os.ReadDir(innerPackageDir) + if err != nil { + slog.Warn("failed to read inner package dir", "package", packageName, "error", err) + continue + } + + for _, innerEntry := range innerEntries { + if isIgnored(innerEntry.Name()) { + continue + } + + srcPath := filepath.Join(innerPackageDir, innerEntry.Name()) + destPath := filepath.Join(packageDir, innerEntry.Name()) + + if innerEntry.IsDir() { + err = copyDir(srcPath, destPath, skipContent) + } else { + err = copyFile(srcPath, destPath) + } + + if err != nil { + return fmt.Errorf("failed to flatten %s structure: %w", packageName, err) + } + } + + // Remove the old src/ directory after flattening to avoid import confusion + if err := os.RemoveAll(srcDir); err != nil { + slog.Warn("failed to remove src/ after flattening", "package", packageName, "error", err) + } - // Stop if we would go beyond the project root - absPath := filepath.Join(path.ResolveRootDir(input.CfgPath), strings.Join(handlerParts[:i], "/")) - if !strings.HasPrefix(absPath, path.ResolveRootDir(input.CfgPath)) { - break } } + return nil } - return adjustedHandler, nil + + // Check top-level directories + if err := flattenDir(artifactDir); err != nil { + return err + } + + // Also check one level deeper (e.g., packages/api/src/api pattern) + topEntries, err := os.ReadDir(artifactDir) + if err != nil { + return err + } + for _, entry := range topEntries { + if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") || entry.Name() == "__pycache__" || entry.Name() == "node_modules" { + continue + } + subDir := filepath.Join(artifactDir, entry.Name()) + if err := flattenDir(subDir); err != nil { + return err + } + } + + return nil } diff --git a/pkg/runtime/python/python_test.go b/pkg/runtime/python/python_test.go new file mode 100644 index 0000000000..28289d12cf --- /dev/null +++ b/pkg/runtime/python/python_test.go @@ -0,0 +1,235 @@ +package python + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/sst/sst/v3/pkg/runtime" +) + +// TestWorkspacePackageIsolation verifies that workspace packages get only their +// own dependencies, not dependencies from other packages. +func TestWorkspacePackageIsolation(t *testing.T) { + exampleDir := filepath.Join("..", "..", "..", "examples", "python-modern-uv") + if _, err := os.Stat(exampleDir); os.IsNotExist(err) { + t.Skip("python-modern-uv example not found, skipping") + } + + t.Run("api package discovers its own packages", func(t *testing.T) { + apiDir := filepath.Join(exampleDir, "packages", "api") + + projectInfo := &projectInfo{ + ProjectRoot: apiDir, + SourceRoot: apiDir, + } + + packages, err := discoverBuildablePackages(projectInfo) + if err != nil { + t.Fatalf("Failed to discover packages: %v", err) + } + + names := make(map[string]bool) + for _, pkg := range packages { + names[pkg.Name] = true + } + if !names["api"] { + t.Errorf("Expected 'api' in discovered packages, got: %v", names) + } + if names["worker"] { + t.Errorf("api package should not include sibling 'worker' package, got: %v", names) + } + }) + + t.Run("worker package discovers its own packages", func(t *testing.T) { + workerDir := filepath.Join(exampleDir, "packages", "worker") + + projectInfo := &projectInfo{ + ProjectRoot: workerDir, + SourceRoot: workerDir, + } + + packages, err := discoverBuildablePackages(projectInfo) + if err != nil { + t.Fatalf("Failed to discover packages: %v", err) + } + + names := make(map[string]bool) + for _, pkg := range packages { + names[pkg.Name] = true + } + if !names["worker"] { + t.Errorf("Expected 'worker' in discovered packages, got: %v", names) + } + if names["api"] { + t.Errorf("worker package should not include sibling 'api' package, got: %v", names) + } + }) +} + +// TestFlatWorkspacePackages tests that flat workspace packages work correctly. +func TestFlatWorkspacePackages(t *testing.T) { + tempDir := t.TempDir() + + structure := map[string]string{ + "pyproject.toml": `[project] +name = "myproject" +version = "0.1.0" + +[tool.uv.workspace] +members = ["backend", "packages/api-auth"] +`, + "backend/pyproject.toml": "[project]\nname = \"backend\"\nversion = \"0.1.0\"\n", + "backend/lib/__init__.py": "", + "backend/lib/utils.py": "def helper(): pass", + "packages/api-auth/pyproject.toml": `[project] +name = "api-auth" +version = "0.1.0" +dependencies = ["backend"] + +[tool.uv.sources] +backend = { workspace = true } +`, + "packages/api-auth/login.py": "def handler(event, context): return {\"status\": \"ok\"}", + } + + for path, content := range structure { + fullPath := filepath.Join(tempDir, path) + os.MkdirAll(filepath.Dir(fullPath), 0755) + os.WriteFile(fullPath, []byte(content), 0644) + } + + info, err := resolveHandler(tempDir, "packages/api-auth/login.handler") + if err != nil { + t.Errorf("Failed to resolve flat workspace handler: %v", err) + } + if info == nil { + t.Error("Expected projectInfo for flat workspace handler") + } +} + +// TestDeploymentWorkflow tests the complete build workflow for key project structures. +// We test flat (simplest) and monorepo (most complex) to cover the main code paths +// without running 10+ uv builds. +func TestDeploymentWorkflow(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + projectRoot, err := findProjectRoot() + if err != nil { + t.Skipf("Could not find project root: %v", err) + } + + tests := []struct { + name string + examplePath string + handler string + runtime string + }{ + { + name: "flat-layout", + examplePath: "examples/python-layouts/flat-layout", + handler: "handler.lambda_handler", + runtime: "python3.11", + }, + { + name: "monorepo-layout-worker", + examplePath: "examples/python-layouts/monorepo-layout", + handler: "services/worker/handler.main", + runtime: "python3.12", + }, + } + + pythonRuntime := New() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exampleAbsPath := filepath.Join(projectRoot, tt.examplePath) + if _, err := os.Stat(exampleAbsPath); os.IsNotExist(err) { + t.Skipf("Example %s not found", exampleAbsPath) + } + + result, err := pythonRuntime.Build(context.Background(), &runtime.BuildInput{ + FunctionID: "deployment-test-" + tt.name, + Handler: tt.handler, + Runtime: tt.runtime, + Properties: json.RawMessage(`{"architecture": "x86_64", "container": false}`), + CfgPath: filepath.Join(exampleAbsPath, "sst.config.ts"), + }) + if err != nil { + t.Fatalf("Build failed: %v", err) + } + + if result.Handler == "" { + t.Error("Build result missing handler") + } + if result.Out == "" { + t.Error("Build result missing output directory") + } + + // Check output has Python files + pythonFiles := 0 + filepath.Walk(result.Out, func(path string, info os.FileInfo, err error) error { + if err == nil && filepath.Ext(path) == ".py" { + pythonFiles++ + } + return nil + }) + if pythonFiles == 0 { + t.Error("No Python files in output directory") + } + }) + } +} + +// TestDeploymentErrorHandling tests error scenarios in deployment workflow +func TestDeploymentErrorHandling(t *testing.T) { + pythonRuntime := New() + + t.Run("nonexistent handler", func(t *testing.T) { + _, err := pythonRuntime.Build(context.Background(), &runtime.BuildInput{ + FunctionID: "error-test", + Handler: "nonexistent/handler.py", + Runtime: "python3.11", + CfgPath: "/tmp/sst.config.ts", + }) + if err == nil { + t.Error("Expected error for nonexistent handler") + } + }) + + t.Run("invalid workdir", func(t *testing.T) { + _, err := pythonRuntime.Build(context.Background(), &runtime.BuildInput{ + FunctionID: "error-test-2", + Handler: "handler.py", + Runtime: "python3.11", + CfgPath: "/nonexistent/directory/sst.config.ts", + }) + if err == nil { + t.Error("Expected error for invalid workdir") + } + }) +} + +// findProjectRoot finds the project root by looking for go.mod +func findProjectRoot() (string, error) { + dir, err := os.Getwd() + if err != nil { + return "", err + } + + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir, nil + } + parent := filepath.Dir(dir) + if parent == dir { + return "", fmt.Errorf("could not find project root (go.mod not found)") + } + dir = parent + } +} diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index 72d8089a85..3e042635cb 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -20,6 +20,24 @@ type Runtime interface { Build(ctx context.Context, input *BuildInput) (*BuildOutput, error) Run(ctx context.Context, input *RunInput) (Worker, error) ShouldRebuild(functionID string, path string) bool + // ShouldRunEagerly controls whether workers are started immediately after a rebuild + // or lazily on first invocation. + // + // Background: When a file changes, SST stops all affected workers and rebuilds them. + // By default (returning true), workers are restarted immediately after rebuild. + // This works well for runtimes like Node.js where esbuild provides precise per-function + // dependency tracking, so only a few functions rebuild on each change. + // + // For Python, we lack precise dependency tracking - a change to shared library code + // triggers rebuilds for ALL 50+ functions. Starting all workers immediately causes: + // - 50+ processes competing for CPU/memory during startup + // - ~2 second startup time per worker + // - Long delays before the system is responsive again + // + // By returning false, Python opts into lazy startup: workers are stopped and marked + // as needing rebuild, but only actually start when invoked. This means only the + // functions you're actively using restart immediately. + ShouldRunEagerly() bool } type Worker interface { @@ -152,7 +170,7 @@ func (c *Collection) Build(ctx context.Context, input *BuildInput) (*BuildOutput } } } - // copying fiels still happens in node + // copying files still happens in node if !input.Dev && false { sourceFile, err := os.Open(from) if err != nil { @@ -221,6 +239,14 @@ func (c *Collection) ShouldRebuild(runtime string, functionID string, file strin return result } +func (c *Collection) ShouldRunEagerly(runtime string) bool { + r, ok := c.Runtime(runtime) + if !ok { + return true // Default to eager for unknown runtimes + } + return r.ShouldRunEagerly() +} + func (c *Collection) AddTarget(input *BuildInput) { input.CfgPath = c.cfgPath c.targets[input.FunctionID] = input diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go new file mode 100644 index 0000000000..2201e101d3 --- /dev/null +++ b/pkg/runtime/runtime_test.go @@ -0,0 +1,82 @@ +package runtime_test + +import ( + "context" + "path/filepath" + "testing" + + "github.com/sst/sst/v3/pkg/runtime" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type mockRuntime struct { + matchFn func(string) bool +} + +func (m *mockRuntime) Match(r string) bool { + return m.matchFn(r) +} +func (m *mockRuntime) Build(ctx context.Context, input *runtime.BuildInput) (*runtime.BuildOutput, error) { + return nil, nil +} +func (m *mockRuntime) Run(ctx context.Context, input *runtime.RunInput) (runtime.Worker, error) { + return nil, nil +} +func (m *mockRuntime) ShouldRebuild(functionID string, path string) bool { + return false +} +func (m *mockRuntime) ShouldRunEagerly() bool { + return true +} + +func TestBuildInputOut(t *testing.T) { + cfgPath := filepath.Join("/project", "sst.config.ts") + workingDir := filepath.Join("/project", ".sst") + + t.Run("dev mode", func(t *testing.T) { + input := &runtime.BuildInput{ + CfgPath: cfgPath, + Dev: true, + FunctionID: "myFunc", + } + expected := filepath.Join(workingDir, "artifacts", "myFunc-dev") + assert.Equal(t, expected, input.Out()) + }) + + t.Run("prod mode", func(t *testing.T) { + input := &runtime.BuildInput{ + CfgPath: cfgPath, + Dev: false, + FunctionID: "myFunc", + } + expected := filepath.Join(workingDir, "artifacts", "myFunc-src") + assert.Equal(t, expected, input.Out()) + }) +} + +func TestCollectionRuntime(t *testing.T) { + t.Run("matching runtime found", func(t *testing.T) { + mr := &mockRuntime{matchFn: func(r string) bool { return r == "nodejs" }} + c := runtime.NewCollection("cfg", mr) + + rt, ok := c.Runtime("nodejs") + require.True(t, ok) + assert.Equal(t, mr, rt) + }) + + t.Run("no match", func(t *testing.T) { + mr := &mockRuntime{matchFn: func(r string) bool { return false }} + c := runtime.NewCollection("cfg", mr) + + _, ok := c.Runtime("python") + assert.False(t, ok) + }) + + t.Run("empty collection", func(t *testing.T) { + c := runtime.NewCollection("cfg") + + _, ok := c.Runtime("anything") + assert.False(t, ok) + }) +} diff --git a/pkg/runtime/rust/rust.go b/pkg/runtime/rust/rust.go index 367cd649a2..50b9d95e1e 100644 --- a/pkg/runtime/rust/rust.go +++ b/pkg/runtime/rust/rust.go @@ -210,3 +210,10 @@ func (r *Runtime) ShouldRebuild(functionID string, file string) bool { } return !strings.HasPrefix(rel, "..") } + +// ShouldRunEagerly returns true for Rust - workers restart immediately after rebuild. +// Rust uses directory-based dependency tracking, so only functions in the changed +// directory will rebuild. +func (r *Runtime) ShouldRunEagerly() bool { + return true +} diff --git a/pkg/runtime/worker/unenv.json b/pkg/runtime/worker/unenv.json deleted file mode 100644 index 48688ea4d9..0000000000 --- a/pkg/runtime/worker/unenv.json +++ /dev/null @@ -1,225 +0,0 @@ -{ - "alias": { - "node-fetch-native/polyfill": "unenv-sst/runtime/mock/empty", - "node:inspector/promises": "unenv-sst/runtime/node/inspector/index", - "node:readline/promises": "unenv-sst/runtime/node/readline/promises/index", - "node:stream/consumers": "node:stream/consumers", - "cross-fetch/polyfill": "unenv-sst/runtime/mock/empty", - "node:stream/promises": "node:stream/promises", - "node:timers/promises": "unenv-sst/runtime/node/timers/promises/index", - "inspector/promises": "unenv-sst/runtime/node/inspector/index", - "node:assert/strict": "node:assert", - "readline/promises": "unenv-sst/runtime/node/readline/promises/index", - "node:dns/promises": "unenv-sst/runtime/node/dns/promises/index", - "stream/consumers": "stream/consumers", - "node:fs/promises": "unenv-sst/runtime/node/fs/promises/index", - "stream/promises": "stream/promises", - "timers/promises": "unenv-sst/runtime/node/timers/promises/index", - "buffer/index.js": "buffer", - "node:path/posix": "unenv-sst/runtime/node/path/index", - "node:path/win32": "unenv-sst/runtime/node/path/index", - "node:stream/web": "node:stream/web", - "node:util/types": "node:util/types", - "assert/strict": "node:assert", - "dns/promises": "unenv-sst/runtime/node/dns/promises/index", - "fs/promises": "unenv-sst/runtime/node/fs/promises/index", - "path/posix": "unenv-sst/runtime/node/path/index", - "path/win32": "unenv-sst/runtime/node/path/index", - "stream/web": "stream/web", - "util/types": "util/types", - "node:_stream_passthrough": "node:_stream_passthrough", - "node:diagnostics_channel": "node:diagnostics_channel", - "node:_stream_transform": "node:_stream_transform", - "node:_stream_readable": "node:_stream_readable", - "node:_stream_writable": "node:_stream_writable", - "_stream_passthrough": "_stream_passthrough", - "diagnostics_channel": "diagnostics_channel", - "node:_http_incoming": "unenv-sst/runtime/mock/proxy-cjs", - "node:_http_outgoing": "unenv-sst/runtime/mock/proxy-cjs", - "node:_stream_duplex": "node:_stream_duplex", - "node:string_decoder": "node:string_decoder", - "node:worker_threads": "unenv-sst/runtime/node/worker_threads/index", - "node:child_process": "unenv-sst/runtime/node/child_process/index", - "_stream_transform": "_stream_transform", - "node-fetch-native": "unenv-sst/runtime/npm/node-fetch", - "node:_http_client": "unenv-sst/runtime/mock/proxy-cjs", - "node:_http_common": "unenv-sst/runtime/mock/proxy-cjs", - "node:_http_server": "unenv-sst/runtime/mock/proxy-cjs", - "node:_stream_wrap": "unenv-sst/runtime/mock/proxy-cjs", - "node:trace_events": "unenv-sst/runtime/node/trace_events/index", - "_stream_readable": "_stream_readable", - "_stream_writable": "_stream_writable", - "isomorphic-fetch": "unenv-sst/runtime/mock/empty", - "node:_http_agent": "unenv-sst/runtime/mock/proxy-cjs", - "node:_tls_common": "unenv-sst/runtime/mock/proxy-cjs", - "node:async_hooks": "unenv-sst/runtime/node/async_hooks/$cloudflare", - "node:detect-libc": "unenv-sst/runtime/mock/proxy-cjs", - "node:querystring": "node:querystring", - "node:bun:sqlite": "unenv-sst/runtime/mock/proxy-cjs", - "node:perf_hooks": "unenv-sst/runtime/node/perf_hooks/index", - "_http_incoming": "unenv-sst/runtime/mock/proxy-cjs", - "_http_outgoing": "unenv-sst/runtime/mock/proxy-cjs", - "_stream_duplex": "_stream_duplex", - "string_decoder": "string_decoder", - "worker_threads": "unenv-sst/runtime/node/worker_threads/index", - "node:_tls_wrap": "unenv-sst/runtime/mock/proxy-cjs", - "node:constants": "unenv-sst/runtime/node/constants/index", - "node:inspector": "unenv-sst/runtime/node/inspector/index", - "child_process": "unenv-sst/runtime/node/child_process/index", - "node:bun:test": "unenv-sst/runtime/mock/proxy-cjs", - "node:bun:wrap": "unenv-sst/runtime/mock/proxy-cjs", - "node:punycode": "unenv-sst/runtime/node/punycode/index", - "node:readline": "unenv-sst/runtime/node/readline/index", - "_http_client": "unenv-sst/runtime/mock/proxy-cjs", - "_http_common": "unenv-sst/runtime/mock/proxy-cjs", - "_http_server": "unenv-sst/runtime/mock/proxy-cjs", - "_stream_wrap": "unenv-sst/runtime/mock/proxy-cjs", - "trace_events": "unenv-sst/runtime/node/trace_events/index", - "node:bun:ffi": "unenv-sst/runtime/mock/proxy-cjs", - "node:bun:jsc": "unenv-sst/runtime/mock/proxy-cjs", - "node:cluster": "unenv-sst/runtime/node/cluster/index", - "node:console": "unenv-sst/runtime/node/console/$cloudflare", - "node:process": "unenv-sst/runtime/node/process/$cloudflare", - "_http_agent": "unenv-sst/runtime/mock/proxy-cjs", - "_tls_common": "unenv-sst/runtime/mock/proxy-cjs", - "async_hooks": "unenv-sst/runtime/node/async_hooks/$cloudflare", - "detect-libc": "unenv-sst/runtime/mock/proxy-cjs", - "querystring": "querystring", - "cross-fetch": "unenv-sst/runtime/npm/cross-fetch", - "node:assert": "node:assert", - "node:buffer": "node:buffer", - "node:crypto": "unenv-sst/runtime/node/crypto/$cloudflare", - "node:domain": "unenv-sst/runtime/node/domain/index", - "node:events": "node:events", - "node:module": "unenv-sst/runtime/node/module/$cloudflare", - "node:stream": "node:stream", - "node:timers": "unenv-sst/runtime/node/timers/$cloudflare", - "node:undici": "unenv-sst/runtime/mock/proxy-cjs", - "bun:sqlite": "unenv-sst/runtime/mock/proxy-cjs", - "perf_hooks": "unenv-sst/runtime/node/perf_hooks/index", - "node-fetch": "unenv-sst/runtime/npm/node-fetch", - "node:dgram": "unenv-sst/runtime/node/dgram/index", - "node:http2": "unenv-sst/runtime/node/http2/index", - "node:https": "unenv-sst/runtime/node/https/index", - "_tls_wrap": "unenv-sst/runtime/mock/proxy-cjs", - "constants": "unenv-sst/runtime/node/constants/index", - "inspector": "unenv-sst/runtime/node/inspector/index", - "node:http": "unenv-sst/runtime/node/http/index", - "node:path": "node:path", - "node:repl": "unenv-sst/runtime/mock/proxy-cjs", - "node:util": "unenv-sst/runtime/node/util/$cloudflare", - "node:wasi": "unenv-sst/runtime/node/wasi/index", - "node:zlib": "node:zlib", - "bun:test": "unenv-sst/runtime/mock/proxy-cjs", - "bun:wrap": "unenv-sst/runtime/mock/proxy-cjs", - "punycode": "unenv-sst/runtime/node/punycode/index", - "readline": "unenv-sst/runtime/node/readline/index", - "fsevents": "unenv-sst/runtime/npm/fsevents", - "inherits": "unenv-sst/runtime/npm/inherits", - "node:bun": "unenv-sst/runtime/mock/proxy-cjs", - "node:dns": "unenv-sst/runtime/node/dns/index", - "node:net": "unenv-sst/runtime/node/net/index", - "node:sys": "unenv-sst/runtime/node/util/$cloudflare", - "node:tls": "unenv-sst/runtime/node/tls/index", - "node:tty": "unenv-sst/runtime/node/tty/index", - "node:url": "node:url", - "bun:ffi": "unenv-sst/runtime/mock/proxy-cjs", - "bun:jsc": "unenv-sst/runtime/mock/proxy-cjs", - "cluster": "unenv-sst/runtime/node/cluster/index", - "console": "unenv-sst/runtime/node/console/$cloudflare", - "process": "unenv-sst/runtime/node/process/$cloudflare", - "node:fs": "unenv-sst/runtime/node/fs/index", - "node:os": "unenv-sst/runtime/node/os/index", - "node:v8": "unenv-sst/runtime/node/v8/index", - "node:vm": "unenv-sst/runtime/node/vm/index", - "node:ws": "unenv-sst/runtime/mock/proxy-cjs", - "assert": "assert", - "buffer": "buffer", - "crypto": "unenv-sst/runtime/node/crypto/$cloudflare", - "domain": "unenv-sst/runtime/node/domain/index", - "events": "events", - "module": "unenv-sst/runtime/node/module/$cloudflare", - "stream": "stream", - "timers": "unenv-sst/runtime/node/timers/$cloudflare", - "undici": "unenv-sst/runtime/mock/proxy-cjs", - "dgram": "unenv-sst/runtime/node/dgram/index", - "http2": "unenv-sst/runtime/node/http2/index", - "https": "unenv-sst/runtime/node/https/index", - "http": "unenv-sst/runtime/node/http/index", - "path": "path", - "repl": "unenv-sst/runtime/mock/proxy-cjs", - "util": "unenv-sst/runtime/node/util/$cloudflare", - "wasi": "unenv-sst/runtime/node/wasi/index", - "zlib": "zlib", - "bun": "unenv-sst/runtime/mock/proxy-cjs", - "dns": "unenv-sst/runtime/node/dns/index", - "net": "unenv-sst/runtime/node/net/index", - "sys": "unenv-sst/runtime/node/util/$cloudflare", - "tls": "unenv-sst/runtime/node/tls/index", - "tty": "unenv-sst/runtime/node/tty/index", - "url": "url", - "fs": "unenv-sst/runtime/node/fs/index", - "os": "unenv-sst/runtime/node/os/index", - "v8": "unenv-sst/runtime/node/v8/index", - "vm": "unenv-sst/runtime/node/vm/index", - "ws": "unenv-sst/runtime/mock/proxy-cjs" - }, - "inject": { - "process": "unenv-sst/runtime/node/process/$cloudflare", - "performance": "unenv-sst/runtime/polyfill/performance", - "console": "unenv-sst/runtime/node/console/$cloudflare", - "setImmediate": [ - "unenv-sst/runtime/node/timers/$cloudflare", - "setImmediate" - ], - "clearImmediate": [ - "unenv-sst/runtime/node/timers/$cloudflare", - "clearImmediate" - ] - }, - "polyfill": [ - "unenv-sst/runtime/polyfill/node-global", - "unenv-sst/runtime/polyfill/process", - "unenv-sst/runtime/polyfill/performance" - ], - "external": [ - "_stream_duplex", - "node:_stream_duplex", - "_stream_passthrough", - "node:_stream_passthrough", - "_stream_readable", - "node:_stream_readable", - "_stream_transform", - "node:_stream_transform", - "_stream_writable", - "node:_stream_writable", - "assert", - "node:assert", - "buffer", - "node:buffer", - "diagnostics_channel", - "node:diagnostics_channel", - "events", - "node:events", - "path", - "node:path", - "querystring", - "node:querystring", - "stream", - "node:stream", - "stream/consumers", - "node:stream/consumers", - "stream/promises", - "node:stream/promises", - "stream/web", - "node:stream/web", - "string_decoder", - "node:string_decoder", - "url", - "node:url", - "util/types", - "node:util/types", - "zlib", - "node:zlib" - ] -} diff --git a/pkg/runtime/worker/unenv.mjs b/pkg/runtime/worker/unenv.mjs deleted file mode 100644 index 27bcb0f27f..0000000000 --- a/pkg/runtime/worker/unenv.mjs +++ /dev/null @@ -1,8 +0,0 @@ -import { env, nodeless, cloudflare } from "unenv-sst"; -const envConfig = env(nodeless, cloudflare, {}); -import { dirname, join } from "path"; -import { fileURLToPath } from "url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); -let json = JSON.stringify(envConfig, null, 2); -json = json.replaceAll("unenv/", "unenv-sst/"); -Bun.write(join(__dirname, "./unenv.json"), json); diff --git a/pkg/runtime/worker/worker.go b/pkg/runtime/worker/worker.go index 52d39d4726..c073e441a6 100644 --- a/pkg/runtime/worker/worker.go +++ b/pkg/runtime/worker/worker.go @@ -2,16 +2,17 @@ package worker import ( "context" - "embed" "encoding/json" "fmt" "log/slog" + "maps" "os" "path/filepath" "strings" "sync" esbuild "github.com/evanw/esbuild/pkg/api" + "github.com/sst/sst/v3/pkg/process" "github.com/sst/sst/v3/pkg/project/path" "github.com/sst/sst/v3/pkg/runtime" "github.com/sst/sst/v3/pkg/runtime/node" @@ -21,32 +22,33 @@ type Runtime struct { contexts map[string]esbuild.BuildContext results map[string]esbuild.BuildResult lock sync.RWMutex - unenv *unenv + unenv map[string]*unenv } type Properties struct { - AccountID string `json:"accountID"` - ScriptName string `json:"scriptName"` - Build node.NodeProperties `json:"build"` + AccountID string `json:"accountID"` + ScriptName string `json:"scriptName"` + Build node.NodeProperties `json:"build"` + Compatibility compatibility `json:"compatibility"` +} + +type compatibility struct { + Date string `json:"date"` + Flags []string `json:"flags"` } type unenv struct { Alias map[string]string `json:"alias"` + External []string `json:"external"` Polyfill []string `json:"polyfill"` } -//go:embed unenv.json -var embedded embed.FS - func New() *Runtime { - data, _ := embedded.ReadFile("unenv.json") - var unenv unenv - json.Unmarshal(data, &unenv) return &Runtime{ contexts: map[string]esbuild.BuildContext{}, results: map[string]esbuild.BuildResult{}, lock: sync.RWMutex{}, - unenv: &unenv, + unenv: map[string]*unenv{}, } } @@ -54,11 +56,22 @@ func (w *Runtime) Build(ctx context.Context, input *runtime.BuildInput) (*runtim var properties Properties json.Unmarshal(input.Properties, &properties) build := properties.Build + unenv, err := w.getUnenv( + ctx, + input.CfgPath, + properties.Compatibility, + ) + if err != nil { + return nil, err + } abs, err := filepath.Abs(input.Handler) if err != nil { return nil, err } + // Windows paths must not be embedded raw in JS string literals: backslashes + // are escape sequences (\p, \f, etc.). Forward slashes work for imports. + importPath := filepath.ToSlash(abs) target := filepath.Join(input.Out(), input.Handler) slog.Info("loader info", "loader", build.Loader) @@ -84,24 +97,30 @@ func (w *Runtime) Build(ctx context.Context, input *runtime.BuildInput) (*runtim "mainFields", build.ESBuild.MainFields, "conditions", build.ESBuild.Conditions, ) + external := uniqueStrings( + []string{"node:*", "cloudflare:workers"}, + unenv.External, + build.ESBuild.External, + ) + alias := stripAliasedExternals(unenv.Alias, external) options := esbuild.BuildOptions{ Platform: esbuild.PlatformNode, Stdin: &esbuild.StdinOptions{ Contents: fmt.Sprintf(` - import handler from "%s" - import { fromCloudflareEnv, wrapCloudflareHandler } from "sst" + import * as _sst_user_module from "%s" + import { wrapCloudflareHandler } from "sst/resource/cloudflare" export * from "%s" - export default wrapCloudflareHandler(handler) - `, abs, abs), + export default wrapCloudflareHandler(_sst_user_module.default) + `, importPath, importPath), ResolveDir: filepath.Dir(abs), Loader: esbuild.LoaderTS, }, NodePaths: append([]string{ filepath.Join(path.ResolvePlatformDir(input.CfgPath), "node_modules"), }, build.ESBuild.NodePaths...), - Alias: w.unenv.Alias, - Inject: w.unenv.Polyfill, - External: []string{"node:*", "cloudflare:workers"}, + Alias: alias, + Inject: unenv.Polyfill, + External: external, Conditions: build.ESBuild.ResolveConditions([]string{"workerd", "worker", "browser"}), Sourcemap: build.ESBuild.ResolveSourcemap(esbuild.SourceMapNone), Loader: loader, @@ -138,16 +157,28 @@ func (w *Runtime) Build(ctx context.Context, input *runtime.BuildInput) (*runtim "sourcemap", options.Sourcemap, "keepNames", options.KeepNames, "define", options.Define, + "external", options.External, "mainFields", options.MainFields, "conditions", options.Conditions, ) + contextKey := buildContextKey(input) w.lock.RLock() - buildContext, ok := w.contexts[input.FunctionID] + buildContext, ok := w.contexts[contextKey] w.lock.RUnlock() if !ok { + prefix := input.FunctionID + "\x00" + w.lock.Lock() + for key, context := range w.contexts { + if !strings.HasPrefix(key, prefix) { + continue + } + context.Dispose() + delete(w.contexts, key) + } + w.lock.Unlock() buildContext, _ = esbuild.Context(options) w.lock.Lock() - w.contexts[input.FunctionID] = buildContext + w.contexts[contextKey] = buildContext w.lock.Unlock() } @@ -175,6 +206,73 @@ func (w *Runtime) Build(ctx context.Context, input *runtime.BuildInput) (*runtim }, nil } +func buildContextKey(input *runtime.BuildInput) string { + return input.FunctionID + "\x00" + input.Handler + "\x00" + string(input.Properties) +} + +func uniqueStrings(groups ...[]string) []string { + seen := map[string]bool{} + result := []string{} + for _, group := range groups { + for _, item := range group { + if seen[item] { + continue + } + seen[item] = true + result = append(result, item) + } + } + return result +} + +func stripAliasedExternals(alias map[string]string, external []string) map[string]string { + result := map[string]string{} + maps.Copy(result, alias) + for _, item := range external { + delete(result, item) + } + return result +} + +func (w *Runtime) getUnenv(ctx context.Context, cfgPath string, compatibility compatibility) (*unenv, error) { + payload, err := json.Marshal(compatibility) + if err != nil { + return nil, err + } + key := string(payload) + + w.lock.RLock() + if cached, ok := w.unenv[key]; ok { + w.lock.RUnlock() + return cached, nil + } + w.lock.RUnlock() + + cmd := process.CommandContext( + ctx, + "node", + filepath.Join(path.ResolvePlatformDir(cfgPath), "src/runtime/worker/unenv.mjs"), + string(payload), + ) + cmd.Dir = path.ResolvePlatformDir(cfgPath) + cmd.Env = []string{} + output, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("failed to load cloudflare unenv config: %w\n%s", err, output) + } + + var result unenv + if err := json.Unmarshal(output, &result); err != nil { + return nil, fmt.Errorf("failed to decode cloudflare unenv config: %w\n%s", err, output) + } + + w.lock.Lock() + w.unenv[key] = &result + w.lock.Unlock() + + return &result, nil +} + func (w *Runtime) Match(runtime string) bool { return runtime == "worker" } @@ -217,6 +315,12 @@ func (r *Runtime) ShouldRebuild(functionID string, file string) bool { return false } +// ShouldRunEagerly returns true for Cloudflare Workers - workers restart immediately after rebuild. +// Workers use esbuild's metafile for precise per-function dependency tracking. +func (r *Runtime) ShouldRunEagerly() bool { + return true +} + func (r *Runtime) Run(ctx context.Context, input *runtime.RunInput) (runtime.Worker, error) { return nil, fmt.Errorf("not implemented") } diff --git a/pkg/runtime/worker/worker_test.go b/pkg/runtime/worker/worker_test.go new file mode 100644 index 0000000000..84c72d7a64 --- /dev/null +++ b/pkg/runtime/worker/worker_test.go @@ -0,0 +1,28 @@ +package worker + +import ( + "testing" +) + +func TestStripAliasedExternals(t *testing.T) { + original := map[string]string{ + "http": "unenv/node/http", + "node:http": "unenv/node/http", + "buffer": "buffer", + } + + got := stripAliasedExternals(original, []string{"http", "node:http", "node:*"}) + + if _, ok := got["http"]; ok { + t.Fatalf("http alias should be removed") + } + if _, ok := got["node:http"]; ok { + t.Fatalf("node:http alias should be removed") + } + if got["buffer"] != "buffer" { + t.Fatalf("buffer alias = %q, want %q", got["buffer"], "buffer") + } + if original["http"] != "unenv/node/http" { + t.Fatalf("original alias map was mutated") + } +} diff --git a/pkg/server/resource/aws-function-environment-update.go b/pkg/server/resource/aws-function-environment-update.go index 1fd7cd07b0..9502cb6697 100644 --- a/pkg/server/resource/aws-function-environment-update.go +++ b/pkg/server/resource/aws-function-environment-update.go @@ -11,9 +11,10 @@ type FunctionEnvironmentUpdate struct { } type FunctionEnvironmentUpdateInputs struct { - FunctionName string `json:"functionName"` - Environment map[string]string `json:"environment"` - Region string `json:"region"` + FunctionName string `json:"functionName"` + Environment map[string]string `json:"environment"` + Region string `json:"region"` + FunctionLastModified string `json:"functionLastModified"` } type FunctionEnvironmentUpdateOutputs struct{} diff --git a/pkg/server/resource/vercel-dns-record.go b/pkg/server/resource/vercel-dns-record.go index 27ac015bf1..bb93cc62cd 100644 --- a/pkg/server/resource/vercel-dns-record.go +++ b/pkg/server/resource/vercel-dns-record.go @@ -22,12 +22,13 @@ type VercelDnsRecord struct { } type VercelDnsRecordInputs struct { - Domain string `json:"domain"` - Type string `json:"type"` - Name string `json:"name"` - Value string `json:"value"` - TeamId string `json:"teamId,omitempty"` - ApiToken string `json:"apiToken"` + Domain string `json:"domain"` + Type string `json:"type"` + Name string `json:"name"` + Value string `json:"value"` + MxPriority int `json:"mxPriority,omitempty"` + TeamId string `json:"teamId,omitempty"` + ApiToken string `json:"apiToken"` } type VercelDnsRecordOutputs struct { @@ -67,17 +68,20 @@ func (r *VercelDnsRecord) createOrUpdateRecord(input *VercelDnsRecordInputs) (st url = fmt.Sprintf("%s?teamId=%s", url, input.TeamId) } - // Use the input type, name, and value for the DNS record + // Use the input type, name, and value for the DNS record. MxPriority is only + // relevant for MX records; omitempty drops it for other types. recordPayload := struct { - Type string `json:"type"` - Name string `json:"name"` - Value string `json:"value"` - Ttl int `json:"ttl"` + Type string `json:"type"` + Name string `json:"name"` + Value string `json:"value"` + MxPriority int `json:"mxPriority,omitempty"` + Ttl int `json:"ttl"` }{ - Type: input.Type, - Name: input.Name, - Value: input.Value, - Ttl: 60, + Type: input.Type, + Name: input.Name, + Value: input.Value, + MxPriority: input.MxPriority, + Ttl: 60, } payloadBytes, err := json.Marshal(recordPayload) diff --git a/pkg/server/scrap/scrap.go b/pkg/server/scrap/scrap.go deleted file mode 100644 index 763ab1db30..0000000000 --- a/pkg/server/scrap/scrap.go +++ /dev/null @@ -1,23 +0,0 @@ -package scrap - -import ( - "context" - "net/rpc" - "time" - - "github.com/sst/sst/v3/pkg/project" -) - -type Scrap struct { -} - -func (r *Scrap) Run(input int, output *int) error { - time.Sleep(time.Minute * 10) - *output = input + 1 - return nil -} - -func Register(ctx context.Context, p *project.Project, r *rpc.Server) error { - r.RegisterName("Scrap", &Scrap{}) - return nil -} diff --git a/pkg/server/server.go b/pkg/server/server.go index 7f57f6effc..5d7f2a16a9 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -19,7 +19,6 @@ import ( "github.com/sst/sst/v3/pkg/server/aws" "github.com/sst/sst/v3/pkg/server/resource" "github.com/sst/sst/v3/pkg/server/runtime" - "github.com/sst/sst/v3/pkg/server/scrap" ) type Server struct { @@ -59,7 +58,6 @@ func (s *Server) Start(ctx context.Context, p *project.Project) error { resource.Register(ctx, p, s.Rpc) aws.Register(ctx, p, s.Rpc) - scrap.Register(ctx, p, s.Rpc) runtime.Register(ctx, p, s.Rpc) server := &http.Server{ diff --git a/pkg/task/task_test.go b/pkg/task/task_test.go new file mode 100644 index 0000000000..7248812d8c --- /dev/null +++ b/pkg/task/task_test.go @@ -0,0 +1,54 @@ +package task_test + +import ( + "context" + "fmt" + "sync/atomic" + "testing" + + "github.com/sst/sst/v3/pkg/task" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRun(t *testing.T) { + t.Run("succeeds first try", func(t *testing.T) { + val, err := task.Run(context.Background(), func() (string, error) { + return "ok", nil + }) + require.NoError(t, err) + assert.Equal(t, "ok", val) + }) + + t.Run("fails twice then succeeds", func(t *testing.T) { + var count atomic.Int32 + val, err := task.Run(context.Background(), func() (string, error) { + n := count.Add(1) + if n < 3 { + return "", fmt.Errorf("fail %d", n) + } + return "ok", nil + }) + require.NoError(t, err) + assert.Equal(t, "ok", val) + }) + + t.Run("fails 3 times returns last error", func(t *testing.T) { + var count atomic.Int32 + _, err := task.Run(context.Background(), func() (string, error) { + n := count.Add(1) + return "", fmt.Errorf("fail %d", n) + }) + require.Error(t, err) + assert.Equal(t, "fail 3", err.Error()) + }) + + t.Run("context cancelled", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + _, err := task.Run(ctx, func() (string, error) { + select {} // block forever + }) + assert.ErrorIs(t, err, context.Canceled) + }) +} diff --git a/pkg/tunnel/proxy.go b/pkg/tunnel/proxy.go index 3540c670ca..b33ba5ce5a 100644 --- a/pkg/tunnel/proxy.go +++ b/pkg/tunnel/proxy.go @@ -29,7 +29,7 @@ func StartProxy(ctx context.Context, username string, host string, key []byte) e defer sshClient.Close() server, err := socks5.New(&socks5.Config{ Dial: func(ctx context.Context, network, addr string) (net.Conn, error) { - fmt.Println(ui.TEXT_INFO_BOLD.Render(("| "), ui.TEXT_NORMAL.Render("Tunneling", network, addr))) + fmt.Println(ui.GetColor(addr).Bold(true).Render("| ") + ui.TEXT_NORMAL.Render("Tunneling "+network+" "+addr)) return sshClient.Dial(network, addr) }, }) diff --git a/pkg/types/python/python.go b/pkg/types/python/python.go index 2735ad9e34..b7bd7b04df 100644 --- a/pkg/types/python/python.go +++ b/pkg/types/python/python.go @@ -11,8 +11,8 @@ import ( "github.com/sst/sst/v3/pkg/project/common" ) -func Generate(root string, links common.Links) error { - projects := fs.FindDown(root, "pyproject.toml") +func Generate(root string, links common.Links, ignore []string) error { + projects := fs.FindDownWithIgnore(root, "pyproject.toml", ignore) files := []io.Writer{} for _, project := range projects { path := filepath.Join(filepath.Dir(project), "sst.pyi") diff --git a/pkg/types/python/python_test.go b/pkg/types/python/python_test.go new file mode 100644 index 0000000000..f7146cdfa3 --- /dev/null +++ b/pkg/types/python/python_test.go @@ -0,0 +1,113 @@ +package python_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/sst/sst/v3/pkg/project/common" + "github.com/sst/sst/v3/pkg/types/python" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerate(t *testing.T) { + t.Run("no pyproject.toml returns nil", func(t *testing.T) { + dir := t.TempDir() + err := python.Generate(dir, common.Links{}, nil) + require.NoError(t, err) + }) + + t.Run("creates sst.pyi with correct types", func(t *testing.T) { + dir := t.TempDir() + os.WriteFile(filepath.Join(dir, "pyproject.toml"), []byte("[project]"), 0644) + + links := common.Links{ + "MyBucket": { + Properties: map[string]interface{}{ + "name": "my-bucket", + "arn": "arn:aws:s3:::my-bucket", + }, + }, + } + + err := python.Generate(dir, links, nil) + require.NoError(t, err) + + content, err := os.ReadFile(filepath.Join(dir, "sst.pyi")) + require.NoError(t, err) + + out := string(content) + assert.Contains(t, out, "class Resource:") + assert.Contains(t, out, "class MyBucket:") + assert.Contains(t, out, "arn: str") + assert.Contains(t, out, "name: str") + assert.Contains(t, out, "class App:") + assert.Contains(t, out, "name: str") + assert.Contains(t, out, "stage: str") + }) + + t.Run("fields are sorted", func(t *testing.T) { + dir := t.TempDir() + os.WriteFile(filepath.Join(dir, "pyproject.toml"), []byte("[project]"), 0644) + + links := common.Links{ + "Db": { + Properties: map[string]interface{}{ + "z_field": "z", + "a_field": "a", + }, + }, + } + + err := python.Generate(dir, links, nil) + require.NoError(t, err) + + content, err := os.ReadFile(filepath.Join(dir, "sst.pyi")) + require.NoError(t, err) + + out := string(content) + aIdx := indexOf(out, "a_field") + zIdx := indexOf(out, "z_field") + assert.Greater(t, aIdx, -1) + assert.Less(t, aIdx, zIdx) + }) + + t.Run("nested pyproject.toml", func(t *testing.T) { + dir := t.TempDir() + sub := filepath.Join(dir, "services", "api") + os.MkdirAll(sub, 0755) + os.WriteFile(filepath.Join(sub, "pyproject.toml"), []byte("[project]"), 0644) + + err := python.Generate(dir, common.Links{}, nil) + require.NoError(t, err) + + assert.FileExists(t, filepath.Join(sub, "sst.pyi")) + }) + + t.Run("ignores configured directories", func(t *testing.T) { + dir := t.TempDir() + ignored := filepath.Join(dir, "services", "legacy") + included := filepath.Join(dir, "services", "api") + os.MkdirAll(ignored, 0755) + os.MkdirAll(included, 0755) + os.WriteFile(filepath.Join(ignored, "pyproject.toml"), []byte("[project]"), 0644) + os.WriteFile(filepath.Join(included, "pyproject.toml"), []byte("[project]"), 0644) + + err := python.Generate(dir, common.Links{}, []string{"services/legacy"}) + require.NoError(t, err) + + assert.FileExists(t, filepath.Join(included, "sst.pyi")) + _, err = os.Stat(filepath.Join(ignored, "sst.pyi")) + assert.True(t, os.IsNotExist(err)) + }) +} + +func indexOf(s, substr string) int { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return i + } + } + return -1 +} diff --git a/pkg/types/rails/rails.go b/pkg/types/rails/rails.go index 1ed743b87b..a33f018b83 100644 --- a/pkg/types/rails/rails.go +++ b/pkg/types/rails/rails.go @@ -5,7 +5,10 @@ import ( ) // Generate is stubbed out β€” Rails support is not yet implemented. -func Generate(root string, links common.Links) error { +func Generate(root string, links common.Links, ignore []string) error { + _ = root + _ = links + _ = ignore return nil // projects := fs.FindDown(root, "config.ru") // files := []io.Writer{} diff --git a/pkg/types/types.go b/pkg/types/types.go index 36fd70cff6..3b80deacfc 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -10,9 +10,9 @@ import ( "github.com/sst/sst/v3/pkg/types/typescript" ) -type Generator = func(root string, complete common.Links) error +type Generator = func(root string, complete common.Links, ignore []string) error -func Generate(cfgPath string, complete common.Links) error { +func Generate(cfgPath string, complete common.Links, ignore []string) error { root := path.ResolveRootDir(cfgPath) // gitroot, err := fs.FindUp(root, ".git") // if err == nil { @@ -20,7 +20,7 @@ func Generate(cfgPath string, complete common.Links) error { // } slog.Info("generating types", "root", root) for _, generator := range All { - err := generator(root, complete) + err := generator(root, complete, ignore) if err != nil { return err } diff --git a/pkg/types/typescript/typescript.go b/pkg/types/typescript/typescript.go index ef1baf1842..e128958d0c 100644 --- a/pkg/types/typescript/typescript.go +++ b/pkg/types/typescript/typescript.go @@ -8,19 +8,46 @@ import ( "strings" "github.com/sst/sst/v3/internal/fs" + "github.com/sst/sst/v3/pkg/bus" "github.com/sst/sst/v3/pkg/js" "github.com/sst/sst/v3/pkg/project/common" ) +type WarningEvent struct { + Message string +} + var mapping = map[string]string{ - "r2BucketBindings": "R2Bucket", - "d1DatabaseBindings": "D1Database", - "kvNamespaceBindings": "KVNamespace", - "queueBindings": "Queue", - "serviceBindings": "Service", + "aiBindings": "Ai", + "r2BucketBindings": "R2Bucket", + "d1DatabaseBindings": "D1Database", + "durableObjectNamespaceBindings": "DurableObjectNamespace", + "kvNamespaceBindings": "KVNamespace", + "queueBindings": "Queue", + "serviceBindings": "Service", + "hyperdriveBindings": "Hyperdrive", + "versionMetadataBindings": "WorkerVersionMetadata", + "workflowBindings": "Workflow", + "rateLimitBindings": "RateLimit", } -func Generate(root string, links common.Links) error { +var header = strings.Join([]string{ + "/* This file is auto-generated by SST. Do not edit. */", + "/* tslint:disable */", + "/* eslint-disable */", + "/* deno-fmt-ignore-file */", + "/* biome-ignore-all lint: auto-generated */", + "", + "", +}, "\n") + +var footer = strings.Join([]string{ + "", + "import \"sst\"", + "export {}", +}, "\n") + +func Generate(root string, links common.Links, ignore []string) error { cloudflareBindings := map[string]string{} for name, link := range links { for _, include := range link.Include { @@ -32,85 +59,85 @@ func Generate(root string, links common.Links) error { } } - header := []byte(strings.Join([]string{ - "/* This file is auto-generated by SST. Do not edit. */", - "/* tslint:disable */", - "/* eslint-disable */", - "/* deno-fmt-ignore-file */", - "/* biome-ignore-all lint: auto-generated */", - "", - "", - }, "\n")) - footer := []byte(strings.Join([]string{ - "", - "import \"sst\"", - "export {}", - }, "\n")) - - packageJsons := fs.FindDown(root, "package.json") + packageJsons := fs.FindDownWithIgnore(root, "package.json", ignore) rootEnv := filepath.Join(root, "sst-env.d.ts") + + foundCloudflareTypes := false for _, packageJson := range packageJsons { - packageJsonFile, err := os.Open(packageJson) + packageJsonData, err := os.ReadFile(packageJson) if err != nil { continue } var data js.PackageJson - err = json.NewDecoder(packageJsonFile).Decode(&data) - if err != nil { + if err := json.Unmarshal(packageJsonData, &data); err != nil { continue } - envPath := filepath.Join(filepath.Dir(packageJson), "sst-env.d.ts") - envFile, err := os.Create(envPath) - if err != nil { - continue + if data.Dependencies["@cloudflare/workers-types"] != "" || data.DevDependencies["@cloudflare/workers-types"] != "" { + foundCloudflareTypes = true + break } - defer envFile.Close() - envFile.Write(header) - defer envFile.Write(footer) + } - properties := map[string]interface{}{} - for name, link := range links { - properties[name] = link.Properties + resources := map[string]interface{}{} + for name, link := range links { + if cfType := cloudflareBindings[name]; cfType != "" && foundCloudflareTypes { + resources[name] = literal{value: `import("@cloudflare/workers-types").` + cfType} + } else { + resources[name] = link.Properties } + } + rootContent := renderRootFile(resources) - if data.Dependencies["@cloudflare/workers-types"] != "" || data.DevDependencies["@cloudflare/workers-types"] != "" { - nonCloudflareLinks := map[string]interface{}{} - for name, link := range properties { - if cloudflareBindings[name] == "" { - nonCloudflareLinks[name] = link - } - } - envFile.WriteString("import \"sst\"\n") - envFile.WriteString("declare module \"sst\" {\n") - envFile.WriteString(" export interface Resource " + infer(nonCloudflareLinks, " ") + "\n") - envFile.WriteString("}" + "\n") - bindings := map[string]interface{}{} - for name, link := range cloudflareBindings { - bindings[name] = literal{value: `cloudflare.` + link} - } - if len(bindings) > 0 { - envFile.WriteString("// cloudflare \n") - envFile.WriteString("import * as cloudflare from \"@cloudflare/workers-types\";\n") - envFile.WriteString("declare module \"sst\" {\n") - envFile.WriteString(" export interface Resource " + infer(bindings, " ") + "\n") - envFile.WriteString("}\n") + for _, packageJson := range packageJsons { + envPath := filepath.Join(filepath.Dir(packageJson), "sst-env.d.ts") + + content := "" + if rootEnv == envPath { + content = rootContent + } else { + rel, err := filepath.Rel(filepath.Dir(envPath), rootEnv) + if err != nil { + continue } - continue + content = renderReferenceFile(filepath.ToSlash(rel)) } - if rootEnv == envPath { - envFile.Write([]byte("declare module \"sst\" {\n")) - envFile.Write([]byte(" export interface Resource " + infer(properties, " ") + "\n")) - envFile.Write([]byte("}\n")) + if err := os.WriteFile(envPath, []byte(content), 0644); err != nil { + continue } + } - rel, err := filepath.Rel(filepath.Dir(envPath), rootEnv) - envFile.WriteString("/// \n") + if len(cloudflareBindings) > 0 && !foundCloudflareTypes { + bus.Publish(&WarningEvent{ + Message: "Cloudflare detected but `@cloudflare/workers-types` is not installed. Install it to get proper types for your bindings.", + }) } return nil } +func renderRootFile(resources map[string]interface{}) string { + var builder strings.Builder + builder.WriteString(header) + builder.WriteString("declare module \"sst\" {\n") + builder.WriteString(" export interface Resource ") + builder.WriteString(infer(resources, " ")) + builder.WriteString("\n") + builder.WriteString("}\n") + builder.WriteString(footer) + return builder.String() +} + +func renderReferenceFile(rel string) string { + var builder strings.Builder + builder.WriteString(header) + builder.WriteString("/// \n") + builder.WriteString(footer) + return builder.String() +} + type literal struct { value string } diff --git a/pkg/types/typescript/typescript_test.go b/pkg/types/typescript/typescript_test.go new file mode 100644 index 0000000000..5914c2a557 --- /dev/null +++ b/pkg/types/typescript/typescript_test.go @@ -0,0 +1,387 @@ +package typescript_test + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/sst/sst/v3/pkg/project/common" + "github.com/sst/sst/v3/pkg/types/typescript" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupProject(t *testing.T, deps map[string]string) string { + t.Helper() + dir := t.TempDir() + pkg := map[string]interface{}{"dependencies": deps} + data, _ := json.Marshal(pkg) + os.WriteFile(filepath.Join(dir, "package.json"), data, 0644) + return dir +} + +func TestGenerate(t *testing.T) { + t.Run("no package.json returns nil", func(t *testing.T) { + dir := t.TempDir() + err := typescript.Generate(dir, common.Links{}, nil) + require.NoError(t, err) + }) + + t.Run("creates sst-env.d.ts with types", func(t *testing.T) { + dir := setupProject(t, map[string]string{}) + + links := common.Links{ + "MyBucket": { + Properties: map[string]interface{}{ + "name": "my-bucket", + "arn": "arn:aws:s3:::my-bucket", + }, + }, + } + + err := typescript.Generate(dir, links, nil) + require.NoError(t, err) + + content, err := os.ReadFile(filepath.Join(dir, "sst-env.d.ts")) + require.NoError(t, err) + + out := string(content) + assert.Contains(t, out, "auto-generated by SST") + assert.Contains(t, out, "\"MyBucket\":") + assert.Contains(t, out, "\"name\": string") + assert.Contains(t, out, "\"arn\": string") + assert.NotContains(t, out, "/// ") + }) + + t.Run("number and boolean types", func(t *testing.T) { + dir := setupProject(t, map[string]string{}) + + links := common.Links{ + "Config": { + Properties: map[string]interface{}{ + "port": 42, + "rate": 3.14, + "enabled": true, + }, + }, + } + + err := typescript.Generate(dir, links, nil) + require.NoError(t, err) + + content, err := os.ReadFile(filepath.Join(dir, "sst-env.d.ts")) + require.NoError(t, err) + + out := string(content) + assert.Contains(t, out, "\"port\": number") + assert.Contains(t, out, "\"rate\": number") + assert.Contains(t, out, "\"enabled\": boolean") + }) + + t.Run("nested properties", func(t *testing.T) { + dir := setupProject(t, map[string]string{}) + + links := common.Links{ + "Db": { + Properties: map[string]interface{}{ + "connection": map[string]interface{}{ + "host": "localhost", + }, + }, + }, + } + + err := typescript.Generate(dir, links, nil) + require.NoError(t, err) + + content, err := os.ReadFile(filepath.Join(dir, "sst-env.d.ts")) + require.NoError(t, err) + + out := string(content) + assert.Contains(t, out, "\"connection\": {") + assert.Contains(t, out, "\"host\": string") + }) + + t.Run("sorted keys", func(t *testing.T) { + dir := setupProject(t, map[string]string{}) + + links := common.Links{ + "Thing": { + Properties: map[string]interface{}{ + "z": "z", + "a": "a", + }, + }, + } + + err := typescript.Generate(dir, links, nil) + require.NoError(t, err) + + content, err := os.ReadFile(filepath.Join(dir, "sst-env.d.ts")) + require.NoError(t, err) + + out := string(content) + aIdx := indexOf(out, "\"a\"") + zIdx := indexOf(out, "\"z\"") + assert.Greater(t, aIdx, -1) + assert.Less(t, aIdx, zIdx) + }) + + t.Run("type key quoted at top level", func(t *testing.T) { + dir := setupProject(t, map[string]string{}) + + links := common.Links{ + "MyResource": { + Type: "aws.s3.Bucket", + Properties: map[string]interface{}{ + "type": "aws.s3.Bucket", + "name": "bucket", + }, + }, + } + + err := typescript.Generate(dir, links, nil) + require.NoError(t, err) + + content, err := os.ReadFile(filepath.Join(dir, "sst-env.d.ts")) + require.NoError(t, err) + + out := string(content) + assert.Contains(t, out, "\"type\": \"aws.s3.Bucket\"") + }) + + t.Run("nested package file is a reference shim", func(t *testing.T) { + dir := t.TempDir() + // root package.json + pkg, _ := json.Marshal(map[string]interface{}{"dependencies": map[string]string{}}) + os.WriteFile(filepath.Join(dir, "package.json"), pkg, 0644) + + sub := filepath.Join(dir, "packages", "web") + os.MkdirAll(sub, 0755) + os.WriteFile(filepath.Join(sub, "package.json"), pkg, 0644) + + links := common.Links{ + "MyBucket": { + Properties: map[string]interface{}{ + "name": "my-bucket", + }, + }, + } + + err := typescript.Generate(dir, links, nil) + require.NoError(t, err) + + assert.FileExists(t, filepath.Join(dir, "sst-env.d.ts")) + assert.FileExists(t, filepath.Join(sub, "sst-env.d.ts")) + + rootContent, err := os.ReadFile(filepath.Join(dir, "sst-env.d.ts")) + require.NoError(t, err) + leafContent, err := os.ReadFile(filepath.Join(sub, "sst-env.d.ts")) + require.NoError(t, err) + + assert.Contains(t, string(rootContent), "\"MyBucket\":") + assert.Contains(t, string(leafContent), "/// ") + assert.NotContains(t, string(leafContent), "\"MyBucket\":") + }) + + t.Run("rerun updates root but not leaf shim", func(t *testing.T) { + dir := t.TempDir() + pkg, _ := json.Marshal(map[string]interface{}{"dependencies": map[string]string{}}) + os.WriteFile(filepath.Join(dir, "package.json"), pkg, 0644) + + sub := filepath.Join(dir, "packages", "web") + os.MkdirAll(sub, 0755) + os.WriteFile(filepath.Join(sub, "package.json"), pkg, 0644) + + links1 := common.Links{ + "Bucket": { + Properties: map[string]interface{}{ + "name": "my-bucket", + }, + }, + } + err := typescript.Generate(dir, links1, nil) + require.NoError(t, err) + + root1, err := os.ReadFile(filepath.Join(dir, "sst-env.d.ts")) + require.NoError(t, err) + leaf1, err := os.ReadFile(filepath.Join(sub, "sst-env.d.ts")) + require.NoError(t, err) + + links2 := common.Links{ + "Bucket": { + Properties: map[string]interface{}{ + "name": "my-bucket", + }, + }, + "Table": { + Properties: map[string]interface{}{ + "name": "my-table", + }, + }, + } + err = typescript.Generate(dir, links2, nil) + require.NoError(t, err) + + root2, err := os.ReadFile(filepath.Join(dir, "sst-env.d.ts")) + require.NoError(t, err) + leaf2, err := os.ReadFile(filepath.Join(sub, "sst-env.d.ts")) + require.NoError(t, err) + + assert.NotEqual(t, string(root1), string(root2)) + assert.Equal(t, string(leaf1), string(leaf2)) + }) + + t.Run("repairs broken leaf shim", func(t *testing.T) { + dir := t.TempDir() + pkg, _ := json.Marshal(map[string]interface{}{"dependencies": map[string]string{}}) + os.WriteFile(filepath.Join(dir, "package.json"), pkg, 0644) + + sub := filepath.Join(dir, "packages", "web") + os.MkdirAll(sub, 0755) + os.WriteFile(filepath.Join(sub, "package.json"), pkg, 0644) + os.WriteFile(filepath.Join(sub, "sst-env.d.ts"), []byte("broken"), 0644) + + err := typescript.Generate(dir, common.Links{}, nil) + require.NoError(t, err) + + leafContent, err := os.ReadFile(filepath.Join(sub, "sst-env.d.ts")) + require.NoError(t, err) + + assert.Contains(t, string(leafContent), "/// ") + assert.NotContains(t, string(leafContent), "broken") + }) + + t.Run("cloudflare bindings fall back to raw shape when workers-types missing", func(t *testing.T) { + dir := setupProject(t, map[string]string{}) + + links := common.Links{ + "MyKV": { + Properties: map[string]interface{}{ + "name": "my-kv", + }, + Include: []common.LinkInclude{ + {Type: "cloudflare.binding", Other: map[string]interface{}{"binding": "kvNamespaceBindings"}}, + }, + }, + } + + err := typescript.Generate(dir, links, nil) + require.NoError(t, err) + + content, err := os.ReadFile(filepath.Join(dir, "sst-env.d.ts")) + require.NoError(t, err) + + out := string(content) + assert.Contains(t, out, "\"MyKV\":") + assert.Contains(t, out, "\"name\": string") + assert.NotContains(t, out, `import("@cloudflare/workers-types")`) + }) + + t.Run("cloudflare leaf gets reference shim like aws", func(t *testing.T) { + dir := t.TempDir() + pkg, _ := json.Marshal(map[string]interface{}{"dependencies": map[string]string{}}) + os.WriteFile(filepath.Join(dir, "package.json"), pkg, 0644) + + sub := filepath.Join(dir, "packages", "web") + os.MkdirAll(sub, 0755) + cfPkg, _ := json.Marshal(map[string]interface{}{"dependencies": map[string]string{"@cloudflare/workers-types": "^4.0.0"}}) + os.WriteFile(filepath.Join(sub, "package.json"), cfPkg, 0644) + + links := common.Links{ + "MyBucket": { + Properties: map[string]interface{}{ + "name": "my-bucket", + }, + }, + "MyKV": { + Properties: map[string]interface{}{ + "name": "my-kv", + }, + Include: []common.LinkInclude{ + {Type: "cloudflare.binding", Other: map[string]interface{}{"binding": "kvNamespaceBindings"}}, + }, + }, + } + + err := typescript.Generate(dir, links, nil) + require.NoError(t, err) + + rootContent, err := os.ReadFile(filepath.Join(dir, "sst-env.d.ts")) + require.NoError(t, err) + leafContent, err := os.ReadFile(filepath.Join(sub, "sst-env.d.ts")) + require.NoError(t, err) + + // Root has everything including Cloudflare augmentations + assert.Contains(t, string(rootContent), "\"MyBucket\":") + assert.Contains(t, string(rootContent), "\"MyKV\":") + assert.Contains(t, string(rootContent), `import("@cloudflare/workers-types").KVNamespace`) + + // Leaf is just a reference shim + assert.Contains(t, string(leafContent), "/// ") + assert.NotContains(t, string(leafContent), "\"MyBucket\":") + assert.NotContains(t, string(leafContent), "\"MyKV\":") + }) + + t.Run("ignores configured directories", func(t *testing.T) { + dir := t.TempDir() + pkg, _ := json.Marshal(map[string]interface{}{"dependencies": map[string]string{}}) + os.WriteFile(filepath.Join(dir, "package.json"), pkg, 0644) + + ignored := filepath.Join(dir, "packages", "docs") + included := filepath.Join(dir, "packages", "web") + os.MkdirAll(ignored, 0755) + os.MkdirAll(included, 0755) + os.WriteFile(filepath.Join(ignored, "package.json"), pkg, 0644) + os.WriteFile(filepath.Join(included, "package.json"), pkg, 0644) + + err := typescript.Generate(dir, common.Links{}, []string{"packages/docs"}) + require.NoError(t, err) + + assert.FileExists(t, filepath.Join(dir, "sst-env.d.ts")) + assert.FileExists(t, filepath.Join(included, "sst-env.d.ts")) + _, err = os.Stat(filepath.Join(ignored, "sst-env.d.ts")) + assert.True(t, os.IsNotExist(err)) + }) + + t.Run("cloudflare durable object binding types", func(t *testing.T) { + dir := setupProject(t, map[string]string{}) + + pkg, _ := json.Marshal(map[string]interface{}{ + "devDependencies": map[string]string{ + "@cloudflare/workers-types": "^4.0.0", + }, + }) + os.WriteFile(filepath.Join(dir, "package.json"), pkg, 0644) + + links := common.Links{ + "Counter": { + Include: []common.LinkInclude{{ + Type: "cloudflare.binding", + Other: map[string]interface{}{ + "binding": "durableObjectNamespaceBindings", + }, + }}, + }, + } + + err := typescript.Generate(dir, links, nil) + require.NoError(t, err) + + content, err := os.ReadFile(filepath.Join(dir, "sst-env.d.ts")) + require.NoError(t, err) + + out := string(content) + assert.Contains(t, out, `"Counter": import("@cloudflare/workers-types").DurableObjectNamespace`) + }) +} + +func indexOf(s, substr string) int { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return i + } + } + return -1 +} diff --git a/platform/functions/bridge/bridge.go b/platform/functions/bridge/bridge.go index 420c4f65d1..51734b8e60 100644 --- a/platform/functions/bridge/bridge.go +++ b/platform/functions/bridge/bridge.go @@ -28,6 +28,7 @@ var SST_REGION = os.Getenv("SST_REGION") var SST_ASSET_BUCKET = os.Getenv("SST_ASSET_BUCKET") var SST_APPSYNC_HTTP = os.Getenv("SST_APPSYNC_HTTP") var SST_APPSYNC_REALTIME = os.Getenv("SST_APPSYNC_REALTIME") +var SST_FUNCTION_STREAMING = os.Getenv("SST_FUNCTION_STREAMING") var ENV_BLACKLIST = map[string]bool{ "SST_DEBUG_ENDPOINT": true, @@ -145,7 +146,22 @@ func run() error { case msg := <-client.Read(): fmt.Println("got message", msg.Type) if msg.Type == bridge.MessageResponse && msg.ID == requestID { - http.Post("http://"+LAMBDA_RUNTIME_API+"/2018-06-01/runtime/invocation/"+requestID+"/response", "application/json", msg.Body) + responseURL := "http://" + LAMBDA_RUNTIME_API + "/2018-06-01/runtime/invocation/" + requestID + "/response" + if SST_FUNCTION_STREAMING == "true" { + req, err := http.NewRequest("POST", responseURL, msg.Body) + if err != nil { + fmt.Println("failed to create streaming request", err) + break loop + } + req.Header.Set("Lambda-Runtime-Function-Response-Mode", "streaming") + req.Header.Set("Transfer-Encoding", "chunked") + req.Header.Set("Content-Type", "application/vnd.awslambda.http-integration-response") + if _, err := http.DefaultClient.Do(req); err != nil { + fmt.Println("failed to send streaming response", err) + } + } else { + http.Post(responseURL, "application/json", msg.Body) + } break loop } if msg.Type == bridge.MessageError && msg.ID == requestID { diff --git a/platform/functions/cf-static-site-router-worker-experimental/index.ts b/platform/functions/cf-static-site-router-worker-experimental/index.ts index e0bfe0944e..61ed8c3e14 100644 --- a/platform/functions/cf-static-site-router-worker-experimental/index.ts +++ b/platform/functions/cf-static-site-router-worker-experimental/index.ts @@ -2,12 +2,17 @@ import path from "node:path"; export interface Env { ASSETS: any; - INDEX_PAGE: string; + INDEX_PAGE?: string; ERROR_PAGE?: string; } export default { async fetch(request: Request, env: Env): Promise { + // If INDEX_PAGE is not set, use native Cloudflare asset handling + if (!env.INDEX_PAGE) { + return env.ASSETS.fetch(request); + } + const url = new URL(request.url); // Requests to exact filename already handled by worker assets, below are handlings @@ -34,15 +39,10 @@ export default { // Handle error page if (env.ERROR_PAGE) { - // TODO: rework this logic once setting - // - htmlHandling: "none", - // - notFoundHandling: "none", url.pathname = env.ERROR_PAGE.endsWith(".html") ? env.ERROR_PAGE.substring(0, env.ERROR_PAGE.length - 5) : env.ERROR_PAGE; - console.log(url.pathname); const res: Response = await env.ASSETS.fetch(new Request(url), request); - console.log(res.status); if (res.status === 200) { const t = await res.text(); return new Response(t, { diff --git a/platform/functions/docker/python.Dockerfile b/platform/functions/docker/python.Dockerfile index e1f1152ec2..4dc049b1b5 100644 --- a/platform/functions/docker/python.Dockerfile +++ b/platform/functions/docker/python.Dockerfile @@ -1,31 +1,33 @@ # Specify the Python version as an ARG ARG PYTHON_VERSION=3.11 -ARG PYTHON_RUNTIME -# Stage 1: Build environment (install build tools and dependencies) -FROM public.ecr.aws/lambda/python:${PYTHON_VERSION} AS build - -# Ensure git and gcc are installed for building dependencies -RUN if [[ "$PYTHON_RUNTIME" == 3.1[2-9]* ]]; then \ - dnf install -y git gcc; \ - else \ - yum install -y git gcc; \ - fi - -# Copy requirements and install dependencies -COPY requirements.txt ${LAMBDA_TASK_ROOT}/requirements.txt - -# Mount the uv image to install the dependencies - uv will not be installed in the final image -RUN --mount=from=ghcr.io/astral-sh/uv,source=/uv,target=/bin/uv \ - uv pip install -r requirements.txt --target ${LAMBDA_TASK_ROOT} --system --compile-bytecode - -# Stage 2: Final runtime image +# Use the Lambda Python base image FROM public.ecr.aws/lambda/python:${PYTHON_VERSION} -# Copy the installed dependencies from the build stage -COPY --from=build ${LAMBDA_TASK_ROOT} ${LAMBDA_TASK_ROOT} - -# Copy the application code into the final image +# Install uv for fast dependency installation +COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv + +# Install git (needed for git-based dependencies like sst SDK) +# and gcc/build tools (needed for C-extension packages like cryptography, numpy) +# Python <=3.11 uses AL2 (yum), Python >=3.12 uses AL2023 (dnf) +RUN if command -v dnf > /dev/null 2>&1; then \ + dnf install -y git gcc python3-devel && dnf clean all; \ + elif command -v yum > /dev/null 2>&1; then \ + yum install -y git gcc python3-devel && yum clean all; \ + fi + +# Copy everything first so workspace packages (referenced as ./pkg in requirements.txt) +# are available during dependency installation. +# +# NOTE: This copies source code before installing deps, which means any code change +# invalidates Docker's layer cache for the pip install step. This is a deliberate +# tradeoff β€” workspace packages must be present for `uv pip install` to resolve +# relative path dependencies (e.g. ./shared, ./core). Users who need better caching +# should provide a custom Dockerfile that copies requirements.txt first. COPY . ${LAMBDA_TASK_ROOT} +# Install dependencies inside the container to ensure native binaries +# are compiled for the correct platform (Linux) +RUN uv pip install -r requirements.txt --target ${LAMBDA_TASK_ROOT} --system + # No need to configure the handler or entrypoint - SST will do that diff --git a/platform/functions/nodejs-bridge/index.mjs b/platform/functions/nodejs-bridge/index.mjs new file mode 100644 index 0000000000..bb3cf40375 --- /dev/null +++ b/platform/functions/nodejs-bridge/index.mjs @@ -0,0 +1,19 @@ +import { promisify } from "node:util"; +import { execFile as _execFile } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const execFile = promisify(_execFile); + +export const handler = async () => { + const binaryPath = join(__dirname, "bootstrap"); + + try { + await execFile(binaryPath); + } catch (err) { + console.error(err); + throw err; + } +}; diff --git a/platform/functions/nodejs-runtime/index.ts b/platform/functions/nodejs-runtime/index.ts index 467e6c4e8a..7702881be0 100644 --- a/platform/functions/nodejs-runtime/index.ts +++ b/platform/functions/nodejs-runtime/index.ts @@ -1,6 +1,8 @@ import path from "node:path"; import fs from "node:fs"; import url from "node:url"; +import http from "node:http"; +import { Writable } from "node:stream"; import type { Context as LambdaContext } from "aws-lambda"; // get first arg @@ -15,16 +17,44 @@ const file = [".js", ".jsx", ".mjs", ".cjs"] return fs.existsSync(file); })!; +const STREAMING_SYMBOL = Symbol.for("aws.lambda.streaming"); + +const awslambda = { + streamifyResponse(handler: any) { + handler[STREAMING_SYMBOL] = true; + return handler; + }, + HttpResponseStream: { + from(responseStream: any, metadata: any) { + responseStream._preludeWritten = true; + if (responseStream._contentType && metadata) { + metadata.headers = metadata.headers || {}; + metadata.headers["Content-Type"] = + metadata.headers["Content-Type"] || responseStream._contentType; + } + const prelude = JSON.stringify(metadata); + responseStream.write(prelude); + // 8 null bytes separator, matching AWS Lambda's protocol + responseStream.write(new Uint8Array(8)); + return responseStream; + }, + }, +}; +(global as any).awslambda = awslambda; + let fn: any; let request: any; let response: any; let context: LambdaContext; async function error(ex: any) { + const errorType = ex instanceof Error ? ex.name : "Error"; + const errorMessage = ex instanceof Error ? ex.message : String(ex); + const trace = ex instanceof Error ? ex.stack?.split("\n") : undefined; const body = JSON.stringify({ - errorType: "Error", - errorMessage: ex.message, - trace: ex.stack?.split("\n"), + errorType, + errorMessage, + trace, }); await fetch( AWS_LAMBDA_RUNTIME_API + @@ -131,29 +161,98 @@ while (true) { (global as any)[Symbol.for("aws.lambda.runtime.requestId")] = context.awsRequestId; - try { - response = await fn(request, context); - } catch (ex: any) { - await error(ex); - continue; - } + const isStreaming = fn[STREAMING_SYMBOL] === true; - while (true) { + if (isStreaming) { try { - await fetch( - AWS_LAMBDA_RUNTIME_API + - `/runtime/invocation/${context.awsRequestId}/response`, + const req = http.request( + `http://${process.env.AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/${context.awsRequestId}/response`, { method: "POST", headers: { - "Content-Type": "application/json", + "Transfer-Encoding": "chunked", + "Content-Type": + "application/vnd.awslambda.http-integration-response", + "Lambda-Runtime-Function-Response-Mode": "streaming", }, - body: JSON.stringify(response), }, ); - break; - } catch (ex) { - await new Promise((resolve) => setTimeout(resolve, 500)); + + const responseStream: any = new Writable({ + write(chunk, encoding, cb) { + // If HttpResponseStream.from() wasn't called, emit a default prelude + // with status 200 on the first write. + if (!responseStream._preludeWritten) { + responseStream._preludeWritten = true; + const metadata = JSON.stringify({ + statusCode: 200, + headers: { + "Content-Type": + responseStream._contentType || "application/octet-stream", + }, + }); + req.write(metadata); + req.write(Buffer.alloc(8)); + } + req.write(chunk, encoding, cb); + }, + final(cb) { + req.end(cb); + }, + }); + responseStream._preludeWritten = false; + responseStream.setContentType = (type: string) => { + responseStream._contentType = type; + }; + + await new Promise((resolve, reject) => { + req.on("error", reject); + // Resolve once the Runtime API acknowledges the response headers. + // The handler continues writing chunks independently via the + // responseStream; the stream is kept alive by the fn() promise chain below. + req.on("response", () => resolve()); + + fn(request, responseStream, context) + .then(() => { + if (!responseStream.writableEnded) { + responseStream.end(); + } + }) + .catch(async (ex: any) => { + if (!responseStream.writableEnded) { + responseStream.end(); + } + reject(ex); + }); + }); + } catch (ex: any) { + await error(ex); + } + } else { + try { + response = await fn(request, context); + } catch (ex: any) { + await error(ex); + continue; + } + + while (true) { + try { + await fetch( + AWS_LAMBDA_RUNTIME_API + + `/runtime/invocation/${context.awsRequestId}/response`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(response), + }, + ); + break; + } catch (ex) { + await new Promise((resolve) => setTimeout(resolve, 500)); + } } } } diff --git a/platform/functions/oac-edge-signer/index.ts b/platform/functions/oac-edge-signer/index.ts index 628aaece38..530d26a5aa 100644 --- a/platform/functions/oac-edge-signer/index.ts +++ b/platform/functions/oac-edge-signer/index.ts @@ -46,13 +46,8 @@ export const handler: CloudFrontRequestHandler = async (event) => { value: hash, }, ]; - - console.log( - `Added SHA256 header for ${request.method} request to ${request.uri}: ${hash}`, - ); } catch (error) { console.error("Error computing SHA256 hash:", error); - // Continue without the header rather than failing the request } return request; diff --git a/platform/functions/python-runtime/index.py b/platform/functions/python-runtime/index.py index 1efa60ef99..27ae1a94ec 100644 --- a/platform/functions/python-runtime/index.py +++ b/platform/functions/python-runtime/index.py @@ -1,30 +1,58 @@ import importlib import json +import logging import os import sys import traceback import time -import requests +from urllib.request import Request, urlopen +from urllib.error import URLError + + +# Ensure Python logging goes to stdout so it appears in the dev TUI +logging.basicConfig( + level=logging.INFO, + stream=sys.stdout, + force=True, +) + + +class LambdaContext: + """Mimics the real AWS Lambda context object with snake_case attributes.""" + + def __init__(self, headers): + self.aws_request_id = headers.get("Lambda-Runtime-Aws-Request-Id", "") + self.invoked_function_arn = headers.get("Lambda-Runtime-Invoked-Function-Arn", "") + self.function_name = os.environ.get("AWS_LAMBDA_FUNCTION_NAME", "") + self.function_version = os.environ.get("AWS_LAMBDA_FUNCTION_VERSION", "") + self.memory_limit_in_mb = int(os.environ.get("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "128")) + self.log_group_name = os.environ.get("AWS_LAMBDA_LOG_GROUP_NAME", "") + self.log_stream_name = os.environ.get("AWS_LAMBDA_LOG_STREAM_NAME", "") + self._deadline_ms = int(headers.get("Lambda-Runtime-Deadline-Ms", "0")) + + def get_remaining_time_in_millis(self): + return max(self._deadline_ms - int(time.time() * 1000), 0) + + +def _post(url, body): + """POST JSON to a URL using stdlib.""" + data = json.dumps(body).encode() + req = Request(url, data=data, headers={"Content-Type": "application/json"}, method="POST") + urlopen(req) -# Error handling function to report errors back to the Lambda runtime API def report_error(ex, context=None): + """Report an error back to the Lambda runtime API.""" error_response = { "errorType": "Error", "errorMessage": str(ex), "trace": traceback.format_exc().split("\n"), } - - endpoint = ( - f"{AWS_LAMBDA_RUNTIME_API}/runtime/init/error" - if context is None - else f"{AWS_LAMBDA_RUNTIME_API}/runtime/invocation/{context['awsRequestId']}/error" - ) - requests.post( - endpoint, - headers={"Content-Type": "application/json"}, - data=json.dumps(error_response), - ) + if context is None: + endpoint = f"{AWS_LAMBDA_RUNTIME_API}/runtime/init/error" + else: + endpoint = f"{AWS_LAMBDA_RUNTIME_API}/runtime/invocation/{context.aws_request_id}/error" + _post(endpoint, error_response) def log(message): @@ -33,64 +61,72 @@ def log(message): sys.stderr.flush() -# Parse the handler from command-line arguments -handler = sys.argv[1] # Expecting the format 'module.function' -AWS_LAMBDA_RUNTIME_API = f"http://{os.environ['AWS_LAMBDA_RUNTIME_API']}/2018-06-01" - -# If the handler is given as a file path, split it to get the directory and module -module_path, function_name = handler.rsplit(".", 1) -module_dir = os.path.dirname(module_path) -module_name = os.path.basename(module_path) +def parse_handler_path(handler_path): + """Parse a handler path like 'module.function' into (module_path, function_name).""" + if "." not in handler_path: + raise ImportError(f"Invalid handler format: {handler_path}. Expected 'module.function'") + module_path, function_name = handler_path.rsplit(".", 1) + python_module_path = module_path.replace("/", ".").replace("\\", ".").lstrip(".") + return python_module_path, function_name -# Add the directory containing the module to the system path -sys.path.insert(0, module_dir) -try: - # Dynamically load the module from the file path - module = importlib.import_module(module_name) - - # Get the function from the module +def get_handler_function(module, function_name): + """Get and validate a callable function from a module.""" + if not hasattr(module, function_name): + available_functions = [name for name in dir(module) if not name.startswith('_')] + raise ImportError( + f"Function '{function_name}' not found in module '{module.__name__}'. " + f"Available functions: {available_functions}" + ) handler_function = getattr(module, function_name) if not callable(handler_function): raise ImportError( - f"{function_name} is not a callable function in {module_name}" + f"'{function_name}' is not a callable function in module '{module.__name__}'" ) + return handler_function + + +def resolve_handler(handler_path, artifact_dir): + """Resolve a handler by importing its module and returning the function.""" + python_module_path, function_name = parse_handler_path(handler_path) + if 'PYTHONPATH' not in os.environ and artifact_dir not in sys.path: + sys.path.insert(0, artifact_dir) + module = importlib.import_module(python_module_path) + return module, get_handler_function(module, function_name) + + +# --- Main --- + +handler = sys.argv[1] +AWS_LAMBDA_RUNTIME_API = f"http://{os.environ['AWS_LAMBDA_RUNTIME_API']}/2018-06-01" +artifact_dir = os.getcwd() + +try: + module, handler_function = resolve_handler(handler, artifact_dir) + log(f"Loaded {handler}") except Exception as ex: + python_module, _ = parse_handler_path(handler) if "." in handler else (handler, "") + log(f"Failed to load handler: {handler}") + log(f" Error: {ex}") + log(f" Module: {python_module}") + log(f" Working dir: {artifact_dir}") + log(f" PYTHONPATH: {os.environ.get('PYTHONPATH', 'not set')}") + log(f" sys.path: {sys.path}") report_error(ex) sys.exit(1) -# Simulating Lambda's event loop +# Lambda event loop while True: try: - # Get the next event to process - response = requests.get(f"{AWS_LAMBDA_RUNTIME_API}/runtime/invocation/next") - response.raise_for_status() - - context = { - "awsRequestId": response.headers.get("Lambda-Runtime-Aws-Request-Id"), - "invokedFunctionArn": response.headers.get( - "Lambda-Runtime-Invoked-Function-Arn" - ), - "getRemainingTimeInMillis": lambda: max( - int(response.headers.get("Lambda-Runtime-Deadline-Ms")) - - int(time.time() * 1000), - 0, - ), - "functionName": os.environ.get("AWS_LAMBDA_FUNCTION_NAME"), - "functionVersion": os.environ.get("AWS_LAMBDA_FUNCTION_VERSION"), - "memoryLimitInMB": os.environ.get("AWS_LAMBDA_FUNCTION_MEMORY_SIZE"), - "logGroupName": os.environ.get("AWS_LAMBDA_LOG_GROUP_NAME"), - "logStreamName": os.environ.get("AWS_LAMBDA_LOG_STREAM_NAME"), - } - - event = response.json() - + resp = urlopen(f"{AWS_LAMBDA_RUNTIME_API}/runtime/invocation/next") + headers = resp.headers + context = LambdaContext(headers) + event = json.loads(resp.read()) except Exception as ex: log(f"Error getting next invocation: {ex}") report_error(ex) continue - # Run the handler function try: result = handler_function(event, context) except Exception as ex: @@ -98,18 +134,28 @@ def log(message): report_error(ex, context) continue - # Send the response back to Lambda - while True: + # Serialize once, retry the POST up to 3 times + try: + response_body = json.dumps(result) + except Exception as ex: + log(f"Error serializing response: {ex}") + report_error(ex, context) + continue + + response_url = f"{AWS_LAMBDA_RUNTIME_API}/runtime/invocation/{context.aws_request_id}/response" + last_error = None + for attempt in range(3): try: - requests.post( - f"{AWS_LAMBDA_RUNTIME_API}/runtime/invocation/{context['awsRequestId']}/response", - headers={"Content-Type": "application/json"}, - data=json.dumps(result), - ) + req = Request(response_url, data=response_body.encode(), headers={"Content-Type": "application/json"}, method="POST") + resp = urlopen(req) + last_error = None break - except Exception as _: - time.sleep(0.5) - continue + except URLError as e: + last_error = e + if attempt < 2: + time.sleep(0.5) + if last_error is not None: + raise RuntimeError(f"Failed to POST invocation response after 3 attempts: {last_error}") from last_error sys.stdout.flush() sys.stderr.flush() diff --git a/platform/package.json b/platform/package.json index 8fd32a6d00..922a33ff54 100644 --- a/platform/package.json +++ b/platform/package.json @@ -24,14 +24,14 @@ "@aws-sdk/client-ssm": "3.478.0", "@aws-sdk/client-sts": "3.478.0", "@aws-sdk/middleware-retry": "3.374.0", - "@pulumi/aws": "6.66.2", - "@pulumi/cloudflare": "6.13.0", + "@pulumi/aws": "7.20.0", + "@pulumi/cloudflare": "6.15.0", "@pulumi/command": "1.0.1", "@pulumi/docker-build": "0.0.14", - "@pulumi/pulumi": "3.210.0", + "@pulumi/pulumi": "3.215.0", "@pulumi/random": "4.16.6", "@pulumi/tls": "5.0.1", - "@pulumiverse/vercel": "1.11.0", + "@pulumiverse/vercel": "4.6.0", "@smithy/smithy-client": "2.1.18", "@tsconfig/node18": "18.2.2", "@types/node": "^22.10.0", @@ -45,7 +45,8 @@ "ts-node": "^10.9.2", "typescript": "5.7.2", "undici": "^6.19.5", - "unenv-sst": "npm:unenv-nightly@2.0.0-20241024-111401-d4156ac" + "unenv": "2.0.0-rc.24", + "@cloudflare/unenv-preset": "2.16.0" }, "devDependencies": { "@aws-sdk/client-iot": "3.501.0", diff --git a/platform/scripts/build b/platform/scripts/build index b4545adeb7..dfa8ff5321 100755 --- a/platform/scripts/build +++ b/platform/scripts/build @@ -9,9 +9,13 @@ bun build ./functions/cf-static-site-router-worker/index.ts --target=node --outd bun build ./functions/cf-ssr-site-router-worker/index.ts --target=node --outdir ./dist/cf-ssr-site-router-worker/ bun build ./functions/nodejs-runtime/index.ts --target=node --outdir ./dist/nodejs-runtime/ bun build ./functions/nodejs-runtime/loop.ts --target=node --outdir ./dist/nodejs-runtime/ -GOARCH=amd64 GOOS=linux go build -trimpath -mod=readonly -ldflags="-buildid=" -o ./dist/bridge/bootstrap ./functions/bridge +GOARCH=amd64 GOOS=linux go build -trimpath -buildvcs=false -mod=readonly -ldflags="-buildid=" -o ./dist/bridge/bootstrap ./functions/bridge node ./scripts/build.mjs +mkdir -p ./dist/nodejs-bridge +cp ./functions/nodejs-bridge/index.mjs ./dist/nodejs-bridge/index.mjs +cp ./dist/bridge/bootstrap ./dist/nodejs-bridge/bootstrap + mkdir -p ./dist/python-runtime/ cp ./functions/python-runtime/index.py ./dist/python-runtime/index.py @@ -19,9 +23,25 @@ mkdir -p ./dist/dockerfiles/ cp ./functions/docker/python.Dockerfile ./dist/dockerfiles/python.Dockerfile cd ./support/bridge-task +mkdir -p build/amd64 build/arm64 +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -mod=readonly -ldflags="-buildid=" -o build/amd64/main . +CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -mod=readonly -ldflags="-buildid=" -o build/arm64/main . + timestamp=$(date +%Y%m%d%H%M%S) -docker buildx create --name multi --driver docker-container --use -docker buildx build --platform linux/amd64,linux/arm64 -t ghcr.io/anomalyco/sst/bridge-task:$timestamp -t ghcr.io/anomalyco/sst/bridge-task:latest $([ "$DOCKER_PUSH" = "true" ] && echo "--push") . +docker buildx inspect >/dev/null 2>&1 || docker buildx create --name multi --driver docker-container --use + +CACHE_ARGS="" +if [ -n "$GITHUB_ACTIONS" ]; then + CACHE_ARGS="--cache-from type=gha,scope=bridge-task --cache-to type=gha,mode=max,scope=bridge-task" +fi +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -f Dockerfile \ + $CACHE_ARGS \ + -t ghcr.io/anomalyco/sst/bridge-task:$timestamp \ + -t ghcr.io/anomalyco/sst/bridge-task:latest \ + $([ "$DOCKER_PUSH" = "true" ] && echo "--push") \ + build cd $ORIGINAL_DIR diff --git a/platform/src/ast/add.mjs b/platform/src/ast/add.mjs index 427dd75193..db144c7f2e 100644 --- a/platform/src/ast/add.mjs +++ b/platform/src/ast/add.mjs @@ -5,8 +5,9 @@ import ts from "typescript"; import prettier from "prettier"; const config = process.argv[2]; -const pkg = process.argv[3]; +const provider = process.argv[3]; const version = process.argv[4]; +const pkgName = process.argv[5] || ""; const code = fs.readFileSync(config); @@ -31,14 +32,31 @@ const appFunctionDeclaration = (property) => property.name.getText() === "app", ); -const returnStatement = appFunctionDeclaration.body?.statements.find( - (statement) => - ts.isReturnStatement(statement) && - ts.isObjectLiteralExpression(statement.expression), -); +const appBody = ts.isMethodDeclaration(appFunctionDeclaration) + ? appFunctionDeclaration.body + : appFunctionDeclaration.initializer?.body; + +// Concise arrow: app: (input) => ({...}) +// Block body: app(input) { return {...}; } +const returnedObject = ts.isParenthesizedExpression(appBody) + ? appBody.expression + : ts.isObjectLiteralExpression(appBody) + ? appBody + : appBody?.statements?.find( + (s) => + ts.isReturnStatement(s) && + ts.isObjectLiteralExpression(s.expression), + )?.expression; + +if (!returnedObject || !ts.isObjectLiteralExpression(returnedObject)) { + console.error( + 'Could not find the returned object in the "app" function. Make sure it returns an object literal.', + ); + process.exit(1); +} // Find the "providers" property inside the "app" function -let providersProperty = returnStatement.expression?.properties.find( +let providersProperty = returnedObject.properties.find( (property) => ts.isPropertyAssignment(property) && property.name.getText() === "providers", @@ -49,30 +67,115 @@ if (!providersProperty) { "providers", ts.factory.createObjectLiteralExpression([]), ); - returnStatement.expression.properties.push(providersProperty); + returnedObject.properties.push(providersProperty); } if (!ts.isObjectLiteralExpression(providersProperty.initializer)) { console.error( - 'The "providers" property must be a plain object, not a dynamic expression like a ternary or variable.' + 'The "providers" property must be a plain object, not a dynamic expression like a ternary or variable.', ); process.exit(1); } -if ( - providersProperty.initializer.properties.find( - (property) => property.name.getText().replaceAll('"', "") === pkg, - ) -) { - process.exit(0); +function getPropertyName(property) { + return property.name.getText().replace(/^['"]|['"]$/g, ""); +} + +function createStringProperty(name, value) { + return ts.factory.createPropertyAssignment( + name, + ts.factory.createStringLiteral(value), + ); +} + +function createProviderValue(versionValue) { + if (!pkgName) { + return ts.factory.createStringLiteral(versionValue); + } + + return ts.factory.createObjectLiteralExpression( + [ + createStringProperty("package", pkgName), + createStringProperty("version", versionValue), + ], + false, + ); +} + +function upsertObjectProperty(properties, name, initializer, overwrite) { + const index = properties.findIndex( + (property) => + ts.isPropertyAssignment(property) && getPropertyName(property) === name, + ); + + if (index === -1) { + properties.push(ts.factory.createPropertyAssignment(name, initializer)); + return; + } + + if (!overwrite) { + return; + } + + properties.splice( + index, + 1, + ts.factory.createPropertyAssignment( + properties[index].name, + initializer, + ), + ); +} + +function updateProviderValue(initializer) { + if (!pkgName) { + return ts.factory.createStringLiteral(version); + } + + if (ts.isStringLiteralLike(initializer)) { + return createProviderValue(initializer.text); + } + + if (!ts.isObjectLiteralExpression(initializer)) { + return createProviderValue(version); + } + + const properties = [...initializer.properties]; + upsertObjectProperty( + properties, + "package", + ts.factory.createStringLiteral(pkgName), + true, + ); + upsertObjectProperty( + properties, + "version", + ts.factory.createStringLiteral(version), + false, + ); + return ts.factory.createObjectLiteralExpression(properties, false); } -// Create a new property node for "foo: {}" + +const existingIndex = providersProperty.initializer.properties.findIndex( + (property) => + ts.isPropertyAssignment(property) && + getPropertyName(property) === provider, +); + const newProperty = ts.factory.createPropertyAssignment( - ts.factory.createStringLiteral(pkg), - ts.factory.createStringLiteral(version), + ts.factory.createStringLiteral(provider), + existingIndex === -1 + ? createProviderValue(version) + : updateProviderValue( + providersProperty.initializer.properties[existingIndex].initializer, + ), ); -providersProperty.initializer.properties.push(newProperty); +if (existingIndex === -1) { + providersProperty.initializer.properties.push(newProperty); +} else { + providersProperty.initializer.properties.splice(existingIndex, 1, newProperty); +} const printer = ts.createPrinter(); const modifiedCode = printer.printNode( diff --git a/platform/src/auto/run.ts b/platform/src/auto/run.ts index 42b58a39f2..3f6caf2c12 100644 --- a/platform/src/auto/run.ts +++ b/platform/src/auto/run.ts @@ -7,6 +7,7 @@ import { } from "@pulumi/pulumi"; import { VisibleError } from "../components/error"; +import { Function } from "../components/aws/function"; export async function run(program: automation.PulumiFn) { process.chdir($cli.paths.root); @@ -15,6 +16,7 @@ export async function run(program: automation.PulumiFn) { addTransformationToAddTags(); addTransformationToCheckBucketsHaveMultiplePolicies(); + Function.reset(); Link.reset(); const outputs = (await program()) || {}; outputs._protect = $app.protect; @@ -28,11 +30,21 @@ function addTransformationToRetainResourcesOnDelete() { ($app.removal === "retain" && [ "aws:dynamodb/table:Table", + "aws:ec2/defaultSecurityGroup:DefaultSecurityGroup", + "aws:ec2/subnet:Subnet", + "aws:ec2/vpc:Vpc", + "aws:rds/cluster:Cluster", + "aws:rds/clusterParameterGroup:ClusterParameterGroup", "aws:rds/instance:Instance", + "aws:rds/parameterGroup:ParameterGroup", + "aws:rds/subnetGroup:SubnetGroup", + "aws:dsql/cluster:Cluster", "aws:s3/bucket:Bucket", "aws:s3/bucketV2:BucketV2", "planetscale:index/database:Database", "planetscale:index/branch:Branch", + "planetscale:index/vitessBranch:VitessBranch", + "planetscale:index/postgresBranch:PostgresBranch", ].includes(args.type)) ) { args.opts.retainOnDelete = args.opts.retainOnDelete ?? true; diff --git a/platform/src/components/aws/alb.ts b/platform/src/components/aws/alb.ts new file mode 100644 index 0000000000..72f0ca8f8e --- /dev/null +++ b/platform/src/components/aws/alb.ts @@ -0,0 +1,568 @@ +import { + ComponentResourceOptions, + Output, + output, +} from "@pulumi/pulumi"; +import { Component, Transform, transform } from "../component.js"; +import { dns as awsDns } from "./dns.js"; +import { VisibleError } from "../error.js"; +import { DnsValidatedCertificate } from "./dns-validated-certificate.js"; +import { Link } from "../link.js"; +import { ec2, lb } from "@pulumi/aws"; +import { Vpc } from "./vpc.js"; +import { Input } from "../input.js"; +import { Dns } from "../dns.js"; +import { listenerKey } from "./helpers/load-balancer.js"; + +export interface AlbListenerArgs { + /** + * The port to listen on. + * + * @example + * ```js + * { + * port: 443 + * } + * ``` + */ + port: number; + /** + * The protocol to listen on. Only `http` and `https` are supported (ALB-only). + * + * @example + * ```js + * { + * protocol: "https" + * } + * ``` + */ + protocol: "http" | "https"; +} + +export interface AlbArgs { + /** + * The VPC to deploy the ALB in. Can be an SST `Vpc` component or a custom VPC configuration. + * + * @example + * + * Using an SST Vpc component: + * ```js + * { + * vpc: myVpc + * } + * ``` + * + * Using a custom VPC: + * ```js + * { + * vpc: { + * id: "vpc-0123456789abcdef0", + * publicSubnets: ["subnet-abc", "subnet-def"], + * privateSubnets: ["subnet-ghi", "subnet-jkl"] + * } + * } + * ``` + */ + vpc: Vpc | Input<{ + /** + * The VPC ID. + */ + id: Input; + /** + * The public subnet IDs. + */ + publicSubnets: Input[]>; + /** + * The private subnet IDs. + */ + privateSubnets: Input[]>; + }>; + /** + * Configure if the load balancer should be public (internet-facing) or private (internal). + * + * When set to `false`, the load balancer endpoint will only be accessible within the VPC. + * + * @default `true` + */ + public?: Input; + /** + * Set a custom domain for the load balancer. + * + * Automatically manages domains hosted on AWS Route 53, Cloudflare, and Vercel. For other + * providers, you'll need to pass in a `cert` that validates domain ownership and add the + * DNS records. + * + * @example + * + * ```js + * { + * domain: "example.com" + * } + * ``` + * + * For domains on Cloudflare: + * ```js + * { + * domain: { + * name: "example.com", + * dns: sst.cloudflare.dns() + * } + * } + * ``` + */ + domain?: + | string + | { + /** + * The custom domain name. + */ + name: string; + /** + * Alias domains that should also point to this load balancer. + */ + aliases?: string[]; + /** + * The ARN of an ACM certificate that proves ownership of the domain. + * By default, a certificate is created and validated automatically. + */ + cert?: Input; + /** + * The DNS provider to use. Defaults to AWS Route 53. + * Set to `false` for manual DNS setup. + * + * @default `sst.aws.dns` + */ + dns?: false | (Dns & {}); + }; + /** + * The listeners for the load balancer. Each entry creates a listener on the specified + * port and protocol. + * + * @example + * ```js + * { + * listeners: [ + * { port: 80, protocol: "http" }, + * { port: 443, protocol: "https" } + * ] + * } + * ``` + */ + listeners: AlbListenerArgs[]; + /** + * [Transform](/docs/components#transform) how this component creates its underlying resources. + */ + transform?: { + /** + * Transform the AWS Load Balancer resource. + */ + loadBalancer?: Transform; + /** + * Transform the AWS Security Group resource for the Load Balancer. + */ + securityGroup?: Transform; + /** + * Transform the AWS Load Balancer listener resource. + */ + listener?: Transform; + }; +} + +interface AlbRef { + ref: true; + loadBalancerArn: Input; +} + +/** + * The `Alb` component lets you create a standalone Application Load Balancer that can be + * shared across multiple services. + * + * @example + * + * #### Create a shared ALB + * + * ```ts title="sst.config.ts" + * const vpc = new sst.aws.Vpc("MyVpc"); + * + * const alb = new sst.aws.Alb("SharedAlb", { + * vpc, + * domain: "app.example.com", + * listeners: [ + * { port: 80, protocol: "http" }, + * { port: 443, protocol: "https" }, + * ], + * }); + * ``` + * + * #### Attach services to the ALB + * + * ```ts title="sst.config.ts" + * new sst.aws.Service("Api", { + * cluster, + * image: "api:latest", + * loadBalancer: { + * instance: alb, + * rules: [ + * { listen: "443/https", forward: "8080/http", conditions: { path: "/api/*" }, priority: 100 }, + * ], + * }, + * }); + * ``` + * + * #### Reference an existing ALB + * + * ```ts title="sst.config.ts" + * const alb = sst.aws.Alb.get("SharedAlb", "arn:aws:elasticloadbalancing:..."); + * ``` + */ +export class Alb extends Component implements Link.Linkable { + private loadBalancer: lb.LoadBalancer; + private securityGroup: ec2.SecurityGroup; + private listeners: Record = {}; + private certificateArn?: Output; + private vpcId: Output; + private _url: Output; + private constructorName: string; + + constructor( + name: string, + args: AlbArgs, + opts: ComponentResourceOptions = {}, + ) { + super(__pulumiType, name, args, opts); + + this.constructorName = name; + + if (args && "ref" in args) { + const ref = args as unknown as AlbRef; + const loadBalancer = lb.LoadBalancer.get( + `${name}LoadBalancer`, + output(ref.loadBalancerArn), + {}, + { parent: this }, + ); + const securityGroup = ec2.SecurityGroup.get( + `${name}SecurityGroup`, + loadBalancer.securityGroups.apply((sgs) => { + if (!sgs?.length) { + throw new VisibleError( + `No security groups found on the referenced ALB "${name}".`, + ); + } + return sgs[0]; + }), + {}, + { parent: this }, + ); + this.loadBalancer = loadBalancer; + this.securityGroup = securityGroup; + this.vpcId = loadBalancer.vpcId; + this._url = loadBalancer.dnsName.apply( + (dnsName) => `http://${dnsName}`, + ); + this.registerOutputs({ _hint: this._url }); + return; + } + + const self = this; + const pub = output(args.public ?? true); + const { vpcId, subnets } = normalizeVpc(); + const domain = normalizeDomain(); + const securityGroup = createSecurityGroup(); + const certificateArn = createSsl(); + const loadBalancer = createLoadBalancer(); + createListeners(); + createDnsRecords(); + + this.loadBalancer = loadBalancer; + this.securityGroup = securityGroup; + this.certificateArn = certificateArn; + this.vpcId = vpcId; + this._url = domain + ? output(`https://${domain.name}/`) + : loadBalancer.dnsName.apply((dnsName) => `http://${dnsName}`); + + this.registerOutputs({ _hint: this._url }); + + function normalizeVpc() { + if (args.vpc instanceof Vpc) { + const vpc = args.vpc; + return { + vpcId: vpc.id, + subnets: pub.apply((isPublic) => + isPublic ? vpc.publicSubnets : vpc.privateSubnets, + ), + }; + } + + return output(args.vpc).apply((vpc) => ({ + vpcId: output(vpc.id), + subnets: pub.apply((isPublic) => + isPublic + ? output(vpc.publicSubnets) + : output(vpc.privateSubnets), + ), + })); + } + + function normalizeDomain() { + if (!args.domain) return undefined; + const raw = + typeof args.domain === "string" ? { name: args.domain } : args.domain; + return { + name: raw.name, + aliases: raw.aliases ?? [], + dns: raw.dns === false ? undefined : raw.dns ?? awsDns(), + cert: raw.cert, + }; + } + + function createSecurityGroup() { + return new ec2.SecurityGroup( + ...transform( + args.transform?.securityGroup, + `${name}SecurityGroup`, + { + description: "Managed by SST", + vpcId, + egress: [ + { + fromPort: 0, + toPort: 0, + protocol: "-1", + cidrBlocks: ["0.0.0.0/0"], + }, + ], + ingress: [ + { + fromPort: 0, + toPort: 0, + protocol: "-1", + cidrBlocks: ["0.0.0.0/0"], + }, + ], + }, + { parent: self }, + ), + ); + } + + function createSsl(): Output { + if (!domain) return output(undefined); + if (domain.cert) return output(domain.cert); + + return new DnsValidatedCertificate( + `${name}Ssl`, + { + domainName: domain.name, + alternativeNames: domain.aliases, + dns: domain.dns!, + }, + { parent: self }, + ).arn; + } + + function createLoadBalancer() { + return new lb.LoadBalancer( + ...transform( + args.transform?.loadBalancer, + `${name}LoadBalancer`, + { + internal: pub.apply((v) => !v), + loadBalancerType: "application", + subnets, + securityGroups: [securityGroup.id], + enableCrossZoneLoadBalancing: true, + + }, + { parent: self }, + ), + ); + } + + function createListeners() { + for (const l of args.listeners) { + const protocol = l.protocol.toUpperCase(); + const port = l.port; + const key = listenerKey(protocol, port); + + const listener = new lb.Listener( + ...transform( + args.transform?.listener, + `${name}Listener${protocol}${port}`, + { + loadBalancerArn: loadBalancer.arn, + port, + protocol, + certificateArn: protocol === "HTTPS" + ? certificateArn.apply((arn) => arn!) as Output + : undefined, + defaultActions: [ + { + type: "fixed-response", + fixedResponse: { + statusCode: "403", + contentType: "text/plain", + messageBody: "Forbidden", + }, + }, + ], + }, + { parent: self }, + ), + ); + + self.listeners[key] = listener; + } + } + + function createDnsRecords() { + if (!domain?.dns) return; + + for (const recordName of [domain.name, ...domain.aliases]) { + const namePrefix = + recordName === domain.name ? name : `${name}${recordName}`; + domain.dns.createAlias( + namePrefix, + { + name: recordName, + aliasName: loadBalancer.dnsName, + aliasZone: loadBalancer.zoneId, + }, + { parent: self }, + ); + } + } + } + + /** + * The URL of the load balancer. If a custom domain is set, this will be the custom + * domain URL (eg. `https://app.example.com/`). Otherwise, it's the ALB's DNS name. + */ + public get url(): Output { + return this._url; + } + + /** + * The ARN of the load balancer. + */ + public get arn(): Output { + return this.loadBalancer.arn; + } + + /** + * The DNS name of the load balancer. + */ + public get dnsName(): Output { + return this.loadBalancer.dnsName; + } + + /** + * The zone ID of the load balancer. + */ + public get zoneId(): Output { + return this.loadBalancer.zoneId; + } + + /** + * The security group ID of the load balancer. + */ + public get securityGroupId(): Output { + return this.securityGroup.id; + } + + /** + * The underlying resources this component creates. + */ + public get nodes() { + return { + /** + * The AWS Load Balancer resource. + */ + loadBalancer: this.loadBalancer, + /** + * The AWS Security Group resource. + */ + securityGroup: this.securityGroup, + /** + * The AWS Listener resources, keyed by "PROTOCOLPORT" (e.g. "HTTPS443"). + */ + listeners: this.listeners, + }; + } + + /** @internal */ + public get _certArn(): Output { + return this.certificateArn ?? output(undefined); + } + + /** @internal */ + public get _vpc(): Output { + return this.vpcId; + } + + /** + * Get a specific listener by protocol and port. + * + * @example + * ```ts + * const listener = alb.getListener("https", 443); + * ``` + */ + public getListener( + protocol: string, + port: number, + ): lb.Listener { + const key = listenerKey(protocol, port); + if (this.listeners[key]) return this.listeners[key]; + + const discovered = lb.Listener.get( + `${this.constructorName}Listener${protocol.toUpperCase()}${port}`, + lb.getListenerOutput({ + loadBalancerArn: this.loadBalancer.arn, + port, + }).arn, + {}, + { parent: this }, + ); + this.listeners[key] = discovered; + return discovered; + } + + /** @internal */ + public getSSTLink() { + return { + properties: { + url: this._url, + }, + }; + } + + /** + * Reference an existing ALB by its ARN. + * + * @param name The name of the component. + * @param loadBalancerArn The ARN of the existing ALB. + * @param opts Component resource options. + * + * @example + * ```ts + * const alb = sst.aws.Alb.get("SharedAlb", "arn:aws:elasticloadbalancing:..."); + * ``` + */ + public static get( + name: string, + loadBalancerArn: Input, + opts?: ComponentResourceOptions, + ): Alb { + return new Alb( + name, + { ref: true, loadBalancerArn } as unknown as AlbArgs, + opts, + ); + } +} + +const __pulumiType = "sst:aws:Alb"; +// @ts-expect-error +Alb.__pulumiType = __pulumiType; diff --git a/platform/src/components/aws/analog.ts b/platform/src/components/aws/analog.ts index c285e928c9..b3585edc1a 100644 --- a/platform/src/components/aws/analog.ts +++ b/platform/src/components/aws/analog.ts @@ -308,21 +308,6 @@ export interface AnalogArgs extends SsrSiteArgs { * ``` */ buildCommand?: SsrSiteArgs["buildCommand"]; - /** - * Configure how the Analog app assets are uploaded to S3. - * - * By default, this is set to the following. Read more about these options below. - * ```js - * { - * assets: { - * textEncoding: "utf-8", - * versionedFilesCacheHeader: "public,max-age=31536000,immutable", - * nonVersionedFilesCacheHeader: "public,max-age=0,s-maxage=86400,stale-while-revalidate=8640" - * } - * } - * ``` - */ - assets?: SsrSiteArgs["assets"]; /** * Configure the Analog app to use an existing CloudFront cache policy. * diff --git a/platform/src/components/aws/apigateway-websocket-route.ts b/platform/src/components/aws/apigateway-websocket-route.ts index 3190705c0e..99be0e4edd 100644 --- a/platform/src/components/aws/apigateway-websocket-route.ts +++ b/platform/src/components/aws/apigateway-websocket-route.ts @@ -7,7 +7,7 @@ import { output, } from "@pulumi/pulumi"; import { Component, Transform, transform } from "../component"; -import { FunctionArgs, FunctionArn } from "./function"; +import { FunctionArgs, FunctionArn } from "./function.js"; import { ApiGatewayWebSocketRouteArgs } from "./apigateway-websocket"; import { apigatewayv2, lambda } from "@pulumi/aws"; import { FunctionBuilder, functionBuilder } from "./helpers/function-builder"; @@ -95,6 +95,7 @@ export class ApiGatewayWebSocketRoute extends Component { { action: "lambda:InvokeFunction", function: fn.arn, + qualifier: fn.qualifier.apply((qualifier) => qualifier!), principal: "apigateway.amazonaws.com", sourceArn: interpolate`${api.executionArn}/*`, }, @@ -110,7 +111,7 @@ export class ApiGatewayWebSocketRoute extends Component { { apiId: api.id, integrationType: "AWS_PROXY", - integrationUri: fn.arn.apply((arn) => { + integrationUri: fn.targetArn.apply((arn) => { const [, partition, , region] = arn.split(":"); return `arn:${partition}:apigateway:${region}:lambda:path/2015-03-31/functions/${arn}/invocations`; }), diff --git a/platform/src/components/aws/apigateway-websocket.ts b/platform/src/components/aws/apigateway-websocket.ts index 2278faadb1..fdd6dc347e 100644 --- a/platform/src/components/aws/apigateway-websocket.ts +++ b/platform/src/components/aws/apigateway-websocket.ts @@ -14,7 +14,7 @@ import { } from "../component"; import { Link } from "../link"; import type { Input } from "../input"; -import { FunctionArgs, FunctionArn } from "./function"; +import { FunctionArgs, FunctionArn } from "./function.js"; import { hashStringToPrettyString, physicalName, logicalName } from "../naming"; import { DnsValidatedCertificate } from "./dns-validated-certificate"; import { RETENTION } from "./logging"; @@ -342,6 +342,22 @@ export interface ApiGatewayWebSocketRouteArgs { lambda?: Input; } >; + /** + * The name of the route. + * + * By default, SST generates a unique suffix from the route path. Setting `name` gives + * you a stable, human-readable name like `MyApiRouteSendMessageHandler`. + * + * Must be unique across all routes. + * + * @example + * ```js + * { + * name: "SendMessage" + * } + * ``` + */ + name?: string; /** * [Transform](/docs/components#transform) how this component creates its underlying * resources. @@ -701,6 +717,12 @@ export class ApiGatewayWebSocket extends Component implements Link.Linkable { * will look for the specific route defined by the user. If no route matches, the `$default` * route will be invoked. * + * :::caution + * [API Gateway has strict rate limits](https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html) for creating and updating resources. Creating one Lambda function for every route can significantly slow down your deployments. + * + * Use a single Lambda and handle routing in code if you don't need specific API Gateway features. + * ::: + * * @param route The path for the route. * @param handler The function that'll be invoked. * @param args Configure the route. @@ -750,9 +772,11 @@ export class ApiGatewayWebSocket extends Component implements Link.Linkable { ) { const prefix = this.constructorName; const suffix = logicalName( - ["$connect", "$disconnect", "$default"].includes(route) - ? route - : hashStringToPrettyString(`${outputId}${route}`, 6), + args.name + ? args.name + : ["$connect", "$disconnect", "$default"].includes(route) + ? route + : hashStringToPrettyString(`${outputId}${route}`, 6), ); const transformed = transform( @@ -819,7 +843,7 @@ export class ApiGatewayWebSocket extends Component implements Link.Linkable { * const authorizer = api.addAuthorizer({ * name: "myCognitoAuthorizer", * jwt: { - * issuer: $interpolate`https://cognito-idp.${aws.getRegionOutput().name}.amazonaws.com/${pool.id}`, + * issuer: $interpolate`https://cognito-idp.${aws.getRegionOutput().region}.amazonaws.com/${pool.id}`, * audiences: [poolClient.id] * } * }); diff --git a/platform/src/components/aws/apigatewayv1-authorizer.ts b/platform/src/components/aws/apigatewayv1-authorizer.ts index 785945bd9b..0c4b6fc614 100644 --- a/platform/src/components/aws/apigatewayv1-authorizer.ts +++ b/platform/src/components/aws/apigatewayv1-authorizer.ts @@ -114,6 +114,7 @@ export class ApiGatewayV1Authorizer extends Component { { action: "lambda:InvokeFunction", function: fn.arn, + qualifier: fn.qualifier.apply((qualifier) => qualifier!), principal: "apigateway.amazonaws.com", sourceArn: interpolate`${api.executionArn}/authorizers/${authorizer.id}`, }, @@ -131,7 +132,7 @@ export class ApiGatewayV1Authorizer extends Component { type, name: args.name, providerArns: args.userPools, - authorizerUri: fn?.invokeArn, + authorizerUri: fn?.targetInvokeArn, authorizerResultTtlInSeconds: args.ttl, identitySource: args.identitySource, }, diff --git a/platform/src/components/aws/apigatewayv1-lambda-route.ts b/platform/src/components/aws/apigatewayv1-lambda-route.ts index 3e69183644..e713d89904 100644 --- a/platform/src/components/aws/apigatewayv1-lambda-route.ts +++ b/platform/src/components/aws/apigatewayv1-lambda-route.ts @@ -1,4 +1,5 @@ import { + all, ComponentResourceOptions, Input, Output, @@ -6,7 +7,7 @@ import { output, } from "@pulumi/pulumi"; import { Component, Transform, transform } from "../component"; -import { FunctionArgs } from "./function"; +import { FunctionArgs } from "./function.js"; import { apigateway, lambda } from "@pulumi/aws"; import { ApiGatewayV1BaseRouteArgs, @@ -46,6 +47,14 @@ export class ApiGatewayV1LambdaRoute extends Component { const self = this; const api = output(args.api); + const fnStreaming = output(args.handler).apply((handler) => + handler && typeof handler === "object" && "streaming" in handler + ? handler.streaming + : undefined, + ); + const streaming = all([output(args.streaming), fnStreaming]).apply( + ([routeStreaming, fnStreaming]) => routeStreaming ?? fnStreaming ?? false, + ); const method = createMethod(name, args, self); const fn = createFunction(); @@ -65,6 +74,7 @@ export class ApiGatewayV1LambdaRoute extends Component { args.handler, { description: interpolate`${api.name} route ${method} ${path}`, + streaming, }, args.handlerTransform, { parent: self }, @@ -77,6 +87,7 @@ export class ApiGatewayV1LambdaRoute extends Component { { action: "lambda:InvokeFunction", function: fn.arn, + qualifier: fn.qualifier.apply((qualifier) => qualifier!), principal: "apigateway.amazonaws.com", sourceArn: interpolate`${api.executionArn}/*`, }, @@ -95,7 +106,12 @@ export class ApiGatewayV1LambdaRoute extends Component { httpMethod: method.httpMethod, integrationHttpMethod: "POST", type: "AWS_PROXY", - uri: fn.invokeArn, + uri: all([streaming, fn]).apply(([s, fn]) => + s ? fn.targetResponseStreamingInvokeArn : fn.targetInvokeArn, + ), + responseTransferMode: streaming.apply((s) => + s ? "STREAM" : "BUFFERED", + ), }, { parent: self, dependsOn: [permission] }, ), diff --git a/platform/src/components/aws/apigatewayv1.ts b/platform/src/components/aws/apigatewayv1.ts index 2f0c65b746..be5b890539 100644 --- a/platform/src/components/aws/apigatewayv1.ts +++ b/platform/src/components/aws/apigatewayv1.ts @@ -14,7 +14,7 @@ import { } from "../component"; import { Link } from "../link"; import type { Input } from "../input"; -import { FunctionArgs, FunctionArn } from "./function"; +import { FunctionArgs, FunctionArn } from "./function.js"; import { hashStringToPrettyString, physicalName, logicalName } from "../naming"; import { VisibleError } from "../error"; import { RETENTION } from "./logging"; @@ -619,6 +619,26 @@ export interface ApiGatewayV1RouteArgs { * ``` */ apiKey?: Input; + /** + * @deprecated Set `streaming: true` on the function definition passed to `api.route()` instead. + */ + streaming?: Input; + /** + * The name of the route. + * + * By default, SST generates a unique suffix from the route path. Setting `name` gives + * you a stable, human-readable name like `MyApiRouteGetUserHandler`. + * + * Must be unique across all routes. + * + * @example + * ```js + * { + * name: "GetUser" + * } + * ``` + */ + name?: string; /** * [Transform](/docs/components#transform) how this component creates its underlying * resources. @@ -782,7 +802,7 @@ export class ApiGatewayV1 extends Component implements Link.Linkable { this.endpointType = endpoint.types; function normalizeRegion() { - return getRegionOutput(undefined, { parent }).name; + return getRegionOutput(undefined, { parent }).region; } function normalizeEndpoint() { @@ -866,10 +886,16 @@ export class ApiGatewayV1 extends Component implements Link.Linkable { }; } - /** - * Add a route to the API Gateway REST API. The route is a combination of an HTTP method and a path, `{METHOD} /{path}`. - * - * A method could be one of `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, or `ANY`. Here `ANY` matches any HTTP method. + /** + * Add a route to the API Gateway REST API. The route is a combination of an HTTP method and a path, `{METHOD} /{path}`. + * + * :::caution + * [API Gateway has strict rate limits](https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html) for creating and updating resources. Creating one Lambda function for every endpoint can significantly slow down your deployments. + * + * Use a single Lambda and handle routing in code if you don't need specific API Gateway features. + * ::: + * + * A method could be one of `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, or `ANY`. Here `ANY` matches any HTTP method. * * The path can be a combination of * - Literal segments, `/notes`, `/notes/new`, etc. @@ -964,7 +990,7 @@ export class ApiGatewayV1 extends Component implements Link.Linkable { const transformed = transform( this.constructorArgs.transform?.route?.args, - this.buildRouteId(method, path), + this.buildRouteId(method, path, args.name), args, { provider: this.constructorOpts.provider }, ); @@ -1029,7 +1055,7 @@ export class ApiGatewayV1 extends Component implements Link.Linkable { const transformed = transform( this.constructorArgs.transform?.route?.args, - this.buildRouteId(method, path), + this.buildRouteId(method, path, args.name), args, { provider: this.constructorOpts.provider }, ); @@ -1087,10 +1113,12 @@ export class ApiGatewayV1 extends Component implements Link.Linkable { return { method, path }; } - private buildRouteId(method: string, path: string) { - const suffix = logicalName( - hashStringToPrettyString([outputId, method, path].join(""), 6), - ); + private buildRouteId(method: string, path: string, name?: string) { + const suffix = name + ? logicalName(name) + : logicalName( + hashStringToPrettyString([outputId, method, path].join(""), 6), + ); return `${this.constructorName}Route${suffix}`; } diff --git a/platform/src/components/aws/apigatewayv2-authorizer.ts b/platform/src/components/aws/apigatewayv2-authorizer.ts index 1d4b2c45e2..de93c10831 100644 --- a/platform/src/components/aws/apigatewayv2-authorizer.ts +++ b/platform/src/components/aws/apigatewayv2-authorizer.ts @@ -114,7 +114,7 @@ export class ApiGatewayV2Authorizer extends Component { identitySources: lamb.apply( (lamb) => lamb.identitySources ?? [defaultIdentitySource], ), - authorizerUri: fn!.invokeArn, + authorizerUri: fn!.targetInvokeArn, ...(args.type === "http" ? { authorizerResultTtlInSeconds: lamb.apply((lamb) => @@ -155,6 +155,7 @@ export class ApiGatewayV2Authorizer extends Component { { action: "lambda:InvokeFunction", function: fn.arn, + qualifier: fn.qualifier.apply((qualifier) => qualifier!), principal: "apigateway.amazonaws.com", sourceArn: interpolate`${api.executionArn}/authorizers/${authorizer.id}`, }, diff --git a/platform/src/components/aws/apigatewayv2-lambda-route.ts b/platform/src/components/aws/apigatewayv2-lambda-route.ts index c20dc38007..f511a27264 100644 --- a/platform/src/components/aws/apigatewayv2-lambda-route.ts +++ b/platform/src/components/aws/apigatewayv2-lambda-route.ts @@ -6,7 +6,7 @@ import { output, } from "@pulumi/pulumi"; import { Component, Transform, transform } from "../component"; -import { FunctionArgs, FunctionArn } from "./function"; +import { FunctionArgs, FunctionArn } from "./function.js"; import { apigatewayv2, lambda } from "@pulumi/aws"; import { ApiGatewayV2BaseRouteArgs, @@ -83,6 +83,7 @@ export class ApiGatewayV2LambdaRoute extends Component { { action: "lambda:InvokeFunction", function: fn.arn, + qualifier: fn.qualifier.apply((qualifier) => qualifier!), principal: "apigateway.amazonaws.com", sourceArn: interpolate`${api.executionArn}/*`, }, @@ -98,7 +99,7 @@ export class ApiGatewayV2LambdaRoute extends Component { { apiId: api.id, integrationType: "AWS_PROXY", - integrationUri: fn.arn, + integrationUri: fn.targetArn, payloadFormatVersion: "2.0", }, { parent: self, dependsOn: [permission] }, diff --git a/platform/src/components/aws/apigatewayv2.ts b/platform/src/components/aws/apigatewayv2.ts index 4334afa92f..848c5a5781 100644 --- a/platform/src/components/aws/apigatewayv2.ts +++ b/platform/src/components/aws/apigatewayv2.ts @@ -8,7 +8,7 @@ import { } from "../component"; import { Link } from "../link"; import type { Input } from "../input"; -import { FunctionArgs, FunctionArn } from "./function"; +import { FunctionArgs, FunctionArn } from "./function.js"; import { hashStringToPrettyString, physicalName, logicalName } from "../naming"; import { VisibleError } from "../error"; import { DnsValidatedCertificate } from "./dns-validated-certificate"; @@ -578,6 +578,22 @@ export interface ApiGatewayV2RouteArgs { lambda?: Input; } >; + /** + * The name of the route. + * + * By default, SST generates a unique suffix from the route path. Setting `name` gives + * you a stable, human-readable name like `MyApiRouteGetUserHandler`. + * + * Must be unique across all routes. + * + * @example + * ```js + * { + * name: "GetUser" + * } + * ``` + */ + name?: string; /** * [Transform](/docs/components#transform) how this component creates its underlying * resources. @@ -1015,6 +1031,12 @@ export class ApiGatewayV2 extends Component implements Link.Linkable { * - An HTTP method and a path, `{METHOD} /{path}`. * - Or a `$default` route. * + * :::caution + * [API Gateway has strict rate limits](https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html) for creating and updating resources. Creating one Lambda function for every endpoint can significantly slow down your deployments. + * + * Use a single Lambda and handle routing in code if you don't need specific API Gateway features. + * ::: + * * :::tip * The `$default` route is a default or catch-all route. It'll match if no other route matches. * ::: @@ -1111,7 +1133,7 @@ export class ApiGatewayV2 extends Component implements Link.Linkable { const route = this.parseRoute(rawRoute); const transformed = transform( this.constructorArgs.transform?.route?.args, - this.buildRouteId(route), + this.buildRouteId(route, args.name), args, { provider: this.constructorOpts.provider }, ); @@ -1165,7 +1187,7 @@ export class ApiGatewayV2 extends Component implements Link.Linkable { const route = this.parseRoute(rawRoute); const transformed = transform( this.constructorArgs.transform?.route?.args, - this.buildRouteId(route), + this.buildRouteId(route, args.name), args, { provider: this.constructorOpts.provider }, ); @@ -1259,7 +1281,7 @@ export class ApiGatewayV2 extends Component implements Link.Linkable { const route = this.parseRoute(rawRoute); const transformed = transform( this.constructorArgs.transform?.route?.args, - this.buildRouteId(route), + this.buildRouteId(route, args.name), args, { provider: this.constructorOpts.provider }, ); @@ -1315,10 +1337,10 @@ export class ApiGatewayV2 extends Component implements Link.Linkable { return `${method} ${path}`; } - private buildRouteId(route: string) { - const suffix = logicalName( - hashStringToPrettyString([outputId, route].join(""), 6), - ); + private buildRouteId(route: string, name?: string) { + const suffix = name + ? logicalName(name) + : logicalName(hashStringToPrettyString([outputId, route].join(""), 6)); return `${this.constructorName}Route${suffix}`; } @@ -1360,7 +1382,7 @@ export class ApiGatewayV2 extends Component implements Link.Linkable { * const authorizer = api.addAuthorizer({ * name: "myCognitoAuthorizer", * jwt: { - * issuer: $interpolate`https://cognito-idp.${aws.getRegionOutput().name}.amazonaws.com/${pool.id}`, + * issuer: $interpolate`https://cognito-idp.${aws.getRegionOutput().region}.amazonaws.com/${pool.id}`, * audiences: [poolClient.id] * } * }); diff --git a/platform/src/components/aws/app-sync-data-source.ts b/platform/src/components/aws/app-sync-data-source.ts index 58b1f47c6d..2f07ea3df4 100644 --- a/platform/src/components/aws/app-sync-data-source.ts +++ b/platform/src/components/aws/app-sync-data-source.ts @@ -5,7 +5,7 @@ import { output, } from "@pulumi/pulumi"; import { Component, transform } from "../component"; -import { Function } from "./function"; +import { Function } from "./function.js"; import { VisibleError } from "../error"; import { AppSyncDataSourceArgs } from "./app-sync"; import { parseDynamoArn } from "./helpers/arn"; @@ -131,7 +131,7 @@ export class AppSyncDataSource extends Component { policy: iam.getPolicyDocumentOutput({ statements: [ ...(lambda - ? [{ actions: ["lambda:*"], resources: [lambda.arn] }] + ? [{ actions: ["lambda:*"], resources: [lambda.targetArn] }] : []), ...(args.dynamodb ? [ @@ -185,7 +185,7 @@ export class AppSyncDataSource extends Component { type, name: args.name, serviceRoleArn: serviceRole?.arn, - lambdaConfig: lambda ? { functionArn: lambda.arn } : undefined, + lambdaConfig: lambda ? { functionArn: lambda.targetArn } : undefined, dynamodbConfig: args.dynamodb ? { tableName: output(args.dynamodb).apply( diff --git a/platform/src/components/aws/app-sync.ts b/platform/src/components/aws/app-sync.ts index 7533b3ec98..7c0b76973d 100644 --- a/platform/src/components/aws/app-sync.ts +++ b/platform/src/components/aws/app-sync.ts @@ -3,7 +3,7 @@ import { ComponentResourceOptions, interpolate, output } from "@pulumi/pulumi"; import { Component, Transform, transform } from "../component"; import { Link } from "../link"; import type { Input } from "../input"; -import { FunctionArgs, FunctionArn } from "./function"; +import { FunctionArgs, FunctionArn } from "./function.js"; import { logicalName } from "../naming"; import { VisibleError } from "../error"; import { AppSyncDataSource } from "./app-sync-data-source"; diff --git a/platform/src/components/aws/astro.ts b/platform/src/components/aws/astro.ts index 71ca6b1942..421c22c533 100644 --- a/platform/src/components/aws/astro.ts +++ b/platform/src/components/aws/astro.ts @@ -121,10 +121,7 @@ export interface AstroArgs extends SsrSiteArgs { */ invalidation?: SsrSiteArgs["invalidation"]; /** - * Set [environment variables](https://docs.astro.build/en/guides/environment-variables/) in your Astro site. These are made available: - * - * 1. In `astro build`, they are loaded into `import.meta.env`. - * 2. Locally while running `astro dev` through `sst dev`. + * Set [environment variables](https://docs.astro.build/en/guides/environment-variables/) in your Astro site. * * :::tip * You can also `link` resources to your Astro site and access them in a type-safe way with the [SDK](/docs/reference/sdk/). We recommend linking since it's more secure. @@ -305,21 +302,6 @@ export interface AstroArgs extends SsrSiteArgs { * ``` */ buildCommand?: SsrSiteArgs["buildCommand"]; - /** - * Configure how the Astro site assets are uploaded to S3. - * - * By default, this is set to the following. Read more about these options below. - * ```js - * { - * assets: { - * textEncoding: "utf-8", - * versionedFilesCacheHeader: "public,max-age=31536000,immutable", - * nonVersionedFilesCacheHeader: "public,max-age=0,s-maxage=86400,stale-while-revalidate=8640" - * } - * } - * ``` - */ - assets?: SsrSiteArgs["assets"]; /** * Configure the Astro site to use an existing CloudFront cache policy. * diff --git a/platform/src/components/aws/aurora.ts b/platform/src/components/aws/aurora.ts index 75a95acc7f..b785f041c4 100644 --- a/platform/src/components/aws/aurora.ts +++ b/platform/src/components/aws/aurora.ts @@ -29,6 +29,10 @@ export interface AuroraArgs { /** * The Aurora engine to use. * + * :::danger + * Changing the engine will cause the database to be destroyed and recreated. + * ::: + * * @example * ```js * { @@ -55,6 +59,11 @@ export interface AuroraArgs { * - Aurora PostgresSQL 13.15 and higher * - Aurora MySQL 3.08.0 and higher * + * :::caution + * Changing the version will cause the database to restart on the next `sst deploy`, + * causing downtime. Learn more about [upgrading databases](/docs/upgrade-aws-databases/). + * ::: + * * @default `"17"` for Postgres, `"3.08.0"` for MySQL * @example * ```js @@ -107,6 +116,10 @@ export interface AuroraArgs { * * By default, it takes the name of the app, and replaces the hyphens with underscores. * + * :::danger + * Changing the database name will cause the database to be destroyed and recreated. + * ::: + * * @default Based on the name of the current app * @example * ```js @@ -735,7 +748,7 @@ export class Aurora extends Component implements Link.Linkable { { parent: self }, ); - const secretId = cluster.tags + const secretId = cluster.tagsAll .apply((tags) => tags?.["sst:ref:password"]) .apply((passwordTag) => { if (!passwordTag) @@ -759,7 +772,7 @@ export class Aurora extends Component implements Link.Linkable { (v) => v.password as string, ); - const proxy = cluster.tags + const proxy = cluster.tagsAll .apply((tags) => tags?.["sst:ref:proxy"]) .apply((proxyTag) => proxyTag @@ -926,6 +939,9 @@ Listening on "${dev.host}:${dev.port}"...`, { parent: self, ignoreChanges: args.version ? [] : ["family"], + // Necessary for the subnet to be deleted after the instance. + // This is either a Pulumi bug or an undocumented feature. + deleteBeforeReplace: false, }, ), ); @@ -946,7 +962,13 @@ Listening on "${dev.host}:${dev.port}"...`, }), parameters: [], }, - { parent: self, ignoreChanges: args.version ? [] : ["family"] }, + { + parent: self, + ignoreChanges: args.version ? [] : ["family"], + // Necessary for the subnet to be deleted after the instance. + // This is either a Pulumi bug or an undocumented feature. + deleteBeforeReplace: false, + }, ), ); } @@ -982,6 +1004,8 @@ Listening on "${dev.host}:${dev.port}"...`, ? toSeconds(scaling.pauseAfter) : undefined, })), + applyImmediately: true, + allowMajorVersionUpgrade: true, skipFinalSnapshot: true, storageEncrypted: true, enableHttpEndpoint: dataApi, @@ -1008,6 +1032,7 @@ Listening on "${dev.host}:${dev.port}"...`, engineVersion: cluster.engineVersion, dbSubnetGroupName: cluster.dbSubnetGroupName, dbParameterGroupName: instanceParameterGroup.name, + autoMinorVersionUpgrade: false, }; // Create primary instance @@ -1291,7 +1316,7 @@ Listening on "${dev.host}:${dev.port}"...`, * * @param name The name of the component. * @param id The ID of the existing Aurora cluster. - * @param opts? Resource options. + * @param opts Resource options. * * @example * Imagine you create a cluster in the `dev` stage. And in your personal stage `frank`, diff --git a/platform/src/components/aws/auth-v1.ts b/platform/src/components/aws/auth-v1.ts index a74f386d5c..34c2065ea5 100644 --- a/platform/src/components/aws/auth-v1.ts +++ b/platform/src/components/aws/auth-v1.ts @@ -6,7 +6,7 @@ import { } from "@pulumi/pulumi"; import { Component, Transform } from "../component"; import { Link } from "../link"; -import { FunctionArgs, Function } from "./function"; +import { FunctionArgs, Function } from "./function.js"; import { PrivateKey } from "@pulumi/tls"; import { s3 } from "@pulumi/aws"; diff --git a/platform/src/components/aws/auth.ts b/platform/src/components/aws/auth.ts index 2aa0128307..b4145f6431 100644 --- a/platform/src/components/aws/auth.ts +++ b/platform/src/components/aws/auth.ts @@ -349,7 +349,10 @@ export class Auth extends Component implements Link.Linkable { { parent: self }, ), ); - router.route("/", issuer.url); + router.route( + "/", + issuer.url.apply((url) => url!), + ); return router; } @@ -365,7 +368,7 @@ export class Auth extends Component implements Link.Linkable { public get url() { return ( this._router?.url ?? - this._issuer.url.apply((v) => (v.endsWith("/") ? v.slice(0, -1) : v)) + this._issuer.url.apply((v) => (v?.endsWith("/") ? v.slice(0, -1) : v)) ); } @@ -402,7 +405,7 @@ export class Auth extends Component implements Link.Linkable { }, include: [ env({ - OPENAUTH_ISSUER: this.url, + OPENAUTH_ISSUER: this.url.apply((url) => url!), }), ], }; diff --git a/platform/src/components/aws/bucket-lambda-subscriber.ts b/platform/src/components/aws/bucket-lambda-subscriber.ts index 6e62ee8348..c27e29787f 100644 --- a/platform/src/components/aws/bucket-lambda-subscriber.ts +++ b/platform/src/components/aws/bucket-lambda-subscriber.ts @@ -6,7 +6,7 @@ import { output, } from "@pulumi/pulumi"; import { Component, transform } from "../component"; -import { Function, FunctionArgs } from "./function"; +import { Function, FunctionArgs } from "./function.js"; import { BucketSubscriberArgs } from "./bucket"; import { lambda, s3 } from "@pulumi/aws"; import { FunctionBuilder, functionBuilder } from "./helpers/function-builder"; @@ -102,6 +102,7 @@ export class BucketLambdaSubscriber extends Component { { action: "lambda:InvokeFunction", function: fn.arn, + qualifier: fn.qualifier.apply((qualifier) => qualifier!), principal: "s3.amazonaws.com", sourceArn: bucket.arn, }, @@ -119,7 +120,7 @@ export class BucketLambdaSubscriber extends Component { lambdaFunctions: [ { id: interpolate`Notification${args.subscriberId}`, - lambdaFunctionArn: fn.arn, + lambdaFunctionArn: fn.targetArn, events, filterPrefix: args.filterPrefix, filterSuffix: args.filterSuffix, diff --git a/platform/src/components/aws/bucket-notification.ts b/platform/src/components/aws/bucket-notification.ts index 3de94d30d7..d8271fcf14 100644 --- a/platform/src/components/aws/bucket-notification.ts +++ b/platform/src/components/aws/bucket-notification.ts @@ -111,6 +111,7 @@ export class BucketNotification extends Component { { action: "lambda:InvokeFunction", function: fn.arn, + qualifier: fn.qualifier.apply((qualifier) => qualifier!), principal: "s3.amazonaws.com", sourceArn: bucket.arn, }, @@ -185,7 +186,7 @@ export class BucketNotification extends Component { .filter((c) => c!.functionBuilder) .map((c) => ({ id: c!.args.name, - lambdaFunctionArn: c!.functionBuilder!.arn, + lambdaFunctionArn: c!.functionBuilder!.targetArn, events: c!.args.events, filterPrefix: c!.args.filterPrefix, filterSuffix: c!.args.filterSuffix, diff --git a/platform/src/components/aws/bucket.ts b/platform/src/components/aws/bucket.ts index d0c6c4aab2..ec23f85cd9 100644 --- a/platform/src/components/aws/bucket.ts +++ b/platform/src/components/aws/bucket.ts @@ -9,7 +9,7 @@ import { hashStringToPrettyString, logicalName } from "../naming"; import { Component, Prettify, Transform, transform } from "../component"; import { Link } from "../link"; import type { Input } from "../input"; -import { FunctionArgs, FunctionArn } from "./function"; +import { FunctionArgs, FunctionArn } from "./function.js"; import { Duration, DurationDays, toSeconds } from "../duration"; import { VisibleError } from "../error"; import { parseBucketArn } from "./helpers/arn"; @@ -72,7 +72,7 @@ interface BucketCorsArgs { allowMethods?: Input[]>; /** * The HTTP headers you want to expose to an origin that calls the bucket. - * @default `[]` + * @default `["ETag"]` * @example * ```js * { @@ -439,7 +439,7 @@ export interface BucketArgs { * allowHeaders: ["*"], * allowOrigins: ["*"], * allowMethods: ["DELETE", "GET", "HEAD", "POST", "PUT"], - * exposeHeaders: [], + * exposeHeaders: ["ETag"], * maxAge: "0 seconds" * } * } @@ -505,11 +505,11 @@ export interface BucketArgs { /** * Transform the S3 Bucket resource. */ - bucket?: Transform; + bucket?: Transform; /** * Transform the S3 Bucket CORS configuration resource. */ - cors?: Transform; + cors?: Transform; /** * Transform the S3 Bucket Policy resource. */ @@ -517,11 +517,11 @@ export interface BucketArgs { /** * Transform the S3 Bucket versioning resource. */ - versioning?: Transform; + versioning?: Transform; /** * Transform the S3 Bucket lifecycle resource. * */ - lifecycle?: Transform; + lifecycle?: Transform; /** * Transform the public access block resource that's attached to the Bucket. * @@ -792,7 +792,7 @@ export interface BucketSubscriberArgs { interface BucketRef { ref: boolean; - bucket: s3.BucketV2; + bucket: s3.Bucket; } /** @@ -858,7 +858,7 @@ export class Bucket extends Component implements Link.Linkable { private constructorName: string; private constructorOpts: ComponentResourceOptions; private isSubscribed: boolean = false; - private bucket: Output; + private bucket: Output; constructor( name: string, @@ -891,7 +891,7 @@ export class Bucket extends Component implements Link.Linkable { // (ie. bucket.name). Also, a bucket can only have one policy. We want to ensure // the policy created here is created first. And SST will throw an error if // another policy is created after this one. - this.bucket = policy.urn.apply(() => bucket); + this.bucket = policy.apply(() => bucket); function normalizeAccess() { return all([args.public, args.access]).apply(([pub, access]) => @@ -925,7 +925,7 @@ export class Bucket extends Component implements Link.Linkable { } function createBucket() { - return new s3.BucketV2( + return new s3.Bucket( ...transform( args.transform?.bucket, `${name}Bucket`, @@ -941,7 +941,7 @@ export class Bucket extends Component implements Link.Linkable { return output(args.versioning).apply((versioning) => { if (!versioning) return; - return new s3.BucketVersioningV2( + return new s3.BucketVersioning( ...transform( args.transform?.versioning, `${name}Versioning`, @@ -990,7 +990,7 @@ export class Bucket extends Component implements Link.Linkable { return resolvedId; }); - return new s3.BucketLifecycleConfigurationV2( + return new s3.BucketLifecycleConfiguration( ...transform( args.transform?.lifecycle, `${name}Lifecycle`, @@ -1105,7 +1105,7 @@ export class Bucket extends Component implements Link.Linkable { return output(args.cors).apply((cors) => { if (cors === false) return; - return new s3.BucketCorsConfigurationV2( + return new s3.BucketCorsConfiguration( ...transform( args.transform?.cors, `${name}Cors`, @@ -1122,7 +1122,7 @@ export class Bucket extends Component implements Link.Linkable { "PUT", ], allowedOrigins: cors?.allowOrigins ?? ["*"], - exposeHeaders: cors?.exposeHeaders, + exposeHeaders: cors?.exposeHeaders ?? ["ETag"], maxAgeSeconds: toSeconds(cors?.maxAge ?? "0 seconds"), }, ], @@ -1178,7 +1178,7 @@ export class Bucket extends Component implements Link.Linkable { * * @param name The name of the component. * @param bucketName The name of the existing S3 Bucket. - * @param opts? Resource options. + * @param opts Resource options. * * @example * Imagine you create a bucket in the `dev` stage. And in your personal stage `frank`, @@ -1206,7 +1206,7 @@ export class Bucket extends Component implements Link.Linkable { ) { return new Bucket(name, { ref: true, - bucket: s3.BucketV2.get(`${name}Bucket`, bucketName, undefined, opts), + bucket: s3.Bucket.get(`${name}Bucket`, bucketName, undefined, opts), } as BucketArgs); } diff --git a/platform/src/components/aws/bus-base-subscriber.ts b/platform/src/components/aws/bus-base-subscriber.ts index 9b3353d1cc..eacc2bdd86 100644 --- a/platform/src/components/aws/bus-base-subscriber.ts +++ b/platform/src/components/aws/bus-base-subscriber.ts @@ -31,6 +31,9 @@ export function createRule( `${name}Rule`, { eventBusName, + state: output(args.enabled ?? true).apply((v: boolean) => + v ? "ENABLED" : "DISABLED", + ), eventPattern: args.pattern ? output(args.pattern).apply((pattern) => JSON.stringify({ diff --git a/platform/src/components/aws/bus-lambda-subscriber.ts b/platform/src/components/aws/bus-lambda-subscriber.ts index fab2b1e55e..8acb53bd1d 100644 --- a/platform/src/components/aws/bus-lambda-subscriber.ts +++ b/platform/src/components/aws/bus-lambda-subscriber.ts @@ -6,7 +6,8 @@ import { output, } from "@pulumi/pulumi"; import { Component, transform } from "../component"; -import { Function, FunctionArgs } from "./function"; +import { Function, FunctionArgs, FunctionArn } from "./function.js"; +import { Workflow } from "./workflow.js"; import { BusBaseSubscriberArgs, createRule } from "./bus-base-subscriber"; import { cloudwatch, lambda } from "@pulumi/aws"; import { FunctionBuilder, functionBuilder } from "./helpers/function-builder"; @@ -15,7 +16,7 @@ export interface Args extends BusBaseSubscriberArgs { /** * The subscriber function. */ - subscriber: Input; + subscriber: Input; } /** @@ -67,6 +68,7 @@ export class BusLambdaSubscriber extends Component { { action: "lambda:InvokeFunction", function: fn.arn, + qualifier: fn.qualifier.apply((qualifier) => qualifier!), principal: "events.amazonaws.com", sourceArn: rule.arn, }, @@ -80,7 +82,7 @@ export class BusLambdaSubscriber extends Component { args?.transform?.target, `${name}Target`, { - arn: fn.arn, + arn: fn.targetArn, rule: rule.name, eventBusName: bus.name, }, diff --git a/platform/src/components/aws/bus.ts b/platform/src/components/aws/bus.ts index d5d2922f82..286e9c0ddf 100644 --- a/platform/src/components/aws/bus.ts +++ b/platform/src/components/aws/bus.ts @@ -2,7 +2,8 @@ import { ComponentResourceOptions, Output, output } from "@pulumi/pulumi"; import { Component, Transform, transform } from "../component"; import { Link } from "../link"; import type { Input } from "../input"; -import { FunctionArgs, FunctionArn } from "./function"; +import { Function, FunctionArgs, FunctionArn } from "./function.js"; +import { Workflow } from "./workflow.js"; import { parseEventBusArn } from "./helpers/arn"; import { BusLambdaSubscriber } from "./bus-lambda-subscriber"; import { cloudwatch } from "@pulumi/aws"; @@ -11,6 +12,31 @@ import { Queue } from "./queue"; import { BusQueueSubscriber } from "./bus-queue-subscriber"; export interface BusArgs { + /** + * Configure logging for the EventBus. + * + * @example + * + * ```js + * new sst.aws.Bus("MyBus", { + * logging: { + * level: "error", + * detail: true, + * }, + * }); + * ``` + */ + logging?: Input<{ + /** + * The level of logging. + */ + level: Input<"error" | "info" | "trace">; + /** + * Whether to include the event detail in the log. + * @default `false` + */ + detail?: Input; + }>; /** * [Transform](/docs/components#transform) how this component creates its underlying * resources. @@ -24,6 +50,19 @@ export interface BusArgs { } export interface BusSubscriberArgs { + /** + * Configures whether the subscriber is enabled. When disabled, the EventBridge + * rule is still created, but it won't receive events. + * + * @default true + * @example + * ```ts + * { + * enabled: false + * } + * ``` + */ + enabled?: Input; /** * Filter the messages that'll be processed by the subscriber. * @@ -220,7 +259,17 @@ export class Bus extends Component implements Link.Linkable { function createBus() { return new cloudwatch.EventBus( - ...transform(args.transform?.bus, `${name}Bus`, {}, { parent: self }), + ...transform( + args.transform?.bus, + `${name}Bus`, + { + logConfig: args.logging && output(args.logging).apply((l) => ({ + level: l.level.toUpperCase(), + includeDetail: l.detail ? "FULL" : "NONE", + })), + }, + { parent: self }, + ), ); } } @@ -292,7 +341,9 @@ export class Bus extends Component implements Link.Linkable { */ public subscribe( name: string, - subscriber: Input, + subscriber: Input< + string | Workflow | Function | FunctionArgs | FunctionArn + >, args: BusSubscriberArgs = {}, ) { return Bus._subscribeFunction( @@ -350,7 +401,9 @@ export class Bus extends Component implements Link.Linkable { public static subscribe( name: string, busArn: Input, - subscriber: Input, + subscriber: Input< + string | Workflow | Function | FunctionArgs | FunctionArn + >, args?: BusSubscriberArgs, ) { return output(busArn).apply((busArn) => { @@ -371,7 +424,9 @@ export class Bus extends Component implements Link.Linkable { subscriberName: string, busName: Input, busArn: string | Output, - subscriber: Input, + subscriber: Input< + string | Workflow | Function | FunctionArgs | FunctionArn + >, args: BusSubscriberArgs = {}, opts: ComponentResourceOptions = {}, ) { @@ -535,7 +590,7 @@ export class Bus extends Component implements Link.Linkable { * * @param name The name of the component. * @param busName The name of the existing EventBus. - * @param opts? Resource options. + * @param opts Resource options. * * @example * Imagine you create a bus in the `dev` stage. And in your personal stage `frank`, diff --git a/platform/src/components/aws/cdn.ts b/platform/src/components/aws/cdn.ts index b858e6ba72..75ceec2f0a 100644 --- a/platform/src/components/aws/cdn.ts +++ b/platform/src/components/aws/cdn.ts @@ -242,6 +242,17 @@ export interface CdnArgs { * Tags to apply to the distribution. */ tags?: Input>>; + /** + * The ARN of a WAF WebACL to associate with the CloudFront distribution. + * + * @example + * ```ts + * { + * webAclArn: "arn:aws:wafv2:us-east-1:123456789012:global/webacl/my-acl/abc123" + * } + * ``` + */ + webAclArn?: Input; /** * [Transform](/docs/components#transform) how this component creates its underlying resources. */ @@ -402,6 +413,8 @@ export class Cdn extends Component { ), waitForDeployment: false, tags: args.tags, + // CloudFront API confusingly names the WAF ARN field "webAclId" + webAclId: args.webAclArn, }, { parent }, ), @@ -525,7 +538,7 @@ export class Cdn extends Component { * * @param name The name of the component. * @param distributionID The id of the existing CDN distribution. - * @param opts? Resource options. + * @param opts Resource options. */ public static get( name: string, diff --git a/platform/src/components/aws/cluster.ts b/platform/src/components/aws/cluster.ts index c4a8781056..b5bb5a9725 100644 --- a/platform/src/components/aws/cluster.ts +++ b/platform/src/components/aws/cluster.ts @@ -35,6 +35,10 @@ type ClusterVpcArgs = { * A list of subnet IDs in the VPC to place the containers in. */ containerSubnets?: Input[]>; + /** + * A list of public subnet IDs in the VPC. + */ + publicSubnets?: Input[]>; /** * A list of subnet IDs in the VPC to place the load balancer in. */ @@ -236,7 +240,7 @@ export class Cluster extends Component { const cluster = ecs.Cluster.get(`${name}Cluster`, ref.id, undefined, { parent: self, }); - const clusterValidated = cluster.tags.apply((tags) => { + const clusterValidated = cluster.tagsAll.apply((tags) => { const refVersion = tags?.["sst:ref:version"] ? parseComponentVersion(tags["sst:ref:version"]) : undefined; @@ -392,8 +396,8 @@ export class Cluster extends Component { * ``` * * @param name Name of the service. - * @param args? Configure the service. - * @param opts? Resource options. + * @param args Configure the service. + * @param opts Resource options. * * @example * @@ -477,8 +481,8 @@ export class Cluster extends Component { * ``` * * @param name Name of the task. - * @param args? Configure the task. - * @param opts? Resource options. + * @param args Configure the task. + * @param opts Resource options. * * @example * @@ -537,7 +541,7 @@ export class Cluster extends Component { * * @param name The name of the component. * @param args The arguments to get the cluster. - * @param opts? Resource options. + * @param opts Resource options. * * @example * Imagine you create a cluster in the `dev` stage. And in your personal stage `frank`, diff --git a/platform/src/components/aws/cognito-identity-pool.ts b/platform/src/components/aws/cognito-identity-pool.ts index dad2ab640a..a2cd2c69dc 100644 --- a/platform/src/components/aws/cognito-identity-pool.ts +++ b/platform/src/components/aws/cognito-identity-pool.ts @@ -165,7 +165,7 @@ export class CognitoIdentityPool extends Component implements Link.Linkable { this.unauthRole = unauthRole; function getRegion() { - return getRegionOutput(undefined, { parent }).name; + return getRegionOutput(undefined, { parent }).region; } function createIdentityPool() { @@ -367,7 +367,7 @@ export class CognitoIdentityPool extends Component implements Link.Linkable { * * @param name The name of the component. * @param identityPoolID The ID of the existing Identity Pool. - * @param opts? Resource options. + * @param opts Resource options. * * @example * Imagine you create a Identity Pool in the `dev` stage. And in your personal stage `frank`, diff --git a/platform/src/components/aws/cognito-user-pool.ts b/platform/src/components/aws/cognito-user-pool.ts index 8ea326f5fb..4aec85a2cd 100644 --- a/platform/src/components/aws/cognito-user-pool.ts +++ b/platform/src/components/aws/cognito-user-pool.ts @@ -1,12 +1,22 @@ -import { ComponentResourceOptions, Output, all, output } from "@pulumi/pulumi"; +import { + ComponentResourceOptions, + Output, + all, + interpolate, + output, +} from "@pulumi/pulumi"; import { Component, Prettify, Transform, transform } from "../component"; import { Input } from "../input"; import { Link } from "../link"; +import { Dns } from "../dns"; import { CognitoIdentityProvider } from "./cognito-identity-provider"; import { CognitoUserPoolClient } from "./cognito-user-pool-client"; import { Function, FunctionArgs, FunctionArn } from "./function.js"; import { VisibleError } from "../error"; -import { cognito, lambda } from "@pulumi/aws"; +import { cognito, getRegionOutput, lambda } from "@pulumi/aws"; +import { dns as awsDns } from "./dns.js"; +import { DnsValidatedCertificate } from "./dns-validated-certificate.js"; +import { useProvider } from "./helpers/provider.js"; import { permission } from "./permission"; import { functionBuilder } from "./helpers/function-builder"; @@ -319,6 +329,75 @@ export interface CognitoUserPoolArgs { * ``` */ triggers?: Input>; + /** + * Configure a domain for the User Pool's hosted UI. + * + * You can use either a Cognito-provided prefix domain or your own custom domain. + * + * @example + * + * Add a Cognito prefix domain. + * + * ```ts + * { + * domain: { + * prefix: "my-app-dev" + * } + * } + * ``` + * + * This creates a domain at `my-app-dev.auth.{region}.amazoncognito.com`. + * + * Add a custom domain. By default, creates an ACM certificate and configures + * DNS records using Route 53. + * + * ```ts + * { + * domain: "auth.example.com" + * } + * ``` + * + * Use a domain hosted on Cloudflare. + * + * ```ts + * { + * domain: { + * name: "auth.example.com", + * dns: sst.cloudflare.dns() + * } + * } + * ``` + */ + domain?: Input< + | string + | { + /** + * Use an Amazon Cognito prefix domain. Creates a domain at + * `{prefix}.auth.{region}.amazoncognito.com`. + * + * Cannot contain "aws", "amazon", or "cognito". + */ + prefix: Input; + } + | { + /** + * The custom domain name. Must be a subdomain (e.g., `auth.example.com`). + */ + name: Input; + /** + * The DNS provider for automatic certificate validation and record creation. + * Set to `false` for manual DNS setup. + * + * @default `sst.aws.dns` + */ + dns?: Input; + /** + * ARN of an existing ACM certificate in `us-east-1`. By default, a certificate + * is created and validated automatically. + */ + cert?: Input; + } + >; /** * [Transform](/docs/components#transform) how this component creates its underlying * resources. @@ -328,6 +407,10 @@ export interface CognitoUserPoolArgs { * Transform the Cognito User Pool resource. */ userPool?: Transform; + /** + * Transform the Cognito User Pool domain resource. + */ + domain?: Transform; }; } @@ -443,6 +526,26 @@ interface CognitoUserPoolRef { * }); * ``` * + * #### Add a hosted UI domain + * + * Use a Cognito prefix domain for the hosted UI. + * + * ```ts title="sst.config.ts" + * new sst.aws.CognitoUserPool("MyUserPool", { + * domain: { + * prefix: "my-app-dev" + * } + * }); + * ``` + * + * Or use your own custom domain. + * + * ```ts title="sst.config.ts" + * new sst.aws.CognitoUserPool("MyUserPool", { + * domain: "auth.example.com" + * }); + * ``` + * * #### Configure triggers * * ```ts title="sst.config.ts" @@ -484,6 +587,7 @@ interface CognitoUserPoolRef { export class CognitoUserPool extends Component implements Link.Linkable { private constructorOpts: ComponentResourceOptions; private userPool: Output; + private _domainUrl?: Output; constructor( name: string, @@ -500,14 +604,25 @@ export class CognitoUserPool extends Component implements Link.Linkable { } const parent = this; + const region = getRegionOutput(undefined, { parent }).region; normalizeAliasesAndUsernames(); const triggers = normalizeTriggers(); const verify = normalizeVerify(); const userPool = createUserPool(); + const domain = normalizeDomain(); + const certificateArn = createSsl(); + const cognitoDomain = createCognitoDomain(); + createDnsRecords(); + this.constructorOpts = opts; this.userPool = userPool; + this._domainUrl = domain?.apply((d) => + d.prefix + ? interpolate`https://${d.prefix}.auth.${region}.amazoncognito.com` + : interpolate`https://${d.name}`, + ); function normalizeAliasesAndUsernames() { all([args.aliases, args.usernames]).apply(([aliases, usernames]) => { @@ -675,18 +790,19 @@ export class CognitoUserPool extends Component implements Link.Linkable { { parent }, ); - new lambda.Permission( - `${name}Permission${key}`, - { - action: "lambda:InvokeFunction", - function: fn.arn, - principal: "cognito-idp.amazonaws.com", - sourceArn: userPool.arn, - }, - { parent }, - ); - return fn.arn; - } + new lambda.Permission( + `${name}Permission${key}`, + { + action: "lambda:InvokeFunction", + function: fn.arn, + qualifier: fn.qualifier.apply((qualifier) => qualifier!), + principal: "cognito-idp.amazonaws.com", + sourceArn: userPool.arn, + }, + { parent }, + ); + return fn.targetArn; + } }), }, { parent }, @@ -694,6 +810,89 @@ export class CognitoUserPool extends Component implements Link.Linkable { ), ); } + + function normalizeDomain() { + if (!args.domain) return; + + return output(args.domain).apply((domain) => { + if (typeof domain === "string") domain = { name: domain }; + + if ("prefix" in domain) { + return { + prefix: domain.prefix, + name: undefined, + dns: undefined, + cert: undefined, + }; + } + + if (domain.dns === false && !domain.cert) { + throw new VisibleError( + `Need to provide a validated certificate via "cert" when DNS is disabled.`, + ); + } + + return { + prefix: undefined, + name: domain.name, + dns: domain.dns === false ? undefined : (domain.dns ?? awsDns()), + cert: domain.cert, + }; + }); + } + + function createSsl() { + if (!domain) return output(undefined); + + return domain.apply((domain) => { + if (domain.prefix) return output(undefined); + if (domain.cert) return output(domain.cert); + + return new DnsValidatedCertificate( + `${name}Ssl`, + { + domainName: domain.name!, + dns: output(domain.dns!), + }, + { parent, provider: useProvider("us-east-1") }, + ).arn; + }); + } + + function createCognitoDomain() { + if (!domain) return; + + return new cognito.UserPoolDomain( + ...transform( + args.transform?.domain, + `${name}Domain`, + { + userPoolId: userPool.id, + domain: domain.apply((d) => (d.prefix ?? d.name)!), + certificateArn: certificateArn as Output, + }, + { parent, deleteBeforeReplace: true }, + ), + ); + } + + function createDnsRecords() { + if (!domain || !cognitoDomain) return; + + domain.apply((domain) => { + if (!domain.name || !domain.dns) return; + + domain.dns.createAlias( + name, + { + name: domain.name, + aliasName: cognitoDomain.cloudfrontDistribution, + aliasZone: cognitoDomain.cloudfrontDistributionZoneId, + }, + { parent }, + ); + }); + } } /** @@ -710,6 +909,13 @@ export class CognitoUserPool extends Component implements Link.Linkable { return this.userPool.arn; } + /** + * If a `domain` is configured, this is the full URL of the hosted UI. + */ + public get domainUrl() { + return this._domainUrl; + } + /** * The underlying [resources](/docs/components/#nodes) this component creates. */ @@ -727,7 +933,7 @@ export class CognitoUserPool extends Component implements Link.Linkable { * * @param name Name of the client. * @param args Configure the client. - * @param opts? Resource options. + * @param opts Resource options. * * @example * @@ -735,7 +941,11 @@ export class CognitoUserPool extends Component implements Link.Linkable { * userPool.addClient("Web"); * ``` */ - public addClient(name: string, args?: CognitoUserPoolClientArgs) { + public addClient( + name: string, + args?: CognitoUserPoolClientArgs, + opts?: ComponentResourceOptions, + ) { // Note: Referencing an existing client will be implemented in the future: // sst.aws.UserPool.getClient("pool", { userPooldID, clientID }); @@ -745,7 +955,7 @@ export class CognitoUserPool extends Component implements Link.Linkable { userPool: this.id, ...args, }, - { provider: this.constructorOpts.provider }, + { provider: this.constructorOpts.provider, ...opts }, ); } diff --git a/platform/src/components/aws/cron-v2.ts b/platform/src/components/aws/cron-v2.ts new file mode 100644 index 0000000000..259e142b7d --- /dev/null +++ b/platform/src/components/aws/cron-v2.ts @@ -0,0 +1,555 @@ +import { all, ComponentResourceOptions, output, Output } from "@pulumi/pulumi"; +import { Component, Transform, transform } from "../component"; +import { Function, FunctionArgs, FunctionArn } from "./function.js"; +import { Input } from "../input.js"; +import { iam, scheduler } from "@pulumi/aws"; +import { functionBuilder, FunctionBuilder } from "./helpers/function-builder"; +import { Task } from "./task"; +import { VisibleError } from "../error"; +import { Workflow } from "./workflow.js"; + +export interface CronV2Args { + /** + * The function that'll be executed when the cron job runs. + * @deprecated Use `function` instead. + * + * @example + * + * ```ts + * { + * job: "src/cron.handler" + * } + * ``` + * + * You can pass in the full function props. + * + * ```ts + * { + * job: { + * handler: "src/cron.handler", + * timeout: "60 seconds" + * } + * } + * ``` + * + * You can also pass in a function ARN. + * + * ```ts + * { + * job: "arn:aws:lambda:us-east-1:000000000000:function:my-sst-app-jayair-MyFunction", + * } + * ``` + */ + job?: Input; + /** + * The function that'll be executed when the cron job runs. + * + * @example + * + * ```ts + * { + * function: "src/cron.handler" + * } + * ``` + * + * You can pass in the full function props. + * + * ```ts + * { + * function: { + * handler: "src/cron.handler", + * timeout: "60 seconds" + * } + * } + * ``` + * + * You can also pass in a function ARN. + * + * ```ts + * { + * function: "arn:aws:lambda:us-east-1:000000000000:function:my-sst-app-jayair-MyFunction", + * } + * ``` + */ + function?: Input; + /** + * The task that'll be executed when the cron job runs. + * + * @example + * + * For example, let's say you have a task. + * + * ```js title="sst.config.ts" + * const cluster = new sst.aws.Cluster("MyCluster"); + * const task = new sst.aws.Task("MyTask", { cluster }); + * ``` + * + * You can then pass in the task to the cron job. + * + * ```js title="sst.config.ts" + * new sst.aws.CronV2("MyCronJob", { + * task, + * schedule: "rate(1 minute)" + * }); + * ``` + * + */ + task?: Task; + /** + * The event that'll be passed to the function or task. + * + * @example + * ```ts + * { + * event: { + * foo: "bar", + * } + * } + * ``` + * + * For Lambda functions, the event will be passed to the function as an event. + * + * ```ts + * function handler(event) { + * console.log(event.foo); + * } + * ``` + * + * For ECS Fargate tasks, the event will be passed to the task as the `SST_EVENT` + * environment variable. + * + * ```ts + * const event = JSON.parse(process.env.SST_EVENT); + * console.log(event.foo); + * ``` + */ + event?: Input; + /** + * The schedule for the cron job. + * + * :::note + * The cron job continues to run even after you exit `sst dev`. + * ::: + * + * @example + * + * You can use a [rate expression](https://docs.aws.amazon.com/lambda/latest/dg/services-cloudwatchevents-expressions.html). + * + * ```ts + * { + * schedule: "rate(5 minutes)" + * // schedule: "rate(1 minute)" + * // schedule: "rate(5 minutes)" + * // schedule: "rate(1 hour)" + * // schedule: "rate(5 hours)" + * // schedule: "rate(1 day)" + * // schedule: "rate(5 days)" + * } + * ``` + * Or a [cron expression](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule-schedule.html#eb-cron-expressions). + * + * ```ts + * { + * schedule: "cron(15 10 * * ? *)", // 10:15 AM (UTC) every day + * } + * ``` + * + * Or an [at expression](https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html#one-time) for a one-time schedule. + * + * ```ts + * { + * schedule: "at(2025-06-01T10:00:00)", + * } + * ``` + */ + schedule: Input<`rate(${string})` | `cron(${string})` | `at(${string})`>; + /** + * The IANA timezone for the cron schedule. When set, the cron expression is + * evaluated in this timezone, with automatic DST handling. + * + * @default `"UTC"` + * @example + * ```ts + * { + * timezone: "America/New_York" + * } + * ``` + */ + timezone?: Input; + /** + * Configures whether the cron job is enabled. When disabled, the cron job won't run. + * @default true + * @example + * ```ts + * { + * enabled: false + * } + * ``` + */ + enabled?: Input; + /** + * The number of retry attempts for failed invocations. Between 0 and 185. + * + * @default `0` + * @example + * ```ts + * { + * retries: 3 + * } + * ``` + */ + retries?: Input; + /** + * The ARN of an SQS queue to use as a dead-letter queue. When all retry + * attempts are exhausted, failed events are sent to this queue. + * + * @example + * ```ts + * { + * dlq: myQueue.arn + * } + * ``` + */ + dlq?: Input; + /** + * [Transform](/docs/components#transform) how this component creates its underlying resources. + */ + transform?: { + /** + * Transform the EventBridge Scheduler Schedule resource. + */ + schedule?: Transform; + /** + * Transform the IAM Role resource. + */ + role?: Transform; + }; +} + +/** + * The `CronV2` component lets you add cron jobs to your app + * using [Amazon EventBridge Scheduler](https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html). The cron job can invoke a `Function` or a container `Task`. + * + * @example + * #### Cron job function + * + * Pass in a `schedule` and a `function` that'll be executed. + * + * ```ts title="sst.config.ts" + * new sst.aws.CronV2("MyCronJob", { + * function: "src/cron.handler", + * schedule: "rate(1 minute)" + * }); + * ``` + * + * #### Cron job container task + * + * Create a container task and pass in a `schedule` and a `task` that'll be executed. + * + * ```ts title="sst.config.ts" {5} + * const cluster = new sst.aws.Cluster("MyCluster"); + * const task = new sst.aws.Task("MyTask", { cluster }); + * + * new sst.aws.CronV2("MyCronJob", { + * task, + * schedule: "rate(1 day)" + * }); + * ``` + * + * #### Set a timezone + * + * ```ts title="sst.config.ts" + * new sst.aws.CronV2("MyCronJob", { + * function: "src/cron.handler", + * schedule: "cron(15 10 * * ? *)", + * timezone: "America/New_York" + * }); + * ``` + * + * #### Configure retries + * + * ```ts title="sst.config.ts" + * new sst.aws.CronV2("MyCronJob", { + * function: "src/cron.handler", + * schedule: "rate(1 minute)", + * retries: 3 + * }); + * ``` + * + * #### One-time schedule + * + * ```ts title="sst.config.ts" + * new sst.aws.CronV2("MyCronJob", { + * function: "src/cron.handler", + * schedule: "at(2025-06-01T10:00:00)" + * }); + * ``` + * + * #### Customize the function + * + * ```js title="sst.config.ts" + * new sst.aws.CronV2("MyCronJob", { + * schedule: "rate(1 minute)", + * function: { + * handler: "src/cron.handler", + * timeout: "60 seconds" + * } + * }); + * ``` + */ +export class CronV2 extends Component { + private _name: string; + private fn?: FunctionBuilder; + private _schedule: scheduler.Schedule; + private _role: iam.Role; + + constructor(name: string, args: CronV2Args, opts?: ComponentResourceOptions) { + super(__pulumiType, name, args, opts); + + const parent = this; + + const fnArgs = normalizeFunction(); + const event = output(args.event || {}); + normalizeTargets(); + const enabled = output(args.enabled ?? true); + const fn = createFunction(); + const role = createRole(); + const schedule = createSchedule(); + + this._name = name; + this.fn = fn; + this._role = role; + this._schedule = schedule; + + function normalizeFunction() { + if (args.job && args.function) + throw new VisibleError( + `You cannot provide both "job" and "function" in the "${name}" CronV2 component. The "job" property has been deprecated. Use "function" instead.`, + ); + + const input = args.function ?? args.job; + return input ? output(input) : undefined; + } + + function normalizeTargets() { + if (fnArgs && args.task) + throw new VisibleError( + `You cannot provide both a function and a task in the "${name}" CronV2 component.`, + ); + if (!fnArgs && !args.task) + throw new VisibleError( + `You must provide either a function or a task in the "${name}" CronV2 component.`, + ); + } + + function createFunction() { + if (!fnArgs) return; + + return fnArgs.apply((fnArgs) => + functionBuilder(`${name}Handler`, fnArgs, {}, undefined, { + parent, + }), + ); + } + + function createRole() { + if (fn) { + return new iam.Role( + ...transform( + args.transform?.role, + `${name}Role`, + { + assumeRolePolicy: iam.assumeRolePolicyForPrincipal({ + Service: "scheduler.amazonaws.com", + }), + inlinePolicies: [ + { + name: "inline", + policy: output(args.dlq).apply((dlq) => + iam.getPolicyDocumentOutput({ + statements: [ + { + actions: ["lambda:InvokeFunction"], + resources: [fn.targetArn], + }, + ...(dlq + ? [ + { + actions: ["sqs:SendMessage"], + resources: [dlq], + }, + ] + : []), + ], + }).json, + ), + }, + ], + }, + { parent }, + ), + ); + } + + return new iam.Role( + ...transform( + args.transform?.role, + `${name}Role`, + { + assumeRolePolicy: iam.assumeRolePolicyForPrincipal({ + Service: "scheduler.amazonaws.com", + }), + inlinePolicies: [ + { + name: "inline", + policy: output(args.dlq).apply((dlq) => + iam.getPolicyDocumentOutput({ + statements: [ + { + actions: ["ecs:RunTask"], + resources: [args.task!.nodes.taskDefinition.arn], + }, + { + actions: ["iam:PassRole"], + resources: [ + args.task!.nodes.executionRole.arn, + args.task!.nodes.taskRole.arn, + ], + }, + ...(dlq + ? [ + { + actions: ["sqs:SendMessage"], + resources: [dlq], + }, + ] + : []), + ], + }).json, + ), + }, + ], + }, + { parent }, + ), + ); + } + + function createSchedule() { + const retryPolicy = { + maximumRetryAttempts: output(args.retries ?? 0), + }; + + const deadLetterConfig = args.dlq ? { arn: args.dlq } : undefined; + + if (fn) { + return new scheduler.Schedule( + ...transform( + args.transform?.schedule, + `${name}Schedule`, + { + scheduleExpression: args.schedule, + scheduleExpressionTimezone: args.timezone, + flexibleTimeWindow: { mode: "OFF" }, + state: enabled.apply((v) => (v ? "ENABLED" : "DISABLED")), + target: { + arn: fn.targetArn, + roleArn: role.arn, + input: event.apply((event) => JSON.stringify(event)), + retryPolicy, + deadLetterConfig, + }, + }, + { parent }, + ), + ); + } + + return new scheduler.Schedule( + ...transform( + args.transform?.schedule, + `${name}Schedule`, + { + scheduleExpression: args.schedule, + scheduleExpressionTimezone: args.timezone, + flexibleTimeWindow: { mode: "OFF" }, + state: enabled.apply((v) => (v ? "ENABLED" : "DISABLED")), + target: { + arn: args.task!.cluster, + roleArn: role.arn, + input: all([event, args.task!.containers]).apply( + ([event, containers]) => { + return JSON.stringify({ + containerOverrides: containers.map((name) => ({ + name, + environment: [ + { + name: "SST_EVENT", + value: JSON.stringify(event), + }, + ], + })), + }); + }, + ), + ecsParameters: { + taskDefinitionArn: args.task!.nodes.taskDefinition.arn, + launchType: "FARGATE", + networkConfiguration: { + subnets: args.task!.subnets, + securityGroups: args.task!.securityGroups, + assignPublicIp: args.task!.assignPublicIp, + }, + }, + retryPolicy, + deadLetterConfig, + }, + }, + { parent }, + ), + ); + } + } + + /** + * The underlying [resources](/docs/components/#nodes) this component creates. + */ + public get nodes() { + const self = this; + return { + /** + * The AWS Lambda Function that'll be invoked when the cron job runs. + * @deprecated Use `nodes.function` instead. + */ + get job() { + if (!self.fn) + throw new VisibleError( + `No function created for the "${self._name}" cron job.`, + ); + return self.fn.apply((fn) => fn.getFunction()); + }, + /** + * The AWS Lambda Function that'll be invoked when the cron job runs. + */ + get function() { + if (!self.fn) + throw new VisibleError( + `No function created for the "${self._name}" cron job.`, + ); + return self.fn.apply((fn) => fn.getFunction()); + }, + /** + * The EventBridge Scheduler Schedule resource. + */ + schedule: this._schedule, + /** + * The IAM Role resource. + */ + role: this._role, + }; + } +} + +const __pulumiType = "sst:aws:CronV2"; +// @ts-expect-error +CronV2.__pulumiType = __pulumiType; diff --git a/platform/src/components/aws/cron.ts b/platform/src/components/aws/cron.ts index a8170798be..892dc2e8ad 100644 --- a/platform/src/components/aws/cron.ts +++ b/platform/src/components/aws/cron.ts @@ -1,11 +1,12 @@ import { all, ComponentResourceOptions, output, Output } from "@pulumi/pulumi"; import { Component, Transform, transform } from "../component"; -import { FunctionArgs, FunctionArn } from "./function"; +import { Function, FunctionArgs, FunctionArn } from "./function.js"; import { Input } from "../input.js"; import { cloudwatch, iam, lambda } from "@pulumi/aws"; import { functionBuilder, FunctionBuilder } from "./helpers/function-builder"; import { Task } from "./task"; import { VisibleError } from "../error"; +import { Workflow } from "./workflow.js"; export interface CronArgs { /** @@ -39,7 +40,7 @@ export interface CronArgs { * } * ``` */ - job?: Input; + job?: Input; /** * The function that'll be executed when the cron job runs. * @@ -70,7 +71,7 @@ export interface CronArgs { * } * ``` */ - function?: Input; + function?: Input; /** * The task that'll be executed when the cron job runs. * @@ -122,7 +123,7 @@ export interface CronArgs { * console.log(event.foo); * ``` */ - event?: Input>>; + event?: Input; /** * The schedule for the cron job. * @@ -181,9 +182,17 @@ export interface CronArgs { } /** + * The `Cron` component has been deprecated. Use [`CronV2`](https://sst.dev/docs/component/aws/cron-v2) instead. + * + * :::caution + * This component has been deprecated. + * ::: + * * The `Cron` component lets you add cron jobs to your app * using [Amazon Event Bus](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-bus.html). The cron job can invoke a `Function` or a container `Task`. * + * @deprecated Use [`CronV2`](https://sst.dev/docs/component/aws/cron-v2) instead. + * * @example * #### Cron job function * @@ -292,6 +301,7 @@ export class Cron extends Component { { action: "lambda:InvokeFunction", function: fn.arn, + qualifier: fn.qualifier.apply((qualifier) => qualifier!), principal: "events.amazonaws.com", sourceArn: rule.arn, }, @@ -342,7 +352,7 @@ export class Cron extends Component { `${name}Target`, fn ? { - arn: fn.arn, + arn: fn.targetArn, rule: rule.name, input: event.apply((event) => JSON.stringify(event)), } diff --git a/platform/src/components/aws/dns.ts b/platform/src/components/aws/dns.ts index 44b11442ed..cb1eecb27d 100644 --- a/platform/src/components/aws/dns.ts +++ b/platform/src/components/aws/dns.ts @@ -157,7 +157,11 @@ export function dns(args: DnsArgs = {}) { type: record.type, name: record.name, ttl: 60, - records: [record.value], + records: [ + output(record).apply((r) => + r.priority !== undefined ? `${r.priority} ${r.value}` : r.value, + ), + ], }, opts, ); diff --git a/platform/src/components/aws/dsql.ts b/platform/src/components/aws/dsql.ts new file mode 100644 index 0000000000..47f32b3fcd --- /dev/null +++ b/platform/src/components/aws/dsql.ts @@ -0,0 +1,741 @@ +import { all, ComponentResourceOptions, output } from "@pulumi/pulumi"; +import { Component, transform, Transform } from "../component"; +import { toDays, type DurationDays } from "../duration"; +import { Link } from "../link"; +import { backup, dsql, ec2, iam, Provider, Region } from "@pulumi/aws"; +import { permission } from "./permission"; +import { useProvider } from "./helpers/provider"; +import { Vpc } from "./vpc"; +import { + parseDsqlPublicEndpoint, + parseDsqlPrivateEndpoint, +} from "./helpers/arn"; +import type { Input } from "../input"; +import { VisibleError } from "../error"; + +export interface DsqlArgs { + /** + * Configure multi-region cluster peering. + * + * Creates a cluster in the current region and a peer cluster in another region, + * linked via a witness region. The witness must differ from both cluster regions. + * + * Learn more about [AWS DSQL regions](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/what-is-aurora-dsql.html#region-availability). + * + * @example + * + * ```ts + * const cluster = new sst.aws.Dsql("MyCluster", { + * regions: { + * witness: "us-west-2", + * peer: "us-east-2" + * } + * }); + * ``` + */ + regions?: { + /** The witness region. Must differ from both cluster regions. */ + witness: Input; + /** The AWS region for the peer cluster. */ + peer: Input; + }; + + /** + * Configure automatic backups for the cluster using AWS Backup. + * + * Set to `true` to use the defaults, or pass an object to customize the schedule and retention. + * + * :::tip + * If multi-region is enabled, backups are scheduled in the current region and + * copied to the peer region. + * ::: + * + * Omit or set to `false` to skip backup creation entirely. + * + * @example + * Enable with defaults (daily at 5 AM UTC, 7-day retention). + * ```ts title="sst.config.ts" + * const cluster = new sst.aws.Dsql("MyCluster", { + * backup: true + * }); + * ``` + * + * Custom schedule and retention. + * ```ts title="sst.config.ts" + * const cluster = new sst.aws.Dsql("MyCluster", { + * backup: { + * schedule: "cron(0 2 ? * * *)", + * retention: "90 days" + * } + * }); + * ``` + */ + backup?: + | boolean + | { + /** + * The schedule for the backups as an [AWS Backup cron expression](https://docs.aws.amazon.com/aws-backup/latest/devguide/API_BackupRule.html). + * + * This uses the same 6-field `cron(...)` format as EventBridge and is evaluated in UTC. + * + * @default `"cron(0 5 ? * * *)"` + * @example + * Back up every day at midnight UTC. + * ```ts + * schedule: "cron(0 0 ? * * *)" + * ``` + * + * Back up every Monday at 3 AM UTC. + * ```ts + * schedule: "cron(0 3 ? * MON *)" + * ``` + */ + schedule?: Input; + /** + * How long to retain backups. Use a day duration like `"7 days"`. + * @default `"7 days"` + */ + retention?: Input; + }; + + /** + * + * Create AWS PrivateLink interface endpoints in a VPC for private connectivity. + * This allows lambdas placed inside a VPC without NAT gateways to connect to the DSQL instance. + * + * :::note + * Currently only single region VPC is supported. + * ::: + * + * @example + * + * ```ts title="sst.config.ts" + * const myVpc = new sst.aws.Vpc("MyVpc"); + * + * const cluster = new sst.aws.Dsql("MyCluster", { + * vpc: myVpc + * }); + * ``` + * + * #### Customize VPC endpoints + * + * ```ts title="sst.config.ts" + * const myVpc = new sst.aws.Vpc("MyVpc"); + * + * const cluster = new sst.aws.Dsql("MyCluster", { + * vpc: { + * instance: vpc, + * endpoints: { + * management: true, + * connection: true, + * } + * } + * }); + * ``` + */ + vpc?: + | Vpc + | { + instance: Vpc; + endpoints?: { + /** + * Endpoint for control plane ops (create, get, update, delete clusters). + * + * @default `false` + */ + management?: boolean; + /** + * Endpoint for PostgreSQL client connections. + * + * @default `true` + */ + connection?: boolean; + }; + }; + + /** + * [Transform](/docs/components#transform) how this component creates its underlying + * resources. + */ + transform?: { + /** + * Transform the DSQL cluster resource. + */ + cluster?: Transform; + /** + * Transform the peer DSQL cluster resource. + */ + peerCluster?: Transform; + /** + * Transform the EC2 security group resource for the DSQL VPC endpoints. + */ + endpointSecurityGroup?: Transform; + /** + * Transform the EC2 VPC endpoint resource for DSQL management operations. + */ + managementEndpoint?: Transform; + /** + * Transform the EC2 VPC endpoint resource for DSQL connections. + */ + connectionEndpoint?: Transform; + /** + * Transform the AWS Backup vault resource. + */ + backupVault?: Transform; + /** + * Transform the AWS Backup plan resource. + */ + backupPlan?: Transform; + /** + * Transform the AWS Backup selection resource. + */ + backupSelection?: Transform; + }; +} + +interface DsqlRef { + ref: boolean; + cluster: dsql.Cluster; + peerCluster?: dsql.Cluster; +} + +/** + * The `Dsql` component lets you add an [Amazon Aurora DSQL](https://aws.amazon.com/rds/aurora/dsql/) cluster to your app. + * + * @example + * + * #### Single-region cluster + * + * ```ts title="sst.config.ts" + * const cluster = new sst.aws.Dsql("MyCluster"); + * ``` + * + * Once linked, you can connect to it from your function code. + * + * ```ts title="src/lambda.ts" + * import { Resource } from "sst"; + * import { AuroraDSQLClient } from "@aws/aurora-dsql-node-postgres-connector"; + * + * const client = new AuroraDSQLClient({ + * host: Resource.MyCluster.endpoint, + * user: "admin", + * }); + * + * await client.connect(); + * const result = await client.query("SELECT NOW() as now"); + * await client.end(); + * ``` + * + * #### Multi-region cluster + * + * ```ts title="sst.config.ts" + * const cluster = new sst.aws.Dsql("MyCluster", { + * regions: { + * witness: "us-west-2", + * peer: "us-east-2" + * } + * }); + * ``` + * + * [Check out the full example](/docs/examples/#aws-dsql-multiregion). + * + * #### With private VPC endpoints + * + * ```ts title="sst.config.ts" + * const vpc = new sst.aws.Vpc("MyVpc"); + * + * const cluster = new sst.aws.Dsql("MyCluster", { + * vpc: { + * instance: vpc, + * endpoints: { connection: true } + * } + * }); + * ``` + * + * [Check out the full example](/docs/examples/#aws-dsql-vpc). + * + * #### With backups + * + * ```ts title="sst.config.ts" + * const cluster = new sst.aws.Dsql("MyCluster", { + * backup: true + * }); + * ``` + * + * #### Link to a function + * + * ```ts title="sst.config.ts" + * new sst.aws.Function("MyFunction", { + * handler: "src/lambda.handler", + * link: [cluster] + * }); + * ``` + * + * You can also use Drizzle ORM to query your DSQL cluster. + * [Check out the Drizzle example](/docs/examples/#aws-dsql-drizzle). + * + * --- + * + * ### Cost + * + * Aurora DSQL is serverless and uses a pay-per-use pricing model. You are charged for + * database activity measured in _Distributed Processing Units_ (DPUs) at $8 per million + * DPUs, and storage at $0.33 per GB-month. When idle, usage scales to zero and you incur + * no DPU charges. + * + * There is a free tier of 100,000 DPUs and 1 GB of storage per month. + * + * For example, a single-region cluster averaging 1.3M DPUs per month with 15 GB of storage + * costs roughly 1.3 x $8 + 15 x $0.33 or **$15 per month**. + * + * Check out the [Aurora DSQL pricing](https://aws.amazon.com/rds/aurora/dsql/pricing/) for more details. + * + */ + +export class Dsql extends Component implements Link.Linkable { + private cluster: dsql.Cluster; + private peerCluster: dsql.Cluster | undefined; + private connectionEndpoint: ec2.VpcEndpoint | undefined; + private constructorName: string; + + constructor( + name: string, + args: DsqlArgs = {}, + opts: ComponentResourceOptions = {}, + ) { + super(__pulumiType, name, args, opts); + this.constructorName = name; + + if (args && "ref" in args) { + const ref = args as unknown as DsqlRef; + this.cluster = ref.cluster; + this.peerCluster = ref.peerCluster; + return; + } + + const parent = this; + const regions = args.regions; + + if (regions && args.vpc) + throw new VisibleError( + `Cannot use "vpc" with multi-region "regions". VPC endpoints are only supported for single-region clusters.`, + ); + + const vpc = normalizeVpc(); + const backupConfig = normalizeBackup(); + + const cluster = createCluster(); + const peerCluster = createPeerCluster(); + const endpoints = createVpcEndpoints(); + createBackup(); + + this.cluster = cluster; + this.peerCluster = peerCluster; + this.connectionEndpoint = endpoints?.connection; + + function createCluster() { + return new dsql.Cluster( + ...transform( + args.transform?.cluster, + `${name}Cluster`, + { + multiRegionProperties: regions + ? { witnessRegion: regions.witness } + : undefined, + }, + { parent }, + ), + ); + } + + function createPeerCluster() { + if (!regions) return; + + const peerProvider = useProvider(regions.peer as Region); + + const peerCluster = new dsql.Cluster( + ...transform( + args.transform?.peerCluster, + `${name}PeerCluster`, + { + multiRegionProperties: { + witnessRegion: regions.witness, + }, + }, + { parent, provider: peerProvider }, + ), + ); + + // DSQL requires both clusters to declare each other β€” two-way handshake. + new dsql.ClusterPeering( + `${name}Peering1`, + { + identifier: cluster.identifier, + clusters: [peerCluster.arn], + witnessRegion: regions.witness, + }, + { parent }, + ); + + new dsql.ClusterPeering( + `${name}Peering2`, + { + identifier: peerCluster.identifier, + clusters: [cluster.arn], + witnessRegion: regions.witness, + }, + { parent, provider: peerProvider }, + ); + + return peerCluster; + } + + function createBackup() { + if (!backupConfig) return; + + const role = new iam.Role( + `${name}BackupRole`, + { + assumeRolePolicy: iam.assumeRolePolicyForPrincipal({ + Service: "backup.amazonaws.com", + }), + managedPolicyArns: [ + "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup", + ], + }, + { parent }, + ); + + const vault = createBackupVault(); + const peerVault = regions + ? createBackupVault("Peer", useProvider(regions.peer as Region)) + : undefined; + + const plan = new backup.Plan( + ...transform( + args.transform?.backupPlan, + `${name}BackupPlan`, + { + rules: [ + { + ruleName: `${name}BackupRule`, + targetVaultName: vault.name, + schedule: backupConfig.schedule, + scheduleExpressionTimezone: "UTC", + lifecycle: { deleteAfter: backupConfig.retention }, + copyActions: peerVault + ? [ + { + destinationVaultArn: peerVault.arn, + lifecycle: { deleteAfter: backupConfig.retention }, + }, + ] + : undefined, + }, + ], + }, + { parent }, + ), + ); + + new backup.Selection( + ...transform( + args.transform?.backupSelection, + `${name}BackupSelection`, + { + planId: plan.id, + iamRoleArn: role.arn, + resources: [cluster.arn], + }, + { parent }, + ), + ); + } + + function createBackupVault(suffix = "", provider?: Provider) { + return new backup.Vault( + ...transform( + args.transform?.backupVault, + `${name}BackupVault${suffix}`, + {}, + { parent, provider }, + ), + ); + } + + function normalizeBackup() { + if (!args.backup) return; + + const config = args.backup === true ? {} : args.backup; + + return { + schedule: output(config.schedule).apply( + (v) => v ?? "cron(0 5 ? * * *)", + ), + retention: output(config.retention).apply((v) => toDays(v ?? "7 days")), + }; + } + + function normalizeVpc() { + if (!args.vpc) return undefined; + + if (args.vpc instanceof Vpc) { + return { + instance: args.vpc, + endpoints: { + management: false, + connection: true, + }, + }; + } + + return { + instance: args.vpc.instance, + endpoints: { + management: args.vpc.endpoints?.management ?? false, + connection: args.vpc.endpoints?.connection ?? true, + }, + }; + } + + function createVpcEndpoints() { + if (!vpc) return; + + const endpointSecurityGroup = new ec2.SecurityGroup( + ...transform( + args.transform?.endpointSecurityGroup, + `${name}DsqlEndpointSecurityGroup`, + { + vpcId: vpc.instance.id, + description: "Allow DSQL access to VPC endpoints", + ingress: [ + ...(vpc.endpoints.management + ? [ + { + protocol: "tcp", + fromPort: 443, + toPort: 443, + cidrBlocks: [vpc.instance.nodes.vpc.cidrBlock], + }, + ] + : []), + ...(vpc.endpoints.connection + ? [ + { + protocol: "tcp", + fromPort: 5432, + toPort: 5432, + cidrBlocks: [vpc.instance.nodes.vpc.cidrBlock], + }, + ] + : []), + ], + egress: [ + { + protocol: "-1", + fromPort: 0, + toPort: 0, + cidrBlocks: ["0.0.0.0/0"], + }, + ], + }, + { parent }, + ), + ); + + let management, connection; + + if (vpc.endpoints.management) { + management = new ec2.VpcEndpoint( + ...transform( + args.transform?.managementEndpoint, + `${name}ManagementEndpoint`, + { + vpcId: vpc.instance.id, + serviceName: cluster.arn.apply((arn) => { + const region = arn.split(":")[3]; + return `com.amazonaws.${region}.dsql`; + }), + vpcEndpointType: "Interface", + subnetIds: vpc.instance.privateSubnets, + privateDnsEnabled: true, + securityGroupIds: [endpointSecurityGroup.id], + }, + { parent }, + ), + ); + } + + if (vpc.endpoints.connection) { + connection = new ec2.VpcEndpoint( + ...transform( + args.transform?.connectionEndpoint, + `${name}ConnectionEndpoint`, + { + vpcId: vpc.instance.id, + serviceName: cluster.vpcEndpointServiceName, + vpcEndpointType: "Interface", + subnetIds: vpc.instance.privateSubnets, + privateDnsEnabled: true, + securityGroupIds: [endpointSecurityGroup.id], + }, + { parent }, + ), + ); + } + + return { connection, management }; + } + } + + /** The region of the cluster. */ + public get region() { + return this.cluster.region; + } + + /** The endpoint of the cluster. */ + public get endpoint() { + // Use the private VPC endpoint hostname when available so linked functions + // inside the VPC don't route through the public IP. + return all([this.cluster.arn, this.connectionEndpoint?.dnsEntries]).apply( + ([arn, dns]) => { + if (!dns) { + return parseDsqlPublicEndpoint(arn); + } + return parseDsqlPrivateEndpoint(arn, dns); + }, + ); + } + + /** + * The peer cluster info. Only available for multi-region clusters. + * + * @example + * ```ts title="sst.config.ts" + * const cluster = new sst.aws.Dsql("MyCluster", { + * regions: { peer: "us-east-2" }, + * }); + * + * return { + * peerRegion: cluster.peer.region, + * peerEndpoint: cluster.peer.endpoint, + * }; + * ``` + */ + public get peer() { + if (!this.peerCluster) + throw new VisibleError( + `Cannot access "peer" on "${this.constructorName}" because it is a single-region cluster. Set "regions.peer" to enable multi-region.`, + ); + const peerCluster = this.peerCluster; + return { + /** The region of the peer cluster. */ + region: peerCluster.region, + /** The endpoint of the peer cluster. */ + endpoint: peerCluster.arn.apply(parseDsqlPublicEndpoint), + }; + } + + /** The underlying [resources](/docs/components/#nodes) this component creates. */ + public get nodes() { + return { + /** The DSQL cluster. */ + cluster: this.cluster, + /** The peer DSQL cluster (multi-region only). */ + peerCluster: this.peerCluster, + }; + } + + /** + * Reference an existing DSQL cluster by identifier. Useful for sharing a cluster + * across stages without creating a new one. + * + * :::tip + * You can use the `static get` method to share a cluster across stages. + * ::: + * + * @example + * + * #### Single-region cluster + * + * ```ts title="sst.config.ts" + * const cluster = $app.stage === "frank" + * ? sst.aws.Dsql.get("MyCluster", { id: "kzttrvbdg4k2o5ze2m2rrwdj7u" }) + * : new sst.aws.Dsql("MyCluster"); + * ``` + * #### Multi-region cluster + * + * ```ts title="sst.config.ts" + * const cluster = sst.aws.Dsql.get("MyCluster", { + * id: "app-dev-mycluster", + * peer: { + * id: "kzttrvbdg4k2o5ze2m2rrwdj7u", + * region: "us-east-2", + * } + * }); + * ``` + */ + public static get( + name: string, + args: { + id: Input; + peer?: { + id: string; + region: string; + }; + }, + opts?: ComponentResourceOptions, + ) { + const cluster = dsql.Cluster.get( + `${name}Cluster`, + args.id, + undefined, + opts, + ); + + const peerCluster = args.peer + ? dsql.Cluster.get(`${name}PeerCluster`, args.peer.id, undefined, { + ...opts, + provider: useProvider(args.peer.region as Region), + }) + : undefined; + + return new Dsql( + name, + { + ref: true, + cluster, + peerCluster, + } satisfies DsqlRef as unknown as DsqlArgs, + opts, + ); + } + + /** @internal */ + public getSSTLink() { + return { + properties: { + region: this.region, + endpoint: this.endpoint, + peer: this.peerCluster + ? { + region: this.peerCluster.region, + endpoint: this.peerCluster.arn.apply(parseDsqlPublicEndpoint), + } + : undefined, + }, + include: [ + permission({ + actions: ["dsql:DbConnect", "dsql:DbConnectAdmin", "dsql:GetCluster"], + resources: this.peerCluster + ? [this.cluster.arn, this.peerCluster.arn] + : [this.cluster.arn], + }), + ], + }; + } +} + +const __pulumiType = "sst:aws:Dsql"; +// @ts-expect-error +Dsql.__pulumiType = __pulumiType; diff --git a/platform/src/components/aws/dynamo-lambda-subscriber.ts b/platform/src/components/aws/dynamo-lambda-subscriber.ts index 4362a3bfba..cdba40da44 100644 --- a/platform/src/components/aws/dynamo-lambda-subscriber.ts +++ b/platform/src/components/aws/dynamo-lambda-subscriber.ts @@ -1,10 +1,9 @@ import { ComponentResourceOptions, Input, output } from "@pulumi/pulumi"; import { Component, transform } from "../component"; -import { FunctionArgs } from "./function"; +import { FunctionArgs } from "./function.js"; import { DynamoSubscriberArgs } from "./dynamo"; import { lambda } from "@pulumi/aws"; import { FunctionBuilder, functionBuilder } from "./helpers/function-builder"; -import { parseFunctionArn } from "./helpers/arn"; export interface Args extends DynamoSubscriberArgs { /** @@ -84,9 +83,7 @@ export class DynamoLambdaSubscriber extends Component { `${name}EventSourceMapping`, { eventSourceArn: dynamo.streamArn, - functionName: fn.arn.apply( - (arn) => parseFunctionArn(arn).functionName, - ), + functionName: fn.targetArn, filterCriteria: args.filters ? output(args.filters).apply((filters) => ({ filters: filters.map((filter) => ({ diff --git a/platform/src/components/aws/dynamo.ts b/platform/src/components/aws/dynamo.ts index c6353c528d..6401ea9d2c 100644 --- a/platform/src/components/aws/dynamo.ts +++ b/platform/src/components/aws/dynamo.ts @@ -8,7 +8,7 @@ import { import { Component, outputId, Transform, transform } from "../component"; import { Link } from "../link"; import type { Input } from "../input"; -import { FunctionArgs, FunctionArn } from "./function"; +import { FunctionArgs, FunctionArn } from "./function.js"; import { hashStringToPrettyString, logicalName } from "../naming"; import { parseDynamoStreamArn } from "./helpers/arn"; import { DynamoLambdaSubscriber } from "./dynamo-lambda-subscriber"; @@ -75,6 +75,19 @@ export interface DynamoArgs { * } * } * ``` + * + * Use an array to create a composite key with multiple attributes. + * + * ```js + * { + * globalIndexes: { + * RegionCategoryIndex: { + * hashKey: ["region", "category"], + * rangeKey: "createdAt" + * } + * } + * } + * ``` */ globalIndexes?: Input< Record< @@ -82,12 +95,32 @@ export interface DynamoArgs { Input<{ /** * The hash key field of the index. This field needs to be defined in the `fields`. + * + * You can also pass in an array of field names to create a composite key with + * up to 4 attributes using the [multi-attribute keys](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.DesignPattern.MultiAttributeKeys.html) pattern. + * + * @example + * ```js + * { + * hashKey: ["region", "category"] + * } + * ``` */ - hashKey: Input; + hashKey: Input; /** * The range key field of the index. This field needs to be defined in the `fields`. + * + * You can also pass in an array of field names to create a composite key with + * up to 4 attributes using the [multi-attribute keys](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.DesignPattern.MultiAttributeKeys.html) pattern. + * + * @example + * ```js + * { + * rangeKey: ["createdAt", "status"] + * } + * ``` */ - rangeKey?: Input; + rangeKey?: Input; /** * The fields to project into the index. * @default `"all"` @@ -346,6 +379,28 @@ interface DynamoRef { * }); * ``` * + * #### Add a composite key global index + * + * Use multi-attribute composite keys in a global index. This is useful when you want + * to combine multiple attributes into a single partition or sort key. + * + * ```ts {8-12} title="sst.config.ts" + * new sst.aws.Dynamo("MyTable", { + * fields: { + * region: "string", + * category: "string", + * createdAt: "number", + * }, + * primaryIndex: { hashKey: "region", rangeKey: "createdAt" }, + * globalIndexes: { + * RegionCategoryIndex: { + * hashKey: ["region", "category"], + * rangeKey: "createdAt" + * } + * } + * }); + * ``` + * * #### Add a local index * * Optionally add a local index to the table. @@ -484,8 +539,28 @@ export class Dynamo extends Component implements Link.Linkable { globalSecondaryIndexes: Object.entries(globalIndexes ?? {}).map( ([name, index]) => ({ name, - hashKey: index.hashKey, - rangeKey: index.rangeKey, + ...(Array.isArray(index.hashKey) || + Array.isArray(index.rangeKey) + ? { + keySchemas: [ + ...[index.hashKey].flat().map((k) => ({ + attributeName: k, + keyType: "HASH", + })), + ...(index.rangeKey + ? [index.rangeKey].flat().map((k) => ({ + attributeName: k, + keyType: "RANGE", + })) + : []), + ], + } + : { + hashKey: index.hashKey, + ...(index.rangeKey + ? { rangeKey: index.rangeKey } + : {}), + }), ...(index.projection === "keys-only" ? { projectionType: "KEYS_ONLY" } : Array.isArray(index.projection) @@ -801,7 +876,7 @@ export class Dynamo extends Component implements Link.Linkable { * * @param name The name of the component. * @param tableName The name of the DynamoDB Table. - * @param opts? Resource options. + * @param opts Resource options. * * @example * Imagine you create a table in the `dev` stage. And in your personal stage `frank`, diff --git a/platform/src/components/aws/efs.ts b/platform/src/components/aws/efs.ts index a7ae1ac372..2f3e1c533c 100644 --- a/platform/src/components/aws/efs.ts +++ b/platform/src/components/aws/efs.ts @@ -355,7 +355,7 @@ export class Efs extends Component { * * @param name The name of the component. * @param fileSystemID The ID of the existing EFS file system. - * @param opts? Resource options. + * @param opts Resource options. * * @example * Imagine you create a EFS file system in the `dev` stage. And in your personal stage diff --git a/platform/src/components/aws/email.ts b/platform/src/components/aws/email.ts index 4c0daac854..a7d6d274aa 100644 --- a/platform/src/components/aws/email.ts +++ b/platform/src/components/aws/email.ts @@ -10,7 +10,7 @@ import { Link } from "../link"; import { Input } from "../input"; import { Dns } from "../dns"; import { dns as awsDns } from "./dns.js"; -import { ses, sesv2 } from "@pulumi/aws"; +import { getRegionOutput, ses, sesv2 } from "@pulumi/aws"; import { permission } from "./permission"; interface Events { @@ -137,6 +137,65 @@ export interface EmailArgs { * ``` */ dmarc?: Input; + /** + * Configure a custom [MAIL FROM domain](https://docs.aws.amazon.com/ses/latest/dg/mail-from.html) + * for the emails you send. This sets the `Return-Path` (envelope sender) to use a subdomain of + * your own domain instead of the default `amazonses.com`. It improves SPF alignment for DMARC + * and routes bounce and complaint feedback through your domain. + * + * This'll create the required MX and SPF DNS records for the MAIL FROM subdomain. + * + * :::note + * This is only valid when `sender` is a domain. The MAIL FROM `domain` must be a subdomain + * of the `sender` domain. + * ::: + * + * @default No custom MAIL FROM domain + * + * @example + * ```js + * { + * mailFrom: { + * domain: "mail.example.com" + * } + * } + * ``` + */ + mailFrom?: Input<{ + /** + * The custom MAIL FROM subdomain. Must be a subdomain of the `sender` domain. + * + * @example + * ```js + * { + * mailFrom: { + * domain: "mail.example.com" + * } + * } + * ``` + */ + domain: Input; + /** + * Control what happens when the required MX record of the custom MAIL FROM domain isn't + * configured correctly. + * + * By default, Amazon SES falls back to using the default `amazonses.com` MAIL FROM domain. + * When set to `true`, Amazon SES returns a `MailFromDomainNotVerified` error instead and the + * emails you attempt to send from this domain are automatically rejected. + * + * @default `false` + * @example + * ```js + * { + * mailFrom: { + * domain: "mail.example.com", + * rejectOnMxFailure: true + * } + * } + * ``` + */ + rejectOnMxFailure?: Input; + }>; /** * Configure event notifications for this Email component. * @@ -167,6 +226,10 @@ export interface EmailArgs { * Transform the SES configuration set resource. */ configurationSet?: Transform; + /** + * Transform the SES MAIL FROM attributes resource. + */ + mailFromAttributes?: Transform; }; } @@ -219,6 +282,17 @@ interface EmailRef { * }); * ``` * + * #### Configuring a custom MAIL FROM domain + * + * ```ts title="sst.config.ts" + * new sst.aws.Email("MyEmail", { + * sender: "example.com", + * mailFrom: { + * domain: "mail.example.com" + * } + * }); + * ``` + * * #### Link to a resource * * You can link it to a function or your Next.js app to send emails. @@ -274,13 +348,16 @@ export class Email extends Component implements Link.Linkable { const isDomain = checkIsDomain(); const dns = normalizeDns(); const dmarc = normalizeDmarc(); + normalizeMailFrom(); const configurationSet = createConfigurationSet(); const identity = createIdentity(); + if (args.mailFrom) createMailFrom(); createEvents(); isDomain.apply((isDomain) => { if (!isDomain) return; createDkimRecords(); createDmarcRecord(); + createMailFromRecords(); waitForVerification(); }); @@ -335,6 +412,22 @@ export class Email extends Component implements Link.Linkable { return args.dmarc ?? `v=DMARC1; p=none;`; } + function normalizeMailFrom() { + all([args.mailFrom, isDomain, args.sender]).apply( + ([mailFrom, isDomain, sender]) => { + if (!mailFrom) return; + if (!isDomain) + throw new Error( + `The "mailFrom" property is only valid when "sender" is a domain.`, + ); + if (!mailFrom.domain.endsWith(`.${sender}`)) + throw new Error( + `The "mailFrom.domain" "${mailFrom.domain}" must be a subdomain of the "sender" domain "${sender}".`, + ); + }, + ); + } + function createConfigurationSet() { return new sesv2.ConfigurationSet( ...transform( @@ -360,6 +453,24 @@ export class Email extends Component implements Link.Linkable { ); } + function createMailFrom() { + const mailFrom = output(args.mailFrom!); + return new sesv2.EmailIdentityMailFromAttributes( + ...transform( + args.transform?.mailFromAttributes, + `${name}MailFrom`, + { + emailIdentity: identity.emailIdentity, + mailFromDomain: mailFrom.domain, + behaviorOnMxFailure: output(mailFrom.rejectOnMxFailure).apply( + (reject) => (reject ? "REJECT_MESSAGE" : "USE_DEFAULT_VALUE"), + ), + }, + { parent: self }, + ), + ); + } + function createEvents() { output(args.events ?? []).apply((events) => events.forEach((event) => { @@ -423,6 +534,36 @@ export class Email extends Component implements Link.Linkable { }); } + function createMailFromRecords() { + all([ + dns, + args.mailFrom, + getRegionOutput(undefined, { parent: self }).region, + ]).apply(([dns, mailFrom, region]) => { + if (!dns || !mailFrom) return; + + dns.createRecord( + name, + { + type: "MX", + name: mailFrom.domain, + value: `feedback-smtp.${region}.amazonses.com`, + priority: 10, + }, + { parent: self }, + ); + dns.createRecord( + name, + { + type: "TXT", + name: mailFrom.domain, + value: `v=spf1 include:amazonses.com ~all`, + }, + { parent: self }, + ); + }); + } + function waitForVerification() { new ses.DomainIdentityVerification( `${name}Verification`, @@ -497,7 +638,7 @@ export class Email extends Component implements Link.Linkable { * * @param name The name of the component. * @param sender The email address or domain name of the existing SES identity. - * @param opts? Resource options. + * @param opts Resource options. * * @example * Imagine you create an Email component in the `dev` stage. And in your personal stage `frank`, diff --git a/platform/src/components/aws/fargate.ts b/platform/src/components/aws/fargate.ts index e5332eddad..d6c1e8efe1 100644 --- a/platform/src/components/aws/fargate.ts +++ b/platform/src/components/aws/fargate.ts @@ -4,7 +4,7 @@ import { ComponentResourceOptions, interpolate, secret } from "@pulumi/pulumi"; import { all, output } from "@pulumi/pulumi"; import { Input } from "../input"; import { Efs } from "./efs"; -import { FunctionArgs } from "./function"; +import { FunctionArgs } from "./function.js"; import { RETENTION } from "./logging"; import { toGBs, toMBs } from "../size"; import { VisibleError } from "../error"; @@ -191,10 +191,54 @@ export interface FargateContainerArgs { * Key-value pairs of build args. Same as the top-level [`image.args`](#image-args). */ args?: Input>>; + /** + * Key-value pairs of [build secrets](https://docs.docker.com/build/building/secrets/) to pass to the Docker build. + * + * Unlike build args, secrets are not persisted in the final image. They are + * available in the Dockerfile via [`--mount=type=secret`](https://docs.docker.com/build/building/secrets/#secret-mounts). + * + * @example + * ```js + * { + * secrets: { + * MY_TOKEN: "my-secret-token", + * } + * } + * ``` + * + * Then in the Dockerfile, reference it as a file: + * ```dockerfile title="Dockerfile" + * RUN --mount=type=secret,id=MY_TOKEN \ + * cat /run/secrets/MY_TOKEN + * ``` + * + * Or as an environment variable: + * ```dockerfile title="Dockerfile" + * RUN --mount=type=secret,id=MY_TOKEN,env=MY_TOKEN \ + * echo $MY_TOKEN + * ``` + */ + secrets?: Input>>; + /** + * Tags to apply to the Docker image. + * @example + * ```js + * { + * tags: ["v1.0.0", "commit-613c1b2"] + * } + * ``` + */ + tags?: Input[]>; /** * The stage to build up to. Same as the top-level [`image.target`](#image-target). */ target?: Input; + /** + * Controls whether Docker build cache is enabled. Same as the top-level + * [`image.cache`](#image-cache). + * @default `true` + */ + cache?: Input; } >; /** @@ -238,7 +282,7 @@ export interface FargateContainerArgs { ssm?: FargateBaseArgs["ssm"]; /** * Mount Amazon EFS file systems into the container. Same as the top-level - * [`efs`](#efs). + * [`volumes`](#volumes). */ volumes?: FargateBaseArgs["volumes"]; } @@ -460,6 +504,34 @@ export interface FargateBaseArgs { * ``` */ args?: Input>>; + /** + * Key-value pairs of [build secrets](https://docs.docker.com/build/building/secrets/) to pass to the Docker build. + * + * Unlike build args, secrets are not persisted in the final image. They are + * available in the Dockerfile via [`--mount=type=secret`](https://docs.docker.com/build/building/secrets/#secret-mounts). + * + * @example + * ```js + * { + * secrets: { + * MY_TOKEN: "my-secret-token", + * } + * } + * ``` + * + * Then in the Dockerfile, reference it as a file: + * ```dockerfile title="Dockerfile" + * RUN --mount=type=secret,id=MY_TOKEN \ + * cat /run/secrets/MY_TOKEN + * ``` + * + * Or as an environment variable: + * ```dockerfile title="Dockerfile" + * RUN --mount=type=secret,id=MY_TOKEN,env=MY_TOKEN \ + * echo $MY_TOKEN + * ``` + */ + secrets?: Input>>; /** * Tags to apply to the Docker image. * @example @@ -480,6 +552,21 @@ export interface FargateBaseArgs { * ``` */ target?: Input; + /** + * Controls whether Docker build cache is enabled. + * @default `true` + * @example + * Disable Docker build caching, useful for environments like Localstack where + * ECR cache export is not supported. + * ```js + * { + * image: { + * cache: false + * } + * } + * ``` + */ + cache?: Input; } >; /** @@ -761,7 +848,7 @@ export function normalizeStorage(args: FargateBaseArgs) { export function normalizeContainers( type: "service" | "task", - args: ServiceArgs, + args: Omit, name: string, architecture: ReturnType, ) { @@ -982,7 +1069,7 @@ export function createExecutionRole( export function createTaskDefinition( name: string, - args: ServiceArgs, + args: Omit, opts: ComponentResourceOptions, parent: Component, containers: ReturnType, @@ -994,7 +1081,7 @@ export function createTaskDefinition( executionRole: ReturnType, ) { const clusterName = args.cluster.nodes.cluster.name; - const region = getRegionOutput({}, opts).name; + const region = getRegionOutput({}, opts).region; const bootstrapData = region.apply((region) => bootstrap.forRegion(region)); const linkEnvs = Link.propertiesToEnv(Link.getProperties(args.link)); const containerDefinitions = output(containers).apply((containers) => @@ -1033,7 +1120,9 @@ export function createTaskDefinition( context: { location: contextPath }, dockerfile: { location: dockerfilePath }, buildArgs: containerImage.args, - secrets: linkEnvs, + secrets: all([linkEnvs, containerImage.secrets ?? {}]).apply( + ([link, secrets]) => ({ ...link, ...secrets }), + ), target: container.image.target, platforms: [container.image.platform], tags: [container.name, ...(container.image.tags ?? [])].map( @@ -1053,23 +1142,27 @@ export function createTaskDefinition( username: authToken.userName, })), ], - cacheFrom: [ - { - registry: { - ref: interpolate`${bootstrapData.assetEcrUrl}:${container.name}-cache`, - }, - }, - ], - cacheTo: [ - { - registry: { - ref: interpolate`${bootstrapData.assetEcrUrl}:${container.name}-cache`, - imageManifest: true, - ociMediaTypes: true, - mode: "max", - }, - }, - ], + ...(container.image.cache !== false + ? { + cacheFrom: [ + { + registry: { + ref: interpolate`${bootstrapData.assetEcrUrl}:${container.name}-cache`, + }, + }, + ], + cacheTo: [ + { + registry: { + ref: interpolate`${bootstrapData.assetEcrUrl}:${container.name}-cache`, + imageManifest: true, + ociMediaTypes: true, + mode: "max", + }, + }, + ], + } + : {}), push: true, }, { parent }, diff --git a/platform/src/components/aws/function.ts b/platform/src/components/aws/function.ts index ffaa7937ac..1da49a2666 100644 --- a/platform/src/components/aws/function.ts +++ b/platform/src/components/aws/function.ts @@ -14,15 +14,22 @@ import { output, secret, unsecret, + rootStackResource, } from "@pulumi/pulumi"; import { bootstrap } from "./helpers/bootstrap.js"; -import { Duration, DurationMinutes, toSeconds } from "../duration.js"; +import { + Duration, + DurationDays, + DurationMinutes, + toDays, + toSeconds, +} from "../duration.js"; import { Size, toMBs } from "../size.js"; import { Component, Prettify, Transform, transform } from "../component.js"; import { Link } from "../link.js"; import { VisibleError } from "../error.js"; import type { Input } from "../input.js"; -import { physicalName } from "../naming.js"; +import { logicalName, physicalName } from "../naming.js"; import { RETENTION } from "./logging.js"; import { cloudwatch, @@ -39,7 +46,7 @@ import { Permission, permission } from "./permission.js"; import { Vpc } from "./vpc.js"; import { Image } from "@pulumi/docker-build"; import { rpc } from "../rpc/rpc.js"; -import { parseRoleArn } from "./helpers/arn.js"; +import { parseRoleArn, splitQualifiedFunctionArn } from "./helpers/arn.js"; import { RandomBytes } from "@pulumi/random"; import { lazy } from "../../util/lazy.js"; import { Efs } from "./efs.js"; @@ -344,12 +351,12 @@ export interface FunctionArgs { * Node.js and Golang are officially supported. While, Python and Rust are * community supported. Support for other runtimes are on the roadmap. * - * @default `"nodejs20.x"` + * @default `"nodejs24.x"` * * @example * ```js * { - * runtime: "nodejs22.x" + * runtime: "nodejs24.x" * } * ``` */ @@ -357,13 +364,17 @@ export interface FunctionArgs { | "nodejs18.x" | "nodejs20.x" | "nodejs22.x" + | "nodejs24.x" | "go" | "rust" + | "provided.al2" | "provided.al2023" | "python3.9" | "python3.10" | "python3.11" | "python3.12" + | "python3.13" + | "python3.14" >; /** * Path to the source code directory for the function. By default, the handler is @@ -426,44 +437,46 @@ export interface FunctionArgs { * * ##### Python * - * For Python, [uv](https://docs.astral.sh/uv/) is used to package the function. + * SST uses [uv](https://docs.astral.sh/uv/) to package the function. * You need to have it installed. * * :::note * You need uv installed for Python functions. * ::: * - * The functions need to be in a [uv workspace](https://docs.astral.sh/uv/concepts/projects/workspaces/#workspace-sources). + * Your handler must live in a [uv workspace](https://docs.astral.sh/uv/concepts/projects/workspaces/) + * with a `pyproject.toml`. Match the Python version to your Lambda runtime. * - * ```js - * { - * handler: "functions/src/functions/api.handler" - * } + * ```toml title="pyproject.toml" + * [project] + * name = "my-project" + * requires-python = "==3.11.*" * ``` * - * The project structure might look something like this. Where there is a - * `pyproject.toml` file in the root and the `functions/` directory is a uv - * workspace with its own `pyproject.toml`. - * - * ```txt - * β”œβ”€β”€ sst.config.ts - * β”œβ”€β”€ pyproject.toml - * └── functions - * β”œβ”€β”€ pyproject.toml - * └── src - * └── functions - * β”œβ”€β”€ __init__.py - * └── api.py + * Install your packages before starting `sst dev`: + * + * ```bash + * uv sync --all-packages * ``` * - * To make sure that the right runtime is used in `sst dev`, make sure to set the - * version of Python in your `pyproject.toml` to match the runtime you are using. + * Use absolute imports within your package. * - * ```toml title="functions/pyproject.toml" - * requires-python = "==3.11.*" + * ```python + * from mypackage.utils import helper * ``` * - * You can refer to [this example of deploying a Python function](/docs/examples/#aws-lambda-python). + * Avoid relative imports β€” they can fail in Lambda. Make sure package directories have `__init__.py`. + * + * Access static files relative to `__file__`. + * + * ```python + * from pathlib import Path + * config = Path(__file__).parent / "config.json" + * ``` + * + * For large dependencies like numpy or pandas, deploy as a container. See [`python.container`](#python-container). + * + * For common project layouts, check out the [Python examples](https://github.com/sst/sst/tree/dev/examples/python-layouts). * * ##### Golang * @@ -662,18 +675,11 @@ export interface FunctionArgs { /** * Enable streaming for the function. * - * Streaming is only supported when using the function `url` is enabled and not when using it - * with API Gateway. + * Streaming is supported with both Function URLs and API Gateway REST API (V1). It is + * not supported with API Gateway HTTP API (V2). * * You'll also need to [wrap your handler](https://docs.aws.amazon.com/lambda/latest/dg/configuration-response-streaming.html) with `awslambda.streamifyResponse` to enable streaming. * - * :::note - * Streaming is currently not supported in `sst dev`. - * ::: - * - * While `sst dev` doesn't support streaming, you can use the - * [`lambda-stream`](https://github.com/astuyve/lambda-stream) package to test locally. - * * Check out the [AWS Lambda streaming example](/docs/examples/#aws-lambda-streaming) for more * details. * @@ -705,53 +711,53 @@ export interface FunctionArgs { logging?: Input< | false | { - /** - * The duration the function logs are kept in CloudWatch. - * - * Not application when an existing log group is provided. - * - * @default `1 month` - * @example - * ```js - * { - * logging: { - * retention: "forever" - * } - * } - * ``` - */ - retention?: Input; - /** - * Assigns the given CloudWatch log group name to the function. This allows you to pass in a previously created log group. - * - * By default, the function creates a new log group when it's created. - * - * @default Creates a log group - * @example - * ```js - * { - * logging: { - * logGroup: "/existing/log-group" - * } - * } - * ``` - */ - logGroup?: Input; - /** - * The [log format](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs-advanced.html) - * of the Lambda function. - * @default `"text"` - * @example - * ```js - * { - * logging: { - * format: "json" - * } - * } - * ``` - */ - format?: Input<"text" | "json">; - } + /** + * The duration the function logs are kept in CloudWatch. + * + * Not application when an existing log group is provided. + * + * @default `1 month` + * @example + * ```js + * { + * logging: { + * retention: "forever" + * } + * } + * ``` + */ + retention?: Input; + /** + * Assigns the given CloudWatch log group name to the function. This allows you to pass in a previously created log group. + * + * By default, the function creates a new log group when it's created. + * + * @default Creates a log group + * @example + * ```js + * { + * logging: { + * logGroup: "/existing/log-group" + * } + * } + * ``` + */ + logGroup?: Input; + /** + * The [log format](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs-advanced.html) + * of the Lambda function. + * @default `"text"` + * @example + * ```js + * { + * logging: { + * format: "json" + * } + * } + * ``` + */ + format?: Input<"text" | "json">; + } >; /** * The [architecture](https://docs.aws.amazon.com/lambda/latest/dg/foundation-arch.html) @@ -813,137 +819,137 @@ export interface FunctionArgs { url?: Input< | boolean | { - /** - * @deprecated The `url.router` prop is now the recommended way to serve your - * function URL through a `Router` component. - */ - route?: Prettify; - /** - * Serve your function URL through a `Router` instead of a standalone Function URL. - * - * By default, this component creates a direct function URL endpoint. But you might - * want to serve it through the distribution of your `Router` as a: - * - * - A path like `/api/users` - * - A subdomain like `api.example.com` - * - Or a combined pattern like `dev.example.com/api` - * - * @example - * - * To serve your function **from a path**, you'll need to configure the root domain - * in your `Router` component. - * - * ```ts title="sst.config.ts" {2} - * const router = new sst.aws.Router("Router", { - * domain: "example.com" - * }); - * ``` - * - * Now set the `router` and the `path` in the `url` prop. - * - * ```ts {4,5} - * { - * url: { - * router: { - * instance: router, - * path: "/api/users" - * } - * } - * } - * ``` - * - * To serve your function **from a subdomain**, you'll need to configure the - * domain in your `Router` component to match both the root and the subdomain. - * - * ```ts title="sst.config.ts" {3,4} - * const router = new sst.aws.Router("Router", { - * domain: { - * name: "example.com", - * aliases: ["*.example.com"] - * } - * }); - * ``` - * - * Now set the `domain` in the `router` prop. - * - * ```ts {5} - * { - * url: { - * router: { - * instance: router, - * domain: "api.example.com" - * } - * } - * } - * ``` - * - * Finally, to serve your function **from a combined pattern** like - * `dev.example.com/api`, you'll need to configure the domain in your `Router` to - * match the subdomain. - * - * ```ts title="sst.config.ts" {3,4} - * const router = new sst.aws.Router("Router", { - * domain: { - * name: "example.com", - * aliases: ["*.example.com"] - * } - * }); - * ``` - * - * And set the `domain` and the `path`. - * - * ```ts {5,6} - * { - * url: { - * router: { - * instance: router, - * domain: "dev.example.com", - * path: "/api/users" - * } - * } - * } - * ``` - */ - router?: Prettify; - /** - * The authorization used for the function URL. Supports [IAM authorization](https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html). - * @default `"none"` - * @example - * ```js - * { - * url: { - * authorization: "iam" - * } - * } - * ``` - */ - authorization?: Input<"none" | "iam">; - /** - * Customize the CORS (Cross-origin resource sharing) settings for the function URL. - * @default `true` - * @example - * Disable CORS. - * ```js - * { - * url: { - * cors: false - * } - * } - * ``` - * Only enable the `GET` and `POST` methods for `https://example.com`. - * ```js - * { - * url: { - * cors: { - * allowMethods: ["GET", "POST"], - * allowOrigins: ["https://example.com"] - * } - * } - * } - * ``` - */ - cors?: Input>; - } + /** + * @deprecated The `url.router` prop is now the recommended way to serve your + * function URL through a `Router` component. + */ + route?: Prettify; + /** + * Serve your function URL through a `Router` instead of a standalone Function URL. + * + * By default, this component creates a direct function URL endpoint. But you might + * want to serve it through the distribution of your `Router` as a: + * + * - A path like `/api/users` + * - A subdomain like `api.example.com` + * - Or a combined pattern like `dev.example.com/api` + * + * @example + * + * To serve your function **from a path**, you'll need to configure the root domain + * in your `Router` component. + * + * ```ts title="sst.config.ts" {2} + * const router = new sst.aws.Router("Router", { + * domain: "example.com" + * }); + * ``` + * + * Now set the `router` and the `path` in the `url` prop. + * + * ```ts {4,5} + * { + * url: { + * router: { + * instance: router, + * path: "/api/users" + * } + * } + * } + * ``` + * + * To serve your function **from a subdomain**, you'll need to configure the + * domain in your `Router` component to match both the root and the subdomain. + * + * ```ts title="sst.config.ts" {3,4} + * const router = new sst.aws.Router("Router", { + * domain: { + * name: "example.com", + * aliases: ["*.example.com"] + * } + * }); + * ``` + * + * Now set the `domain` in the `router` prop. + * + * ```ts {5} + * { + * url: { + * router: { + * instance: router, + * domain: "api.example.com" + * } + * } + * } + * ``` + * + * Finally, to serve your function **from a combined pattern** like + * `dev.example.com/api`, you'll need to configure the domain in your `Router` to + * match the subdomain. + * + * ```ts title="sst.config.ts" {3,4} + * const router = new sst.aws.Router("Router", { + * domain: { + * name: "example.com", + * aliases: ["*.example.com"] + * } + * }); + * ``` + * + * And set the `domain` and the `path`. + * + * ```ts {5,6} + * { + * url: { + * router: { + * instance: router, + * domain: "dev.example.com", + * path: "/api/users" + * } + * } + * } + * ``` + */ + router?: Prettify; + /** + * The authorization used for the function URL. Supports [IAM authorization](https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html). + * @default `"none"` + * @example + * ```js + * { + * url: { + * authorization: "iam" + * } + * } + * ``` + */ + authorization?: Input<"none" | "iam">; + /** + * Customize the CORS (Cross-origin resource sharing) settings for the function URL. + * @default `true` + * @example + * Disable CORS. + * ```js + * { + * url: { + * cors: false + * } + * } + * ``` + * Only enable the `GET` and `POST` methods for `https://example.com`. + * ```js + * { + * url: { + * cors: { + * allowMethods: ["GET", "POST"], + * allowOrigins: ["https://example.com"] + * } + * } + * } + * ``` + */ + cors?: Input>; + } >; /** * Configure how your function is bundled. @@ -1004,14 +1010,14 @@ export interface FunctionArgs { * function package. * * :::tip - * If esbuild is giving you an error about a package, try adding it to the `install` list. + * If esbuild is giving you an error about a package, try adding it to `install`. * ::: * * This will allow your functions to be able to use these dependencies when deployed. They - * just won't be tree shaken. You however still need to have them in your `package.json`. + * just won't be tree shaken. * * :::caution - * Packages listed here still need to be in your `package.json`. + * If you don't specify a version, the package still needs to be in your `package.json`. * ::: * * Esbuild will ignore them while traversing the imports in your code. So these are the @@ -1022,12 +1028,14 @@ export interface FunctionArgs { * ```js * { * nodejs: { - * install: ["pg"] + * install: { pg: "8.13.1" } * } * } * ``` + * + * Passing `["packageName"]` is the same as passing `{ packageName: "*" }`. */ - install?: Input; + install?: Input>; /** * Use this to insert a string at the beginning of the generated JS file. * @@ -1169,7 +1177,28 @@ export interface FunctionArgs { * * You can refer to [this example of using a container image](/docs/examples/#aws-lambda-python-container). */ - container?: Input; + container?: Input< + | boolean + | { + /** + * Controls whether Docker build cache is enabled. + * @default `true` + * @example + * Disable Docker build caching, useful for environments like Localstack where + * ECR cache export is not supported. + * ```js + * { + * python: { + * container: { + * cache: false + * } + * } + * } + * ``` + */ + cache?: Input; + } + >; }>; /** * Add additional files to copy into the function package. Takes a list of objects @@ -1283,6 +1312,10 @@ export interface FunctionArgs { /** * Enable versioning for the function. * + * :::note + * Durable functions enable this by default. + * ::: + * * @default `false` * @example * ```js @@ -1403,22 +1436,22 @@ export interface FunctionArgs { * ``` */ vpc?: - | Vpc - | Input<{ - /** - * A list of VPC security group IDs. - */ - securityGroups: Input[]>; - /** - * A list of VPC subnet IDs. - */ - privateSubnets: Input[]>; - /** - * A list of VPC subnet IDs. - * @deprecated Use `privateSubnets` instead. - */ - subnets?: Input[]>; - }>; + | Vpc + | Input<{ + /** + * A list of VPC security group IDs. + */ + securityGroups: Input[]>; + /** + * A list of VPC subnet IDs. + */ + privateSubnets: Input[]>; + /** + * A list of VPC subnet IDs. + * @deprecated Use `privateSubnets` instead. + */ + subnets?: Input[]>; + }>; /** * Hook into the Lambda function build process. @@ -1439,6 +1472,28 @@ export interface FunctionArgs { */ postbuild(dir: string): Promise; }; + /** + * Configure the lambda function as a [AWS durable function](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html). + * + * :::caution + * This property is meant to be used internally by [Workflow](/docs/component/aws/workflow). + * Prefer the component if you want to use the [SDK](/docs/component/aws/workflow#sdk) or if you are not very familiar with durable functions limitations. + * ::: + */ + durable?: + | boolean + | { + /** + * Maximum execution time for the durable function. + * @default `14 days` + */ + timeout?: Input; + /** + * Number of days to retain the function's execution state. + * @default `30 days` + */ + retention?: Input; + }; /** * [Transform](/docs/components#transform) how this component creates its underlying * resources. @@ -1503,7 +1558,7 @@ export interface FunctionArgs { * * ```ts title="sst.config.ts" * new sst.aws.Function("MyFunction", { - * runtime: "python3.11", + * runtime: "python3.13", * handler: "functions/src/functions/api.handler" * }); * ``` @@ -1581,11 +1636,14 @@ export interface FunctionArgs { * print(Resource.MyBucket.name) * ``` * - * Where the `sst` package can be added to your `pyproject.toml`. + * Where the `sst-sdk` package can be added to your `pyproject.toml`. * * ```toml title="functions/pyproject.toml" + * [project] + * dependencies = ["sst-sdk"] + * * [tool.uv.sources] - * sst = { git = "https://github.com/sst/sst.git", subdirectory = "sdk/python", branch = "dev" } + * sst-sdk = { git = "https://github.com/anomalyco/sst.git", subdirectory = "sdk/python", branch = "dev" } * ``` * * @@ -1654,6 +1712,7 @@ export interface FunctionArgs { */ export class Function extends Component implements Link.Linkable { private constructorName: string; + private durable: boolean; private function: Output; private role: iam.Role; private logGroup: Output; @@ -1667,10 +1726,19 @@ export class Function extends Component implements Link.Linkable { }), ); + private static readonly devBridgeCode = lazy( + () => new Map>(), + ); + public static readonly appsync = lazy(() => rpc.call("Provider.Aws.Appsync", {}), ); + /** @internal */ + public static reset() { + Function.devBridgeCode().clear(); + } + constructor( name: string, args: FunctionArgs, @@ -1678,17 +1746,23 @@ export class Function extends Component implements Link.Linkable { ) { super(__pulumiType, name, args, opts); this.constructorName = name; + this.durable = Boolean(args.durable); const parent = this; const dev = normalizeDev(); const isContainer = all([args.python, dev]).apply( - ([python, dev]) => !dev && (python?.container ?? false), + ([python, dev]) => !dev && !!python?.container, + ); + const containerCache = all([args.python]).apply(([python]) => + typeof python?.container === "object" + ? python.container.cache ?? true + : true, ); const partition = getPartitionOutput({}, opts).partition; - const region = getRegionOutput({}, opts).name; + const region = getRegionOutput({}, opts).region; const bootstrapData = region.apply((region) => bootstrap.forRegion(region)); const injections = normalizeInjections(); - const runtime = output(args.runtime ?? "nodejs20.x"); + const runtime = output(args.runtime ?? "nodejs24.x"); const timeout = normalizeTimeout(); const memory = normalizeMemory(); const storage = output(args.storage).apply((v) => v ?? "512 MB"); @@ -1699,8 +1773,10 @@ export class Function extends Component implements Link.Linkable { const volume = normalizeVolume(); const url = normalizeUrl(); const copyFiles = normalizeCopyFiles(); + const durable = normalizeDurable(); const policies = output(args.policies ?? []); const vpc = normalizeVpc(); + const nodejs = normalizeNodeJs(); const linkData = buildLinkData(); const linkPermissions = buildLinkPermissions(); @@ -1734,12 +1810,10 @@ export class Function extends Component implements Link.Linkable { Object.fromEntries(input.map((item) => [item.name, item.properties])), ), copyFiles, - properties: output({ nodejs: args.nodejs, python: args.python }).apply( - (val) => ({ - ...(val.nodejs || val.python), - architecture, - }), - ), + properties: output({ nodejs, python: args.python }).apply((val) => ({ + ...(val.nodejs || val.python), + architecture, + })), dev, }); @@ -1758,7 +1832,7 @@ export class Function extends Component implements Link.Linkable { args.handler, args.bundle, args.runtime, - args.nodejs, + nodejs, copyFiles, ]).apply( ([name, links, handler, bundle, runtime, nodejs, copyFiles]) => { @@ -1767,7 +1841,7 @@ export class Function extends Component implements Link.Linkable { links, handler: handler, bundle: bundle, - runtime: runtime || "nodejs20.x", + runtime: runtime || "nodejs24.x", copyFiles, properties: nodejs, }; @@ -1789,6 +1863,19 @@ export class Function extends Component implements Link.Linkable { ); } + function normalizeNodeJs() { + return output(args.nodejs).apply((nodejs) => + nodejs?.install && Array.isArray(nodejs.install) + ? { + ...nodejs, + install: Object.fromEntries( + nodejs.install.map((dep) => [dep, "*"]), + ), + } + : nodejs, + ); + } + function normalizeInjections() { return output(args.injections).apply((injections) => injections ?? []); } @@ -1808,7 +1895,9 @@ export class Function extends Component implements Link.Linkable { bootstrapData, Function.encryptionKey().base64, args.link, - ]).apply(async ([environment, dev, bootstrap, key, link]) => { + args.streaming, + dev.apply((dev) => dev ? Function.appsync() : undefined), + ]).apply(([environment, dev, bootstrap, key, link, streaming, appsync]) => { const result = environment ?? {}; result.SST_RESOURCE_App = JSON.stringify({ name: $app.name, @@ -1824,7 +1913,6 @@ export class Function extends Component implements Link.Linkable { result.SST_KEY = key; result.SST_KEY_FILE = "resource.enc"; if (dev) { - const appsync = await Function.appsync(); result.SST_REGION = process.env.SST_AWS_REGION!; result.SST_APPSYNC_HTTP = appsync.http; result.SST_APPSYNC_REALTIME = appsync.realtime; @@ -1835,6 +1923,9 @@ export class Function extends Component implements Link.Linkable { if (process.env.SST_FUNCTION_TIMEOUT) { result.SST_FUNCTION_TIMEOUT = process.env.SST_FUNCTION_TIMEOUT; } + if (streaming) { + result.SST_FUNCTION_STREAMING = "true"; + } } return result; }); @@ -1854,10 +1945,18 @@ export class Function extends Component implements Link.Linkable { ); } + if (args.durable && logging?.format && logging?.format != "json") { + throw new VisibleError( + `Durable functions require "logging.format" to be set to "json"`, + ); + } + + const defaultFormat = args.durable ? "json" : "text"; + return { logGroup: logging?.logGroup, retention: logging?.retention ?? "1 month", - format: logging?.format ?? "text", + format: logging?.format ?? defaultFormat, }; }); } @@ -1897,10 +1996,10 @@ export class Function extends Component implements Link.Linkable { : url.cors === true || url.cors === undefined ? defaultCors : { - ...defaultCors, - ...url.cors, - maxAge: url.cors.maxAge && toSeconds(url.cors.maxAge), - }; + ...defaultCors, + ...url.cors, + maxAge: url.cors.maxAge && toSeconds(url.cors.maxAge), + }; return { authorization, @@ -1967,6 +2066,19 @@ export class Function extends Component implements Link.Linkable { }); } + function normalizeDurable() { + if (!args.durable) return; + const config = args.durable === true ? {} : args.durable; + return { + timeout: output(config.timeout).apply((v) => + toSeconds(v ?? "14 days"), + ), + retention: output(config.retention).apply((v) => + toDays(v ?? "30 days"), + ), + }; + } + function buildLinkData() { return output(args.link || []).apply((links) => Link.build(links)); } @@ -1979,10 +2091,7 @@ export class Function extends Component implements Link.Linkable { return all([runtime, dev, isContainer]).apply( async ([runtime, dev, isContainer]) => { if (dev) { - return { - handler: "bootstrap", - bundle: path.join($cli.paths.platform, "dist", "bridge"), - }; + return resolveDevBridge(); } const buildResult = buildInput.apply(async (input) => { @@ -2004,6 +2113,22 @@ export class Function extends Component implements Link.Linkable { bundle: buildResult.out, sourcemaps: buildResult.sourcemaps, }; + + function resolveDevBridge() { + if (durable) { + return { + handler: "index.handler", + bundle: path.join($cli.paths.platform, "dist", "nodejs-bridge"), + sourcemaps: undefined, + }; + } + + return { + handler: "bootstrap", + bundle: path.join($cli.paths.platform, "dist", "bridge"), + sourcemaps: undefined, + }; + } }, ); } @@ -2076,21 +2201,21 @@ export class Function extends Component implements Link.Linkable { name: path.posix.join(handlerDir, `${newHandlerFileName}.mjs`), content: streaming ? [ - ...split.outer, - `export const ${newHandlerFunction} = awslambda.streamifyResponse(async (event, responseStream, context) => {`, - ...split.inner, - ` const { ${oldHandlerFunction}: rawHandler} = await import("./${oldHandlerFileName}${newHandlerFileExt}");`, - ` return rawHandler(event, responseStream, context);`, - `});`, - ].join("\n") + ...split.outer, + `export const ${newHandlerFunction} = awslambda.streamifyResponse(async (event, responseStream, context) => {`, + ...split.inner, + ` const { ${oldHandlerFunction}: rawHandler} = await import("./${oldHandlerFileName}${newHandlerFileExt}");`, + ` return rawHandler(event, responseStream, context);`, + `});`, + ].join("\n") : [ - ...split.outer, - `export const ${newHandlerFunction} = async (event, context) => {`, - ...split.inner, - ` const { ${oldHandlerFunction}: rawHandler} = await import("./${oldHandlerFileName}${newHandlerFileExt}");`, - ` return rawHandler(event, context);`, - `};`, - ].join("\n"), + ...split.outer, + `export const ${newHandlerFunction} = async (event, context) => {`, + ...split.inner, + ` const { ${oldHandlerFunction}: rawHandler} = await import("./${oldHandlerFileName}${newHandlerFileExt}");`, + ` return rawHandler(event, context);`, + `};`, + ].join("\n"), }, }; }, @@ -2119,20 +2244,20 @@ export class Function extends Component implements Link.Linkable { ...linkPermissions, ...(dev ? [ - { - effect: "allow", - actions: ["appsync:*"], - resources: ["*"], - }, - { - effect: "allow", - actions: ["s3:*"], - resources: [ - interpolate`arn:${partition}:s3:::${bootstrapData.asset}`, - interpolate`arn:${partition}:s3:::${bootstrapData.asset}/*`, - ], - }, - ] + { + effect: "allow", + actions: ["appsync:*"], + resources: ["*"], + }, + { + effect: "allow", + actions: ["s3:*"], + resources: [ + interpolate`arn:${partition}:s3:::${bootstrapData.asset}`, + interpolate`arn:${partition}:s3:::${bootstrapData.asset}/*`, + ], + }, + ] : []), ].map((item) => ({ effect: (() => { @@ -2153,28 +2278,29 @@ export class Function extends Component implements Link.Linkable { { assumeRolePolicy: !dev ? iam.assumeRolePolicyForPrincipal({ - Service: "lambda.amazonaws.com", - }) + Service: "lambda.amazonaws.com", + }) : iam.getPolicyDocumentOutput({ - statements: [ - { - actions: ["sts:AssumeRole"], - principals: [ - { - type: "Service", - identifiers: ["lambda.amazonaws.com"], - }, - { - type: "AWS", - identifiers: [ - interpolate`arn:${partition}:iam::${getCallerIdentityOutput({}, opts).accountId + statements: [ + { + actions: ["sts:AssumeRole"], + principals: [ + { + type: "Service", + identifiers: ["lambda.amazonaws.com"], + }, + { + type: "AWS", + identifiers: [ + interpolate`arn:${partition}:iam::${ + getCallerIdentityOutput({}, opts).accountId }:root`, - ], - }, - ], - }, - ], - }).json, + ], + }, + ], + }, + ], + }).json, // if there are no statements, do not add an inline policy. // adding an inline policy with no statements will cause an error. inlinePolicies: policy.apply(({ statements }) => @@ -2185,13 +2311,18 @@ export class Function extends Component implements Link.Linkable { ...policies, ...(logging ? [ - interpolate`arn:${partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole`, - ] + interpolate`arn:${partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole`, + ] : []), ...(vpc ? [ - interpolate`arn:${partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole`, - ] + interpolate`arn:${partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole`, + ] + : []), + ...(durable + ? [ + interpolate`arn:${partition}:iam::aws:policy/service-role/AWSLambdaBasicDurableExecutionRolePolicy`, + ] : []), ], ), @@ -2205,11 +2336,12 @@ export class Function extends Component implements Link.Linkable { // The build artifact directory already exists, with all the user code and // config files. It also has the dockerfile, we need to now just build and push to // the container registry. - return all([isContainer, dev, bundle]).apply( + return all([isContainer, dev, bundle, containerCache]).apply( ([ isContainer, dev, bundle, // We need the bundle to be resolved because of implicit dockerfiles even though we don't use it here + containerCache, ]) => { if (!isContainer || dev) return; @@ -2228,23 +2360,27 @@ export class Function extends Component implements Link.Linkable { `${name}-src`, ), }, - cacheFrom: [ - { - registry: { - ref: $interpolate`${bootstrapData.assetEcrUrl}:${name}-cache`, - }, - }, - ], - cacheTo: [ - { - registry: { - ref: $interpolate`${bootstrapData.assetEcrUrl}:${name}-cache`, - imageManifest: true, - ociMediaTypes: true, - mode: "max", - }, - }, - ], + ...(containerCache !== false + ? { + cacheFrom: [ + { + registry: { + ref: $interpolate`${bootstrapData.assetEcrUrl}:${name}-cache`, + }, + }, + ], + cacheTo: [ + { + registry: { + ref: $interpolate`${bootstrapData.assetEcrUrl}:${name}-cache`, + imageManifest: true, + ociMediaTypes: true, + mode: "max", + }, + }, + ], + } + : {}), platforms: [ architecture.apply((v) => v === "arm64" ? "linux/arm64" : "linux/amd64", @@ -2278,6 +2414,7 @@ export class Function extends Component implements Link.Linkable { isContainer, logGroup.apply((l) => l?.arn), dev, + region, ]).apply( async ([ bundle, @@ -2287,118 +2424,143 @@ export class Function extends Component implements Link.Linkable { isContainer, logGroupArn, dev, + regionName, ]) => { if (isContainer) return; - const zipPath = path.resolve( - $cli.paths.work, - "artifacts", - name, - "code.zip", - ); - await fs.promises.mkdir(path.dirname(zipPath), { - recursive: true, - }); + if (dev) { + const cacheKey = `${regionName}:${bundle}`; + const cache = Function.devBridgeCode(); + const existing = cache.get(cacheKey); + if (existing) return existing; + + const created = createCode(); + cache.set(cacheKey, created); + created.catch(() => cache.delete(cacheKey)); + return created; + } - await new Promise(async (resolve, reject) => { - const ws = fs.createWriteStream(zipPath); - const archive = archiver("zip", { - // Ensure deterministic zip file hashes - // https://github.com/archiverjs/node-archiver/issues/397#issuecomment-554327338 - statConcurrency: 1, - }); - archive.on("warning", reject); - archive.on("error", reject); - // archive has been finalized and the output file descriptor has closed, resolve promise - // this has to be done before calling `finalize` since the events may fire immediately after. - // see https://www.npmjs.com/package/archiver - ws.once("close", () => { - resolve(zipPath); - }); - archive.pipe(ws); + return createCode(); - const files = []; + async function createCode() { + const zipPath = path.resolve( + $cli.paths.work, + "artifacts", + dev + ? `dev-bridge-${regionName}-${logicalName(path.basename(bundle))}` + : name, + "code.zip", + ); + await fs.promises.mkdir(path.dirname(zipPath), { + recursive: true, + }); - for (const item of [ - { - from: bundle, - to: ".", - isDir: true, - }, - ...(!dev ? copyFiles : []), - ]) { - if (!item.isDir) { - files.push({ - from: item.from, - to: item.to, - }); - } - const found = await glob("**", { - cwd: item.from, - dot: true, - ignore: - sourcemaps?.map((item) => path.relative(bundle, item)) || [], + await new Promise(async (resolve, reject) => { + const ws = fs.createWriteStream(zipPath); + const archive = archiver("zip", { + // Ensure deterministic zip file hashes + // https://github.com/archiverjs/node-archiver/issues/397#issuecomment-554327338 + statConcurrency: 1, }); - files.push( - ...found.map((file) => ({ - from: path.join(item.from, file), - to: path.join(item.to, file), - })), - ); - } - files.sort((a, b) => a.to.localeCompare(b.to)); - for (const file of files) { - archive.file(file.from, { - name: file.to, - date: new Date(0), + archive.on("warning", reject); + archive.on("error", reject); + // archive has been finalized and the output file descriptor has closed, resolve promise + // this has to be done before calling `finalize` since the events may fire immediately after. + // see https://www.npmjs.com/package/archiver + ws.once("close", () => { + resolve(zipPath); }); - } + archive.pipe(ws); - // Add handler wrapper into the zip - if (wrapper) { - archive.append(wrapper.content, { - name: wrapper.name, - date: new Date(0), - }); - } + const files = []; - await archive.finalize(); - }); - - // Calculate hash of the zip file - const hash = crypto.createHash("sha256"); - hash.update(await fs.promises.readFile(zipPath, "utf-8")); - const hashValue = hash.digest("hex"); - const assetBucket = region.apply((region) => - bootstrap.forRegion(region).then((d) => d.asset), - ); - if (logGroupArn && sourcemaps) { - let index = 0; - for (const file of sourcemaps) { - new s3.BucketObjectv2( - `${name}Sourcemap${index}`, + for (const item of [ { - key: interpolate`sourcemap/${logGroupArn}/${hashValue}.${path.basename( - file, - )}`, - bucket: assetBucket, - source: new asset.FileAsset(file), + from: bundle, + to: ".", + isDir: true, }, - { parent, retainOnDelete: true }, - ); - index++; + ...(!dev ? copyFiles : []), + ]) { + if (!item.isDir) { + files.push({ + from: item.from, + to: item.to, + }); + } + const found = await glob("**", { + cwd: item.from, + dot: true, + ignore: + sourcemaps?.map((item) => path.relative(bundle, item)) || + [], + }); + files.push( + ...found.map((file) => ({ + from: path.join(item.from, file), + to: path.join(item.to, file), + })), + ); + } + files.sort((a, b) => a.to.localeCompare(b.to)); + for (const file of files) { + archive.file(file.from, { + name: file.to, + date: new Date(0), + }); + } + + // Add handler wrapper into the zip + if (wrapper) { + archive.append(wrapper.content, { + name: wrapper.name, + date: new Date(0), + }); + } + + await archive.finalize(); + }); + + const hash = crypto.createHash("sha256"); + hash.update(await fs.promises.readFile(zipPath)); + const hashValue = hash.digest("hex"); + const assetBucket = region.apply((region) => + bootstrap.forRegion(region).then((d) => d.asset), + ); + if (logGroupArn && sourcemaps) { + let index = 0; + for (const file of sourcemaps) { + new s3.BucketObjectv2( + `${name}Sourcemap${index}`, + { + key: interpolate`sourcemap/${logGroupArn}/${hashValue}.${path.basename( + file, + )}`, + bucket: assetBucket, + source: new asset.FileAsset(file), + }, + { parent, retainOnDelete: true }, + ); + index++; + } } - } - return new s3.BucketObjectv2( - `${name}Code`, - { - key: interpolate`assets/${name}-code-${hashValue}.zip`, - bucket: assetBucket, - source: new asset.FileArchive(zipPath), - }, - { parent }, - ); + return new s3.BucketObjectv2( + dev + ? `DevBridgeCode${logicalName(regionName)}${logicalName(path.basename(bundle))}` + : `${name}Code`, + { + key: dev + ? `assets/dev-bridge-code-${hashValue}.zip` + : interpolate`assets/${name}-code-${hashValue}.zip`, + bucket: assetBucket, + source: new asset.FileArchive(zipPath), + }, + dev + ? { parent: rootStackResource, provider: opts?.provider } + : { parent }, + ); + } }, ); } @@ -2413,8 +2575,9 @@ export class Function extends Component implements Link.Linkable { args.transform?.logGroup, `${name}LogGroup`, { - name: interpolate`/aws/lambda/${args.name ?? physicalName(64, `${name}Function`) - }`, + name: interpolate`/aws/lambda/${ + args.name ?? physicalName(64, `${name}Function`) + }`, retentionInDays: RETENTION[logging.retention], }, { parent, ignoreChanges: ["name"] }, @@ -2474,38 +2637,44 @@ export class Function extends Component implements Link.Linkable { }, layers: args.layers, tags: args.tags, - publish: output(args.versioning).apply((v) => v ?? false), + publish: output(args.versioning).apply( + (v) => v ?? Boolean(args.durable), + ), reservedConcurrentExecutions: concurrency?.reserved, + durableConfig: durable && { + executionTimeout: durable.timeout, + retentionPeriod: durable.retention, + }, ...(isContainer ? { - packageType: "Image", - imageUri: imageAsset!.ref.apply( - (ref) => ref?.replace(":latest", ""), - ), - imageConfig: { - commands: [ - all([handler, runtime]).apply(([handler, runtime]) => { - // If a python container image we have to rewrite the handler path so lambdaric is happy - // This means no leading . and replace all / with . - if (isContainer && runtime.includes("python")) { - return handler - .replace(/\.\//g, "") - .replace(/\//g, "."); - } - return handler; - }), - ], - }, - } + packageType: "Image", + imageUri: imageAsset!.ref.apply( + (ref) => ref?.replace(":latest", ""), + ), + imageConfig: { + commands: [ + all([handler, runtime]).apply(([handler, runtime]) => { + // If a python container image we have to rewrite the handler path so lambdaric is happy + // This means no leading . and replace all / with . + if (isContainer && runtime.includes("python")) { + return handler + .replace(/\.\//g, "") + .replace(/\//g, "."); + } + return handler; + }), + ], + }, + } : { - packageType: "Zip", - s3Bucket: zipAsset!.bucket, - s3Key: zipAsset!.key, - handler: unsecret(handler), - runtime: runtime.apply((v) => - v === "go" || v === "rust" ? "provided.al2023" : v, - ), - }), + packageType: "Zip", + s3Bucket: zipAsset!.bucket, + s3Key: zipAsset!.key, + handler: unsecret(handler), + runtime: runtime.apply((v) => + v === "go" || v === "rust" ? "provided.al2023" : v, + ), + }), }, { parent }, ); @@ -2515,18 +2684,26 @@ export class Function extends Component implements Link.Linkable { ...transformed[1], ...(dev ? { - description: transformed[1].description - ? output(transformed[1].description).apply( - (v) => `${v.substring(0, 240)} (live)`, - ) - : "live", - runtime: "provided.al2023", - architectures: ["x86_64"], - } + description: transformed[1].description + ? output(transformed[1].description).apply( + (v) => `${v.substring(0, 240)} (live)`, + ) + : "live", + runtime: resolveDevRuntime(), + architectures: ["x86_64"], + } : {}), }, transformed[2], ); + + function resolveDevRuntime() { + if (durable) { + return "nodejs24.x"; + } + + return "provided.al2023"; + } }, ); } @@ -2535,12 +2712,35 @@ export class Function extends Component implements Link.Linkable { return url.apply((url) => { if (url === undefined) return output(undefined); - // create the function url + const authorization = output(url.authorization ?? "none"); + const isOac = output(url.route?.routerProtection).apply( + (p) => p?.mode === "oac" || p?.mode === "oac-with-edge-signing", + ); + const isIam = all([isOac, authorization]).apply( + ([oac, authorization]) => oac || authorization === "iam", + ); + + /** + * Lambda Function URLs only accept alias names in the explicit `qualifier` + * field. Durable functions with URLs therefore need an alias target here, + * even when the underlying function is still on `$LATEST`. + * See https://github.com/hashicorp/terraform-provider-aws/issues/31459 + */ + const qualifier = durable + ? new lambda.Alias(`${name}Durable`, { + functionName: fn.arn, + functionVersion: fn.version, + }).name + : undefined; + const fnUrl = new lambda.FunctionUrl( `${name}Url`, { - functionName: fn.name, - authorizationType: url.authorization === "iam" ? "AWS_IAM" : "NONE", + functionName: durable ? fn.arn : fn.name, + qualifier, + authorizationType: isIam.apply((isIam) => + isIam ? "AWS_IAM" : "NONE", + ), invokeMode: streaming.apply((streaming) => streaming ? "RESPONSE_STREAM" : "BUFFERED", ), @@ -2548,18 +2748,84 @@ export class Function extends Component implements Link.Linkable { }, { parent }, ); - if (url.authorization === "none") { - new lambda.Permission( - `${name}InvokeFunction`, - { - action: "lambda:InvokeFunction", - function: fn.name, - principal: "*", - }, - { parent }, - ); + + if (!url.route) { + authorization.apply((authorization) => { + if (authorization !== "none") return; + + new lambda.Permission( + `${name}PublicFunctionUrlAccess`, + { + action: "lambda:InvokeFunctionUrl", + function: fn.name, + principal: "*", + functionUrlAuthType: "NONE", + }, + { parent }, + ); + new lambda.Permission( + `${name}InvokeFunction`, + { + action: "lambda:InvokeFunction", + function: fn.name, + principal: "*", + invokedViaFunctionUrl: true, + }, + { parent }, + ); + }); + return fnUrl.functionUrl; } - if (!url.route) return fnUrl.functionUrl; + + // Create permissions based on Router protection mode + all([isOac, authorization, url.route.routerDistributionArn]).apply( + ([oac, authorization, distributionArn]) => { + if (oac && distributionArn) { + new lambda.Permission( + `${name}CloudFrontFunctionUrlAccess`, + { + action: "lambda:InvokeFunctionUrl", + function: fn.name, + principal: "cloudfront.amazonaws.com", + sourceArn: distributionArn, + }, + { parent }, + ); + new lambda.Permission( + `${name}CloudFrontInvokeFunction`, + { + action: "lambda:InvokeFunction", + function: fn.name, + principal: "cloudfront.amazonaws.com", + sourceArn: distributionArn, + invokedViaFunctionUrl: true, + }, + { parent }, + ); + } else if (authorization === "none") { + new lambda.Permission( + `${name}PublicFunctionUrlAccess`, + { + action: "lambda:InvokeFunctionUrl", + function: fn.name, + principal: "*", + functionUrlAuthType: "NONE", + }, + { parent }, + ); + new lambda.Permission( + `${name}PublicInvokeFunction`, + { + action: "lambda:InvokeFunction", + function: fn.name, + principal: "*", + invokedViaFunctionUrl: true, + }, + { parent }, + ); + } + }, + ); // add router route const routeNamespace = crypto @@ -2572,11 +2838,40 @@ export class Function extends Component implements Link.Linkable { { store: url.route.routerKvStoreArn, namespace: routeNamespace, - entries: fnUrl.functionUrl.apply((fnUrl) => ({ - metadata: JSON.stringify({ - host: new URL(fnUrl).host, - }), - })), + entries: all([fnUrl.functionUrl, isOac, url.route]).apply( + ([fnUrlValue, oac, route]) => { + const timeouts = [ + "connectionTimeout" as const, + "readTimeout" as const, + "keepAliveTimeout" as const, + ].flatMap((k) => { + const value = route[k]; + return value ? [[k, toSeconds(value)]] : []; + }); + return { + metadata: JSON.stringify({ + host: new URL(fnUrlValue).host, + rewrite: route.rewrite, + origin: { + ...(oac + ? { + originAccessControlConfig: { + enabled: true, + signingBehavior: "always", + signingProtocol: "sigv4", + originType: "lambda", + }, + } + : {}), + connectionAttempts: route.connectionAttempts, + ...(timeouts.length + ? { timeouts: Object.fromEntries(timeouts) } + : {}), + }, + }), + }; + }, + ), purge: false, }, { parent }, @@ -2696,6 +2991,51 @@ export class Function extends Component implements Link.Linkable { return this.function.arn; } + /** @internal */ + private get useQualifiedTarget() { + return this.function.publish.apply( + (publish) => (publish ?? false) || this.durable, + ); + } + + /** @internal */ + public get targetArn() { + return this.useQualifiedTarget.apply((useQualifiedTarget) => + useQualifiedTarget ? this.function.qualifiedArn : this.arn, + ); + } + + /** @internal */ + public get qualifier() { + return this.targetArn.apply( + (arn) => splitQualifiedFunctionArn(arn).qualifier, + ); + } + + /** @internal */ + public get targetInvokeArn() { + return this.useQualifiedTarget.apply((useQualifiedTarget) => + useQualifiedTarget + ? this.function.qualifiedInvokeArn + : this.function.invokeArn, + ); + } + + /** @internal */ + public get targetResponseStreamingInvokeArn() { + return this.useQualifiedTarget.apply((useQualifiedTarget) => + useQualifiedTarget + ? all([ + this.arn, + this.function.qualifiedArn, + this.function.responseStreamingInvokeArn, + ]).apply(([arn, qualifiedArn, responseStreamingInvokeArn]) => + responseStreamingInvokeArn.replace(arn, qualifiedArn), + ) + : this.function.responseStreamingInvokeArn, + ); + } + /** * Add environment variables lazily to the function after the function is created. * @@ -2724,7 +3064,8 @@ export class Function extends Component implements Link.Linkable { { functionName: this.name, environment, - region: getRegionOutput(undefined, { parent: this }).name, + region: getRegionOutput(undefined, { parent: this }).region, + functionLastModified: this.function.lastModified, }, { parent: this }, ); @@ -2778,17 +3119,34 @@ export class Function extends Component implements Link.Linkable { properties: { name: this.name, url: this.urlEndpoint, + ...(this.durable + ? { + qualifier: this.qualifier, + } + : {}), }, include: [ permission({ - actions: ["lambda:InvokeFunction"], - resources: [this.function.arn], + actions: [ + "lambda:InvokeFunction", + ...(this.durable + ? [ + "lambda:ListDurableExecutionsByFunction", + "lambda:GetDurableExecution", + "lambda:GetDurableExecutionHistory", + "lambda:StopDurableExecution", + "lambda:SendDurableExecutionCallbackSuccess", + "lambda:SendDurableExecutionCallbackFailure", + "lambda:SendDurableExecutionCallbackHeartbeat", + ] + : []), + ], + resources: [this.durable ? interpolate`${this.arn}:*` : this.arn], }), ], }; } } - const __pulumiType = "sst:aws:Function"; // @ts-expect-error Function.__pulumiType = __pulumiType; diff --git a/platform/src/components/aws/helpers/arn.ts b/platform/src/components/aws/helpers/arn.ts index d5636efb6d..e5cf4f4a05 100644 --- a/platform/src/components/aws/helpers/arn.ts +++ b/platform/src/components/aws/helpers/arn.ts @@ -11,6 +11,20 @@ export function parseFunctionArn(arn: string) { return { functionName }; } +export function splitQualifiedFunctionArn(arn: string) { + // Unqualified: arn:aws:lambda:region:account-id:function:function-name (7 parts) + // Qualified: arn:aws:lambda:region:account-id:function:function-name:alias-or-version (8 parts) + const parts = arn.split(":"); + if (parts.length <= 7) { + return { unqualifiedArn: arn, qualifier: undefined }; + } + return { + unqualifiedArn: parts.slice(0, 7).join(":"), + qualifier: parts[7], + }; +} + + export function parseBucketArn(arn: string) { // arn:aws:s3:::bucket-name const bucketName = arn.split(":")[5]; @@ -137,3 +151,32 @@ export function parseOpenSearch(arn: string) { ); return { tableName }; } + +export function parseDsqlPublicEndpoint(arn: string) { + const parts = arn.split(":"); + const region = parts[3]; + const clusterId = parts[5]?.split("/")[1]; + if (!arn.startsWith("arn:") || !clusterId) + throw new VisibleError( + `The provided ARN "${arn}" is not a DSQL cluster ARN.`, + ); + return `${clusterId}.dsql.${region}.on.aws`; +} + +export function parseDsqlPrivateEndpoint( + clusterArn: string, + dnsEntries: { dnsName?: string }[], +) { + const clusterId = clusterArn.split(":")[5]?.split("/")[1]; + if (!clusterArn.startsWith("arn:") || !clusterId) + throw new VisibleError( + `The provided ARN "${clusterArn}" is not a DSQL cluster ARN.`, + ); + const wildcardEntry = dnsEntries.find((e) => e.dnsName?.startsWith("*.")); + const privateDnsName = wildcardEntry?.dnsName ?? dnsEntries[0]?.dnsName; + if (!privateDnsName) + throw new VisibleError( + `The VPC endpoint has no DNS entries.`, + ); + return privateDnsName.replace("*", clusterId); +} diff --git a/platform/src/components/aws/helpers/container-builder.ts b/platform/src/components/aws/helpers/container-builder.ts index b2e1101b7d..624e64e024 100644 --- a/platform/src/components/aws/helpers/container-builder.ts +++ b/platform/src/components/aws/helpers/container-builder.ts @@ -1,4 +1,4 @@ -import { all, ComponentResourceOptions } from "@pulumi/pulumi"; +import { output, ComponentResourceOptions } from "@pulumi/pulumi"; import { Semaphore } from "../../../util/semaphore"; import { Image, ImageArgs } from "@pulumi/docker-build"; @@ -11,9 +11,10 @@ export function imageBuilder( args: ImageArgs, opts?: ComponentResourceOptions, ) { - // Wait for the all args values to be resolved before acquiring the semaphore - return all([args]).apply(async ([args]) => { + // Wait for all arg values to be resolved before acquiring the semaphore. + return output(args).apply(async (args) => { await limiter.acquire(name); + const image = new Image( name, { diff --git a/platform/src/components/aws/helpers/function-builder.ts b/platform/src/components/aws/helpers/function-builder.ts index c063637145..400630b608 100644 --- a/platform/src/components/aws/helpers/function-builder.ts +++ b/platform/src/components/aws/helpers/function-builder.ts @@ -5,30 +5,63 @@ import { Output, output, } from "@pulumi/pulumi"; -import { Function, FunctionArn, FunctionArgs } from "../function"; +import { Function, FunctionArgs, FunctionArn } from "../function.js"; +import { Workflow } from "../workflow.js"; import { transform, Transform } from "../../component"; import { VisibleError } from "../../error"; +import { splitQualifiedFunctionArn } from "./arn.js"; export type FunctionBuilder = Output<{ getFunction: () => Function; arn: Output; - invokeArn: Output; + targetArn: Output; + qualifier: Output; + targetInvokeArn: Output; + targetResponseStreamingInvokeArn: Output; }>; export function functionBuilder( name: string, - definition: Input, + definition: Input, defaultArgs: Pick< FunctionArgs, - "description" | "link" | "environment" | "permissions" | "url" | "_skipHint" + | "description" + | "link" + | "environment" + | "permissions" + | "url" + | "streaming" + | "_skipHint" >, argsTransform?: Transform, opts?: ComponentResourceOptions, ): FunctionBuilder { + function buildResult(fn: Function) { + return { + getFunction: () => fn, + arn: fn.arn, + targetArn: fn.targetArn, + qualifier: fn.qualifier, + targetInvokeArn: fn.targetInvokeArn, + targetResponseStreamingInvokeArn: fn.targetResponseStreamingInvokeArn, + }; + } + return output(definition).apply((definition) => { + if (definition instanceof Workflow) { + return buildResult(definition.getFunction()); + } + + if (definition instanceof Function) { + return buildResult(definition); + } + if (typeof definition === "string") { // Case 1: The definition is an ARN if (definition.startsWith("arn:")) { + const { unqualifiedArn, qualifier } = splitQualifiedFunctionArn( + definition, + ); const parts = definition.split(":"); return { getFunction: () => { @@ -36,10 +69,15 @@ export function functionBuilder( "Cannot access the created function because it is referenced as an ARN.", ); }, - arn: output(definition), - invokeArn: output( + arn: output(unqualifiedArn), + targetArn: output(definition), + qualifier: output(qualifier), + targetInvokeArn: output( `arn:${parts[1]}:apigateway:${parts[3]}:lambda:path/2015-03-31/functions/${definition}/invocations`, ), + targetResponseStreamingInvokeArn: output( + `arn:${parts[1]}:apigateway:${parts[3]}:lambda:path/2021-11-15/functions/${definition}/response-streaming-invocations`, + ), }; } @@ -52,11 +90,7 @@ export function functionBuilder( opts || {}, ), ); - return { - getFunction: () => fn, - arn: fn.arn, - invokeArn: fn.nodes.function.invokeArn, - }; + return buildResult(fn); } // Case 3: The definition is a FunctionArgs @@ -92,11 +126,7 @@ export function functionBuilder( opts || {}, ), ); - return { - getFunction: () => fn, - arn: fn.arn, - invokeArn: fn.nodes.function.invokeArn, - }; + return buildResult(fn); } throw new Error(`Invalid function definition for the "${name}" Function`); }); diff --git a/platform/src/components/aws/helpers/load-balancer.ts b/platform/src/components/aws/helpers/load-balancer.ts new file mode 100644 index 0000000000..783c18a1f9 --- /dev/null +++ b/platform/src/components/aws/helpers/load-balancer.ts @@ -0,0 +1,17 @@ +/** + * Build a listener key from protocol and port (e.g. "HTTPS443"). + */ +export function listenerKey(protocol: string, port: number): string { + return `${protocol.toUpperCase()}${port}`; +} + +/** + * Build a target group key from container name, protocol and port (e.g. "appHTTP3000"). + */ +export function targetKey( + container: string, + protocol: string, + port: number, +): string { + return `${container}${protocol.toUpperCase()}${port}`; +} diff --git a/platform/src/components/aws/helpers/site-builder.ts b/platform/src/components/aws/helpers/site-builder.ts index c15dc8127c..0a4283fffb 100644 --- a/platform/src/components/aws/helpers/site-builder.ts +++ b/platform/src/components/aws/helpers/site-builder.ts @@ -1,4 +1,4 @@ -import { all, CustomResourceOptions } from "@pulumi/pulumi"; +import { output, CustomResourceOptions } from "@pulumi/pulumi"; import { Semaphore } from "../../../util/semaphore"; import { local } from "@pulumi/command"; @@ -11,8 +11,8 @@ export function siteBuilder( args: local.CommandArgs, opts?: CustomResourceOptions, ) { - // Wait for the all args values to be resolved before acquiring the semaphore - return all([args]).apply(async ([args]) => { + // Wait for all arg values to be resolved before acquiring the semaphore. + return output(args).apply(async (args) => { await limiter.acquire(name); let waitOn; @@ -20,9 +20,9 @@ export function siteBuilder( const command = new local.Command(name, args, opts); waitOn = command.urn; - // When running `sst diff`, `local.Command`'s `create` and `update` are not called. - // So we will also run `local.runOutput` to get the output of the command. - if ($cli.command === "diff") { + // When running `sst diff` or `sst refresh`, `local.Command`'s `create` and `update` are not called. + // So we also run `local.runOutput` to get the output of the command. + if ($cli.command === "diff" || $cli.command === "refresh") { waitOn = local.runOutput( { command: args.create!, diff --git a/platform/src/components/aws/helpers/subscriber.ts b/platform/src/components/aws/helpers/subscriber.ts index cda92c5ebf..a3c8fa5ac2 100644 --- a/platform/src/components/aws/helpers/subscriber.ts +++ b/platform/src/components/aws/helpers/subscriber.ts @@ -1,5 +1,5 @@ import { Input, output } from "@pulumi/pulumi"; -import { FunctionArgs, FunctionArn } from "../function"; +import { FunctionArgs, FunctionArn } from "../function.js"; import { Queue } from "../queue"; export function isFunctionSubscriber( diff --git a/platform/src/components/aws/https-redirect.ts b/platform/src/components/aws/https-redirect.ts index 1fa897e0a5..c3cfc4da66 100644 --- a/platform/src/components/aws/https-redirect.ts +++ b/platform/src/components/aws/https-redirect.ts @@ -2,7 +2,7 @@ import { ComponentResourceOptions, all, output } from "@pulumi/pulumi"; import { DnsValidatedCertificate } from "./dns-validated-certificate.js"; import { Bucket } from "./bucket.js"; import { Component } from "../component.js"; -import { logicalName } from "../naming.js"; +import { logicalName, physicalName } from "../naming.js"; import { useProvider } from "./helpers/provider.js"; import { Input } from "../input.js"; import { Dns } from "../dns.js"; @@ -85,7 +85,7 @@ export class HttpsRedirect extends Component { } function createBucketWebsite() { - return new s3.BucketWebsiteConfigurationV2( + return new s3.BucketWebsiteConfiguration( `${name}BucketWebsite`, { bucket: bucket.name, @@ -140,6 +140,7 @@ export class HttpsRedirect extends Component { functionArn: new cloudfront.Function( `${name}CloudfrontFunctionRequest`, { + name: physicalName(64, `${name}CloudfrontFunctionRequest`), runtime: "cloudfront-js-2.0", code: ` import cf from "cloudfront"; @@ -148,6 +149,7 @@ async function handler(event) { return event.request; }`, }, + { ignoreChanges: ["name"] }, ).arn, }, ], diff --git a/platform/src/components/aws/index.ts b/platform/src/components/aws/index.ts index ae22d23ef6..78bd610e70 100644 --- a/platform/src/components/aws/index.ts +++ b/platform/src/components/aws/index.ts @@ -1,3 +1,4 @@ +export * from "./alb.js"; export * from "./analog.js"; export * from "./apigatewayv1.js"; export * from "./apigatewayv2.js"; @@ -12,7 +13,9 @@ export * from "./cluster.js"; export * from "./cognito-identity-pool.js"; export * from "./cognito-user-pool.js"; export * from "./cron.js"; +export * from "./cron-v2.js"; export * from "./dns.js"; +export * from "./dsql.js"; export * from "./dynamo.js"; export * from "./efs.js"; export * from "./email.js"; @@ -40,6 +43,7 @@ export * from "./static-site.js"; export * from "./svelte-kit.js"; export * from "./vector.js"; export * from "./vpc.js"; +export * from "./workflow.js"; export { linkable } from "./linkable.js"; export { permission } from "./permission.js"; export { iamEdit } from "./iam-edit.js"; diff --git a/platform/src/components/aws/kinesis-stream-lambda-subscriber.ts b/platform/src/components/aws/kinesis-stream-lambda-subscriber.ts index d0f1c79efa..4d8770c41b 100644 --- a/platform/src/components/aws/kinesis-stream-lambda-subscriber.ts +++ b/platform/src/components/aws/kinesis-stream-lambda-subscriber.ts @@ -5,7 +5,6 @@ import { Input } from "../input.js"; import { FunctionArgs } from "./function.js"; import { KinesisStreamLambdaSubscriberArgs } from "./kinesis-stream.js"; import { FunctionBuilder, functionBuilder } from "./helpers/function-builder"; -import { parseFunctionArn } from "./helpers/arn"; export interface Args extends KinesisStreamLambdaSubscriberArgs { /** @@ -82,9 +81,7 @@ export class KinesisStreamLambdaSubscriber extends Component { `${name}EventSourceMapping`, { eventSourceArn: stream.arn, - functionName: fn.arn.apply( - (arn) => parseFunctionArn(arn).functionName, - ), + functionName: fn.targetArn, startingPosition: "LATEST", filterCriteria: args.filters && { filters: output(args.filters).apply((filters) => diff --git a/platform/src/components/aws/kinesis-stream.ts b/platform/src/components/aws/kinesis-stream.ts index 954ff5e588..6e0c23e81a 100644 --- a/platform/src/components/aws/kinesis-stream.ts +++ b/platform/src/components/aws/kinesis-stream.ts @@ -1,6 +1,5 @@ -import * as aws from "@pulumi/aws"; - import { ComponentResourceOptions, Output, all, output } from "@pulumi/pulumi"; +import { kinesis, lambda } from "@pulumi/aws"; import { Component, Transform, transform } from "../component.js"; import { Input } from "../input.js"; import { Link } from "../link.js"; @@ -20,7 +19,7 @@ export interface KinesisStreamArgs { /** * Transform the Kinesis stream resource. */ - stream?: Transform; + stream?: Transform; }; } @@ -72,7 +71,7 @@ export interface KinesisStreamLambdaSubscriberArgs { /** * Transform the Lambda Event Source Mapping resource. */ - eventSourceMapping?: Transform; + eventSourceMapping?: Transform; }; } @@ -121,7 +120,7 @@ export interface KinesisStreamLambdaSubscriberArgs { export class KinesisStream extends Component implements Link.Linkable { private constructorName: string; private constructorOpts: ComponentResourceOptions; - private stream: aws.kinesis.Stream; + private stream: kinesis.Stream; constructor( name: string, @@ -137,7 +136,7 @@ export class KinesisStream extends Component implements Link.Linkable { this.constructorOpts = opts; function createStream() { - return new aws.kinesis.Stream( + return new kinesis.Stream( ...transform( args?.transform?.stream, `${name}Stream`, diff --git a/platform/src/components/aws/linkable.ts b/platform/src/components/aws/linkable.ts index d50241f9b5..f46a2c217d 100644 --- a/platform/src/components/aws/linkable.ts +++ b/platform/src/components/aws/linkable.ts @@ -1,5 +1,5 @@ import { VisibleError } from "../error"; -import { FunctionPermissionArgs } from "./function"; +import { FunctionPermissionArgs } from "./function.js"; export const URL_UNAVAILABLE = "http://url-unavailable-in-dev.mode"; diff --git a/platform/src/components/aws/mysql.ts b/platform/src/components/aws/mysql.ts index 627301e6da..c6b598dada 100644 --- a/platform/src/components/aws/mysql.ts +++ b/platform/src/components/aws/mysql.ts @@ -21,6 +21,12 @@ import { RdsRoleLookup } from "./providers/rds-role-lookup"; export interface MysqlArgs { /** * The MySQL engine version. Check out the [available versions in your region](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/MySQL.Concepts.VersionMgmt.html). + * + * :::caution + * Changing the version will cause the database to restart on the next `sst deploy`, + * causing downtime. Learn more about [upgrading databases](/docs/upgrade-aws-databases/). + * ::: + * * @default `"8.0.40"` * @example * ```js @@ -33,7 +39,7 @@ export interface MysqlArgs { /** * The username of the master user. * - * :::caution + * :::danger * Changing the username will cause the database to be destroyed and recreated. * ::: * @@ -72,6 +78,10 @@ export interface MysqlArgs { * underscores. By default, it takes the name of the app, and replaces the hyphens with * underscores. * + * :::danger + * Changing the database name will cause the database to be destroyed and recreated. + * ::: + * * @default Based on the name of the current app * @example * ```js @@ -84,6 +94,11 @@ export interface MysqlArgs { /** * The type of instance to use for the database. Check out the [supported instance types](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.DBInstanceClass.Types.html). * + * :::caution + * Changing the instance type will cause the database to restart on the next `sst deploy`, + * causing downtime. Learn more about [upgrading databases](/docs/upgrade-aws-databases/). + * ::: + * * @default `"t4g.micro"` * @example * ```js @@ -91,10 +106,6 @@ export interface MysqlArgs { * instance: "m7g.xlarge" * } * ``` - * - * By default, these changes are not applied immediately by RDS. Instead, they are - * applied in the next maintenance window. Check out the [full list](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_ModifyInstance.Settings.html) - * of props that are not applied immediately. */ instance?: Input; /** @@ -135,53 +146,53 @@ export interface MysqlArgs { proxy?: Input< | boolean | { - /** - * Additional credentials the proxy can use to connect to the database. You don't - * need to specify the master user credentials as they are always added by default. - * - * :::note - * This component will not create the MySQL users listed here. You need to - * create them manually in the database. - * ::: - * - * @example - * ```js - * { - * credentials: [ - * { - * username: "metabase", - * password: "Passw0rd!" - * } - * ] - * } - * ``` - * - * You can use a `Secret` to manage the password. - * - * ```js - * { - * credentials: [ - * { - * username: "metabase", - * password: new sst.Secret("MyDBPassword").value - * } - * ] - * } - * ``` - */ - credentials?: Input< - Input<{ - /** - * The username of the user. - */ - username: Input; - /** - * The password of the user. - */ - password: Input; - }>[] - >; - } + /** + * Additional credentials the proxy can use to connect to the database. You don't + * need to specify the master user credentials as they are always added by default. + * + * :::note + * This component will not create the MySQL users listed here. You need to + * create them manually in the database. + * ::: + * + * @example + * ```js + * { + * credentials: [ + * { + * username: "metabase", + * password: "Passw0rd!" + * } + * ] + * } + * ``` + * + * You can use a `Secret` to manage the password. + * + * ```js + * { + * credentials: [ + * { + * username: "metabase", + * password: new sst.Secret("MyDBPassword").value + * } + * ] + * } + * ``` + */ + credentials?: Input< + Input<{ + /** + * The username of the user. + */ + username: Input; + /** + * The password of the user. + */ + password: Input; + }>[] + >; + } >; /** * Enable [Multi-AZ](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html) @@ -206,6 +217,24 @@ export interface MysqlArgs { * ``` */ multiAz?: Input; + /** + * Enable [Blue/Green deployments](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/blue-green-deployments.html) + * for version, instance type, and parameter group upgrades. + * Learn more about [upgrading databases](/docs/upgrade-aws-databases/). + * + * When enabled, a staging (green) instance is created, updated, + * verified, then promoted to replace the production (blue) instance. + * This minimizes downtime during upgrades. + * + * @default `false` + * @example + * ```js + * { + * blueGreen: true + * } + * ``` + */ + blueGreen?: Input; /** * @internal */ @@ -237,13 +266,13 @@ export interface MysqlArgs { * ``` */ vpc: - | Vpc - | Input<{ - /** - * A list of subnet IDs in the VPC. - */ - subnets: Input[]>; - }>; + | Vpc + | Input<{ + /** + * A list of subnet IDs in the VPC. + */ + subnets: Input[]>; + }>; /** * Configure how this component works in `sst dev`. * @@ -468,6 +497,7 @@ export class Mysql extends Component implements Link.Linkable { const instanceType = output(args.instance).apply((v) => v ?? "t4g.micro"); const username = output(args.username).apply((v) => v ?? "root"); const storage = normalizeStorage(); + const blueGreen = output(args.blueGreen).apply((v) => v ?? false); const dbName = output(args.database).apply( (v) => v ?? $app.name.replaceAll("-", "_"), ); @@ -497,7 +527,7 @@ export class Mysql extends Component implements Link.Linkable { parent: self, }); - const input = instance.tags.apply((tags) => { + const input = instance.tagsAll.apply((tags) => { return { proxyId: output(ref.proxyId), passwordTag: tags?.["sst:ref:password"], @@ -507,8 +537,8 @@ export class Mysql extends Component implements Link.Linkable { const proxy = input.proxyId.apply((proxyId) => proxyId ? rds.Proxy.get(`${name}Proxy`, proxyId, undefined, { - parent: self, - }) + parent: self, + }) : undefined, ); @@ -610,13 +640,13 @@ Listening on "${dev.host}:${dev.port}"...`, return args.password ? output(args.password) : new RandomPassword( - `${name}Password`, - { - length: 32, - special: false, - }, - { parent: self }, - ).result; + `${name}Password`, + { + length: 32, + special: false, + }, + { parent: self }, + ).result; } function createSubnetGroup() { @@ -649,7 +679,13 @@ Listening on "${dev.host}:${dev.port}"...`, }, ], }, - { parent: self }, + { + parent: self, + ignoreChanges: args.version ? [] : ["family"], + // Necessary for the parameter group to be deleted AFTER upgrading the instance. + // This is either a Pulumi bug or an undocumented feature. + deleteBeforeReplace: false, + }, ), ); } @@ -692,11 +728,18 @@ Listening on "${dev.host}:${dev.port}"...`, username, password, parameterGroupName: parameterGroup.name, + applyImmediately: true, + allowMajorVersionUpgrade: true, + autoMinorVersionUpgrade: false, skipFinalSnapshot: true, storageEncrypted: true, storageType: "gp3", allocatedStorage: 20, - maxAllocatedStorage: storage, + // Blue/green deployments require maxAllocatedStorage to be at least + // 10% higher than allocatedStorage for autoscaling headroom. + maxAllocatedStorage: all([storage, blueGreen]).apply(([s, bg]) => + bg ? Math.max(s, 22) : s, + ), multiAz, backupRetentionPeriod: 7, // performance insights is only supported on .micro and .small MySQL instances @@ -704,12 +747,17 @@ Listening on "${dev.host}:${dev.port}"...`, performanceInsightsEnabled: instanceType.apply( (v) => !v.endsWith(".micro") && !v.endsWith(".small"), ), + blueGreenUpdate: blueGreen.apply((bg) => ({ enabled: bg })), tags: { "sst:component-version": _version.toString(), "sst:ref:password": secret.id, }, }, - { parent: self, deleteBeforeReplace: true }, + { + parent: self, + deleteBeforeReplace: true, + ignoreChanges: args.version ? [] : ["engineVersion"], + }, ), ); } @@ -731,6 +779,7 @@ Listening on "${dev.host}:${dev.port}"...`, username: instance.username, password: instance.password.apply((v) => v!), parameterGroupName: instance.parameterGroupName, + applyImmediately: true, skipFinalSnapshot: true, storageEncrypted: instance.storageEncrypted.apply((v) => v!), storageType: instance.storageType, @@ -739,7 +788,10 @@ Listening on "${dev.host}:${dev.port}"...`, (v) => v!, ), }, - { parent: self }, + { + parent: self, + ignoreChanges: args.version ? [] : ["engineVersion"], + }, ), ), ); @@ -945,7 +997,7 @@ Listening on "${dev.host}:${dev.port}"...`, * * @param name The name of the component. * @param args The arguments to get the MySQL database. - * @param opts? Resource options. + * @param opts Resource options. * * @example * Imagine you create a database in the `dev` stage. And in your personal stage `frank`, diff --git a/platform/src/components/aws/nextjs.ts b/platform/src/components/aws/nextjs.ts index 48ffb415a2..59c378ccf8 100644 --- a/platform/src/components/aws/nextjs.ts +++ b/platform/src/components/aws/nextjs.ts @@ -381,23 +381,7 @@ export interface NextjsArgs extends SsrSiteArgs { * ``` */ domain?: SsrSiteArgs["domain"]; - /** - * Configure how the Next.js app assets are uploaded to S3. - * - * By default, this is set to the following. Read more about these options below. - * ```js - * { - * assets: { - * textEncoding: "utf-8", - * versionedFilesCacheHeader: "public,max-age=31536000,immutable", - * nonVersionedFilesCacheHeader: "public,max-age=0,s-maxage=86400,stale-while-revalidate=8640" - * } - * } - * ``` - * Read more about these options below. - * @default `Object` - */ - assets?: SsrSiteArgs["assets"]; + /** * Configure the [OpenNext](https://opennext.js.org) version used to build the Next.js app. * @@ -582,9 +566,9 @@ export interface NextjsArgs extends SsrSiteArgs { * ``` */ export class Nextjs extends SsrSite { - private revalidationQueue?: Output; - private revalidationTable?: Output; - private revalidationFunction?: Output; + private declare revalidationQueue?: Output; + private declare revalidationTable?: Output; + private declare revalidationFunction?: Output; constructor( name: string, @@ -655,10 +639,10 @@ export class Nextjs extends SsrSite { revalidationTable?.name, bucket.arn, bucket.name, - getRegionOutput(undefined, { parent: bucket }).name, + getRegionOutput(undefined, { parent: bucket }).region, revalidationQueue?.arn, revalidationQueue?.url, - getRegionOutput(undefined, { parent: revalidationQueue }).name, + getRegionOutput(undefined, { parent: revalidationQueue }).region, ]).apply( ([ tableArn, @@ -676,7 +660,7 @@ export class Nextjs extends SsrSite { bundle: path.join(outputPath, serverOrigin.bundle), handler: serverOrigin.handler, streaming: serverOrigin.streaming, - runtime: "nodejs20.x" as const, + runtime: "nodejs24.x" as const, environment: { CACHE_BUCKET_NAME: bucketName, CACHE_BUCKET_KEY_PREFIX: "_cache", @@ -759,7 +743,7 @@ export class Nextjs extends SsrSite { description: `${name} image optimizer`, handler: imageOptimizerOrigin.handler, bundle: path.join(outputPath, imageOptimizerOrigin.bundle), - runtime: "nodejs20.x" as const, + runtime: "nodejs24.x" as const, architecture: "arm64" as const, environment: { BUCKET_NAME: bucketName, @@ -896,7 +880,7 @@ export class Nextjs extends SsrSite { description: `${name} ISR revalidator`, handler: revalidationFunction.handler, bundle: path.join(outputPath, revalidationFunction.bundle), - runtime: "nodejs20.x", + runtime: "nodejs24.x", timeout: "30 seconds", permissions: [ { @@ -981,7 +965,7 @@ export class Nextjs extends SsrSite { outputPath, openNextOutput.additionalProps.initializationFunction.bundle, ), - runtime: "nodejs20.x", + runtime: "nodejs24.x", timeout: "900 seconds", memory: `${Math.min( 10240, diff --git a/platform/src/components/aws/nuxt.ts b/platform/src/components/aws/nuxt.ts index f9d488614a..0929d6a51f 100644 --- a/platform/src/components/aws/nuxt.ts +++ b/platform/src/components/aws/nuxt.ts @@ -378,21 +378,6 @@ export interface NuxtArgs extends SsrSiteArgs { * ``` */ buildCommand?: SsrSiteArgs["buildCommand"]; - /** - * Configure how the Nuxt app assets are uploaded to S3. - * - * By default, this is set to the following. Read more about these options below. - * ```js - * { - * assets: { - * textEncoding: "utf-8", - * versionedFilesCacheHeader: "public,max-age=31536000,immutable", - * nonVersionedFilesCacheHeader: "public,max-age=0,s-maxage=86400,stale-while-revalidate=8640" - * } - * } - * ``` - */ - assets?: SsrSiteArgs["assets"]; /** * Configure the Nuxt app to use an existing CloudFront cache policy. * @@ -497,6 +482,12 @@ export class Nuxt extends SsrSite { protected buildPlan(outputPath: Output): Output { return outputPath.apply((outputPath) => { + const nitro = JSON.parse( + fs.readFileSync( + path.join(outputPath, ".output", "nitro.json"), + "utf-8", + ), + ); const basepath = fs .readFileSync(path.join(outputPath, "nuxt.config.ts"), "utf-8") .match(/baseURL: ['"](.*)['"]/)?.[1]; @@ -507,6 +498,7 @@ export class Nuxt extends SsrSite { description: "Server handler for Nuxt", handler: "index.handler", bundle: path.join(outputPath, ".output", "server"), + streaming: nitro?.config?.awsLambda?.streaming === true, }, assets: [ { diff --git a/platform/src/components/aws/open-search.ts b/platform/src/components/aws/open-search.ts index cdb3a43e31..ac3775bdd6 100644 --- a/platform/src/components/aws/open-search.ts +++ b/platform/src/components/aws/open-search.ts @@ -17,6 +17,12 @@ import { DevCommand } from "../experimental/dev-command.js"; export interface OpenSearchArgs { /** * The OpenSearch engine version. Check out the [available versions](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/what-is.html#choosing-version). + * + * :::caution + * Changing the version will cause the domain to restart on the next `sst deploy`, + * causing downtime. Learn more about [upgrading databases](/docs/upgrade-aws-databases/). + * ::: + * * @default `"OpenSearch_2.17"` * @example * ```js @@ -29,7 +35,7 @@ export interface OpenSearchArgs { /** * The username of the master user. * - * :::caution + * :::danger * Changing the username will cause the domain to be destroyed and recreated. * ::: * @@ -63,6 +69,11 @@ export interface OpenSearchArgs { /** * The type of instance to use for the domain. Check out the [supported instance types](https://docs.aws.amazon.com/opensearch-service/latest/developerguide/supported-instance-types.html). * + * :::caution + * Changing the instance type will cause the domain to restart on the next `sst deploy`, + * causing downtime. Learn more about [upgrading databases](/docs/upgrade-aws-databases/). + * ::: + * * @default `"t3.small"` * @example * ```js @@ -306,7 +317,7 @@ export class OpenSearch extends Component implements Link.Linkable { //}); const domain = opensearch.Domain.get(`${name}Domain`, ref.id); - const input = domain.tags.apply((tags) => { + const input = domain.tagsAll.apply((tags) => { if (!tags?.["sst:ref:username"]) throw new VisibleError( `Failed to get username for OpenSearch ${name}.`, @@ -388,16 +399,16 @@ Listening on "${dev.url}"...`, return args.password ? output(args.password) : new RandomPassword( - `${name}Password`, - { - length: 32, - minLower: 1, - minUpper: 1, - minNumeric: 1, - minSpecial: 1, - }, - { parent: self }, - ).result; + `${name}Password`, + { + length: 32, + minLower: 1, + minUpper: 1, + minNumeric: 1, + minSpecial: 1, + }, + { parent: self }, + ).result; } function createSecret() { @@ -549,7 +560,7 @@ Listening on "${dev.url}"...`, * * @param name The name of the component. * @param id The ID of the existing OpenSearch component. - * @param opts? Resource options. + * @param opts Resource options. * * @example * Imagine you create a domain in the `dev` stage. And in your personal stage `frank`, diff --git a/platform/src/components/aws/opencontrol.ts b/platform/src/components/aws/opencontrol.ts index f3009f4cf2..519e23c38f 100644 --- a/platform/src/components/aws/opencontrol.ts +++ b/platform/src/components/aws/opencontrol.ts @@ -45,15 +45,18 @@ export interface OpenControlArgs { */ server: Input; } - /** + * The `OpenControl` component has been deprecated. It should not be used for new projects. + * + * :::caution + * This component has been deprecated. + * ::: + * * The `OpenControl` component lets you deploy your * [OpenControl](https://opencontrol.ai) server to * [AWS Lambda](https://aws.amazon.com/lambda/). * - * :::note - * OpenControl is currently in beta. - * ::: + * @deprecated Use OpenControl outside of SST instead. * * @example * diff --git a/platform/src/components/aws/postgres-v1.ts b/platform/src/components/aws/postgres-v1.ts index fe05a63e9c..77351b5aa0 100644 --- a/platform/src/components/aws/postgres-v1.ts +++ b/platform/src/components/aws/postgres-v1.ts @@ -20,11 +20,17 @@ function parseACU(acu: ACU) { export interface PostgresArgs { /** * The Postgres engine version. Check out the [available versions in your region](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.Aurora_Fea_Regions_DB-eng.Feature.ServerlessV2.html#Concepts.Aurora_Fea_Regions_DB-eng.Feature.ServerlessV2.apg). - * @default `"15.5"` + * + * :::caution + * Changing the version will cause the database to restart on the next `sst deploy`, + * causing downtime. Learn more about [upgrading databases](/docs/upgrade-aws-databases/). + * ::: + * + * @default `"17"` * @example * ```js * { - * version: "13.9" + * version: "15.5" * } * ``` */ @@ -284,7 +290,7 @@ export class Postgres extends Component implements Link.Linkable { } function normalizeVersion() { - return output(args.version).apply((version) => version ?? "15.5"); + return output(args.version).apply((version) => version ?? "17"); } function normalizeDatabaseName() { @@ -320,6 +326,8 @@ export class Postgres extends Component implements Link.Linkable { masterUsername: "postgres", manageMasterUserPassword: true, serverlessv2ScalingConfiguration: scaling, + applyImmediately: true, + allowMajorVersionUpgrade: true, skipFinalSnapshot: true, enableHttpEndpoint: true, dbSubnetGroupName: subnetGroup?.name, @@ -328,7 +336,10 @@ export class Postgres extends Component implements Link.Linkable { ? undefined : output(args.vpc).securityGroups, }, - { parent }, + { + parent, + ignoreChanges: args.version ? [] : ["engineVersion"], + }, ), ); } @@ -344,8 +355,12 @@ export class Postgres extends Component implements Link.Linkable { engine: rds.EngineType.AuroraPostgresql, engineVersion: cluster.engineVersion, dbSubnetGroupName: subnetGroup?.name, + autoMinorVersionUpgrade: false, + }, + { + parent, + ignoreChanges: args.version ? [] : ["engineVersion"], }, - { parent }, ), ); } diff --git a/platform/src/components/aws/postgres.ts b/platform/src/components/aws/postgres.ts index eb110df869..ab54670771 100644 --- a/platform/src/components/aws/postgres.ts +++ b/platform/src/components/aws/postgres.ts @@ -23,6 +23,12 @@ export type { PostgresArgs as PostgresV1Args } from "./postgres-v1"; export interface PostgresArgs { /** * The Postgres engine version. Check out the [available versions in your region](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/PostgreSQL.Concepts.General.DBVersions.html). + * + * :::caution + * Changing the version will cause the database to restart on the next `sst deploy`, + * causing downtime. Learn more about [upgrading databases](/docs/upgrade-aws-databases/). + * ::: + * * @default `"17"` * @example * ```js @@ -35,7 +41,7 @@ export interface PostgresArgs { /** * The username of the master user. * - * :::caution + * :::danger * Changing the username will cause the database to be destroyed and recreated. * ::: * @@ -74,6 +80,10 @@ export interface PostgresArgs { * underscores. By default, it takes the name of the app, and replaces the hyphens with * underscores. * + * :::danger + * Changing the database name will cause the database to be destroyed and recreated. + * ::: + * * @default Based on the name of the current app * @example * ```js @@ -86,6 +96,11 @@ export interface PostgresArgs { /** * The type of instance to use for the database. Check out the [supported instance types](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.DBInstanceClass.Types.html). * + * :::caution + * Changing the instance type will cause the database to restart on the next `sst deploy`, + * causing downtime. Learn more about [upgrading databases](/docs/upgrade-aws-databases/). + * ::: + * * @default `"t4g.micro"` * @example * ```js @@ -93,10 +108,6 @@ export interface PostgresArgs { * instance: "m7g.xlarge" * } * ``` - * - * By default, these changes are not applied immediately by RDS. Instead, they are - * applied in the next maintenance window. Check out the [full list](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_ModifyInstance.Settings.html) - * of props that are not applied immediately. */ instance?: Input; /** @@ -208,6 +219,24 @@ export interface PostgresArgs { * ``` */ multiAz?: Input; + /** + * Enable [Blue/Green deployments](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/blue-green-deployments.html) + * for version, instance type, and parameter group upgrades. + * Learn more about [upgrading databases](/docs/upgrade-aws-databases/). + * + * When enabled, a staging (green) instance is created, updated, + * verified, then promoted to replace the production (blue) instance. + * This minimizes downtime during upgrades. + * + * @default `false` + * @example + * ```js + * { + * blueGreen: true + * } + * ``` + */ + blueGreen?: Input; /** * @internal */ @@ -477,6 +506,7 @@ export class Postgres extends Component implements Link.Linkable { const instanceType = output(args.instance).apply((v) => v ?? "t4g.micro"); const username = output(args.username).apply((v) => v ?? "postgres"); const storage = normalizeStorage(); + const blueGreen = output(args.blueGreen).apply((v) => v ?? false); const dbName = output(args.database).apply( (v) => v ?? $app.name.replaceAll("-", "_"), ); @@ -506,7 +536,7 @@ export class Postgres extends Component implements Link.Linkable { parent: self, }); - const input = instance.tags.apply((tags) => { + const input = instance.tagsAll.apply((tags) => { registerVersion( tags?.["sst:component-version"] ? parseInt(tags["sst:component-version"]) @@ -683,6 +713,9 @@ Listening on "${dev.host}:${dev.port}"...`, { parent: self, ignoreChanges: args.version ? [] : ["family"], + // Necessary for the parameter group to be deleted AFTER upgrading the instance. + // This is either a Pulumi bug or an undocumented feature. + deleteBeforeReplace: false, }, ), ); @@ -726,14 +759,22 @@ Listening on "${dev.host}:${dev.port}"...`, username, password, parameterGroupName: parameterGroup.name, + applyImmediately: true, + allowMajorVersionUpgrade: true, + autoMinorVersionUpgrade: false, skipFinalSnapshot: true, storageEncrypted: true, storageType: "gp3", allocatedStorage: 20, - maxAllocatedStorage: storage, + // Blue/green deployments require maxAllocatedStorage to be at least + // 10% higher than allocatedStorage for autoscaling headroom. + maxAllocatedStorage: all([storage, blueGreen]).apply(([s, bg]) => + bg ? Math.max(s, 22) : s, + ), multiAz, backupRetentionPeriod: 7, performanceInsightsEnabled: true, + blueGreenUpdate: blueGreen.apply((bg) => ({ enabled: bg })), tags: { "sst:component-version": _version.toString(), "sst:lookup:password": secret.id, @@ -765,6 +806,7 @@ Listening on "${dev.host}:${dev.port}"...`, username: instance.username, password: instance.password.apply((v) => v!), parameterGroupName: instance.parameterGroupName, + applyImmediately: true, skipFinalSnapshot: true, storageEncrypted: instance.storageEncrypted.apply((v) => v!), storageType: instance.storageType, @@ -982,7 +1024,7 @@ Listening on "${dev.host}:${dev.port}"...`, * * @param name The name of the component. * @param args The arguments to get the Postgres database. - * @param opts? Resource options. + * @param opts Resource options. * * @example * Imagine you create a database in the `dev` stage. And in your personal stage `frank`, diff --git a/platform/src/components/aws/providers/function-environment-update.ts b/platform/src/components/aws/providers/function-environment-update.ts index a0f160d6c8..4d19354ac0 100644 --- a/platform/src/components/aws/providers/function-environment-update.ts +++ b/platform/src/components/aws/providers/function-environment-update.ts @@ -14,6 +14,10 @@ export interface FunctionEnvironmentUpdateInputs { * The region of the function to update. */ region: Input; + /** + * The last modified timestamp of the function to update + */ + functionLastModified: Input; } /** diff --git a/platform/src/components/aws/queue-lambda-subscriber.ts b/platform/src/components/aws/queue-lambda-subscriber.ts index e725552f1c..159a542e2b 100644 --- a/platform/src/components/aws/queue-lambda-subscriber.ts +++ b/platform/src/components/aws/queue-lambda-subscriber.ts @@ -5,12 +5,11 @@ import { output, } from "@pulumi/pulumi"; import { Component, transform, Transform } from "../component"; -import { Function, FunctionArgs } from "./function"; +import { Function, FunctionArgs } from "./function.js"; import { QueueSubscriberArgs } from "./queue"; import { lambda } from "@pulumi/aws"; import { toSeconds } from "../duration"; import { FunctionBuilder, functionBuilder } from "./helpers/function-builder"; -import { parseFunctionArn } from "./helpers/arn"; export interface Args extends QueueSubscriberArgs { /** @@ -96,9 +95,7 @@ export class QueueLambdaSubscriber extends Component { batch?.window ? toSeconds(batch.window) : 0, ), eventSourceArn: queue.arn, - functionName: fn.arn.apply( - (arn) => parseFunctionArn(arn).functionName, - ), + functionName: fn.targetArn, filterCriteria: args.filters && { filters: output(args.filters).apply((filters) => filters.map((filter) => ({ diff --git a/platform/src/components/aws/queue.ts b/platform/src/components/aws/queue.ts index 8081f04803..e2b612c2ec 100644 --- a/platform/src/components/aws/queue.ts +++ b/platform/src/components/aws/queue.ts @@ -8,7 +8,7 @@ import { import { Component, Transform, transform } from "../component"; import { Link } from "../link"; import type { Input } from "../input"; -import { FunctionArgs, FunctionArn } from "./function"; +import { FunctionArgs, FunctionArn } from "./function.js"; import { VisibleError } from "../error"; import { hashStringToPrettyString, logicalName } from "../naming"; import { parseQueueArn } from "./helpers/arn"; @@ -620,7 +620,7 @@ export class Queue extends Component implements Link.Linkable { * * @param name The name of the component. * @param queueUrl The URL of the existing SQS Queue. - * @param opts? Resource options. + * @param opts Resource options. * * @example * Imagine you create a queue in the `dev` stage. And in your personal stage `frank`, diff --git a/platform/src/components/aws/react.ts b/platform/src/components/aws/react.ts index 79c2a04013..bf33494ad8 100644 --- a/platform/src/components/aws/react.ts +++ b/platform/src/components/aws/react.ts @@ -306,21 +306,6 @@ export interface ReactArgs extends SsrSiteArgs { * ``` */ buildCommand?: SsrSiteArgs["buildCommand"]; - /** - * Configure how the React app assets are uploaded to S3. - * - * By default, this is set to the following. Read more about these options below. - * ```js - * { - * assets: { - * textEncoding: "utf-8", - * versionedFilesCacheHeader: "public,max-age=31536000,immutable", - * nonVersionedFilesCacheHeader: "public,max-age=0,s-maxage=86400,stale-while-revalidate=8640" - * } - * } - * ``` - */ - assets?: SsrSiteArgs["assets"]; /** * Configure the React app to use an existing CloudFront cache policy. By default, * a new cache policy is created. Note that CloudFront has a limit of 20 cache diff --git a/platform/src/components/aws/realtime-lambda-subscriber.ts b/platform/src/components/aws/realtime-lambda-subscriber.ts index cc83201635..ac473551da 100644 --- a/platform/src/components/aws/realtime-lambda-subscriber.ts +++ b/platform/src/components/aws/realtime-lambda-subscriber.ts @@ -6,12 +6,11 @@ import { output, } from "@pulumi/pulumi"; import { Component, transform } from "../component"; -import { Function, FunctionArgs } from "./function"; +import { Function, FunctionArgs } from "./function.js"; import { RealtimeSubscriberArgs } from "./realtime"; import { lambda } from "@pulumi/aws"; import { iot } from "@pulumi/aws"; import { FunctionBuilder, functionBuilder } from "./helpers/function-builder"; -import { parseFunctionArn } from "./helpers/arn"; export interface Args extends RealtimeSubscriberArgs { /** @@ -79,7 +78,7 @@ export class RealtimeLambdaSubscriber extends Component { sqlVersion: "2016-03-23", sql: interpolate`SELECT * FROM '${filter}'`, enabled: true, - lambdas: [{ functionArn: fn.arn }], + lambdas: [{ functionArn: fn.targetArn }], }, { parent: self }, ), @@ -91,7 +90,8 @@ export class RealtimeLambdaSubscriber extends Component { `${name}Permission`, { action: "lambda:InvokeFunction", - function: fn.arn.apply((arn) => parseFunctionArn(arn).functionName), + function: fn.arn, + qualifier: fn.qualifier.apply((qualifier) => qualifier!), principal: "iot.amazonaws.com", sourceArn: rule.arn, }, diff --git a/platform/src/components/aws/realtime.ts b/platform/src/components/aws/realtime.ts index 212ec7afcb..0aae114a8f 100644 --- a/platform/src/components/aws/realtime.ts +++ b/platform/src/components/aws/realtime.ts @@ -2,7 +2,7 @@ import { ComponentResourceOptions, Output, all } from "@pulumi/pulumi"; import { Component, Transform, transform } from "../component"; import { Link } from "../link"; import type { Input } from "../input"; -import { Function, FunctionArgs, FunctionArn } from "./function"; +import { Function, FunctionArgs, FunctionArn } from "./function.js"; import { hashStringToPrettyString, logicalName } from "../naming"; import { RealtimeLambdaSubscriber } from "./realtime-lambda-subscriber"; import { iot, lambda } from "@pulumi/aws"; diff --git a/platform/src/components/aws/redis-v1.ts b/platform/src/components/aws/redis-v1.ts index 48cd76863f..653543e442 100644 --- a/platform/src/components/aws/redis-v1.ts +++ b/platform/src/components/aws/redis-v1.ts @@ -32,6 +32,11 @@ export interface RedisArgs { * * Check out the [supported versions](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/supported-engine-versions.html). * + * :::caution + * Changing the version will cause the instance to restart on the next `sst deploy`, + * causing downtime. Learn more about [upgrading databases](/docs/upgrade-aws-databases/). + * ::: + * * @default `"7.1"` for Redis, `"7.2"` for Valkey * @example * ```js @@ -44,6 +49,11 @@ export interface RedisArgs { /** * The type of instance to use for the nodes of the Redis cluster. Check out the [supported instance types](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/CacheNodes.SupportedTypes.html). * + * :::caution + * Changing the instance type will cause the instance to restart on the next `sst deploy`, + * causing downtime. Learn more about [upgrading databases](/docs/upgrade-aws-databases/). + * ::: + * * @default `"t4g.micro"` * @example * ```js @@ -428,10 +438,13 @@ Listening on "${dev.host}:${dev.port}"...`, numNodeGroups: nodes, replicasPerNodeGroup: 0, multiAzEnabled: false, + applyImmediately: true, + autoMinorVersionUpgrade: false, atRestEncryptionEnabled: true, transitEncryptionEnabled: true, transitEncryptionMode: "required", authToken, + authTokenUpdateStrategy: "ROTATE", subnetGroupName: subnetGroup.name, securityGroupIds: vpc.securityGroups, tags: { @@ -521,7 +534,7 @@ Listening on "${dev.host}:${dev.port}"...`, * * @param name The name of the component. * @param clusterID The id of the existing Redis cluster. - * @param opts? Resource options. + * @param opts Resource options. * * @example * Imagine you create a cluster in the `dev` stage. And in your personal stage `frank`, @@ -553,7 +566,7 @@ Listening on "${dev.host}:${dev.port}"...`, undefined, opts, ); - const secret = cluster.tags.apply((tags) => + const secret = cluster.tagsAll.apply((tags) => tags?.["sst:auth-token-ref"] ? secretsmanager.getSecretVersionOutput( { diff --git a/platform/src/components/aws/redis.ts b/platform/src/components/aws/redis.ts index f36736c85e..a4f69483b2 100644 --- a/platform/src/components/aws/redis.ts +++ b/platform/src/components/aws/redis.ts @@ -23,6 +23,10 @@ export interface RedisArgs { * - `"redis"`: The open-source version of Redis. * - `"valkey"`: [Valkey](https://valkey.io/) is a Redis-compatible in-memory key-value store. * + * :::danger + * Changing the engine will cause the database to be destroyed and recreated. + * ::: + * * @default `"redis"` */ engine?: Input<"redis" | "valkey">; @@ -33,6 +37,11 @@ export interface RedisArgs { * * Check out the [supported versions](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/supported-engine-versions.html). * + * :::caution + * Changing the version will cause the instance to restart on the next `sst deploy`, + * causing downtime. Learn more about [upgrading databases](/docs/upgrade-aws-databases/). + * ::: + * * @default `"7.1"` for Redis, `"7.2"` for Valkey * @example * ```js @@ -45,6 +54,11 @@ export interface RedisArgs { /** * The type of instance to use for the nodes of the Redis instance. Check out the [supported instance types](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/CacheNodes.SupportedTypes.html). * + * :::caution + * Changing the instance type will cause the instance to restart on the next `sst deploy`, + * causing downtime. Learn more about [upgrading databases](/docs/upgrade-aws-databases/). + * ::: + * * @default `"t4g.micro"` * @example * ```js @@ -361,7 +375,7 @@ export class Redis extends Component implements Link.Linkable { { parent: self }, ); - const input = cluster.tags.apply((tags) => { + const input = cluster.tagsAll.apply((tags) => { registerVersion( tags?.["sst:component-version"] ? parseInt(tags["sst:component-version"]) @@ -534,7 +548,12 @@ Listening on "${dev.host}:${dev.port}"...`, ], ), }, - { parent: self }, + { + parent: self, + // Necessary for the parameter group to be deleted AFTER upgrading the instance. + // This is either a Pulumi bug or an undocumented feature. + deleteBeforeReplace: false, + }, ), ); } @@ -566,9 +585,12 @@ Listening on "${dev.host}:${dev.port}"...`, clusterMode: "disabled", }), multiAzEnabled: false, + applyImmediately: true, + autoMinorVersionUpgrade: false, atRestEncryptionEnabled: true, transitEncryptionEnabled: true, transitEncryptionMode: "required", + authTokenUpdateStrategy: "ROTATE", authToken, subnetGroupName: subnetGroup.name, parameterGroupName: parameterGroup.name, @@ -666,7 +688,7 @@ Listening on "${dev.host}:${dev.port}"...`, * * @param name The name of the component. * @param clusterId The id of the existing Redis cluster. - * @param opts? Resource options. + * @param opts Resource options. * * @example * Imagine you create a cluster in the `dev` stage. And in your personal stage `frank`, diff --git a/platform/src/components/aws/remix.ts b/platform/src/components/aws/remix.ts index 65d955705b..27284638e1 100644 --- a/platform/src/components/aws/remix.ts +++ b/platform/src/components/aws/remix.ts @@ -307,21 +307,6 @@ export interface RemixArgs extends SsrSiteArgs { * @default `"build"` */ buildDirectory?: Input; - /** - * Configure how the Remix app assets are uploaded to S3. - * - * By default, this is set to the following. Read more about these options below. - * ```js - * { - * assets: { - * textEncoding: "utf-8", - * versionedFilesCacheHeader: "public,max-age=31536000,immutable", - * nonVersionedFilesCacheHeader: "public,max-age=0,s-maxage=86400,stale-while-revalidate=8640" - * } - * } - * ``` - */ - assets?: SsrSiteArgs["assets"]; /** * Configure the Remix app to use an existing CloudFront cache policy. * diff --git a/platform/src/components/aws/router.ts b/platform/src/components/aws/router.ts index 70e8f522a3..bfd5805f01 100644 --- a/platform/src/components/aws/router.ts +++ b/platform/src/components/aws/router.ts @@ -1,3 +1,4 @@ +import path from "path"; import { ComponentResourceOptions, Output, @@ -5,19 +6,27 @@ import { interpolate, output, } from "@pulumi/pulumi"; +import { asset } from "@pulumi/pulumi"; +import { iam, lambda } from "@pulumi/aws"; import crypto from "crypto"; import { Component, Transform, transform } from "../component"; import { Link } from "../link"; import type { Input } from "../input"; import { Cdn, CdnArgs } from "./cdn"; -import { cloudfront } from "@pulumi/aws"; +import { cloudfront, cloudwatch, wafv2 } from "@pulumi/aws"; +import { useProvider } from "./helpers/provider"; import { hashStringToPrettyString, physicalName } from "../naming"; import { Bucket } from "./bucket"; import { OriginAccessControl } from "./providers/origin-access-control"; import { VisibleError } from "../error"; import { RouterUrlRoute } from "./router-url-route"; import { RouterBucketRoute } from "./router-bucket-route"; -import { DurationSeconds } from "../duration"; +import { DurationSeconds, toSeconds } from "../duration"; +import { FunctionArn } from "./function.js"; +import { parseLambdaEdgeArn } from "./helpers/arn"; +import { Size, toMBs } from "../size"; +import { RETENTION } from "./logging"; +import { minify } from "../../util/minify"; interface InlineUrlRouteArgs extends InlineBaseRouteArgs { /** @@ -455,6 +464,226 @@ export interface RouterBucketRouteArgs extends RouteArgs { }>; } +export interface WafLoggingArgs { + /** + * Filter which requests are logged. + * + * - `"all"` logs every request evaluated by the WAF. + * - `"blocked"` only logs requests that were blocked. + * + * @default `"all"` + * @example + * ```js + * { + * waf: { + * logging: { + * include: "blocked" + * } + * } + * } + * ``` + */ + include?: Input<"all" | "blocked">; + /** + * The duration the WAF logs are kept in CloudWatch. + * + * @default `"1 month"` + * @example + * ```js + * { + * waf: { + * logging: { + * retention: "3 months" + * } + * } + * } + * ``` + */ + retention?: Input; + /** + * Configure which parts of the request are redacted from the logs. Redacted + * fields are replaced with `REDACTED` in the log output. + * + * By default, the query string and the `cookie` and `authorization` headers + * are redacted since they commonly contain PII or credentials. + * + * Set to `false` to disable all redaction. + * + * @default `{ queryString: true, headers: ["cookie", "authorization"] }` + * @example + * + * Disable all redaction. + * + * ```js + * { + * waf: { + * logging: { + * redact: false + * } + * } + * } + * ``` + * + * Redact everything. + * + * ```js + * { + * waf: { + * logging: { + * redact: { + * queryString: true, + * uriPath: true, + * method: true, + * headers: ["cookie", "authorization"] + * } + * } + * } + * } + * ``` + */ + redact?: Input< + | false + | { + /** + * Redact the query string from the logs. The query string is the + * part of a URL after the `?` and can contain tokens, user IDs, + * or other sensitive parameters. + * @default `true` + */ + queryString?: Input; + /** + * Redact the URI path from the logs. The URI path identifies the + * resource being accessed, like `/users/123/profile`. + * @default `false` + */ + uriPath?: Input; + /** + * Redact the HTTP method from the logs (GET, POST, etc.). + * @default `false` + */ + method?: Input; + /** + * A list of header names to redact from the logs. Must be lowercase. + * @default `["cookie", "authorization"]` + * @example + * ```js + * { + * headers: ["cookie", "authorization", "x-api-key"] + * } + * ``` + */ + headers?: Input; + } + >; +} + +export interface WafArgs { + /** + * The rate limit per IP address. Requests from an IP that exceed this limit + * within a 5-minute window will be blocked. + * + * @default `2000` + * @example + * ```js + * { + * waf: { + * rateLimitPerIp: 1000 + * } + * } + * ``` + */ + rateLimitPerIp?: Input; + /** + * Configure which AWS managed rule groups to enable. + * + * @default All managed rules enabled + * @example + * ```js + * { + * waf: { + * managedRules: { + * coreRuleSet: true, + * knownBadInputs: true, + * sqlInjection: false + * } + * } + * } + * ``` + */ + managedRules?: Input<{ + /** + * Enable the AWS Core Rule Set (CRS) which provides protection against common + * web vulnerabilities. + * @default `true` + */ + coreRuleSet?: Input; + /** + * Enable protection against known bad inputs, including Log4j vulnerabilities. + * @default `true` + */ + knownBadInputs?: Input; + /** + * Enable SQL injection protection. + * @default `true` + */ + sqlInjection?: Input; + }>; + /** + * Configure WAF logging to CloudWatch. When set to `true`, all WAF-evaluated + * requests are logged with a 1-month retention. Or pass in an object to + * customize what is logged, how long logs are retained, and which fields + * are redacted. + * + * :::tip + * WAF logging is off by default. Enabling it will incur additional + * [CloudWatch costs](https://aws.amazon.com/cloudwatch/pricing/) depending + * on log volume. + * ::: + * + * @default Logging is disabled + * @example + * + * Enable with defaults. + * + * ```js + * { + * waf: { + * logging: true + * } + * } + * ``` + * + * Only log blocked requests. + * + * ```js + * { + * waf: { + * logging: { + * include: "blocked", + * retention: "3 months" + * } + * } + * } + * ``` + * + * Redact sensitive fields. + * + * ```js + * { + * waf: { + * logging: { + * redact: { + * queryString: true, + * headers: ["cookie"] + * } + * } + * } + * } + * ``` + */ + logging?: Input; +} + export interface RouterArgs { /** * Set a custom domain for your Router. @@ -814,6 +1043,133 @@ export interface RouterArgs { } >; + /** + * Configure Lambda function URL protection through CloudFront Origin Access Control. + * + * When set, all Functions and SSR sites routing through this Router automatically + * inherit the protection mode. + * + * @default `"none"` + * + * The available options are: + * - `"none"`: Lambda URLs are publicly accessible. + * - `"oac"`: Lambda URLs protected by CloudFront Origin Access Control. Requires + * manual `x-amz-content-sha256` header for POST requests. + * - `"oac-with-edge-signing"`: Full protection with automatic header signing via + * Lambda@Edge. Works with external webhooks and callbacks. Higher cost and latency + * but works out of the box. + * + * :::note + * Switching from `"none"` to `"oac"` or `"oac-with-edge-signing"` may cause brief + * 403 errors (~10-60s) during deployment while CloudFront edge nodes pick up the new + * signing configuration. For zero-disruption upgrades, set `protection` when first + * creating the Router. + * ::: + * + * :::note + * When using `"oac-with-edge-signing"`, request bodies are limited to 1MB due to + * Lambda@Edge payload limits. For file uploads larger than 1MB, consider using + * presigned S3 URLs or the `"oac"` mode with manual header signing. + * ::: + * + * :::note + * When removing a stage that uses `"oac-with-edge-signing"`, deletion may take + * 5-10 minutes while AWS removes the Lambda@Edge replicated functions from all + * edge locations. + * ::: + * + * @example + * ```js + * { + * protection: "oac" + * } + * ``` + * + * @example + * ```js + * { + * protection: "oac-with-edge-signing" + * } + * ``` + * + * @example + * ```js + * // Custom Lambda@Edge configuration + * { + * protection: { + * mode: "oac-with-edge-signing", + * edgeFunction: { + * memory: "256 MB", + * timeout: "10 seconds" + * } + * } + * } + * ``` + */ + protection?: Input< + | "none" + | "oac" + | "oac-with-edge-signing" + | { + mode: "oac-with-edge-signing"; + edgeFunction?: { + /** + * Custom Lambda@Edge function ARN to use for request signing. + * If provided, this function will be used instead of creating a new one. + * Must be a qualified ARN (with version) and deployed in us-east-1. + */ + arn?: Input; + /** + * Memory size for the auto-created Lambda@Edge function. + * Only used when arn is not provided. + * @default `"128 MB"` + */ + memory?: Input; + /** + * Timeout for the auto-created Lambda@Edge function. + * Only used when arn is not provided. + * @default `"5 seconds"` + */ + timeout?: Input; + }; + } + >; + /** + * Enable AWS WAF (Web Application Firewall) to protect your Router from common + * web exploits and bots. + * + * :::tip + * WAF provides protection against SQL injection, cross-site scripting (XSS), + * and other common attacks. + * ::: + * + * @default WAF is disabled + * @example + * + * Enable with sensible defaults. + * + * ```js + * { + * waf: true + * } + * ``` + * + * Or customize the configuration. + * + * ```js + * { + * waf: { + * rateLimitPerIp: 1000, + * managedRules: { + * coreRuleSet: true, + * knownBadInputs: true, + * sqlInjection: true + * } + * } + * } + * ``` + */ + waf?: Input; /** * [Transform](/docs/components#transform) how this component creates its underlying * resources. @@ -827,6 +1183,18 @@ export interface RouterArgs { * Transform the CloudFront CDN resource. */ cdn?: Transform; + /** + * Transform the WAF WebACL resource. + */ + waf?: Transform; + /** + * Transform the CloudWatch LogGroup resource used for WAF logs. + */ + wafLogGroup?: Transform; + /** + * Transform the WAF WebACL logging configuration resource. + */ + wafLogging?: Transform; }; /** * @internal @@ -1003,6 +1371,7 @@ export class Router extends Component implements Link.Linkable { private kvStoreArn?: Output; private kvNamespace?: Output; private hasInlineRoutes: Output; + private _protectionMode: Output; constructor( name: string, @@ -1021,11 +1390,27 @@ export class Router extends Component implements Link.Linkable { this.kvStoreArn = ref.kvStoreArn; this.kvNamespace = ref.kvNamespace; this.hasInlineRoutes = ref.hasInlineRoutes; + this._protectionMode = ref.protection; registerOutputs(); return; } const hasInlineRoutes = args.routes !== undefined; + const protection = normalizeProtection(); + + if (hasInlineRoutes) { + protection.apply((p) => { + if (p.mode !== "none") + throw new VisibleError( + `Cannot set "protection" on a Router with inline routes. Use lazy routes instead.`, + ); + }); + } + + const waf = createWaf(); + const wafArn = waf?.arn; + const wafLogging = normalizeWafLogging(); + createWafLogging(); let cdn, kvStoreArn, kvNamespace; if (hasInlineRoutes) { @@ -1041,12 +1426,13 @@ export class Router extends Component implements Link.Linkable { this.kvStoreArn = kvStoreArn; this.kvNamespace = kvNamespace; this.hasInlineRoutes = output(hasInlineRoutes); + this._protectionMode = protection; registerOutputs(); function reference() { const ref = args as unknown as RouterRef; const cdn = Cdn.get(`${name}Cdn`, ref.distributionID, { parent: self }); - const tags = cdn.nodes.distribution.tags.apply((tags) => { + const tags = cdn.nodes.distribution.tagsAll.apply((tags) => { if (tags?.["sst:ref:version"] !== _refVersion.toString()) { throw new VisibleError( [ @@ -1060,6 +1446,7 @@ export class Router extends Component implements Link.Linkable { kvStoreArn: tags?.["sst:ref:kv"], kvNamespace: tags?.["sst:ref:kv-namespace"], hasInlineRoutes: tags?.["sst:ref:kv"] === undefined, + protection: tags?.["sst:ref:protection"] ?? "none", }; }); @@ -1068,15 +1455,281 @@ export class Router extends Component implements Link.Linkable { kvStoreArn: tags.kvStoreArn, kvNamespace: tags.kvNamespace, hasInlineRoutes: tags.hasInlineRoutes, + protection: tags.protection.apply((p) => ({ + mode: p as ProtectionConfig["mode"], + })), }; } + function createWaf(): wafv2.WebAcl | undefined { + if (!args.waf) return undefined; + + const wafInput = output(args.waf); + const config = wafInput.apply((waf) => + typeof waf === "boolean" || !waf ? {} : waf, + ); + const rateLimitPerIp = config.apply((c) => c.rateLimitPerIp ?? 2000); + const managedRules = config.apply((c) => c.managedRules ?? {}); + const enableCoreRuleSet = managedRules.apply( + (m) => m.coreRuleSet !== false, + ); + const enableKnownBadInputs = managedRules.apply( + (m) => m.knownBadInputs !== false, + ); + const enableSqlInjection = managedRules.apply( + (m) => m.sqlInjection !== false, + ); + + // Build rules array dynamically based on config + const rules = all([ + rateLimitPerIp, + enableCoreRuleSet, + enableKnownBadInputs, + enableSqlInjection, + ]).apply(([rateLimit, coreRuleSet, knownBadInputs, sqlInjection]) => { + const r: wafv2.WebAclArgs["rules"] = []; + let priority = 0; + + r.push({ + name: "RateLimitPerIP", + priority: priority++, + action: { block: {} }, + statement: { + rateBasedStatement: { + limit: rateLimit, + aggregateKeyType: "IP", + }, + }, + visibilityConfig: { + cloudwatchMetricsEnabled: true, + metricName: `${name}RateLimitPerIP`, + sampledRequestsEnabled: true, + }, + }); + + if (coreRuleSet) { + r.push({ + name: "AWSManagedRulesCommonRuleSet", + priority: priority++, + overrideAction: { none: {} }, + statement: { + managedRuleGroupStatement: { + vendorName: "AWS", + name: "AWSManagedRulesCommonRuleSet", + // Set SizeRestrictions_BODY to COUNT to avoid blocking large request bodies + ruleActionOverrides: [ + { + name: "SizeRestrictions_BODY", + actionToUse: { count: {} }, + }, + ], + }, + }, + visibilityConfig: { + cloudwatchMetricsEnabled: true, + metricName: `${name}AWSManagedRulesCommonRuleSet`, + sampledRequestsEnabled: true, + }, + }); + } + + if (knownBadInputs) { + r.push({ + name: "AWSManagedRulesKnownBadInputsRuleSet", + priority: priority++, + overrideAction: { none: {} }, + statement: { + managedRuleGroupStatement: { + vendorName: "AWS", + name: "AWSManagedRulesKnownBadInputsRuleSet", + }, + }, + visibilityConfig: { + cloudwatchMetricsEnabled: true, + metricName: `${name}AWSManagedRulesKnownBadInputsRuleSet`, + sampledRequestsEnabled: true, + }, + }); + } + + if (sqlInjection) { + r.push({ + name: "AWSManagedRulesSQLiRuleSet", + priority: priority++, + overrideAction: { none: {} }, + statement: { + managedRuleGroupStatement: { + vendorName: "AWS", + name: "AWSManagedRulesSQLiRuleSet", + }, + }, + visibilityConfig: { + cloudwatchMetricsEnabled: true, + metricName: `${name}AWSManagedRulesSQLiRuleSet`, + sampledRequestsEnabled: true, + }, + }); + } + + return r; + }); + + // WAF must be created in us-east-1 for CloudFront + return new wafv2.WebAcl( + ...transform( + args.transform?.waf, + `${name}Waf`, + { + scope: "CLOUDFRONT", + defaultAction: { allow: {} }, + rules, + visibilityConfig: { + cloudwatchMetricsEnabled: true, + metricName: `${name}Waf`, + sampledRequestsEnabled: true, + }, + }, + { parent: self, provider: useProvider("us-east-1") }, + ), + ); + } + + function normalizeWafLogging() { + return output(args.waf) + .apply((waf) => { + if (!waf || typeof waf === "boolean") return undefined; + return output(waf.logging); + }) + .apply((logging) => { + if (!logging) return undefined; + const l = ( + typeof logging === "boolean" ? {} : logging + ) as WafLoggingArgs; + const defaultRedact = { + queryString: true as boolean, + headers: ["cookie", "authorization"] as string[], + }; + const redact = l.redact; + return { + include: (l.include ?? "all") as "all" | "blocked", + retention: (l.retention ?? "1 month") as keyof typeof RETENTION, + redact: + redact === false + ? undefined + : redact ?? defaultRedact, + }; + }); + } + + function createWafLogging() { + if (!waf) return; + + wafLogging.apply((logging) => { + if (!logging) return; + + // CloudWatch Log Group name MUST start with "aws-waf-logs-" + const logGroup = new cloudwatch.LogGroup( + ...transform( + args.transform?.wafLogGroup, + `${name}WafLogGroup`, + { + name: `aws-waf-logs-${physicalName(64, name)}`, + retentionInDays: RETENTION[logging.retention], + }, + { + parent: self, + provider: useProvider("us-east-1"), + ignoreChanges: ["name"], + }, + ), + ); + + const redactedFields = logging.redact + ? output(logging.redact).apply((redact) => { + if (!redact || typeof redact === "boolean") return []; + const fields: wafv2.WebAclLoggingConfigurationArgs["redactedFields"] & + {}[] = []; + if (redact.method) fields.push({ method: {} }); + if (redact.queryString) fields.push({ queryString: {} }); + if (redact.uriPath) fields.push({ uriPath: {} }); + if (redact.headers) { + for (const header of redact.headers) { + fields.push({ + singleHeader: { name: header.toLowerCase() }, + }); + } + } + return fields; + }) + : undefined; + + const loggingFilter = + logging.include === "blocked" + ? { + defaultBehavior: "DROP", + filters: [ + { + behavior: "KEEP" as const, + requirement: "MEETS_ANY" as const, + conditions: [ + { + actionCondition: { + action: "BLOCK", + }, + }, + ], + }, + ], + } + : undefined; + + new wafv2.WebAclLoggingConfiguration( + ...transform( + args.transform?.wafLogging, + `${name}WafLogging`, + { + resourceArn: waf!.arn, + logDestinationConfigs: [logGroup.arn], + ...(redactedFields ? { redactedFields } : {}), + ...(loggingFilter ? { loggingFilter } : {}), + }, + { + parent: self, + provider: useProvider("us-east-1"), + }, + ), + ); + }); + } + function registerOutputs() { self.registerOutputs({ _hint: args._skipHint ? undefined : self.url, }); } + function normalizeProtection(): Output { + return output(args.protection).apply((protection) => { + if (!protection) return { mode: "none" as const }; + + if (typeof protection === "string") { + return { mode: protection }; + } + + if ( + protection.mode === "oac-with-edge-signing" && + protection.edgeFunction?.arn + ) { + const arn = protection.edgeFunction.arn; + if (typeof arn === "string") { + parseLambdaEdgeArn(arn); + } + } + + return protection; + }); + } + function handleInlineRoutes() { let defaultCachePolicy: cloudfront.CachePolicy; let defaultCfFunction: cloudfront.Function; @@ -1394,6 +2047,7 @@ async function handler(event) { .map((d) => d.behavior), domain: args.domain, wait: true, + webAclArn: wafArn, }, { parent: self }, ), @@ -1408,6 +2062,7 @@ async function handler(event) { const requestFunction = createRequestFunction(); const responseFunction = createResponseFunction(); const cachePolicyId = createCachePolicy().id; + const edgeFunction = createLambdaEdgeFunction(); const distribution = createDistribution(); return { kvNamespace, kvStoreArn, distribution }; @@ -1571,7 +2226,10 @@ async function handler(event) { } if (route.type === "url") setUrlOrigin(route.metadata.host, route.metadata.origin); if (route.type === "bucket") setS3Origin(route.metadata.domain, route.metadata.origin); - if (route.type === "site") await routeSite(route.routeNs, route.metadata); + if (route.type === "site") { + const response = await routeSite(route.routeNs, route.metadata); + return response || event.request; + } return event.request; }`, }, @@ -1605,6 +2263,79 @@ async function handler(event) { }); } + function createLambdaEdgeFunction() { + return protection.apply((protectionConfig) => { + if ( + protectionConfig.mode !== "oac-with-edge-signing" || + protectionConfig.edgeFunction?.arn + ) { + return undefined; + } + + const edgeConfig = protectionConfig.edgeFunction; + const memory = edgeConfig?.memory ? toMBs(edgeConfig.memory) : 128; + const timeout = edgeConfig?.timeout + ? toSeconds(edgeConfig.timeout) + : 5; + + const edgeRole = new iam.Role( + ...transform( + undefined, + `${name}EdgeFunctionRole`, + { + name: physicalName(64, `${name}EdgeRole`), + assumeRolePolicy: JSON.stringify({ + Version: "2012-10-17", + Statement: [ + { + Action: "sts:AssumeRole", + Effect: "Allow", + Principal: { + Service: [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com", + ], + }, + }, + ], + }), + managedPolicyArns: [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + }, + { parent: self, ignoreChanges: ["name"] }, + ), + ); + + const edgeFn = new lambda.Function( + ...transform( + undefined, + `${name}EdgeFunction`, + { + name: physicalName(64, `${name}EdgeFn`), + runtime: "nodejs24.x", + handler: "index.handler", + role: edgeRole.arn, + code: new asset.FileArchive( + path.join($cli.paths.platform, "dist", "oac-edge-signer"), + ), + publish: true, + timeout: timeout, + memorySize: memory, + description: `${name} Lambda@Edge function for OAC request signing`, + }, + { + parent: self, + provider: useProvider("us-east-1"), + ignoreChanges: ["name"], + }, + ), + ); + + return edgeFn; + }); + } + function createDistribution() { return new Cdn( ...transform( @@ -1652,12 +2383,44 @@ async function handler(event) { ? [{ eventType: "viewer-response", functionArn: resFn.arn }] : []), ]), + lambdaFunctionAssociations: all([ + protection, + edgeFunction, + ]).apply(([protectionConfig, autoEdgeFunction]) => { + if (protectionConfig.mode !== "oac-with-edge-signing") { + return []; + } + + if (protectionConfig.edgeFunction?.arn) { + return [ + { + eventType: "origin-request", + lambdaArn: protectionConfig.edgeFunction.arn, + includeBody: true, + }, + ]; + } + + if (autoEdgeFunction) { + return [ + { + eventType: "origin-request", + lambdaArn: autoEdgeFunction.qualifiedArn, + includeBody: true, + }, + ]; + } + + return []; + }), }, tags: { "sst:ref:kv": kvStoreArn, "sst:ref:kv-namespace": kvNamespace, "sst:ref:version": _refVersion.toString(), + "sst:ref:protection": protection.apply((p) => p.mode), }, + webAclArn: wafArn, }, { parent: self }, ), @@ -1700,6 +2463,16 @@ async function handler(event) { return this.hasInlineRoutes; } + /** @internal */ + public get _protection() { + return this._protectionMode; + } + + /** @internal */ + public get _distributionArn() { + return this.cdn.apply((cdn) => cdn.nodes.distribution.arn); + } + /** * The underlying [resources](/docs/components/#nodes) this component creates. */ @@ -1897,7 +2670,7 @@ async function handler(event) { * * @param name The name of the component. * @param distributionID The ID of the existing Router distribution. - * @param opts? Resource options. + * @param opts Resource options. * * This is useful when you create a Router in one stage and want to share it in * another. It avoids having to create a new Router in the other stage. @@ -1948,6 +2721,11 @@ const __pulumiType = "sst:aws:Router"; // @ts-expect-error Router.__pulumiType = __pulumiType; +// CloudFront Functions have a 10KB limit on the forwarded request size. +// We reserve 512 bytes for origin-request overhead CloudFront can still add after +// this viewer-request function runs, so borderline requests fail with a clear 431. +const CLOUDFRONT_FUNCTION_SAFE_HEADER_LIMIT = 10240 - 512; + export const CF_BLOCK_CLOUDFRONT_URL_INJECTION = ` if (event.request.headers.host.value.includes('cloudfront.net')) { return { @@ -1960,7 +2738,9 @@ if (event.request.headers.host.value.includes('cloudfront.net')) { }; }`; -export const CF_ROUTER_INJECTION = ` +// CloudFront Function handler code injected into site and router CF functions. +// NOTE: This string is size-sensitive β€” CloudFront Functions have a 10KB limit. +export const CF_ROUTER_INJECTION = minify` async function routeSite(kvNamespace, metadata) { const baselessUri = metadata.base ? event.request.uri.replace(metadata.base, "") @@ -2000,29 +2780,35 @@ async function routeSite(kvNamespace, metadata) { } } - // Route to S3 custom 404 (no servers) - if (metadata.custom404) { + // Route to S3 custom 404 (SPA fallback, no servers) + if (metadata.custom404 && !metadata.errorResponseCode) { event.request.uri = metadata.s3.dir + (metadata.base ? metadata.base : "") + metadata.custom404; setS3Origin(metadata.s3.domain); return; } + // Route unmatched to S3 (triggers customErrorResponses) + if (metadata.s3 && !metadata.servers) { + event.request.uri = metadata.s3.dir + event.request.uri; + setS3Origin(metadata.s3.domain); + return; + } + // Route to image optimizer if (metadata.image && baselessUri.startsWith(metadata.image.route)) { + setForwardedHost(); setNextjsCacheKey(); + if (isRequestHeaderTooLarge()) return buildOversizedHeadersResponse(); setUrlOrigin(metadata.image.host, metadata.image.originAccessControlConfig ? { originAccessControlConfig: metadata.image.originAccessControlConfig } : undefined); return; } // Route to servers if (metadata.servers){ - event.request.headers["x-forwarded-host"] = event.request.headers.host; - ${ - // Note: In SvelteKit, form action requests contain "/" in request query string - // ie. POST request with query string "?/action" - // CloudFront does not allow query string with "/". It needs to be encoded. - "" - } + setForwardedHost(); + // In SvelteKit, form action requests contain "/" in request query string + // ie. POST request with query string "?/action" + // CloudFront does not allow query string with "/". It needs to be encoded. for (var key in event.request.querystring) { if (key.includes("/")) { event.request.querystring[encodeURIComponent(key)] = event.request.querystring[key]; @@ -2031,49 +2817,30 @@ async function routeSite(kvNamespace, metadata) { } setNextjsGeoHeaders(); setNextjsCacheKey(); + if (isRequestHeaderTooLarge()) return buildOversizedHeadersResponse(); setUrlOrigin(findNearestServer(metadata.servers), metadata.origin); } + // Inject the CloudFront viewer country, region, latitude, and longitude headers into + // the request headers for OpenNext to use them function setNextjsGeoHeaders() { - ${ - // Inject the CloudFront viewer country, region, latitude, and longitude headers into - // the request headers for OpenNext to use them for OpenNext to use them - "" - } - if(event.request.headers["cloudfront-viewer-city"]) { - event.request.headers["x-open-next-city"] = event.request.headers["cloudfront-viewer-city"]; - } - if(event.request.headers["cloudfront-viewer-country"]) { - event.request.headers["x-open-next-country"] = event.request.headers["cloudfront-viewer-country"]; - } - if(event.request.headers["cloudfront-viewer-region"]) { - event.request.headers["x-open-next-region"] = event.request.headers["cloudfront-viewer-region"]; - } - if(event.request.headers["cloudfront-viewer-latitude"]) { - event.request.headers["x-open-next-latitude"] = event.request.headers["cloudfront-viewer-latitude"]; - } - if(event.request.headers["cloudfront-viewer-longitude"]) { - event.request.headers["x-open-next-longitude"] = event.request.headers["cloudfront-viewer-longitude"]; + var keys = ["city","country","region","latitude","longitude"]; + for (var i=0; i ${CLOUDFRONT_FUNCTION_SAFE_HEADER_LIMIT}; + } + + function buildOversizedHeadersResponse() { + return { + statusCode: 431, + statusDescription: "Request Header Fields Too Large", + headers: { + "cache-control": { value: "no-store" }, + "content-type": { value: "text/plain; charset=utf-8" }, + }, + body: { + encoding: "text", + data: "Request headers are too large. Reduce cookie size and try again.", + }, + }; + } + + function getRequestHeaderSize() { + var size = 0; + + for (var key in event.request.headers) { + var header = event.request.headers[key]; + if (header.multiValue) { + for (var i=0; i; + /** + * Rewrite the request path. + * + * @example + * + * If the route path is `/api/*` and a request comes in for `/api/users/profile`, + * the request path the destination sees is `/api/users/profile`. + * + * If you want to serve the route from the root, you can rewrite the request + * path to `/users/profile`. + * + * ```js + * router: { + * instance: router, + * path: "/api", + * rewrite: { + * regex: "^/api/(.*)$", + * to: "/$1" + * } + * } + * ``` + */ + rewrite?: Input<{ + /** + * The regex to match the request path. + */ + regex: Input; + /** + * The replacement for the matched path. + */ + to: Input; + }>; + /** + * The number of seconds that CloudFront waits for a response after routing a + * request to the destination. Must be between 1 and 60 seconds. + * + * When compared to the `connectionTimeout`, this is the total time for the + * request. + * + * @default `"20 seconds"` + * @example + * ```js + * router: { + * instance: router, + * readTimeout: "60 seconds" + * } + * ``` + */ + readTimeout?: Input; + /** + * The number of seconds that CloudFront should try to maintain the connection + * to the destination after receiving the last packet of the response. Must be + * between 1 and 60 seconds. + * + * @default `"5 seconds"` + * @example + * ```js + * router: { + * instance: router, + * keepAliveTimeout: "10 seconds" + * } + * ``` + */ + keepAliveTimeout?: Input; + /** + * The number of seconds that CloudFront waits before timing out and closing the + * connection to the origin. Must be between 1 and 10 seconds. + * + * @default `"10 seconds"` + * @example + * ```js + * router: { + * instance: router, + * connectionTimeout: "3 seconds" + * } + * ``` + */ + connectionTimeout?: Input; + /** + * The number of times that CloudFront attempts to connect to the origin. Must be + * between 1 and 3. + * + * @default `3` + * @example + * ```js + * router: { + * instance: router, + * connectionAttempts: 1 + * } + * ``` + */ + connectionAttempts?: Input; }; export type RouterRouteArgsDeprecated = { @@ -2320,6 +3252,13 @@ export function normalizeRouteArgs( ), routerKvNamespace: v.instance._kvNamespace!, routerKvStoreArn: v.instance._kvStoreArn!, + routerProtection: v.instance._protection, + routerDistributionArn: v.instance._distributionArn, + rewrite: v.rewrite, + readTimeout: v.readTimeout, + keepAliveTimeout: v.keepAliveTimeout, + connectionTimeout: v.connectionTimeout, + connectionAttempts: v.connectionAttempts, }; }); }); diff --git a/platform/src/components/aws/service-v1.ts b/platform/src/components/aws/service-v1.ts index 2b6885ef96..02740277c5 100644 --- a/platform/src/components/aws/service-v1.ts +++ b/platform/src/components/aws/service-v1.ts @@ -132,9 +132,9 @@ export class Service extends Component implements Link.Linkable { this._url = !self.loadBalancer ? undefined : all([self.domain, self.loadBalancer?.dnsName]).apply( - ([domain, loadBalancer]) => - domain ? `https://${domain}/` : `http://${loadBalancer}`, - ); + ([domain, loadBalancer]) => + domain ? `https://${domain}/` : `http://${loadBalancer}`, + ); registerHint(); registerReceiver(); @@ -162,7 +162,7 @@ export class Service extends Component implements Link.Linkable { } function normalizeRegion() { - return getRegionOutput(undefined, { parent: self }).name; + return getRegionOutput(undefined, { parent: self }).region; } function normalizeArchitecture() { @@ -545,12 +545,13 @@ export class Service extends Component implements Link.Linkable { { assumeRolePolicy: !$dev ? iam.assumeRolePolicyForPrincipal({ - Service: "ecs-tasks.amazonaws.com", - }) + Service: "ecs-tasks.amazonaws.com", + }) : iam.assumeRolePolicyForPrincipal({ - AWS: interpolate`arn:aws:iam::${getCallerIdentityOutput().accountId + AWS: interpolate`arn:aws:iam::${ + getCallerIdentityOutput().accountId }:root`, - }), + }), inlinePolicies: policy.apply(({ statements }) => statements ? [{ name: "inline", policy: policy.json }] : [], ), diff --git a/platform/src/components/aws/service.ts b/platform/src/components/aws/service.ts index 698c39feea..3ea034162a 100644 --- a/platform/src/components/aws/service.ts +++ b/platform/src/components/aws/service.ts @@ -38,6 +38,8 @@ import { } from "./fargate.js"; import { Dns } from "../dns.js"; import { hashStringToPrettyString } from "../naming.js"; +import { Alb } from "./alb.js"; +import { listenerKey, targetKey } from "./helpers/load-balancer.js"; type Port = `${number}/${"http" | "https" | "tcp" | "udp" | "tcp_udp" | "tls"}`; @@ -221,6 +223,89 @@ interface ServiceRules { }>; } +type AlbPort = `${number}/${"http" | "https"}`; + +export interface ServiceAlbRule { + /** + * The port and protocol to listen on, in `{port}/{protocol}` format. Must match a listener on the ALB. + * + * @example + * ```js + * { + * listen: "443/https" + * } + * ``` + */ + listen: AlbPort; + /** + * The container port and protocol to forward traffic to. Uses the format `{port}/{protocol}`. + * + * The protocol must match what the container actually speaks β€” using `"3000/https"` when + * the container speaks HTTP will cause health check failures. + * + * @example + * ```js + * { + * forward: "8080/http" + * } + * ``` + */ + forward: AlbPort; + /** + * The name of the container to forward the traffic to. Required when multiple containers + * are configured. + */ + container?: string; + /** + * The conditions for the listener rule. At least one condition (path, query, or header) + * must be specified. The ALB owns the default action β€” services only add conditional rules. + * + * @example + * ```js + * { + * conditions: { + * path: "/api/*" + * } + * } + * ``` + */ + conditions: { + /** + * Path pattern to match. Supports wildcards (`*` and `?`). + */ + path?: Input; + /** + * Query string conditions. + */ + query?: Input< + Input<{ + key?: Input; + value: Input; + }>[] + >; + /** + * HTTP header condition. + */ + header?: Input<{ + name: Input; + values: Input>[]; + }>; + }; + /** + * Explicit priority for the listener rule (1–50000). + * Must be unique per listener across ALL services sharing the ALB. + * Use non-overlapping ranges per service (e.g., Service A: 100-199, Service B: 200-299). + * + * @example + * ```js + * { + * priority: 100 + * } + * ``` + */ + priority: number; +} + interface ServiceContainerArgs extends FargateContainerArgs { /** * Configure the health check for the container. Same as the top-level @@ -552,7 +637,8 @@ export interface ServiceArgs extends FargateBaseArgs { * } * ``` */ - loadBalancer?: Input<{ + loadBalancer?: Input< + | { /** * Configure if the load balancer should be public or private. * @@ -939,7 +1025,52 @@ export interface ServiceArgs extends FargateBaseArgs { }> > >; - }>; + } + | { + /** + * The `Alb` instance to attach this service to. When provided, the service creates + * target groups and listener rules on the shared ALB instead of creating its own + * load balancer. + * + * ECS tasks use the VPC's default security group, which allows all traffic within the + * VPC CIDR. For tighter security, add an explicit security group ingress rule from the + * ALB's security group using `transform`. + * + * @example + * ```js + * { + * loadBalancer: { + * instance: alb, + * rules: [ + * { listen: "443/https", forward: "8080/http", conditions: { path: "/api/*" }, priority: 100 } + * ] + * } + * } + * ``` + */ + instance: Alb; + /** + * The rules for routing traffic from the ALB to this service's containers. + * Each rule must have explicit conditions and priority. + */ + rules: Prettify[]; + /** + * Configure health checks for the target groups. Uses the same format as the inline + * health check config, keyed by `{port}/{protocol}`. + */ + health?: Record< + AlbPort, + Input<{ + path?: Input; + interval?: Input; + timeout?: Input; + healthyThreshold?: Input; + unhealthyThreshold?: Input; + successCodes?: Input; + }> + >; + } + >; /** * Configure the CloudMap service registry for the service. * @@ -1389,6 +1520,11 @@ export interface ServiceArgs extends FargateBaseArgs { * Transform the AWS Application Auto Scaling target resource. */ autoScalingTarget?: Transform; + /** + * Transform the AWS Load Balancer listener rule resource. Only applies when + * attaching to an external ALB via the `loadBalancer.instance` prop. + */ + listenerRule?: Transform; } >; } @@ -1640,7 +1776,7 @@ export class Service extends Component implements Link.Linkable { const self = this; const clusterArn = args.cluster.nodes.cluster.arn; const clusterName = args.cluster.nodes.cluster.name; - const region = getRegionOutput({}, opts).name; + const region = getRegionOutput({}, opts).region; const dev = normalizeDev(); const wait = output(args.wait ?? false); const architecture = normalizeArchitecture(args); @@ -1648,7 +1784,8 @@ export class Service extends Component implements Link.Linkable { const memory = normalizeMemory(cpu, args); const storage = normalizeStorage(args); const containers = normalizeContainers("service", args, name, architecture); - const lbArgs = normalizeLoadBalancer(); + const albAttachment = detectAlbAttachment(); + const lbArgs = albAttachment ? undefined : normalizeLoadBalancer(); const scaling = normalizeScaling(); const capacity = normalizeCapacity(); const vpc = normalizeVpc(); @@ -1660,7 +1797,7 @@ export class Service extends Component implements Link.Linkable { this.taskRole = taskRole; if (dev) { - this.devUrl = !lbArgs ? undefined : dev.url; + this.devUrl = (!lbArgs && !args.loadBalancer) ? undefined : dev.url; registerReceiver(); return; } @@ -1679,30 +1816,59 @@ export class Service extends Component implements Link.Linkable { taskRole, executionRole, ); - const certificateArn = createSsl(); - const loadBalancer = createLoadBalancer(); - const targetGroups = createTargets(); - createListeners(); + let loadBalancer: lb.LoadBalancer | undefined; + let targetGroups: ReturnType; + let targetEntries: Output<{ targetGroup: lb.TargetGroup; containerName: string; containerPort: number }[]>; + let effectiveLbArn: Output | undefined; + let effectiveDomain: Output; + let effectiveDnsName: Output | undefined; + const certificateArn = albAttachment ? output(undefined) : createSsl(); + if (albAttachment) { + all([albAttachment.instance._vpc, vpc.id]).apply( + ([albVpcId, clusterVpcId]) => { + if (albVpcId !== clusterVpcId) { + throw new VisibleError( + `The ALB VPC "${albVpcId}" does not match the cluster VPC "${clusterVpcId}" in Service "${name}". The ALB and cluster must be in the same VPC.`, + ); + } + }, + ); + const { targets: albTargets, entries: albEntries } = createAlbTargetsAndEntries(albAttachment); + targetGroups = output(albTargets); + targetEntries = albEntries; + createAlbListenerRules(albAttachment, albTargets); + effectiveLbArn = albAttachment.instance.arn; + effectiveDomain = output(undefined); + effectiveDnsName = albAttachment.instance.dnsName; + } else { + loadBalancer = createLoadBalancer(); + targetGroups = createTargets(); + targetEntries = computeTargetEntries(); + createListeners(); + createDnsRecords(); + if (loadBalancer) { + effectiveLbArn = loadBalancer.arn; + effectiveDnsName = loadBalancer.dnsName; + } + effectiveDomain = lbArgs?.domain?.apply((d) => d?.name) ?? output(undefined); + } const cloudmapService = createCloudmapService(); const service = createService(); const autoScalingTarget = createAutoScaling(); - createDnsRecords(); this._service = service; this.cloudmapService = cloudmapService; this.executionRole = executionRole; this.taskDefinition = taskDefinition; - this.loadBalancer = loadBalancer; + this.loadBalancer = loadBalancer ?? albAttachment?.instance.nodes.loadBalancer; this.autoScalingTarget = autoScalingTarget; - this.domain = lbArgs?.domain - ? lbArgs.domain.apply((domain) => domain?.name) - : output(undefined); - this._url = !self.loadBalancer - ? undefined - : all([self.domain, self.loadBalancer?.dnsName]).apply( - ([domain, loadBalancer]) => - domain ? `https://${domain}/` : `http://${loadBalancer}`, - ); + this.domain = effectiveDomain; + this._url = effectiveDnsName + ? all([effectiveDomain, effectiveDnsName]).apply( + ([domain, dnsName]) => + domain ? `https://${domain}/` : `http://${dnsName}`, + ) + : undefined; this.registerOutputs({ _hint: this._url }); registerReceiver(); @@ -1741,7 +1907,9 @@ export class Service extends Component implements Link.Linkable { } function normalizeScaling() { - return all([lbArgs?.type, args.scaling]).apply(([type, v]) => { + // External ALB is always "application" type + const lbType = albAttachment ? output("application" as const) : lbArgs?.type; + return all([lbType, args.scaling]).apply(([type, v]) => { if (type !== "application" && v?.requestCount) throw new VisibleError( `Request count scaling is only supported for http/https protocols.`, @@ -1770,12 +1938,15 @@ export class Service extends Component implements Link.Linkable { } function normalizeLoadBalancer() { - const loadBalancer = ((args.loadBalancer ?? - args.public) as typeof args.loadBalancer)!; + const loadBalancer = args.loadBalancer ?? args.public; if (!loadBalancer) return; + // ALB attachment case is handled by detectAlbAttachment() before this is called + const inlineLoadBalancer = output(loadBalancer).apply( + (lb) => lb as Exclude, + ); // normalize rules - const rules = all([loadBalancer, containers]).apply( + const rules = all([inlineLoadBalancer, containers]).apply( ([lb, containers]) => { // validate rules const lbRules = lb.rules ?? lb.ports; @@ -1873,7 +2044,7 @@ export class Service extends Component implements Link.Linkable { ); // normalize domain - const domain = output(loadBalancer).apply((lb) => { + const domain = output(inlineLoadBalancer).apply((lb) => { if (!lb.domain) return undefined; // normalize domain @@ -1893,37 +2064,49 @@ export class Service extends Component implements Link.Linkable { ); // normalize public/private - const pub = output(loadBalancer).apply((lb) => lb?.public ?? true); + const pub = output(inlineLoadBalancer).apply((lb) => + "public" in lb ? lb.public ?? true : true, + ); // normalize health check - const health = all([type, rules, loadBalancer]).apply( + const health = all([type, rules, inlineLoadBalancer]).apply( ([type, rules, lb]) => Object.fromEntries( - Object.entries(lb?.health ?? {}).map(([k, v]) => { - if ( - !rules.find( - (r) => `${r.forwardPort}/${r.forwardProtocol}` === k, + Object.entries(("health" in lb ? lb.health : {}) ?? {}).map( + ([k, v]) => { + if ( + !rules.find( + (r) => `${r.forwardPort}/${r.forwardProtocol}` === k, + ) ) - ) - throw new VisibleError( - `Cannot configure health check for "${k}". Make sure it is defined in "loadBalancer.ports".`, - ); - return [ - k, - { - path: v.path ?? (type === "application" ? "/" : undefined), - interval: v.interval ? toSeconds(v.interval) : 30, - timeout: v.timeout - ? toSeconds(v.timeout) - : type === "application" - ? 5 - : 6, - healthyThreshold: v.healthyThreshold ?? 5, - unhealthyThreshold: v.unhealthyThreshold ?? 2, - matcher: v.successCodes ?? "200", - }, - ]; - }), + throw new VisibleError( + `Cannot configure health check for "${k}". Make sure it is defined in "loadBalancer.ports".`, + ); + const protocol = k.split("/")[1]; + return [ + k, + { + path: ["http", "https"].includes(protocol) + ? v.path ?? "/" + : undefined, + interval: v.interval ? toSeconds(v.interval) : 30, + timeout: v.timeout + ? toSeconds(v.timeout) + : type === "application" + ? 5 + : 6, + healthyThreshold: v.healthyThreshold ?? 5, + unhealthyThreshold: v.unhealthyThreshold ?? 2, + protocol: ["http", "https"].includes(protocol) + ? undefined + : "TCP", + matcher: ["http", "https"].includes(protocol) + ? v.successCodes ?? "200" + : undefined, + }, + ]; + }, + ), ), ); @@ -1989,7 +2172,7 @@ export class Service extends Component implements Link.Linkable { const container = r.container; const forwardProtocol = r.forwardProtocol.toUpperCase(); const forwardPort = r.forwardPort; - const targetId = `${container}${forwardProtocol}${forwardPort}`; + const targetId = targetKey(container!, forwardProtocol, forwardPort); const target = targets[targetId] ?? new lb.TargetGroup( @@ -2015,6 +2198,37 @@ export class Service extends Component implements Link.Linkable { }); } + function computeTargetEntries() { + if (!lbArgs || !targetGroups) + return output( + [] as { + targetGroup: lb.TargetGroup; + containerName: string; + containerPort: number; + }[], + ); + return all([lbArgs.rules, targetGroups]).apply(([rules, targets]) => { + const entries: { + targetGroup: lb.TargetGroup; + containerName: string; + containerPort: number; + }[] = []; + const seen = new Set(); + for (const rule of rules) { + if (rule.type !== "forward") continue; + const targetId = targetKey(rule.container!, rule.forwardProtocol, rule.forwardPort); + if (seen.has(targetId)) continue; + seen.add(targetId); + entries.push({ + targetGroup: targets[targetId], + containerName: rule.container!, + containerPort: rule.forwardPort, + }); + } + return entries; + }); + } + function createListeners() { if (!lbArgs || !loadBalancer || !targetGroups) return; @@ -2027,7 +2241,7 @@ export class Service extends Component implements Link.Linkable { rules.forEach((r) => { const listenProtocol = r.listenProtocol.toUpperCase(); const listenPort = r.listenPort; - const listenerId = `${listenProtocol}${listenPort}`; + const listenerId = listenerKey(listenProtocol, listenPort); listenersById[listenerId] = listenersById[listenerId] ?? []; listenersById[listenerId].push(r); }); @@ -2225,18 +2439,13 @@ export class Service extends Component implements Link.Linkable { enable: true, rollback: true, }, - loadBalancers: - lbArgs && - all([lbArgs.rules, targetGroups!]).apply(([rules, targets]) => - Object.values(targets).map((target) => ({ - targetGroupArn: target.arn, - containerName: target.port.apply( - (port) => - rules.find((r) => r.forwardPort === port)!.container!, - ), - containerPort: target.port.apply((port) => port!), - })), - ), + loadBalancers: targetEntries.apply((entries) => + entries.map((e) => ({ + targetGroupArn: e.targetGroup.arn, + containerName: e.containerName, + containerPort: e.containerPort, + })), + ), enableExecuteCommand: true, serviceRegistries: cloudmapService && { registryArn: cloudmapService.arn, @@ -2333,23 +2542,22 @@ export class Service extends Component implements Link.Linkable { targetTrackingScalingPolicyConfiguration: { predefinedMetricSpecification: { predefinedMetricType: "ALBRequestCountPerTarget", - resourceLabel: all([ - loadBalancer?.arn, - targetGroup.arn, - ]).apply(([loadBalancerArn, targetGroupArn]) => { - // arn:...:loadbalancer/app/frank-MyServiceLoadBalan/005af2ad12da1e52 - // => app/frank-MyServiceLoadBalan/005af2ad12da1e52 - const lbPart = loadBalancerArn - ?.split(":") - .pop() - ?.split("/") - .slice(1) - .join("/"); - // arn:...:targetgroup/HTTP20250103004618450100000001/e0811b8cf3a60762 - // => targetgroup/HTTP20250103004618450100000001 - const tgPart = targetGroupArn?.split(":").pop(); - return `${lbPart}/${tgPart}`; - }), + resourceLabel: all([effectiveLbArn!, targetGroup.arn]).apply( + ([lbArn, targetGroupArn]) => { + // arn:...:loadbalancer/app/frank-MyServiceLoadBalan/005af2ad12da1e52 + // => app/frank-MyServiceLoadBalan/005af2ad12da1e52 + const lbPart = lbArn + .split(":") + .pop() + ?.split("/") + .slice(1) + .join("/"); + // arn:...:targetgroup/HTTP20250103004618450100000001/e0811b8cf3a60762 + // => targetgroup/HTTP20250103004618450100000001 + const tgPart = targetGroupArn.split(":").pop(); + return `${lbPart}/${tgPart}`; + }, + ), }, targetValue: requestCount, scaleInCooldown, @@ -2386,6 +2594,214 @@ export class Service extends Component implements Link.Linkable { }); } + function detectAlbAttachment() { + const loadBalancer = args.loadBalancer; + if ( + !loadBalancer || + typeof loadBalancer !== "object" || + !("instance" in loadBalancer) || + !(loadBalancer.instance instanceof Alb) + ) { + return undefined; + } + return loadBalancer; + } + + function createAlbTargetsAndEntries( + attachment: NonNullable, + ) { + const rules = attachment.rules; + const health = attachment.health ?? {}; + + if (rules.length === 0) { + throw new VisibleError( + `You must provide at least one rule in "loadBalancer.rules" when using an external ALB in Service "${name}".`, + ); + } + + // Validate container names (no resources created here) + containers.apply((ctrs) => { + const containerNames = new Set(ctrs.map((c) => c.name)); + if (ctrs.length > 1) { + for (const rule of rules) { + if (!rule.container) { + throw new VisibleError( + `You must provide a "container" name in each rule when there is more than one container in Service "${name}".`, + ); + } + } + } + for (const rule of rules) { + const cn = rule.container ?? ctrs[0].name; + if (!containerNames.has(cn)) { + throw new VisibleError( + `Container "${cn}" in "loadBalancer.rules" does not match any container in Service "${name}". Available: ${[...containerNames].join(", ")}.`, + ); + } + } + }); + + // Create target groups in a plain loop (no apply) + const targets: Record = {}; + const rawEntries: { + targetGroup: lb.TargetGroup; + explicitContainer: string | undefined; + containerPort: number; + }[] = []; + + for (const rule of rules) { + const parts = rule.forward.split("/"); + const forwardPort = parseInt(parts[0]); + const forwardProtocol = parts[1].toUpperCase(); + // Use explicit container or component name for keying/naming + const containerNameForKey = rule.container ?? name; + const tgtId = targetKey(containerNameForKey, forwardProtocol, forwardPort); + + if (!targets[tgtId]) { + const healthKey = `${forwardPort}/${parts[1]}` as AlbPort; + const healthCheck = health[healthKey]; + const normalizedHealth = healthCheck + ? output(healthCheck).apply((h) => ({ + path: h.path ?? "/", + interval: h.interval ? toSeconds(h.interval) : 30, + timeout: h.timeout ? toSeconds(h.timeout) : 5, + healthyThreshold: h.healthyThreshold ?? 5, + unhealthyThreshold: h.unhealthyThreshold ?? 2, + matcher: h.successCodes ?? "200", + })) + : { + path: "/", + interval: 30, + timeout: 5, + healthyThreshold: 5, + unhealthyThreshold: 2, + matcher: "200", + }; + + const tg = new lb.TargetGroup( + ...transform( + args.transform?.target, + `${name}Target${tgtId}`, + { + namePrefix: forwardProtocol, + port: forwardPort, + protocol: forwardProtocol, + targetType: "ip", + vpcId: attachment.instance._vpc, + healthCheck: normalizedHealth, + }, + { parent: self }, + ), + ); + targets[tgtId] = tg; + rawEntries.push({ + targetGroup: tg, + explicitContainer: rule.container, + containerPort: forwardPort, + }); + } + } + + // Resolve actual container names for ECS registration + const entries = containers.apply((ctrs) => + rawEntries.map((e) => ({ + targetGroup: e.targetGroup, + containerName: e.explicitContainer ?? ctrs[0].name, + containerPort: e.containerPort, + })), + ); + + return { targets, entries }; + } + + function createAlbListenerRules( + attachment: NonNullable, + albTargets: Record, + ) { + const rules = attachment.rules; + const prioritiesByListener = new Map>(); + + for (const rule of rules) { + if (rule.priority < 1 || rule.priority > 50000) { + throw new VisibleError( + `Priority ${rule.priority} must be between 1 and 50000 in Service "${name}". When sharing an ALB, ensure non-overlapping priority ranges across services.`, + ); + } + + const seen = + prioritiesByListener.get(rule.listen) ?? new Set(); + if (seen.has(rule.priority)) { + throw new VisibleError( + `Duplicate priority ${rule.priority} on listener "${rule.listen}" in Service "${name}".`, + ); + } + seen.add(rule.priority); + prioritiesByListener.set(rule.listen, seen); + + if ( + !rule.conditions?.path && + !rule.conditions?.query && + !rule.conditions?.header + ) { + throw new VisibleError( + `At least one condition (path, query, or header) must be set for rules on an external ALB in Service "${name}".`, + ); + } + + const listenerParts = rule.listen.split("/"); + const listenerPort = parseInt(listenerParts[0]); + const listenerProtocol = listenerParts[1]; + + const forwardParts = rule.forward.split("/"); + const forwardPort = parseInt(forwardParts[0]); + const forwardProtocol = forwardParts[1].toUpperCase(); + const containerNameForKey = rule.container ?? name; + const tgtId = targetKey(containerNameForKey, forwardProtocol, forwardPort); + + const targetGroup = albTargets[tgtId]; + if (!targetGroup) { + throw new VisibleError( + `Target group "${tgtId}" not found. Ensure the forward port matches in Service "${name}".`, + ); + } + + const listenerResource = + attachment.instance.getListener(listenerProtocol, listenerPort); + + new lb.ListenerRule( + ...transform( + args.transform?.listenerRule, + `${name}AlbRule${listenerProtocol.toUpperCase()}${listenerPort}P${rule.priority}`, + { + listenerArn: listenerResource.arn, + priority: rule.priority, + actions: [ + { + type: "forward", + targetGroupArn: targetGroup.arn, + }, + ], + conditions: [ + { + pathPattern: rule.conditions.path + ? { values: [rule.conditions.path] } + : undefined, + queryStrings: rule.conditions.query, + httpHeader: rule.conditions.header + ? output(rule.conditions.header).apply((h) => ({ + httpHeaderName: h.name, + values: h.values, + })) + : undefined, + }, + ], + }, + { parent: self }, + ), + ); + } + } + function registerReceiver() { all([containers]).apply(([val]) => { for (const container of val) { @@ -2512,7 +2928,6 @@ export class Service extends Component implements Link.Linkable { * The Amazon Cloud Map service. */ get cloudmapService() { - console.log("NODES GETTER"); if (self.dev) throw new VisibleError( "Cannot access `nodes.cloudmapService` in dev mode.", diff --git a/platform/src/components/aws/sns-topic-lambda-subscriber.ts b/platform/src/components/aws/sns-topic-lambda-subscriber.ts index 2181c451ba..cd7f98431c 100644 --- a/platform/src/components/aws/sns-topic-lambda-subscriber.ts +++ b/platform/src/components/aws/sns-topic-lambda-subscriber.ts @@ -6,7 +6,7 @@ import { output, } from "@pulumi/pulumi"; import { Component, transform } from "../component"; -import { Function, FunctionArgs } from "./function"; +import { Function, FunctionArgs } from "./function.js"; import { SnsTopicSubscriberArgs } from "./sns-topic"; import { lambda, sns } from "@pulumi/aws"; import { FunctionBuilder, functionBuilder } from "./helpers/function-builder"; @@ -73,6 +73,7 @@ export class SnsTopicLambdaSubscriber extends Component { { action: "lambda:InvokeFunction", function: fn.arn, + qualifier: fn.qualifier.apply((qualifier) => qualifier!), principal: "sns.amazonaws.com", sourceArn: topic.arn, }, @@ -88,7 +89,7 @@ export class SnsTopicLambdaSubscriber extends Component { { topic: topic.arn, protocol: "lambda", - endpoint: fn.arn, + endpoint: fn.targetArn, filterPolicy: args.filter && jsonStringify(args.filter), }, { parent: self, dependsOn: [permission] }, diff --git a/platform/src/components/aws/sns-topic.ts b/platform/src/components/aws/sns-topic.ts index 7140b1e257..0febb74ed9 100644 --- a/platform/src/components/aws/sns-topic.ts +++ b/platform/src/components/aws/sns-topic.ts @@ -2,7 +2,7 @@ import { ComponentResourceOptions, Output, all, output } from "@pulumi/pulumi"; import { Component, outputId, Transform, transform } from "../component"; import { Link } from "../link"; import type { Input } from "../input"; -import { FunctionArgs, FunctionArn } from "./function"; +import { FunctionArgs, FunctionArn } from "./function.js"; import { hashStringToPrettyString, logicalName } from "../naming"; import { parseTopicArn } from "./helpers/arn"; import { SnsTopicLambdaSubscriber } from "./sns-topic-lambda-subscriber"; @@ -645,7 +645,7 @@ export class SnsTopic extends Component implements Link.Linkable { * * @param name The name of the component. * @param topicArn The ARN of the existing SNS Topic. - * @param opts? Resource options. + * @param opts Resource options. * * @example * Imagine you create a topic in the `dev` stage. And in your personal stage `frank`, diff --git a/platform/src/components/aws/solid-start.ts b/platform/src/components/aws/solid-start.ts index 266a9f70c0..14448169f7 100644 --- a/platform/src/components/aws/solid-start.ts +++ b/platform/src/components/aws/solid-start.ts @@ -299,21 +299,6 @@ export interface SolidStartArgs extends SsrSiteArgs { * ``` */ buildCommand?: SsrSiteArgs["buildCommand"]; - /** - * Configure how the SolidStart app assets are uploaded to S3. - * - * By default, this is set to the following. Read more about these options below. - * ```js - * { - * assets: { - * textEncoding: "utf-8", - * versionedFilesCacheHeader: "public,max-age=31536000,immutable", - * nonVersionedFilesCacheHeader: "public,max-age=0,s-maxage=86400,stale-while-revalidate=8640" - * } - * } - * ``` - */ - assets?: SsrSiteArgs["assets"]; /** * Configure the SolidStart app to use an existing CloudFront cache policy. * diff --git a/platform/src/components/aws/ssr-site.ts b/platform/src/components/aws/ssr-site.ts index 29f6e01a87..f8063d18f3 100644 --- a/platform/src/components/aws/ssr-site.ts +++ b/platform/src/components/aws/ssr-site.ts @@ -11,15 +11,14 @@ import { interpolate, ComponentResourceOptions, Resource, + asset as pulumiAsset, } from "@pulumi/pulumi"; -import * as pulumi from "@pulumi/pulumi"; -import * as aws from "@pulumi/aws"; import { Cdn, CdnArgs } from "./cdn.js"; import { Function, FunctionArgs, FunctionArn } from "./function.js"; import { parseLambdaEdgeArn } from "./helpers/arn.js"; import { Bucket, BucketArgs } from "./bucket.js"; import { BucketFile, BucketFiles } from "./providers/bucket-files.js"; -import { logicalName } from "../naming.js"; +import { logicalName, physicalName } from "../naming.js"; import { Input } from "../input.js"; import { Component, @@ -40,6 +39,7 @@ import { CF_ROUTER_INJECTION, CF_BLOCK_CLOUDFRONT_URL_INJECTION, KV_SITE_METADATA, + ProtectionConfig, RouterRouteArgsDeprecated, normalizeRouteArgs, RouterRouteArgs, @@ -325,17 +325,17 @@ export interface SsrSiteArgs extends BaseSsrSiteArgs { /** * The runtime environment for the server function. * - * @default `"nodejs20.x"` + * @default `"nodejs24.x"` * @example * ```js * { * server: { - * runtime: "nodejs22.x" + * runtime: "nodejs24.x" * } * } * ``` */ - runtime?: Input<"nodejs18.x" | "nodejs20.x" | "nodejs22.x">; + runtime?: Input<"nodejs18.x" | "nodejs20.x" | "nodejs22.x" | "nodejs24.x">; /** * The maximum amount of time the server function can run. * @@ -605,6 +605,51 @@ export interface SsrSiteArgs extends BaseSsrSiteArgs { * ``` */ vpc?: FunctionArgs["vpc"]; + /** + * Configure how the assets are uploaded to S3. + * + * By default, this is set to the following. Read more about these options below. + * ```js + * { + * assets: { + * textEncoding: "utf-8", + * versionedFilesCacheHeader: "public,max-age=31536000,immutable", + * nonVersionedFilesCacheHeader: "public,max-age=0,s-maxage=86400,stale-while-revalidate=8640" + * } + * } + * ``` + * + * #### Limits + * + * CloudFront distributions have a **limit of 25 cache behaviors** per distribution. + * Each top-level file or directory in your app's public asset directory creates a cache + * behavior. If you have too many, you'll hit the `TooManyCacheBehaviors` error. This is + * most common with `SvelteKit`, `SolidStart`, `Nuxt`, and `Analog` apps. + * + * For example, in the case of SvelteKit, the static assets are in the `static/` + * directory. If you have a file and a directory in it, it'll create 2 cache behaviors. + * + * ```bash frame="none" + * static/ + * β”œβ”€β”€ icons/ # Cache behavior for /icons/* + * └── logo.png # Cache behavior for /logo.png + * ``` + * + * So if you have many of these at the top-level, you'll hit the limit. You can request a + * limit increase through AWS Support. + * + * Alternatively, you can move some of these into subdirectories. For example, moving them + * to an `images/` directory, will only create 1 cache behavior. + * + * ```bash frame="none" + * static/ + * └── images/ # Cache behavior for /images/* + * β”œβ”€β”€ icons/ + * └── logo.png + * ``` + * + * Learn more about these [CloudFront limits](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html#limits-web-distributions). + */ assets?: Input<{ /** * Character encoding for text based assets, like HTML, CSS, JS. This is @@ -844,7 +889,7 @@ export abstract class SsrSite extends Component implements Link.Linkable { headersConfig: { headerBehavior: "whitelist", headers: { - items: ["x-open-next-cache-key"], + items: ["x-open-next-cache-key", "x-forwarded-host"], }, }, queryStringsConfig: { @@ -898,8 +943,8 @@ async function handler(event) { metadata = JSON.parse(v); } catch (e) {} - await routeSite(kvNamespace, metadata); - return event.request; + const response = await routeSite(kvNamespace, metadata); + return response || event.request; }`, }, { parent: self }, @@ -985,11 +1030,7 @@ async function handler(event) { return []; } - // Use provided ARN if available - if ( - "edgeFunction" in protectionConfig && - protectionConfig.edgeFunction?.arn - ) { + if (protectionConfig.edgeFunction?.arn) { return [ { eventType: "origin-request", @@ -999,7 +1040,6 @@ async function handler(event) { ]; } - // Use auto-created function if available if (autoEdgeFunction) { return [ { @@ -1024,16 +1064,18 @@ async function handler(event) { createInvalidation(); // Create Lambda permissions based on protection mode - all([distribution, servers, imageOptimizer, protection]).apply( - ([dist, servers, imgOptimizer, protection]) => { - if (!dist) return; + all([distribution, route, servers, imageOptimizer, protection]).apply( + ([dist, routeVal, servers, imgOptimizer, protection]) => { + const distributionArn = dist + ? dist.nodes.distribution.arn + : routeVal?.routerDistributionArn; + if (!distributionArn) return; // Server functions servers.forEach(({ region, server }) => { const provider = useProvider(region); if (protection.mode === "none") { - // Create explicit public access permission for none mode new lambda.Permission( `${name}PublicFunctionUrlAccess${logicalName(region)}`, { @@ -1048,14 +1090,13 @@ async function handler(event) { protection.mode === "oac" || protection.mode === "oac-with-edge-signing" ) { - // Create CloudFront-specific permission for OAC modes new lambda.Permission( `${name}CloudFrontFunctionUrlAccess${logicalName(region)}`, { action: "lambda:InvokeFunctionUrl", function: server.nodes.function.name, principal: "cloudfront.amazonaws.com", - sourceArn: dist.nodes.distribution.arn, + sourceArn: distributionArn, }, { provider, parent: self }, ); @@ -1065,7 +1106,8 @@ async function handler(event) { action: "lambda:InvokeFunction", function: server.nodes.function.name, principal: "cloudfront.amazonaws.com", - sourceArn: dist.nodes.distribution.arn, + sourceArn: distributionArn, + invokedViaFunctionUrl: true, }, { provider, parent: self }, ); @@ -1095,7 +1137,7 @@ async function handler(event) { action: "lambda:InvokeFunctionUrl", function: imgOptimizer.nodes.function.name, principal: "cloudfront.amazonaws.com", - sourceArn: dist.nodes.distribution.arn, + sourceArn: distributionArn, }, { parent: self }, ); @@ -1105,7 +1147,8 @@ async function handler(event) { action: "lambda:InvokeFunction", function: imgOptimizer.nodes.function.name, principal: "cloudfront.amazonaws.com", - sourceArn: dist.nodes.distribution.arn, + sourceArn: distributionArn, + invokedViaFunctionUrl: true, }, { parent: self }, ); @@ -1175,7 +1218,7 @@ async function handler(event) { function normalizeRegions() { return output( - args.regions ?? [getRegionOutput(undefined, { parent: self }).name], + args.regions ?? [getRegionOutput(undefined, { parent: self }).region], ).apply((regions) => { if (regions.length === 0) throw new VisibleError( @@ -1221,6 +1264,11 @@ async function handler(event) { throw new VisibleError( `Cannot provide both "edge" and "route". Use the "edge" prop on the "Router" component when serving your site through a Router.`, ); + + if (args.protection) + throw new VisibleError( + `Cannot set "protection" when routing through a Router. Set "protection" on the Router component instead.`, + ); } return route; @@ -1240,20 +1288,20 @@ async function handler(event) { ); } - function normalizeProtection() { + function normalizeProtection(): Output { + if (route) { + return route.apply((r) => r.routerProtection); + } + return output(args.protection).apply((protection) => { - // Default to "none" if not specified if (!protection) return { mode: "none" as const }; - // Handle string values if (typeof protection === "string") { return { mode: protection }; } - // Handle object form - validate ARN if provided if ( protection.mode === "oac-with-edge-signing" && - "edgeFunction" in protection && protection.edgeFunction?.arn ) { const arn = protection.edgeFunction.arn; @@ -1267,29 +1315,26 @@ async function handler(event) { } function createLambdaEdgeFunction() { + if (route) return output(undefined); + return protection.apply((protectionConfig) => { - // Only create function if mode is oac-with-edge-signing and no ARN is provided if ( protectionConfig.mode !== "oac-with-edge-signing" || - ("edgeFunction" in protectionConfig && - protectionConfig.edgeFunction?.arn) + protectionConfig.edgeFunction?.arn ) { return undefined; } - const edgeConfig = - "edgeFunction" in protectionConfig - ? protectionConfig.edgeFunction - : {}; + const edgeConfig = protectionConfig.edgeFunction; const memory = edgeConfig?.memory ? toMBs(edgeConfig.memory) : 128; const timeout = edgeConfig?.timeout ? toSeconds(edgeConfig.timeout) : 5; - // Create IAM role for Lambda@Edge using SST transform pattern - const edgeRole = new aws.iam.Role( + const edgeRole = new iam.Role( ...transform( undefined, `${name}EdgeFunctionRole`, { + name: physicalName(64, `${name}EdgeRole`), assumeRolePolicy: JSON.stringify({ Version: "2012-10-17", Statement: [ @@ -1309,31 +1354,31 @@ async function handler(event) { "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", ], }, - { parent: self }, + { parent: self, ignoreChanges: ["name"] }, ), ); - // Create the Lambda@Edge function using SST transform pattern - const edgeFunction = new aws.lambda.Function( + const edgeFunction = new lambda.Function( ...transform( undefined, `${name}EdgeFunction`, { - runtime: "nodejs22.x", + name: physicalName(64, `${name}EdgeFn`), + runtime: "nodejs24.x", handler: "index.handler", role: edgeRole.arn, - code: new pulumi.asset.FileArchive( + code: new pulumiAsset.FileArchive( path.join($cli.paths.platform, "dist", "oac-edge-signer"), ), - publish: true, // Required for Lambda@Edge + publish: true, timeout: timeout, memorySize: memory, description: `${name} Lambda@Edge function for OAC request signing`, }, { parent: self, - // Lambda@Edge functions must be created in us-east-1 provider: useProvider("us-east-1"), + ignoreChanges: ["name"], }, ), ); @@ -1349,7 +1394,7 @@ async function handler(event) { `${name}DevServer`, { description: `${name} dev server`, - runtime: "nodejs20.x", + runtime: "nodejs24.x", timeout: "20 seconds", memory: "128 MB", bundle: path.join( @@ -1426,7 +1471,7 @@ async function handler(event) { ...planServer, description: planServer.description ?? `${name} server`, runtime: output(args.server?.runtime).apply( - (v) => v ?? planServer.runtime ?? "nodejs20.x", + (v) => v ?? planServer.runtime ?? "nodejs24.x", ), timeout, memory: output(args.server?.memory).apply( @@ -1439,10 +1484,23 @@ async function handler(event) { nodejs: { ...planServer.nodejs, format: "esm" as const, - install: output(args.server?.install).apply((install) => [ - ...(install ?? []), - ...(planServer.nodejs?.install ?? []), - ]), + install: output({ + install: args.server?.install, + planInstall: planServer.nodejs?.install, + }).apply(({ install, planInstall }) => { + const result: Record = {}; + for (const pkg of install ?? []) { + result[pkg] = "*"; + } + if (Array.isArray(planInstall)) { + for (const pkg of planInstall) { + result[pkg] = "*"; + } + } else if (planInstall) { + Object.assign(result, planInstall); + } + return result; + }), loader: args.server?.loader ?? planServer.nodejs?.loader, }, environment: output(args.environment).apply((environment) => ({ @@ -1494,7 +1552,7 @@ async function handler(event) { job: { description: `${name} warmer`, bundle: path.join($cli.paths.platform, "dist", "ssr-warmer"), - runtime: "nodejs20.x", + runtime: "nodejs24.x", handler: "index.handler", timeout: "900 seconds", memory: "128 MB", @@ -1683,7 +1741,7 @@ async function handler(event) { bucketName: bucket.name, files: bucketFiles, purge, - region: getRegionOutput(undefined, { parent: self }).name, + region: getRegionOutput(undefined, { parent: self }).region, }, { parent: self }, ); @@ -1790,7 +1848,7 @@ async function handler(event) { } : undefined, servers: servers.map((s) => [ - new URL(s.url).host, + new URL(s.url!).host, supportedRegions[s.region as keyof typeof supportedRegions].lat, supportedRegions[s.region as keyof typeof supportedRegions].lon, ]), @@ -1845,8 +1903,32 @@ async function handler(event) { } function createInvalidation() { - all([args.invalidation, outputPath, plan]).apply( - ([invalidationRaw, outputPath, plan]) => { + // Resolve server/image-optimizer code hashes so that server-only code + // changes also trigger a CloudFront invalidation. Without this, the + // version below is derived purely from static assets and an SSR deploy + // that only changes the Lambda bundle produces an identical hash, + // causing Pulumi to skip the DistributionInvalidation update. + const serverCodeHashes = servers.apply((list) => + all(list.map(({ server }) => server.nodes.function.codeSha256)), + ); + const imageOptimizerCodeHash = imageOptimizer.apply( + (opt) => opt?.nodes.function.codeSha256, + ); + + all([ + args.invalidation, + outputPath, + plan, + serverCodeHashes, + imageOptimizerCodeHash, + ]).apply( + ([ + invalidationRaw, + outputPath, + plan, + serverCodeHashes, + imageOptimizerCodeHash, + ]) => { // Normalize invalidation if (invalidationRaw === false) return; const invalidation = { @@ -1884,6 +1966,12 @@ async function handler(event) { } else { const hash = crypto.createHash("md5"); + // Fold in server bundle hashes so that changes to server-only + // code (e.g. loaders, handlers) invalidate the CloudFront cache + // even when no static assets changed. + serverCodeHashes.forEach((h) => hash.update(h)); + if (imageOptimizerCodeHash) hash.update(imageOptimizerCodeHash); + cachedS3Files.forEach((item) => { // The below options are needed to support following symlinks when building zip files: // - nodir: This will prevent symlinks themselves from being copied into the zip. diff --git a/platform/src/components/aws/static-site.ts b/platform/src/components/aws/static-site.ts index c6498701b1..d7aa009a3e 100644 --- a/platform/src/components/aws/static-site.ts +++ b/platform/src/components/aws/static-site.ts @@ -907,7 +907,7 @@ export class StaticSite extends Component implements Link.Linkable { function getBucketDetails() { const s3Bucket = bucket ? bucket.nodes.bucket - : s3.BucketV2.get(`${name}Assets`, assets.bucket!, undefined, { + : s3.Bucket.get(`${name}Assets`, assets.bucket!, undefined, { parent: self, }); @@ -984,7 +984,7 @@ export class StaticSite extends Component implements Link.Linkable { bucketName, files: bucketFiles, purge: assets.purge, - region: getRegionOutput(undefined, { parent: self }).name, + region: getRegionOutput(undefined, { parent: self }).region, }, { parent: self }, ); @@ -1008,7 +1008,15 @@ export class StaticSite extends Component implements Link.Linkable { bucketDomain, errorPage, route, - ]).apply(async ([outputPath, assets, bucketDomain, errorPage, route]) => { + args.errorPage, + ]).apply(async ([ + outputPath, + assets, + bucketDomain, + errorPage, + route, + hasErrorPage, + ]) => { const kvEntries: Record = {}; const dirs: string[] = []; // Router append .html and index.html suffixes to requests to s3 routes: @@ -1040,7 +1048,8 @@ export class StaticSite extends Component implements Link.Linkable { kvEntries["metadata"] = JSON.stringify({ base: route?.pathPrefix === "/" ? undefined : route?.pathPrefix, - custom404: errorPage, + custom404: hasErrorPage ? undefined : errorPage, + errorResponseCode: hasErrorPage ? 404 : undefined, s3: { domain: bucketDomain, dir: assets.path ? "/" + assets.path : "", @@ -1120,8 +1129,8 @@ async function handler(event) { metadata = JSON.parse(v); } catch (e) {} - await routeSite(kvNamespace, metadata); - return event.request; + const response = await routeSite(kvNamespace, metadata); + return response || event.request; }`, }, { parent: self }, @@ -1201,11 +1210,38 @@ async function handler(event) { : []), ]), }, - }, - { parent: self }, - ), - ); - } + customErrorResponses: all([ + args.errorPage, + errorPage, + route, + ]).apply(([hasCustomErrorPage, errorPage, route]) => { + if (!hasCustomErrorPage) return []; + const base = + route?.pathPrefix && route.pathPrefix !== "/" + ? route.pathPrefix + : "/"; + const pagePath = path.posix.join(base, errorPage); + + return [ + { + errorCode: 403, + responseCode: 404, + responsePagePath: pagePath, + errorCachingMinTtl: 0, + }, + { + errorCode: 404, + responseCode: 404, + responsePagePath: pagePath, + errorCachingMinTtl: 0, + }, + ]; + }), + }, + { parent: self }, + ), + ); + } function createInvalidation() { all([outputPath, args.assets, args.invalidation]).apply( diff --git a/platform/src/components/aws/step-functions.ts b/platform/src/components/aws/step-functions.ts index 815ba36a56..b76414173b 100644 --- a/platform/src/components/aws/step-functions.ts +++ b/platform/src/components/aws/step-functions.ts @@ -29,7 +29,7 @@ import { Input } from "../input"; import { RETENTION } from "./logging"; import { physicalName } from "../naming"; import { functionBuilder } from "./helpers/function-builder"; -import { Function } from "./function"; +import { Function } from "./function.js"; export interface StepFunctionsArgs { /** @@ -581,13 +581,13 @@ export class StepFunctions extends Component implements Link.Linkable { ...args, resource: "arn:aws:states:::lambda:invoke", arguments: { - FunctionName: fn.arn, + FunctionName: fn.targetArn, Payload: args.payload, }, permissions: [ { actions: ["lambda:InvokeFunction"], - resources: [fn.arn], + resources: [fn.targetArn], }, ], }); diff --git a/platform/src/components/aws/step-functions/state.ts b/platform/src/components/aws/step-functions/state.ts index 428b707be4..8fc879e73f 100644 --- a/platform/src/components/aws/step-functions/state.ts +++ b/platform/src/components/aws/step-functions/state.ts @@ -1,7 +1,7 @@ import { randomBytes } from "crypto"; import { Duration, toSeconds } from "../../duration"; import { Input } from "../../input"; -import { FunctionPermissionArgs } from "../function"; +import { FunctionPermissionArgs } from "../function.js"; export type JSONata = `{% ${string} %}`; diff --git a/platform/src/components/aws/step-functions/task.ts b/platform/src/components/aws/step-functions/task.ts index 99e35cd05b..54e1ddfd46 100644 --- a/platform/src/components/aws/step-functions/task.ts +++ b/platform/src/components/aws/step-functions/task.ts @@ -7,7 +7,7 @@ import { FunctionArgs, FunctionArn, FunctionPermissionArgs, -} from "../function"; +} from "../function.js"; import { CatchArgs, Failable, @@ -353,8 +353,16 @@ export interface LambdaInvokeArgs extends TaskBaseArgs { * } * } * ``` + * + * Or, you can pass in a JSONata expression that evaluates to the full payload. + * + * ```ts + * { + * payload: "{% $states.input %}" + * } + * ``` */ - payload?: Record>; + payload?: Input>>; } export interface SnsPublishArgs extends TaskBaseArgs { diff --git a/platform/src/components/aws/svelte-kit.ts b/platform/src/components/aws/svelte-kit.ts index 99aa6c7779..4f86e7ebaa 100644 --- a/platform/src/components/aws/svelte-kit.ts +++ b/platform/src/components/aws/svelte-kit.ts @@ -301,21 +301,6 @@ export interface SvelteKitArgs extends SsrSiteArgs { * ``` */ buildCommand?: SsrSiteArgs["buildCommand"]; - /** - * Configure how the SvelteKit app assets are uploaded to S3. - * - * By default, this is set to the following. Read more about these options below. - * ```js - * { - * assets: { - * textEncoding: "utf-8", - * versionedFilesCacheHeader: "public,max-age=31536000,immutable", - * nonVersionedFilesCacheHeader: "public,max-age=0,s-maxage=86400,stale-while-revalidate=8640" - * } - * } - * ``` - */ - assets?: SsrSiteArgs["assets"]; /** * Configure the SvelteKit app to use an existing CloudFront cache policy. * diff --git a/platform/src/components/aws/tan-stack-start.ts b/platform/src/components/aws/tan-stack-start.ts index dd378f3503..a003b3515e 100644 --- a/platform/src/components/aws/tan-stack-start.ts +++ b/platform/src/components/aws/tan-stack-start.ts @@ -218,7 +218,7 @@ export interface TanStackStartArgs extends SsrSiteArgs { * } * } * ``` - * + * * Finally, to serve your TanStack Start app **from a combined pattern** like * `dev.example.com/docs`, you'll need to configure the domain in your `Router` to * match the subdomain, and set the `domain` and the `path`. @@ -251,21 +251,6 @@ export interface TanStackStartArgs extends SsrSiteArgs { * ``` */ buildCommand?: SsrSiteArgs["buildCommand"]; - /** - * Configure how the TanStack Start app assets are uploaded to S3. - * - * By default, this is set to the following. Read more about these options below. - * ```js - * { - * assets: { - * textEncoding: "utf-8", - * versionedFilesCacheHeader: "public,max-age=31536000,immutable", - * nonVersionedFilesCacheHeader: "public,max-age=0,s-maxage=86400,stale-while-revalidate=8640" - * } - * } - * ``` - */ - assets?: SsrSiteArgs["assets"]; /** * Configure the TanStack Start app to use an existing CloudFront cache policy. * @@ -427,12 +412,12 @@ export class TanStackStart extends SsrSite { ); } - const serverOutputPath = path.join(outputPath, ".output", "server"); + const serverOutputPath = path.resolve(outputPath, ".output", "server"); // If basepath is configured, nitro.mjs will have a line that looks like this: // return createRouter$2({ routeTree: Nr, defaultPreload: "intent", defaultErrorComponent: ce, defaultNotFoundComponent: () => jsx(de, {}), scrollRestoration: true, basepath: "/tan" }); let basepath; - + try { const serverNitroChunk = fs.readFileSync( path.join(serverOutputPath, "chunks", "_", "server.mjs"), diff --git a/platform/src/components/aws/task.ts b/platform/src/components/aws/task.ts index 20df5b8b15..e4c8242724 100644 --- a/platform/src/components/aws/task.ts +++ b/platform/src/components/aws/task.ts @@ -2,9 +2,10 @@ import { all, ComponentResourceOptions, Output, output } from "@pulumi/pulumi"; import { Component, Prettify } from "../component.js"; import { Link } from "../link.js"; import { Cluster } from "./cluster.js"; -import { ecs, iam } from "@pulumi/aws"; +import { ec2, ecs, iam } from "@pulumi/aws"; import { permission } from "./permission.js"; import { Vpc } from "./vpc.js"; +import { VisibleError } from "../error.js"; import { Function } from "./function.js"; import { FargateBaseArgs, @@ -72,22 +73,30 @@ export interface TaskArgs extends FargateBaseArgs { */ containers?: Input>[]; /** - * Assign a public IP address to the task. + * Make the task publicly accessible from the internet. * - * Defaults: - * - If an SST VPC component is passed to the `vpc` property, tasks run in public subnets - * by default and `publicIp` defaults to `true`. - * - If a non-SST VPC is used, tasks run in the specified subnets and `publicIp` defaults - * to `false`. + * :::note + * Tasks in an SST VPC are placed in public subnets with a public IP by default for + * outbound internet access. The `public` property controls whether the task is + * _reachable_ from the internet. + * ::: + * + * If you are using a custom VPC, you must also set `vpc.publicSubnets` on the Cluster. + * + * @default false * * @example * ```ts * { - * publicIp: true + * public: true * } * ``` */ - publicIp?: Input; + public?: boolean; + /** + * @deprecated Use `public` instead. + */ + publicIp?: boolean; /** * Configure how this component works in `sst dev`. * @@ -252,8 +261,8 @@ export interface TaskArgs extends FargateBaseArgs { * * By default, this uses a _Linux/X86_ _Fargate_ container with 0.25 vCPUs at $0.04048 per * vCPU per hour and 0.5 GB of memory at $0.004445 per GB per hour. It includes 20GB of - * _Ephemeral Storage_ for free with additional storage at $0.000111 per GB per hour. Each - * container also gets a public IPv4 address at $0.005 per hour. + * _Ephemeral Storage_ for free with additional storage at $0.000111 per GB per hour. When + * using an SST VPC, each task also gets a public IPv4 address at $0.005 per hour. * * It works out to $0.04048 x 0.25 + $0.004445 x 0.5 + $0.005. Or **$0.02 per hour** * your task runs for. @@ -267,15 +276,19 @@ export interface TaskArgs extends FargateBaseArgs { */ export class Task extends Component implements Link.Linkable { private readonly _cluster: Cluster; + private readonly publicSecurityGroup?: ec2.SecurityGroup; private readonly vpc: { + id: Input; isSstVpc: boolean; - containerSubnets: Output[]>; - securityGroups: Output[]>; + publicSubnets: Output[]>; + containerSubnets: Output[]>; + securityGroups: Output[]>; }; private readonly executionRole: iam.Role; private readonly taskRole: iam.Role; private readonly _taskDefinition: Output; - private readonly _publicIp: Output; + private readonly isPublic: boolean; + private readonly hasPublicIp: boolean; private readonly containerNames: Output[]>; private readonly dev: boolean; @@ -293,8 +306,14 @@ export class Task extends Component implements Link.Linkable { const memory = normalizeMemory(cpu, args); const storage = normalizeStorage(args); const containers = normalizeContainers("task", args, name, architecture); + if (args.public !== undefined && args.publicIp !== undefined) + throw new VisibleError( + `Do not set both "public" and "publicIp" for the "${name}" Task. "publicIp" has been deprecated, use "public" instead.`, + ); + const isPublic = args.public ?? false; const vpc = normalizeVpc(); - const publicIp = normalizePublicIp(); + const hasPublicIp = isPublic || (args.publicIp ?? vpc.isSstVpc); + const publicSecurityGroup = createPublicSecurityGroup(); const taskRole = createTaskRole( name, @@ -326,9 +345,9 @@ export class Task extends Component implements Link.Linkable { return [ { ...v[0], - image: output( - "ghcr.io/anomalyco/sst/bridge-task:20241224005724", - ), + entrypoint: undefined, + command: undefined, + image: output("ghcr.io/anomalyco/sst/bridge-task:latest"), environment: { ...v[0].environment, SST_TASK_ID: name, @@ -351,10 +370,12 @@ export class Task extends Component implements Link.Linkable { ); this._cluster = args.cluster; + this.publicSecurityGroup = publicSecurityGroup; this.vpc = vpc; this.executionRole = executionRole; this._taskDefinition = taskDefinition; - this._publicIp = publicIp; + this.isPublic = isPublic; + this.hasPublicIp = hasPublicIp; this.containerNames = containers.apply((v) => v.map((v) => output(v.name))); this.registerOutputs({ _task: all([args.dev, containers]).apply(([v, containers]) => ({ @@ -379,7 +400,9 @@ export class Task extends Component implements Link.Linkable { if (args.cluster.vpc instanceof Vpc) { const vpc = args.cluster.vpc; return { + id: vpc.id, isSstVpc: true, + publicSubnets: vpc.publicSubnets, containerSubnets: vpc.publicSubnets, securityGroups: vpc.securityGroups, }; @@ -387,7 +410,15 @@ export class Task extends Component implements Link.Linkable { // "vpc" is object return { + id: output(args.cluster.vpc).apply((v) => v.id), isSstVpc: false, + publicSubnets: output(args.cluster.vpc).apply((v) => { + if (isPublic && !v.publicSubnets?.length) + throw new VisibleError( + `Set "vpc.publicSubnets" on the Cluster to use "public" on the "${name}" Task.`, + ); + return (v.publicSubnets ?? []).map((v) => output(v)); + }), containerSubnets: output(args.cluster.vpc).apply((v) => v.containerSubnets.map((v) => output(v)), ), @@ -397,9 +428,32 @@ export class Task extends Component implements Link.Linkable { }; } - function normalizePublicIp() { - return all([args.publicIp, vpc.isSstVpc]).apply( - ([publicIp, isSstVpc]) => publicIp ?? isSstVpc, + + function createPublicSecurityGroup() { + if (!isPublic) return; + return new ec2.SecurityGroup( + `${name}PublicSecurityGroup`, + { + description: "Managed by SST", + vpcId: vpc.id, + egress: [ + { + fromPort: 0, + toPort: 0, + protocol: "-1", + cidrBlocks: ["0.0.0.0/0"], + }, + ], + ingress: [ + { + fromPort: 0, + toPort: 0, + protocol: "-1", + cidrBlocks: ["0.0.0.0/0"], + }, + ], + }, + { parent: self }, ); } } @@ -432,6 +486,10 @@ export class Task extends Component implements Link.Linkable { * @internal */ public get securityGroups() { + if (this.isPublic && this.publicSecurityGroup) + return all([this.vpc.securityGroups, this.publicSecurityGroup.id]).apply( + ([sgs, publicSgId]) => [...sgs, publicSgId], + ); return this.vpc.securityGroups; } @@ -440,6 +498,7 @@ export class Task extends Component implements Link.Linkable { * @internal */ public get subnets() { + if (this.isPublic || this.vpc.isSstVpc) return this.vpc.publicSubnets; return this.vpc.containerSubnets; } @@ -448,7 +507,7 @@ export class Task extends Component implements Link.Linkable { * @internal */ public get assignPublicIp() { - return this._publicIp; + return output(this.hasPublicIp); } /** @@ -468,6 +527,11 @@ export class Task extends Component implements Link.Linkable { * The Amazon ECS Task Definition. */ taskDefinition: this._taskDefinition, + /** + * The AWS Security Group for public tasks. Only created when `public` + * is `true`. + */ + publicSecurityGroup: this.publicSecurityGroup, }; } diff --git a/platform/src/components/aws/vector.ts b/platform/src/components/aws/vector.ts index ecc1d384b0..f78174c5ad 100644 --- a/platform/src/components/aws/vector.ts +++ b/platform/src/components/aws/vector.ts @@ -46,11 +46,19 @@ interface VectorRef { } /** + * The `Vector` component has been deprecated. It should not be used for new projects. + * + * :::caution + * This component has been deprecated. + * ::: + * * The `Vector` component lets you store and retrieve vector data in your app. * * - It uses a vector database powered by [RDS Postgres Serverless v2](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-serverless-v2.html). * - Provides a [SDK](/docs/reference/sdk/) to query, put, and remove the vector data. * + * @deprecated Use a dedicated vector database provider instead. + * * @example * * #### Create the database diff --git a/platform/src/components/aws/vpc-v1.ts b/platform/src/components/aws/vpc-v1.ts index 7bda017e49..df0470d100 100644 --- a/platform/src/components/aws/vpc-v1.ts +++ b/platform/src/components/aws/vpc-v1.ts @@ -244,7 +244,7 @@ export class Vpc extends Component { args?.transform?.elasticIp, `${name}ElasticIp${i + 1}`, { - vpc: true, + domain: "vpc", }, { parent }, ), diff --git a/platform/src/components/aws/vpc.ts b/platform/src/components/aws/vpc.ts index d6e4d1127e..e303ecc69b 100644 --- a/platform/src/components/aws/vpc.ts +++ b/platform/src/components/aws/vpc.ts @@ -20,7 +20,7 @@ import { Vpc as VpcV1 } from "./vpc-v1"; import { Link } from "../link"; import { VisibleError } from "../error"; import { PrivateKey } from "@pulumi/tls"; -import * as pulumi from "@pulumi/pulumi"; +import { rootStackResource } from "@pulumi/pulumi"; export type { VpcArgs as VpcV1Args } from "./vpc-v1"; @@ -387,12 +387,14 @@ export class Vpc extends Component implements Link.Linkable { private securityGroup: ec2.SecurityGroup; private natGateways: Output; private natInstances: Output; + private natSecurityGroup: Output; private elasticIps: Output; private _publicSubnets: Output; private _privateSubnets: Output; private publicRouteTables: Output; private privateRouteTables: Output; private bastionInstance: Output; + private bastionSecurityGroup: Output; private cloudmapNamespace: servicediscovery.PrivateDnsNamespace; private privateKeyValue: Output; public static v1 = VpcV1; @@ -418,8 +420,10 @@ export class Vpc extends Component implements Link.Linkable { this.privateRouteTables = output(ref.privateRouteTables); this.natGateways = output(ref.natGateways); this.natInstances = output(ref.natInstances); + this.natSecurityGroup = output(ref.natSecurityGroup); this.elasticIps = ref.elasticIps; this.bastionInstance = ref.bastionInstance; + this.bastionSecurityGroup = output(ref.bastionSecurityGroup); this.cloudmapNamespace = ref.cloudmapNamespace; this.privateKeyValue = output(ref.privateKeyValue); registerOutputs(); @@ -439,9 +443,9 @@ export class Vpc extends Component implements Link.Linkable { const { publicSubnets, publicRouteTables } = createPublicSubnets(); const elasticIps = createElasticIps(); const natGateways = createNatGateways(); - const natInstances = createNatInstances(); + const { natInstances, natSecurityGroup } = createNatInstances(); const { privateSubnets, privateRouteTables } = createPrivateSubnets(); - const bastionInstance = createBastion(); + const { bastionInstance, bastionSecurityGroup } = createBastion(); const cloudmapNamespace = createCloudmapNamespace(); this.vpc = vpc; @@ -449,12 +453,14 @@ export class Vpc extends Component implements Link.Linkable { this.securityGroup = securityGroup; this.natGateways = natGateways; this.natInstances = natInstances; + this.natSecurityGroup = natSecurityGroup; this.elasticIps = elasticIps; this._publicSubnets = publicSubnets; this._privateSubnets = privateSubnets; this.publicRouteTables = publicRouteTables; this.privateRouteTables = privateRouteTables; this.bastionInstance = output(bastionInstance); + this.bastionSecurityGroup = bastionSecurityGroup; this.cloudmapNamespace = cloudmapNamespace; this.privateKeyValue = output(privateKeyValue); registerOutputs(); @@ -465,7 +471,7 @@ export class Vpc extends Component implements Link.Linkable { parent: self, }); - const vpcId = vpc.tags.apply((tags) => { + const vpcId = vpc.tagsAll.apply((tags) => { registerVersion( tags?.["sst:component-version"] ? parseInt(tags["sst:component-version"]) @@ -497,24 +503,27 @@ export class Vpc extends Component implements Link.Linkable { ); const securityGroup = ec2.SecurityGroup.get( `${name}SecurityGroup`, - ec2 - .getSecurityGroupsOutput( - { - filters: [ - { name: "group-name", values: ["default"] }, - { name: "vpc-id", values: [vpcId] }, - ], - }, - { parent: self }, - ) - .ids.apply((ids) => { - if (!ids.length) { - throw new VisibleError( - `Security group not found in VPC ${vpcId}`, - ); - } - return ids[0]; - }), + all([ + ec2 + .getSecurityGroupsOutput( + { + filters: [ + { name: "group-name", values: ["default"] }, + { name: "vpc-id", values: [vpcId] }, + ], + }, + { parent: self }, + ) + .ids, + vpcId, + ]).apply(([ids, vpcId]) => { + if (!ids.length) { + throw new VisibleError( + `Security group not found in VPC ${vpcId}`, + ); + } + return ids[0]; + }), undefined, { parent: self }, ); @@ -615,6 +624,23 @@ export class Vpc extends Component implements Link.Linkable { }), ), ); + const natSecurityGroup = ec2 + .getSecurityGroupsOutput( + { + filters: [ + { name: "tag:sst:is-nat-sg", values: ["true"] }, + { name: "vpc-id", values: [vpcId] }, + ], + }, + { parent: self }, + ) + .ids.apply((ids) => + ids.length + ? ec2.SecurityGroup.get(`${name}NatInstanceSecurityGroup`, ids[0], undefined, { + parent: self + }) + : undefined, + ); const elasticIps = all([natGateways, natInstances]).apply( ([natGateways, natInstances]) => { if (natGateways.length) { @@ -672,6 +698,23 @@ export class Vpc extends Component implements Link.Linkable { }) : undefined, ); + const bastionSecurityGroup = ec2 + .getSecurityGroupsOutput( + { + filters: [ + { name: "tag:sst:is-bastion-sg", values: ["true"] }, + { name: "vpc-id", values: [vpcId] }, + ], + }, + { parent: self }, + ) + .ids.apply((ids) => + ids.length + ? ec2.SecurityGroup.get(`${name}BastionSecurityGroup`, ids[0], undefined, { + parent: self + }) + : undefined, + ); // Note: can also use servicediscovery.getDnsNamespaceOutput() here, ie. // ```ts @@ -730,8 +773,10 @@ export class Vpc extends Component implements Link.Linkable { privateRouteTables, natGateways, natInstances, + natSecurityGroup, elasticIps, bastionInstance, + bastionSecurityGroup, cloudmapNamespace, privateKeyValue, }; @@ -982,7 +1027,7 @@ export class Vpc extends Component implements Link.Linkable { args.transform?.elasticIp, `${name}ElasticIp${i + 1}`, { - vpc: true, + domain: "vpc", }, { parent: self }, ), @@ -1016,9 +1061,13 @@ export class Vpc extends Component implements Link.Linkable { function createNatInstances() { return nat.apply((nat) => { - if (nat?.type !== "ec2") return output([]); + if (nat?.type !== "ec2") + return output({ + natSecurityGroup: undefined, + natInstances: [] as ec2.Instance[], + }); - const sg = new ec2.SecurityGroup( + const natSecurityGroup = new ec2.SecurityGroup( ...transform( args.transform?.natSecurityGroup, `${name}NatInstanceSecurityGroup`, @@ -1040,6 +1089,9 @@ export class Vpc extends Component implements Link.Linkable { cidrBlocks: ["0.0.0.0/0"], }, ], + tags: { + "sst:is-nat-sg": "true", + }, }, { parent: self }, ), @@ -1102,9 +1154,9 @@ export class Vpc extends Component implements Link.Linkable { { parent: self }, ).id; - return all([zones, publicSubnets, elasticIps, keyPair, bastion]).apply( - ([zones, publicSubnets, elasticIps, keyPair, bastion]) => - zones.map((_, i) => { + return all([zones, publicSubnets, elasticIps, keyPair, bastion, natSecurityGroup]).apply( + ([zones, publicSubnets, elasticIps, keyPair, bastion, natSecurityGroup]) => ({ + natInstances: zones.map((_, i) => { const instance = new ec2.Instance( ...transform( args.transform?.natInstance, @@ -1113,7 +1165,7 @@ export class Vpc extends Component implements Link.Linkable { instanceType: nat.ec2.instance, ami, subnetId: publicSubnets[i].id, - vpcSecurityGroupIds: [sg.id], + vpcSecurityGroupIds: [natSecurityGroup.id], iamInstanceProfile: instanceProfile.name, sourceDestCheck: false, keyName: keyPair?.keyName, @@ -1129,19 +1181,21 @@ export class Vpc extends Component implements Link.Linkable { ), ); - new ec2.EipAssociation( - `${name}NatInstanceEipAssociation${i + 1}`, - { - instanceId: instance.id, - allocationId: elasticIps[i]?.id ?? nat.ip![i], - }, - { - parent: self, - aliases: [{ parent: pulumi.rootStackResource }], - }, - ); + new ec2.EipAssociation( + `${name}NatInstanceEipAssociation${i + 1}`, + { + instanceId: instance.id, + allocationId: elasticIps[i]?.id ?? nat.ip![i], + }, + { + parent: self, + aliases: [{ parent: rootStackResource }], + }, + ); - return instance; + return instance; + }), + natSecurityGroup, }), ); }); @@ -1268,13 +1322,21 @@ export class Vpc extends Component implements Link.Linkable { } function createBastion() { - return all([bastion, natInstances, keyPair]).apply( - ([bastion, natInstances, keyPair]) => { - if (!bastion.enabled) return undefined; + return all([bastion, natInstances, natSecurityGroup, keyPair]).apply( + ([bastion, natInstances, natSecurityGroup, keyPair]) => { + if (!bastion.enabled) + return { + bastionSecurityGroup: undefined, + bastionInstance: undefined, + }; - if (natInstances.length) return natInstances[0]; + if (natInstances.length) + return { + bastionSecurityGroup: natSecurityGroup, + bastionInstance: natInstances[0], + }; - const sg = new ec2.SecurityGroup( + const bastionSecurityGroup = new ec2.SecurityGroup( ...transform( args.transform?.bastionSecurityGroup, `${name}BastionSecurityGroup`, @@ -1296,6 +1358,9 @@ export class Vpc extends Component implements Link.Linkable { cidrBlocks: ["0.0.0.0/0"], }, ], + tags: { + "sst:is-bastion-sg": "true", + }, }, { parent: self }, ), @@ -1367,7 +1432,7 @@ export class Vpc extends Component implements Link.Linkable { }, { parent: self }, ); - return new ec2.Instance( + const bastionInstance = new ec2.Instance( ...transform( args.transform?.bastionInstance, `${name}BastionInstance`, @@ -1375,7 +1440,7 @@ export class Vpc extends Component implements Link.Linkable { instanceType: "t4g.nano", ami: ami.id, subnetId: publicSubnets.apply((v) => v[0].id), - vpcSecurityGroupIds: [sg.id], + vpcSecurityGroupIds: [bastionSecurityGroup.id], iamInstanceProfile: instanceProfile.apply( (ip) => ip.name, ), @@ -1387,6 +1452,7 @@ export class Vpc extends Component implements Link.Linkable { { parent: self }, ), ); + return { bastionInstance, bastionSecurityGroup }; }, ); } @@ -1474,6 +1540,10 @@ export class Vpc extends Component implements Link.Linkable { * The Amazon EC2 NAT instances. */ natInstances: this.natInstances, + /** + * The Amazon EC2 Security Group for the NAT instances. + */ + natSecurityGroup: this.natSecurityGroup, /** * The Amazon EC2 Elastic IP. */ @@ -1498,6 +1568,10 @@ export class Vpc extends Component implements Link.Linkable { * The Amazon EC2 bastion instance. */ bastionInstance: this.bastionInstance, + /** + * The Amazon EC2 Security Group for the bastion instance. + */ + bastionSecurityGroup: this.bastionSecurityGroup, /** * The AWS Cloudmap namespace. */ @@ -1516,7 +1590,7 @@ export class Vpc extends Component implements Link.Linkable { * * @param name The name of the component. * @param vpcId The ID of the existing VPC. - * @param opts? Resource options. + * @param opts Resource options. * * @example * Imagine you create a VPC in the `dev` stage. And in your personal stage `frank`, diff --git a/platform/src/components/aws/workflow.ts b/platform/src/components/aws/workflow.ts new file mode 100644 index 0000000000..ba4657dca9 --- /dev/null +++ b/platform/src/components/aws/workflow.ts @@ -0,0 +1,421 @@ +import { ComponentResourceOptions, Input, output } from "@pulumi/pulumi"; +import { Duration, DurationDays, DurationMinutes } from "../duration.js"; +import { Component } from "../component.js"; +import { RETENTION } from "./logging.js"; +import { Function, FunctionArgs } from "./function.js"; + +export interface WorkflowArgs + extends Omit< + FunctionArgs, + | "concurrency" + | "durable" + | "injections" + | "live" + | "logging" + | "retries" + | "runtime" + | "streaming" + | "timeout" + | "transform" + | "url" + | "versioning" + | "_skipHint" + | "_skipMetadata" + > { + /** + * The language runtime for the workflow. + * + * AWS Lambda durable functions currently support `"nodejs22.x"`, `"nodejs24.x"`, and + * `"python3.13"`. + * + * @default `"nodejs24.x"` + * @example + * ```js + * { + * runtime: "python3.13" + * } + * ``` + */ + runtime?: Input<"nodejs22.x" | "nodejs24.x" | "python3.13">; + /** + * Number of days to retain the workflow execution state. + * + * @default `"30 days"` + */ + retention?: Input; + /** + * Configure timeout limits for the workflow execution and each underlying Lambda invocation. + */ + timeout?: Input<{ + /** + * Maximum execution time for the entire workflow execution, from when it starts until it completes. + * + * This includes time spent across retries, replays, waits, and all durable invocations. + * + * @default `"14 days"` + */ + execution?: Input; + /** + * Maximum execution time for each underlying Lambda invocation. + * + * This is not a per-step timeout. A single invocation can run multiple steps before the + * workflow yields, waits, or replays. + * + * @default `"5 minutes"` + */ + invocation?: Input; + }>; + /** + * Configure the workflow logs in CloudWatch. Or pass in `false` to disable writing logs. + * The only supported log format is `json`. + * + * @default `{retention: "1 month", format: "json"}` + */ + logging?: + | false + | { + /** + * The log format for the workflow. + * + * AWS Lambda durable functions require structured JSON logs, so `"json"` is the only + * supported value. + * + * @default `"json"` + */ + format?: Input<"json">; + /** + * The duration the workflow logs are kept in CloudWatch. + * + * Not applicable when an existing log group is provided. + * + * @default `1 month` + * @example + * ```js + * { + * logging: { + * retention: "forever" + * } + * } + * ``` + */ + retention?: Input; + /** + * Assigns the given CloudWatch log group name to the workflow. This allows you to + * pass in a previously created log group. + * + * By default, the workflow creates a new log group when it's created. + * + * @default Creates a log group + * @example + * ```js + * { + * logging: { + * logGroup: "/existing/log-group" + * } + * } + * ``` + */ + logGroup?: Input; + }; + /** + * [Transform](/docs/components#transform) how this component creates its underlying resources. + */ + transform?: { + /** + * Transform the underlying SST Function component resources. + */ + function?: FunctionArgs["transform"]; + }; +} + +/** + * The `Workflow` component lets you add serverless workflows to your app using + * [AWS Lambda Durable Functions](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html). + * + * It's a thin wrapper around the [`Function`](/docs/component/aws/function) component + * with durable execution enabled. + * It includes an [SDK](/docs/components/aws/workflow/#sdk) that wraps the AWS SDK with a simpler interface, adds helper methods, and makes it easier to integrate with other SST components. + * + * @example + * + * #### Minimal example + * + * ```ts title="sst.config.ts" + * new sst.aws.Workflow("MyWorkflow", { + * handler: "src/workflow.handler", + * }); + * ``` + * + * ```ts title="src/workflow.ts" + * import { workflow } from "sst/aws/workflow"; + * + * export const handler = workflow.handler(async (event, ctx) => { + * const user = await ctx.step("load-user", async () => { + * return { id: "user_123", email: "alice@example.com" }; + * }); + * + * await ctx.wait("pause-before-email", "1 minute"); + * + * return ctx.step("send-email", async () => { + * return { sent: true, userId: user.id }; + * }); + * }); + * ``` + * + * #### Configure timeout and retention + * + * ```ts {3-7} title="sst.config.ts" + * new sst.aws.Workflow("MyWorkflow", { + * handler: "src/workflow.handler", + * retention: "30 days", + * timeout: { + * execution: "2 hours", + * invocation: "30 seconds", + * }, + * }); + * ``` + * + * #### Link resources + * + * ```ts {1,5} title="sst.config.ts" + * const bucket = new sst.aws.Bucket("MyBucket"); + * + * new sst.aws.Workflow("MyWorkflow", { + * handler: "src/workflow.handler", + * link: [bucket], + * }); + * ``` + * + * ```ts title="src/workflow.ts" + * import { Resource } from "sst"; + * import { workflow } from "sst/aws/workflow"; + * + * export const handler = workflow.handler(async (event, ctx) => { + * return ctx.step("get-bucket-name", async () => { + * return Resource.MyBucket.name; + * }); + * }); + * ``` + * + * #### Trigger with a cron job + * + * ```ts {5-8} title="sst.config.ts" + * const workflow = new sst.aws.Workflow("MyWorkflow", { + * handler: "src/workflow.handler", + * }); + * + * new sst.aws.CronV2("MyCron", { + * schedule: "rate(1 minute)", + * function: workflow, + * }); + * ``` + * + * ```ts title="src/workflow.ts" + * import { workflow } from "sst/aws/workflow"; + * + * export const handler = workflow.handler(async (event, ctx) => { + * await ctx.step("start", async ({ logger }) => { + * logger.info({ message: "Workflow invoked by cron" }); + * }); + * }); + * ``` + * + * [Check out the full example](/docs/examples/#aws-workflow-cron). + * + * #### Subscribe to a bus + * + * ```ts {6-9} title="sst.config.ts" + * const workflow = new sst.aws.Workflow("MyWorkflow", { + * handler: "src/workflow.handler", + * }); + * + * const bus = new sst.aws.Bus("MyBus"); + * + * bus.subscribe("MySubscriber", workflow, { + * pattern: { + * detailType: ["app.workflow.requested"], + * }, + * }); + * ``` + * + * ```ts title="src/workflow.ts" + * import { workflow } from "sst/aws/workflow"; + * + * interface Event { + * "detail-type": string; + * detail: { + * properties: { + * message: string; + * requestId: string; + * }; + * }; + * } + * + * export const handler = workflow.handler(async (event, ctx) => { + * await ctx.step("start", async ({ logger }) => { + * logger.info({ + * message: "Workflow invoked by bus", + * requestId: event.detail.properties.requestId, + * }); + * }); + * }); + * ``` + * + * [Check out the full example](/docs/examples/#aws-workflow-bus). + * + * --- + * + * ### Limitations + * + * Durable workflows replay from the top on resume and retry. Keep the control flow + * deterministic, and move side effects like API calls, database writes, timestamps, and random + * ID generation inside durable operations like `ctx.step()`. + * + * :::caution + * Workflow handlers have versioning enabled. Deploying an update won't update existing running workflows. + * ::: + * + * Before using workflows in production, review the + * [AWS best practices for durable functions](https://docs.aws.amazon.com/lambda/latest/dg/durable-best-practices.html). + * + * --- + * + * ### Cost + * + * A workflow has no idle monthly cost. You pay the standard Lambda request and compute charges + * for each invocation. + * + * :::tip + * When a workflow is suspended in a `wait`, functions don't incur costs until execution resumes. + * ::: + * + * Lambda durable functions usage is billed separately. + * + * - Durable operations like starting an execution, completing a step, and creating a wait are + * billed at $8.00 per 1 million operations. + * - Data written by durable operations is billed at $0.25 per GB. + * - Retained execution state is billed at $0.15 per GB-month. + * + * For example, a workflow with two `step()` calls and one `wait()` uses four durable operations: + * one start, two steps, and one wait. That's about **$0.000032 per execution** for durable + * operations, before Lambda compute, requests, written data, and retention. + * + * These are rough _us-east-1_ estimates. Check out the + * [AWS Lambda pricing](https://aws.amazon.com/lambda/pricing/#Lambda_Durable_Functions_Pricing) + * for more details. + */ +export class Workflow extends Component { + private readonly fn: Function; + + constructor( + name: string, + args: WorkflowArgs, + opts?: ComponentResourceOptions, + ) { + super(__pulumiType, name, args, opts); + + const timeouts = normalizeTimeouts(); + const logging = normalizeLogging(); + + this.fn = new Function( + `${name}Handler`, + { + ...args, + logging, + versioning: true, // deployments should not override running workflows + timeout: timeouts.invocation, + durable: { + timeout: timeouts.execution, + retention: args.retention, + }, + transform: args.transform?.function, + }, + { parent: this }, + ); + + this.registerOutputs({ + name: this.name, + arn: this.arn, + qualifier: this.qualifier, + }); + + function normalizeTimeouts() { + const timeouts = output(args.timeout); + + return { + invocation: timeouts.apply( + (timeout) => timeout?.invocation ?? "5 minutes", + ), + execution: timeouts.apply( + (timeout) => timeout?.execution ?? "14 days", + ), + }; + } + + function normalizeLogging() { + if (args.logging === undefined) return undefined; + + return output(args.logging).apply((logging) => { + if (logging === false) return false; + return { + ...logging, + format: "json" as const, + }; + }); + } + } + + /** @internal */ + public getFunction() { + return this.fn; + } + + /** + * The underlying [resources](/docs/components/#nodes) this component creates. + */ + public get nodes() { + return { + /** + * The SST Function component backing the workflow. + */ + function: this.fn, + }; + } + + /** + * The name of the Lambda function backing the workflow. + */ + public get name() { + return this.fn.name; + } + + /** + * The ARN of the Lambda function backing the workflow. + */ + public get arn() { + return this.fn.arn; + } + + /** + * The published version qualifier backing the workflow. + */ + public get qualifier() { + return this.fn.qualifier; + } + + /** @internal */ + public getSSTLink() { + const link = this.fn.getSSTLink(); + return { + properties: { + name: link.properties.name, + qualifier: this.qualifier, + }, + include: link.include, + }; + } +} + +const __pulumiType = "sst:aws:Workflow"; +// @ts-expect-error +Workflow.__pulumiType = __pulumiType; diff --git a/platform/src/components/base/base-ssr-site.ts b/platform/src/components/base/base-ssr-site.ts index c50f4f80b7..fc3631da61 100644 --- a/platform/src/components/base/base-ssr-site.ts +++ b/platform/src/components/base/base-ssr-site.ts @@ -1,6 +1,6 @@ import path from "path"; import fs from "fs"; -import { Output, Resource, all, output } from "@pulumi/pulumi"; +import { Resource, all, output } from "@pulumi/pulumi"; import { Prettify } from "../component"; import { Input } from "../input"; import { Link } from "../link.js"; @@ -13,101 +13,108 @@ export interface BaseSsrSiteArgs { buildCommand?: Input; environment?: Input>>; link?: Input; - path?: Input; + path?: string; } export function buildApp( parent: Resource, name: string, args: BaseSsrSiteArgs, - sitePath: Output, - buildCommand?: Output, + sitePath: Input, + buildCommand?: Input, + buildEnvironment?: Input>>, ) { return all([ sitePath, buildCommand ?? args.buildCommand, args.link, args.environment, - ]).apply(([sitePath, userCommand, links, environment]) => { - const cmd = resolveBuildCommand(); - const result = runBuild(); - return result.id.apply(() => sitePath); + buildEnvironment, + ]).apply( + ([sitePath, userCommand, links, environment, extraBuildEnvironment]) => { + const cmd = resolveBuildCommand(); + const result = runBuild(); + return result.id.apply(() => sitePath); - function resolveBuildCommand() { - if (userCommand) return userCommand; + function resolveBuildCommand() { + if (userCommand) return userCommand; - // Ensure that the site has a build script defined - if (!userCommand) { - if (!fs.existsSync(path.join(sitePath, "package.json"))) { - throw new VisibleError(`No package.json found at "${sitePath}".`); - } - const packageJson = JSON.parse( - fs.readFileSync(path.join(sitePath, "package.json")).toString(), - ); - if (!packageJson.scripts || !packageJson.scripts.build) { - throw new VisibleError( - `No "build" script found within package.json in "${sitePath}".`, + // Ensure that the site has a build script defined + if (!userCommand) { + if (!fs.existsSync(path.join(sitePath, "package.json"))) { + throw new VisibleError(`No package.json found at "${sitePath}".`); + } + const packageJson = JSON.parse( + fs.readFileSync(path.join(sitePath, "package.json")).toString(), ); + if (!packageJson.scripts || !packageJson.scripts.build) { + throw new VisibleError( + `No "build" script found within package.json in "${sitePath}".`, + ); + } } - } - if ( - fs.existsSync(path.join(sitePath, "yarn.lock")) || - fs.existsSync(path.join($cli.paths.root, "yarn.lock")) - ) - return "yarn run build"; - if ( - fs.existsSync(path.join(sitePath, "pnpm-lock.yaml")) || - fs.existsSync(path.join($cli.paths.root, "pnpm-lock.yaml")) - ) - return "pnpm run build"; - if ( - fs.existsSync(path.join(sitePath, "bun.lockb")) || - fs.existsSync(path.join($cli.paths.root, "bun.lockb")) || - fs.existsSync(path.join(sitePath, "bun.lock")) || - fs.existsSync(path.join($cli.paths.root, "bun.lock")) - ) - return "bun run build"; + if ( + fs.existsSync(path.join(sitePath, "yarn.lock")) || + fs.existsSync(path.join($cli.paths.root, "yarn.lock")) + ) + return "yarn run build"; + if ( + fs.existsSync(path.join(sitePath, "pnpm-lock.yaml")) || + fs.existsSync(path.join($cli.paths.root, "pnpm-lock.yaml")) + ) + return "pnpm run build"; + if ( + fs.existsSync(path.join(sitePath, "bun.lockb")) || + fs.existsSync(path.join($cli.paths.root, "bun.lockb")) || + fs.existsSync(path.join(sitePath, "bun.lock")) || + fs.existsSync(path.join($cli.paths.root, "bun.lock")) + ) + return "bun run build"; - return "npm run build"; - } + return "npm run build"; + } - function runBuild() { - // Build link environment variables to inject - const linkData = Link.build(links || []); - const linkEnvs = output(linkData).apply((linkData) => { - const envs: Record = { - SST_RESOURCE_App: JSON.stringify({ - name: $app.name, - stage: $app.stage, - }), - }; - for (const datum of linkData) { - envs[`SST_RESOURCE_${datum.name}`] = JSON.stringify(datum.properties); - } - return envs; - }); + function runBuild() { + // Build link environment variables to inject + const linkData = Link.build(links || []); + const linkEnvs = output(linkData).apply((linkData) => { + const envs: Record = { + SST_RESOURCE_App: JSON.stringify({ + name: $app.name, + stage: $app.stage, + }), + }; + for (const datum of linkData) { + envs[`SST_RESOURCE_${datum.name}`] = JSON.stringify( + datum.properties, + ); + } + return envs; + }); - // Run build - return siteBuilder( - `${name}Builder`, - { - create: cmd, - update: cmd, - dir: path.join($cli.paths.root, sitePath), - environment: linkEnvs.apply((linkEnvs) => ({ - SST: "1", - ...process.env, - ...environment, - ...linkEnvs, - })), - triggers: [Date.now().toString()], - }, - { - parent, - ignoreChanges: process.env.SKIP ? ["*"] : undefined, - }, - ); - } - }); + // Run build + return siteBuilder( + `${name}Builder`, + { + create: cmd, + update: cmd, + dir: path.join($cli.paths.root, sitePath), + environment: linkEnvs.apply((linkEnvs) => ({ + SST: "1", + ...process.env, + ...(environment ?? {}), + ...linkEnvs, + ...(extraBuildEnvironment ?? {}), + })), + triggers: [Date.now().toString()], + }, + { + parent, + ignoreChanges: process.env.SKIP ? ["*"] : undefined, + }, + ); + } + }, + ); } diff --git a/platform/src/components/cloudflare/ai.ts b/platform/src/components/cloudflare/ai.ts new file mode 100644 index 0000000000..376192ef99 --- /dev/null +++ b/platform/src/components/cloudflare/ai.ts @@ -0,0 +1,77 @@ +import { ComponentResourceOptions } from "@pulumi/pulumi"; +import { Component } from "../component"; +import { Link } from "../link"; +import { binding } from "./binding"; + +export interface AiArgs {} + +/** + * The `Ai` component lets you add a [Cloudflare Workers AI](https://developers.cloudflare.com/workers-ai/) binding to + * your app. + * + * @example + * + * #### Minimal example + * + * ```ts title="sst.config.ts" + * const ai = new sst.cloudflare.Ai("MyAi"); + * ``` + * + * #### Link to a worker + * + * You can link AI to a worker. + * + * ```ts {3} title="sst.config.ts" + * new sst.cloudflare.Worker("MyWorker", { + * handler: "./index.ts", + * link: [ai], + * url: true + * }); + * ``` + * + * Once linked, you can use the SDK to interact with the AI binding. + * + * ```ts title="index.ts" {3} + * import { Resource } from "sst"; + * + * const result = await Resource.MyAi.run("@cf/meta/llama-3-8b-instruct", { + * prompt: "What is the origin of the phrase 'Hello, World'" + * }); + * ``` + */ +export class Ai extends Component implements Link.Linkable { + constructor(name: string, args?: AiArgs, opts?: ComponentResourceOptions) { + super(__pulumiType, name, args, opts); + } + + /** + * When you link an AI binding, it will be available to the worker and you can + * interact with it using its [API methods](https://developers.cloudflare.com/workers-ai/). + * + * @example + * ```ts title="index.ts" {3} + * import { Resource } from "sst"; + * + * const result = await Resource.MyAi.run("@cf/meta/llama-3-8b-instruct", { + * prompt: "What is the origin of the phrase 'Hello, World'" + * }); + * ``` + * + * @internal + */ + getSSTLink() { + return { + properties: {}, + include: [ + binding({ + type: "aiBindings", + properties: {}, + }), + ], + }; + } +} + +const __pulumiType = "sst:cloudflare:Ai"; +// @ts-expect-error +Ai.__pulumiType = __pulumiType; diff --git a/platform/src/components/cloudflare/astro.ts b/platform/src/components/cloudflare/astro.ts new file mode 100644 index 0000000000..fa91f69c1c --- /dev/null +++ b/platform/src/components/cloudflare/astro.ts @@ -0,0 +1,355 @@ +import fs from "fs/promises"; +import path from "path"; +import { ComponentResourceOptions, Output } from "@pulumi/pulumi"; +import { VisibleError } from "../error.js"; +import { Plan, SsrSite, SsrSiteArgs } from "./ssr-site.js"; +import { existsAsync } from "../../util/fs.js"; +import { isALteB } from "../../util/compare-semver.js"; +import { getPackageVersion } from "../../util/package.js"; +import { + validateFrameworkConfig, + validateNoWranglerFile, +} from "./helpers/validation.js"; + +export interface AstroArgs extends SsrSiteArgs { + /** + * Configure how this component works in `sst dev`. + * + * :::note + * In `sst dev` your Astro site is run in dev mode; it's not deployed. + * ::: + * + * Instead of deploying your Astro site, this starts it in dev mode. It's run + * as a separate process in the `sst dev` multiplexer. Read more about + * [`sst dev`](/docs/reference/cli/#dev). + * + * To disable dev mode, pass in `false`. + */ + dev?: SsrSiteArgs["dev"]; + /** + * Path to the directory where your Astro site is located. This path is relative to your `sst.config.ts`. + * + * By default it assumes your Astro site is in the root of your SST app. + * @default `"."` + * + * @example + * + * If your Astro site is in a package in your monorepo. + * + * ```js + * { + * path: "packages/web" + * } + * ``` + */ + path?: SsrSiteArgs["path"]; + /** + * [Link resources](/docs/linking/) to your Astro site. This will: + * + * 1. Grant the permissions needed to access the resources. + * 2. Allow you to access them in your site using `sst/resource`. + * + * @example + * + * Takes a list of resources to link to the function. + * + * ```js + * { + * link: [bucket, stripeKey] + * } + * ``` + * + * Access linked resources in your site with + * [`sst/resource`](/docs/reference/sdk/#sstresource). This works in both + * `sst dev` and after deploy. + * + * ```astro + * --- + * import { Resource } from "sst/resource"; + * + * const files = await Resource.MyBucket.list(); + * --- + * ``` + * + */ + link?: SsrSiteArgs["link"]; + /** + * Set [environment variables](https://docs.astro.build/en/guides/environment-variables/) in your Astro site. + * + * :::tip + * You can also `link` resources to your Astro site and access them in a type-safe way with the [SDK](/docs/reference/sdk/). We recommend linking since it's more secure. + * ::: + * + * Recall that in Astro, you need to prefix your environment variables with `PUBLIC_` to access them on the client-side. [Read more here](https://docs.astro.build/en/guides/environment-variables/). + * + * @example + * ```js + * { + * environment: { + * API_URL: api.url, + * // Accessible on the client-side + * PUBLIC_STRIPE_PUBLISHABLE_KEY: "pk_test_123" + * } + * } + * ``` + */ + environment?: SsrSiteArgs["environment"]; + /** + * Set a custom domain for your Astro site. + * + * @example + * + * ```js + * { + * domain: "my-app.com" + * } + * ``` + * + * Redirect alternate domains to the main domain. + * + * ```js + * { + * domain: { + * name: "my-app.com", + * redirects: ["www.my-app.com"] + * } + * } + * ``` + * + * Or keep visitors on alternate domains with aliases. + * + * ```js + * { + * domain: { + * name: "app1.my-app.com", + * aliases: ["app2.my-app.com"] + * } + * } + * ``` + */ + domain?: SsrSiteArgs["domain"]; + /** + * The command used internally to build your Astro site. + * + * @default `"npm run build"` + * + * @example + * + * If you want to use a different build command. + * ```js + * { + * buildCommand: "yarn build" + * } + * ``` + */ + buildCommand?: SsrSiteArgs["buildCommand"]; +} + +/** + * The `Astro` component lets you deploy an [Astro](https://astro.build) site to Cloudflare. + * + * :::caution + * Features like `sst dev` support and `sst/resource` bindings require Astro v6 or newer. + * ::: + * + * @example + * + * #### Minimal example + * + * Deploy the Astro site that's in the project root. + * + * ```js title="sst.config.ts" + * new sst.cloudflare.Astro("MyWeb"); + * ``` + * + * #### Change the path + * + * Deploys the Astro site in the `my-astro-app/` directory. + * + * ```js {2} title="sst.config.ts" + * new sst.cloudflare.Astro("MyWeb", { + * path: "my-astro-app/" + * }); + * ``` + * + * #### Add a custom domain + * + * Set a custom domain for your Astro site. + * + * ```js {2} title="sst.config.ts" + * new sst.cloudflare.Astro("MyWeb", { + * domain: "my-app.com" + * }); + * ``` + * + * #### Redirect www to apex domain + * + * Redirect `www.my-app.com` to `my-app.com`. + * + * ```js {4} title="sst.config.ts" + * new sst.cloudflare.Astro("MyWeb", { + * domain: { + * name: "my-app.com", + * redirects: ["www.my-app.com"] + * } + * }); + * ``` + * + * #### Add domain aliases + * + * Allow visitors to use alternate domains without redirecting. + * + * ```js {4} title="sst.config.ts" + * new sst.cloudflare.Astro("MyWeb", { + * domain: { + * name: "app1.my-app.com", + * aliases: ["app2.my-app.com"] + * } + * }); + * ``` + * + * #### Link resources + * + * [Link resources](/docs/linking/) to your Astro site. This will grant permissions + * to the resources and allow you to access it in your site. + * + * ```ts {4} title="sst.config.ts" + * const bucket = new sst.cloudflare.Bucket("MyBucket"); + * + * new sst.cloudflare.Astro("MyWeb", { + * link: [bucket] + * }); + * ``` + * + * Add this to your `astro.config.mjs` for SST to work correctly. + * + * ```js title="astro.config.mjs" + * import { defineConfig } from "astro/config"; + * import cloudflare from "@astrojs/cloudflare"; + * + * export default defineConfig({ + * adapter: cloudflare({ + * configPath: process.env.SST_WRANGLER_PATH, + * }), + * }); + * ``` + * + * Use `sst/resource` for linked resources. + * + * ```astro title="src/pages/index.astro" + * --- + * import { Resource } from "sst/resource"; + * + * const files = await Resource.MyBucket.list(); + * --- + * ``` + * + * [Check out the full example](/docs/examples/#cloudflare-astro). + */ +export class Astro extends SsrSite { + constructor( + name: string, + args: AstroArgs = {}, + opts: ComponentResourceOptions = {}, + ) { + super(__pulumiType, name, args, opts); + } + + protected validate(sitePath: string): void { + // Only validate configPath requirement for Astro v6+ + // If version cannot be determined, default to v6+ (validate) + const astroVersion = getPackageVersion(sitePath, "astro"); + const isV6Plus = !astroVersion || isALteB("6.0.0", astroVersion); + + if (isV6Plus) { + validateFrameworkConfig({ + sitePath, + configName: "astro.config", + componentName: "Astro", + }); + } + validateNoWranglerFile(sitePath, "Astro"); + } + + protected buildPlan(outputPath: Output): Output { + return outputPath.apply(async (outputPath) => { + const distPath = path.join(outputPath, "dist"); + const legacyServer = path.join(distPath, "_worker.js", "index.js"); + if (await existsAsync(legacyServer)) { + // Astro v5 writes both the Worker bundle and assets into `dist/`. + const ignorePath = path.join(distPath, ".assetsignore"); + const ignorePatterns = (await existsAsync(ignorePath)) + ? (await fs.readFile(ignorePath, "utf-8")).split("\n") + : []; + let dirty = false; + ["_worker.js", "_routes.json"].forEach((pattern) => { + if (ignorePatterns.includes(pattern)) return; + ignorePatterns.push(pattern); + dirty = true; + }); + + if (dirty) { + await fs.appendFile(ignorePath, "\n_worker.js\n_routes.json"); + } + + return { + server: "./dist/_worker.js/index.js", + assets: "./dist", + }; + } + + const wranglerPath = path.join(distPath, "server", "wrangler.json"); + if (await existsAsync(wranglerPath)) { + type WranglerConfig = { + main?: string; + assets?: { + directory?: string; + }; + }; + + const serverPath = path.dirname(wranglerPath); + const wrangler = JSON.parse( + await fs.readFile(wranglerPath, "utf-8"), + ) as WranglerConfig; + const main = wrangler.main ?? "entry.mjs"; + const serverEntry = path.resolve(serverPath, main); + const assetsDirectory = wrangler.assets?.directory; + const assetsPath = assetsDirectory + ? path.resolve(serverPath, assetsDirectory) + : path.join(distPath, "client"); + + if (await existsAsync(serverEntry)) { + return { + server: toRelativePath(serverEntry), + assets: toRelativePath(assetsPath), + }; + } + } + + throw new VisibleError( + `SSR server bundle not found in the build output at:\n` + + ` "${path.resolve(distPath)}".\n\n` + + `Expected either Astro v5 output in \`dist/_worker.js/index.js\` or Astro v6+ output in \`dist/server/wrangler.json\`.\n` + + `If your Astro project is entirely pre-rendered, use the \`sst.cloudflare.StaticSite\` component instead of \`sst.cloudflare.Astro\`.`, + ); + + function toRelativePath(filePath: string) { + const relative = path.relative(outputPath, filePath); + return relative.startsWith(".") ? relative : `./${relative}`; + } + }); + } + + /** + * The URL of the Astro site. + * + * If the `domain` is set, this is the URL with the custom domain. + * Otherwise, it's the auto-generated Worker URL. + */ + public get url() { + return super.url; + } +} +const __pulumiType = "sst:cloudflare:Astro"; +// @ts-expect-error +Astro.__pulumiType = __pulumiType; diff --git a/platform/src/components/cloudflare/binding.ts b/platform/src/components/cloudflare/binding.ts index 1f438b362b..604112afb1 100644 --- a/platform/src/components/cloudflare/binding.ts +++ b/platform/src/components/cloudflare/binding.ts @@ -18,6 +18,11 @@ import { Input } from "../input"; +export interface AiBinding { + type: "aiBindings"; + properties: Record; +} + export interface KvBinding { type: "kvNamespaceBindings"; properties: { @@ -62,14 +67,61 @@ export interface D1DatabaseBinding { }; } +export interface HyperdriveBinding { + type: "hyperdriveBindings"; + properties: { + id: Input; + }; +} + +export interface DurableObjectNamespaceBinding { + type: "durableObjectNamespaceBindings"; + properties: { + className: Input; + scriptName?: Input; + environment?: Input; + }; +} + +export interface VersionMetadataBinding { + type: "versionMetadataBindings"; + properties: Record; +} + +export interface WorkflowBinding { + type: "workflowBindings"; + properties: { + workflowName: Input; + className: Input; + scriptName: Input; + }; +} + +export interface RateLimitBinding { + type: "rateLimitBindings"; + properties: { + namespaceId: Input; + simple: Input<{ + limit: Input; + period: Input; + }>; + }; +} + export type Binding = + | AiBinding | KvBinding | SecretTextBinding | ServiceBinding | PlainTextBinding | QueueBinding | R2BucketBinding - | D1DatabaseBinding; + | D1DatabaseBinding + | HyperdriveBinding + | VersionMetadataBinding + | WorkflowBinding + | DurableObjectNamespaceBinding + | RateLimitBinding; export function binding(input: Binding & {}) { return { diff --git a/platform/src/components/cloudflare/bucket.ts b/platform/src/components/cloudflare/bucket.ts index 2281e2d4ca..c76b58fe0d 100644 --- a/platform/src/components/cloudflare/bucket.ts +++ b/platform/src/components/cloudflare/bucket.ts @@ -4,8 +4,15 @@ import { Component, Transform, transform } from "../component"; import { Link } from "../link.js"; import { binding } from "./binding.js"; import { DEFAULT_ACCOUNT_ID } from "./account-id"; +import type { Input } from "../input"; export interface BucketArgs { + /** + * The Cloudflare account ID to use for this R2 Bucket. + * Overrides the default account ID set via `CLOUDFLARE_DEFAULT_ACCOUNT_ID`. + * @internal + */ + accountId?: Input; /** * [Transform](/docs/components/#transform) how this component creates its underlying * resources. @@ -73,7 +80,7 @@ export class Bucket extends Component implements Link.Linkable { `${name}Bucket`, { name: "", - accountId: DEFAULT_ACCOUNT_ID, + accountId: args?.accountId ?? DEFAULT_ACCOUNT_ID, }, { parent }, ), diff --git a/platform/src/components/cloudflare/cron.ts b/platform/src/components/cloudflare/cron.ts index ffd6c0c889..6b6598b282 100644 --- a/platform/src/components/cloudflare/cron.ts +++ b/platform/src/components/cloudflare/cron.ts @@ -6,10 +6,12 @@ import { WorkerArgs } from "./worker"; import { DEFAULT_ACCOUNT_ID } from "./account-id.js"; import { Input } from "../input.js"; import { WorkerBuilder, workerBuilder } from "./helpers/worker-builder"; +import { VisibleError } from "../error"; export interface CronArgs { /** * The worker that'll be executed when the cron job runs. + * @deprecated Use `worker` instead. * * @example * @@ -30,7 +32,30 @@ export interface CronArgs { * } * ``` */ - job: Input; + job?: Input; + /** + * The worker that'll be executed when the cron job runs. + * + * @example + * + * ```ts + * { + * worker: "src/cron.ts" + * } + * ``` + * + * You can pass in the full worker props. + * + * ```ts + * { + * worker: { + * handler: "src/cron.ts", + * link: [bucket] + * } + * } + * ``` + */ + worker?: Input; /** * The schedule for the cron job. * @@ -55,6 +80,12 @@ export interface CronArgs { * ``` */ schedules: Input; + /** + * The Cloudflare account ID to use for this Cron and its worker. + * Overrides the default account ID set via `CLOUDFLARE_DEFAULT_ACCOUNT_ID`. + * @internal + */ + accountId?: Input; /** * [Transform](/docs/components/#transform) how this component creates its underlying * resources. @@ -84,21 +115,21 @@ export interface CronArgs { * }; * ``` * - * Pass in a `schedules` and a `job` worker that'll be executed. + * Pass in a `schedules` and a `worker` that'll be executed. * * ```ts title="sst.config.ts" * new sst.cloudflare.Cron("MyCronJob", { - * job: "cron.ts", + * worker: "cron.ts", * schedules: ["* * * * *"] * }); * ``` * - * #### Customize the function + * #### Customize the worker * * ```js title="sst.config.ts" * new sst.cloudflare.Cron("MyCronJob", { * schedules: ["* * * * *"], - * job: { + * worker: { * handler: "cron.ts", * link: [bucket] * } @@ -114,14 +145,26 @@ export class Cron extends Component { const parent = this; + const workerArgs = normalizeWorker(); const worker = createWorker(); const trigger = createTrigger(); - this.worker = worker; this.trigger = trigger; + function normalizeWorker() { + if (args.job && args.worker) + throw new VisibleError( + `You cannot provide both "job" and "worker" in the "${name}" Cron component. The "job" property has been deprecated. Use "worker" instead.`, + ); + return args.worker ?? args.job; + } + function createWorker() { - return workerBuilder(`${name}Handler`, args.job); + if (!workerArgs) + throw new VisibleError( + `You must provide a "worker" for the "${name}" Cron component.`, + ); + return workerBuilder(`${name}Handler`, workerArgs, undefined, undefined, args.accountId); } function createTrigger() { @@ -131,7 +174,7 @@ export class Cron extends Component { args.transform?.trigger, `${name}Trigger`, { - accountId: DEFAULT_ACCOUNT_ID, + accountId: args.accountId ?? DEFAULT_ACCOUNT_ID, scriptName: worker.script.scriptName, schedules: schedules.map((s) => ({ cron: s })), }, diff --git a/platform/src/components/cloudflare/d1.ts b/platform/src/components/cloudflare/d1.ts index 41ad7eda8b..2f0e1b752e 100644 --- a/platform/src/components/cloudflare/d1.ts +++ b/platform/src/components/cloudflare/d1.ts @@ -4,8 +4,15 @@ import { Component, Transform, transform } from "../component"; import { Link } from "../link"; import { binding } from "./binding"; import { DEFAULT_ACCOUNT_ID } from "."; +import type { Input } from "../input"; export interface D1Args { + /** + * The Cloudflare account ID to use for this D1 database. + * Overrides the default account ID set via `CLOUDFLARE_DEFAULT_ACCOUNT_ID`. + * @internal + */ + accountId?: Input; /** * [Transform](/docs/components/#transform) how this component creates its underlying * resources. @@ -70,7 +77,6 @@ export class D1 extends Component implements Link.Linkable { } const parent = this; - const db = createDB(); this.database = db; @@ -82,7 +88,7 @@ export class D1 extends Component implements Link.Linkable { `${name}Database`, { name: "", - accountId: DEFAULT_ACCOUNT_ID, + accountId: args?.accountId ?? DEFAULT_ACCOUNT_ID, }, { parent }, ), @@ -153,7 +159,7 @@ export class D1 extends Component implements Link.Linkable { * ::: * * @param name The name of the component. - * @param databaseId The database ID of the existing D1 Database. + * @param args The database ID and optional account ID of the existing D1 Database. * * @example * Imagine you create a D1 Database in the `dev` stage. And in your personal @@ -162,7 +168,7 @@ export class D1 extends Component implements Link.Linkable { * * ```ts title="sst.config.ts" * const d1 = $app.stage === "giorgio" - * ? sst.cloudflare.D1.get("MyD1", "my-database-id") + * ? sst.cloudflare.D1.get("MyD1", { databaseId: "my-database-id" }) * : new sst.cloudflare.D1("MyD1"); * ``` * @@ -175,14 +181,40 @@ export class D1 extends Component implements Link.Linkable { * }; * ``` */ + public static get( + name: string, + args: { databaseId: string; accountId?: string }, + opts?: ComponentResourceOptions, + ): D1; + /** + * @deprecated Use the object-based `args` signature instead: `get(name, { databaseId }, opts)`. + */ public static get( name: string, databaseId: string, opts?: ComponentResourceOptions, + ): D1; + + public static get( + name: string, + argsOrDatabaseId: { databaseId: string; accountId?: string } | string, + opts?: ComponentResourceOptions, ) { + if (typeof argsOrDatabaseId === "string") { + const database = cloudflare.D1Database.get( + `${name}Database`, + `${DEFAULT_ACCOUNT_ID}/${argsOrDatabaseId}`, + undefined, + opts, + ); + return new D1(name, { + ref: true, + database, + } as D1Args); + } const database = cloudflare.D1Database.get( `${name}Database`, - `${DEFAULT_ACCOUNT_ID}/${databaseId}`, + `${argsOrDatabaseId.accountId ?? DEFAULT_ACCOUNT_ID}/${argsOrDatabaseId.databaseId}`, undefined, opts, ); diff --git a/platform/src/components/cloudflare/dns.ts b/platform/src/components/cloudflare/dns.ts index fe51dcb6d3..05f3d16a66 100644 --- a/platform/src/components/cloudflare/dns.ts +++ b/platform/src/components/cloudflare/dns.ts @@ -62,6 +62,12 @@ export interface DnsArgs { * ``` */ zone?: Input; + /** + * The Cloudflare account ID to use for zone lookups. + * Overrides the default account ID set via `CLOUDFLARE_DEFAULT_ACCOUNT_ID`. + * @internal + */ + accountId?: Input; /** * Configure ALIAS DNS records as [proxy records](https://developers.cloudflare.com/learning-paths/get-started-free/onboarding/proxy-dns-records/). * @@ -118,7 +124,7 @@ export function dns(args: DnsArgs = {}) { const zone = new ZoneLookup( `${namePrefix}${recordType}${recordName}ZoneLookup`, { - accountId: DEFAULT_ACCOUNT_ID, + accountId: args.accountId ?? DEFAULT_ACCOUNT_ID, domain: recordName.replace(/\.$/, ""), }, opts, @@ -169,6 +175,7 @@ export function dns(args: DnsArgs = {}) { zoneId: zone.id, type: "CAA", name: zone.name, + accountId: args.accountId, data: { flags: "0", tag: "issue", @@ -183,6 +190,7 @@ export function dns(args: DnsArgs = {}) { zoneId: zone.id, type: "CAA", name: zone.name, + accountId: args.accountId, data: { flags: "0", tag: "issuewild", @@ -232,6 +240,9 @@ export function dns(args: DnsArgs = {}) { : { content: record.value, }), + ...(record.priority !== undefined + ? { priority: record.priority } + : {}), ttl: output(proxy).apply((proxy) => (proxy ? 1 : 60)), }, opts, diff --git a/platform/src/components/cloudflare/durable-object.ts b/platform/src/components/cloudflare/durable-object.ts new file mode 100644 index 0000000000..d139adfba8 --- /dev/null +++ b/platform/src/components/cloudflare/durable-object.ts @@ -0,0 +1,144 @@ +import { ComponentResourceOptions, output, type Output } from "@pulumi/pulumi"; +import { Component } from "../component.js"; +import { Link } from "../link.js"; +import type { Input } from "../input.js"; +import { binding } from "./binding.js"; + +export interface DurableObjectArgs { + /** + * The name of the class in your worker handler file that extends `DurableObject`. + * + * @example + * ```js + * { + * className: "Counter" + * } + * ``` + */ + className: Input; +} + +/** + * Use the `DurableObject` component to register a + * [Cloudflare Durable Object](https://developers.cloudflare.com/durable-objects/) + * for a worker. + * + * Create the Durable Object and then link it to a `sst.cloudflare.Worker`. SST + * adds the Durable Object binding automatically. The `className` must match the + * exported Durable Object class name in your worker code. + * + * Durable Objects require migrations on the worker. Use the Worker's + * `migrations` field like Wrangler's top-level `migrations` config: keep the + * full ordered history there, and SST uses it to build the Cloudflare migration + * payload for the Worker. + * + * On the first deploy, add the class with `newSqliteClasses`. If you later + * rename the class, keep the original migration and add a new migration with a + * unique tag and `renamedClasses`. + * + * @example + * + * ```ts title="sst.config.ts" + * const counter = new sst.cloudflare.DurableObject("Counter", { + * className: "Counter", + * }); + * + * new sst.cloudflare.Worker("Api", { + * handler: "src/worker.ts", + * link: [counter], + * migrations: [{ + * tag: "v1", + * newSqliteClasses: [counter.className], + * }], + * url: true, + * }); + * ``` + * + * To rename a deployed class from `Counter` to `CounterV2`, update `className` + * and the exported class name, then append a migration. + * + * ```ts title="sst.config.ts" + * const counter = new sst.cloudflare.DurableObject("Counter", { + * className: "CounterV2", + * }); + * + * new sst.cloudflare.Worker("Api", { + * handler: "src/worker.ts", + * link: [counter], + * migrations: [ + * { + * tag: "v1", + * newSqliteClasses: ["Counter"], + * }, + * { + * tag: "v2", + * renamedClasses: [{ from: "Counter", to: counter.className }], + * }, + * ], + * url: true, + * }); + * ``` + * + * ```ts title="src/worker.ts" + * import { Resource } from "sst"; + * import { DurableObject } from "cloudflare:workers"; + * + * export default { + * async fetch() { + * const stub = Resource.Counter.getByName("global"); + * return stub.fetch("https://counter/"); + * }, + * }; + * + * export class Counter extends DurableObject { + * async fetch() { + * return new Response("hello from the durable object"); + * } + * } + * ``` + */ +export class DurableObject extends Component implements Link.Linkable { + /** + * The exported Durable Object class name. + */ + public readonly className: Output; + + constructor( + name: string, + args: DurableObjectArgs, + opts?: ComponentResourceOptions, + ) { + super(__pulumiType, name, args, opts); + this.className = output(args.className); + } + + /** + * When you link a Durable Object to a worker, SST adds a Cloudflare Durable + * Object namespace binding. + * + * @internal + */ + public getSSTLink() { + const properties = { + className: this.className, + }; + + return { + properties, + include: [ + binding({ + type: "durableObjectNamespaceBindings", + properties, + }), + { + type: "cloudflare.durableObject", + ...properties, + }, + ], + }; + } +} + +const __pulumiType = "sst:cloudflare:DurableObject"; +// @ts-expect-error +DurableObject.__pulumiType = __pulumiType; diff --git a/platform/src/components/cloudflare/experimental/astro.ts b/platform/src/components/cloudflare/experimental/astro.ts deleted file mode 100644 index a87283dff1..0000000000 --- a/platform/src/components/cloudflare/experimental/astro.ts +++ /dev/null @@ -1,236 +0,0 @@ -import fs from "fs/promises"; -import path from "path"; -import { ComponentResourceOptions, Output } from "@pulumi/pulumi"; -import { VisibleError } from "../../error.js"; -import { Plan, SsrSite, SsrSiteArgs } from "../ssr-site.js"; -import { existsAsync } from "../../../util/fs.js"; - -export interface AstroArgs extends SsrSiteArgs { - /** - * Configure how this component works in `sst dev`. - * - * :::note - * In `sst dev` your Astro site is run in dev mode; it's not deployed. - * ::: - * - * Instead of deploying your Astro site, this starts it in dev mode. It's run - * as a separate process in the `sst dev` multiplexer. Read more about - * [`sst dev`](/docs/reference/cli/#dev). - * - * To disable dev mode, pass in `false`. - */ - dev?: SsrSiteArgs["dev"]; - /** - * Path to the directory where your Astro site is located. This path is relative to your `sst.config.ts`. - * - * By default it assumes your Astro site is in the root of your SST app. - * @default `"."` - * - * @example - * - * If your Astro site is in a package in your monorepo. - * - * ```js - * { - * path: "packages/web" - * } - * ``` - */ - path?: SsrSiteArgs["path"]; - /** - * [Link resources](/docs/linking/) to your Astro site. This will: - * - * 1. Grant the permissions needed to access the resources. - * 2. Allow you to access it in your site using [`Astro.locals.runtime`](https://docs.astro.build/en/guides/integrations-guide/cloudflare/#environment-variables-and-secrets). - * - * @example - * - * Takes a list of resources to link to the function. - * - * ```js - * { - * link: [bucket, stripeKey] - * } - * ``` - * - * You can access the linked resources as bindings in your Astro site. - * - * ```js - * const { env } = Astro.locals.runtime; - * const files = await env.MyBucket.list(); - * ``` - */ - link?: SsrSiteArgs["link"]; - /** - * Set [environment variables](https://docs.astro.build/en/guides/environment-variables/) in your Astro site. These are made available: - * - * 1. In `astro build`, they are loaded into [`Astro.locals.runtime`](https://docs.astro.build/en/guides/integrations-guide/cloudflare/#environment-variables-and-secrets). - * 2. Locally while running `astro dev` through `sst dev`. - * - * :::tip - * You can also `link` resources to your Astro site and access them in a type-safe way with the [SDK](/docs/reference/sdk/). We recommend linking since it's more secure. - * ::: - * - * Recall that in Astro, you need to prefix your environment variables with `PUBLIC_` to access them on the client-side. [Read more here](https://docs.astro.build/en/guides/environment-variables/). - * - * @example - * ```js - * { - * environment: { - * API_URL: api.url, - * // Accessible on the client-side - * PUBLIC_STRIPE_PUBLISHABLE_KEY: "pk_test_123" - * } - * } - * ``` - * - * You can access the environment variables in your Astro site as follows: - * - * ```js - * const { env } = Astro.locals.runtime; - * const apiUrl = env.API_URL; - * const stripeKey = env.PUBLIC_STRIPE_PUBLISHABLE_KEY; - * ``` - */ - environment?: SsrSiteArgs["environment"]; - /** - * Set a custom domain for your Astro site. - * - * @example - * - * ```js - * { - * domain: "my-app.com" - * } - * ``` - */ - domain?: SsrSiteArgs["domain"]; - /** - * The command used internally to build your Astro site. - * - * @default `"npm run build"` - * - * @example - * - * If you want to use a different build command. - * ```js - * { - * buildCommand: "yarn build" - * } - * ``` - */ - buildCommand?: SsrSiteArgs["buildCommand"]; -} - -/** - * The `Astro` component lets you deploy an [Astro](https://astro.build) site to Cloudflare. - * - * @example - * - * #### Minimal example - * - * Deploy the Astro site that's in the project root. - * - * ```js title="sst.config.ts" - * new sst.cloudflare.Astro("MyWeb"); - * ``` - * - * #### Change the path - * - * Deploys the Astro site in the `my-astro-app/` directory. - * - * ```js {2} title="sst.config.ts" - * new sst.cloudflare.Astro("MyWeb", { - * path: "my-astro-app/" - * }); - * ``` - * - * #### Add a custom domain - * - * Set a custom domain for your Astro site. - * - * ```js {2} title="sst.config.ts" - * new sst.cloudflare.Astro("MyWeb", { - * domain: "my-app.com" - * }); - * ``` - * - * #### Link resources - * - * [Link resources](/docs/linking/) to your Astro site. This will grant permissions - * to the resources and allow you to access it in your site. - * - * ```ts {4} title="sst.config.ts" - * const bucket = new sst.cloudflare.Bucket("MyBucket"); - * - * new sst.cloudflare.Astro("MyWeb", { - * link: [bucket] - * }); - * ``` - * - * You can access the linked resources as bindings in your Astro site. - * - * ```astro title="src/pages/index.astro" - * --- - * const { env } = Astro.locals.runtime; - * - * const files = await env.MyBucket.list(); - * --- - * ``` - */ -export class Astro extends SsrSite { - constructor( - name: string, - args: AstroArgs = {}, - opts: ComponentResourceOptions = {}, - ) { - super(__pulumiType, name, args, opts); - } - - protected buildPlan(outputPath: Output): Output { - return outputPath.apply(async (outputPath) => { - const distPath = path.join(outputPath, "dist"); - if (!(await existsAsync(path.join(distPath, "_worker.js", "index.js")))) { - throw new VisibleError( - `SSR server bundle "_worker.js" not found in the build output at:\n` + - ` "${path.resolve(distPath)}".\n\n` + - `If your Astro project is entirely pre-rendered, use the \`sst.cloudflare.StaticSite\` component instead of \`sst.cloudflare.Astro\`.`, - ); - } - - // Ensure `.assetsignore` file exists and contains `_worker.js` and `_routes.json` - const ignorePath = path.join(outputPath, "dist", ".assetsignore"); - const ignorePatterns = (await existsAsync(ignorePath)) - ? (await fs.readFile(ignorePath, "utf-8")).split("\n") - : []; - let dirty = false; - ["_worker.js", "_routes.json"].forEach((pattern) => { - if (ignorePatterns.includes(pattern)) return; - ignorePatterns.push(pattern); - dirty = true; - }); - - if (dirty) { - await fs.appendFile(ignorePath, "\n_worker.js\n_routes.json"); - } - - return { - server: "./dist/_worker.js/index.js", - assets: "./dist", - }; - }); - } - - /** - * The URL of the Astro site. - * - * If the `domain` is set, this is the URL with the custom domain. - * Otherwise, it's the auto-generated Worker URL. - */ - public get url() { - return super.url; - } -} -const __pulumiType = "sst:cloudflare:Astro"; -// @ts-expect-error -Astro.__pulumiType = __pulumiType; diff --git a/platform/src/components/cloudflare/experimental/index.ts b/platform/src/components/cloudflare/experimental/index.ts index 95a8c5b303..befab1affb 100644 --- a/platform/src/components/cloudflare/experimental/index.ts +++ b/platform/src/components/cloudflare/experimental/index.ts @@ -1,4 +1,3 @@ -export * from "./astro"; +export * from "../astro"; export * from "./solid-start"; export * from "./static-site"; -//export * from "./remix.ts.old"; diff --git a/platform/src/components/cloudflare/experimental/solid-start.ts b/platform/src/components/cloudflare/experimental/solid-start.ts index b562bde7c3..f0ed3bb253 100644 --- a/platform/src/components/cloudflare/experimental/solid-start.ts +++ b/platform/src/components/cloudflare/experimental/solid-start.ts @@ -89,6 +89,28 @@ export interface SolidStartArgs extends SsrSiteArgs { * domain: "my-app.com" * } * ``` + * + * Redirect alternate domains to the main domain. + * + * ```js + * { + * domain: { + * name: "my-app.com", + * redirects: ["www.my-app.com"] + * } + * } + * ``` + * + * Or keep visitors on alternate domains with aliases. + * + * ```js + * { + * domain: { + * name: "app1.my-app.com", + * aliases: ["app2.my-app.com"] + * } + * } + * ``` */ domain?: SsrSiteArgs["domain"]; /** @@ -141,6 +163,32 @@ export interface SolidStartArgs extends SsrSiteArgs { * }); * ``` * + * #### Redirect www to apex domain + * + * Redirect `www.my-app.com` to `my-app.com`. + * + * ```js {4} title="sst.config.ts" + * new sst.cloudflare.SolidStart("MyWeb", { + * domain: { + * name: "my-app.com", + * redirects: ["www.my-app.com"] + * } + * }); + * ``` + * + * #### Add domain aliases + * + * Allow visitors to use alternate domains without redirecting. + * + * ```js {4} title="sst.config.ts" + * new sst.cloudflare.SolidStart("MyWeb", { + * domain: { + * name: "app1.my-app.com", + * aliases: ["app2.my-app.com"] + * } + * }); + * ``` + * * #### Link resources * * [Link resources](/docs/linking/) to your SolidStart app. This will grant permissions diff --git a/platform/src/components/cloudflare/experimental/static-site.ts b/platform/src/components/cloudflare/experimental/static-site.ts index 3b9b60ddf1..eade3702bb 100644 --- a/platform/src/components/cloudflare/experimental/static-site.ts +++ b/platform/src/components/cloudflare/experimental/static-site.ts @@ -1,298 +1,4 @@ -import path from "path"; -import { ComponentResourceOptions } from "@pulumi/pulumi"; -import { Component } from "../../component.js"; -import { Link } from "../../link.js"; -import { Input } from "../../input.js"; -import { Worker } from "../worker.js"; -import { - BaseStaticSiteArgs, - buildApp, - prepare, -} from "../../base/base-static-site.js"; - -export interface StaticSiteArgs extends BaseStaticSiteArgs { - /** - * Path to the directory where your static site is located. By default this assumes your static site is in the root of your SST app. - * - * This directory will be uploaded to KV. The path is relative to your `sst.config.ts`. - * - * :::note - * If the `build` options are specified, `build.output` will be uploaded to KV instead. - * ::: - * - * If you are using a static site generator, like Vite, you'll need to configure the `build` options. When these are set, the `build.output` directory will be uploaded to KV instead. - * - * @default `"."` - * - * @example - * - * Change where your static site is located. - * - * ```js - * { - * path: "packages/web" - * } - * ``` - */ - path?: BaseStaticSiteArgs["path"]; - /** - * Configure if your static site needs to be built. This is useful if you are using a static site generator. - * - * The `build.output` directory will be uploaded to KV instead. - * - * @example - * For a Vite project using npm this might look like this. - * - * ```js - * { - * build: { - * command: "npm run build", - * output: "dist" - * } - * } - * ``` - */ - build?: BaseStaticSiteArgs["build"]; - /** - * Set a custom domain for your static site. Supports domains hosted on Cloudflare. - * - * :::tip - * You can migrate an externally hosted domain to Cloudflare by - * [following this guide](https://developers.cloudflare.com/dns/zone-setups/full-setup/setup/). - * ::: - * - * @example - * - * ```js - * { - * domain: "domain.com" - * } - * ``` - */ - domain?: Input; -} - -/** - * The `StaticSite` component lets you deploy a static website to Cloudflare. It uses [Cloudflare KV storage](https://developers.cloudflare.com/kv/) to store your files and [Cloudflare Workers](https://developers.cloudflare.com/workers/) to serve them. - * - * It can also `build` your site by running your static site generator, like [Vite](https://vitejs.dev) and uploading the build output to Cloudflare KV. - * - * @example - * - * #### Minimal example - * - * Simply uploads the current directory as a static site. - * - * ```js - * new sst.aws.StaticSite("MyWeb"); - * ``` - * - * #### Change the path - * - * Change the `path` that should be uploaded. - * - * ```js - * new sst.aws.StaticSite("MyWeb", { - * path: "path/to/site" - * }); - * ``` - * - * #### Deploy a Vite SPA - * - * Use [Vite](https://vitejs.dev) to deploy a React/Vue/Svelte/etc. SPA by specifying the `build` config. - * - * ```js - * new sst.aws.StaticSite("MyWeb", { - * build: { - * command: "npm run build", - * output: "dist" - * } - * }); - * ``` - * - * #### Deploy a Jekyll site - * - * Use [Jekyll](https://jekyllrb.com) to deploy a static site. - * - * ```js - * new sst.aws.StaticSite("MyWeb", { - * errorPage: "404.html", - * build: { - * command: "bundle exec jekyll build", - * output: "_site" - * } - * }); - * ``` - * - * #### Deploy a Gatsby site - * - * Use [Gatsby](https://www.gatsbyjs.com) to deploy a static site. - * - * ```js - * new sst.aws.StaticSite("MyWeb", { - * errorPage: "404.html", - * build: { - * command: "npm run build", - * output: "public" - * } - * }); - * ``` - * - * #### Deploy an Angular SPA - * - * Use [Angular](https://angular.dev) to deploy a SPA. - * - * ```js - * new sst.aws.StaticSite("MyWeb", { - * build: { - * command: "ng build --output-path dist", - * output: "dist" - * } - * }); - * ``` - * - * #### Add a custom domain - * - * Set a custom domain for your site. - * - * ```js {2} - * new sst.aws.StaticSite("MyWeb", { - * domain: "my-app.com" - * }); - * ``` - * - * #### Redirect www to apex domain - * - * Redirect `www.my-app.com` to `my-app.com`. - * - * ```js {4} - * new sst.aws.StaticSite("MyWeb", { - * domain: { - * name: "my-app.com", - * redirects: ["www.my-app.com"] - * } - * }); - * ``` - * - * #### Set environment variables - * - * Set `environment` variables for the build process of your static site. These will be used locally and on deploy. - * - * :::tip - * For Vite, the types for the environment variables are also generated. This can be configured through the `vite` prop. - * ::: - * - * For some static site generators like Vite, [environment variables](https://vitejs.dev/guide/env-and-mode) prefixed with `VITE_` can be accessed in the browser. - * - * ```ts {5-7} - * const bucket = new sst.aws.Bucket("MyBucket"); - * - * new sst.aws.StaticSite("MyWeb", { - * environment: { - * BUCKET_NAME: bucket.name, - * // Accessible in the browser - * VITE_STRIPE_PUBLISHABLE_KEY: "pk_test_123" - * }, - * build: { - * command: "npm run build", - * output: "dist" - * } - * }); - * ``` - */ -export class StaticSite extends Component implements Link.Linkable { - private server: Worker; - - constructor( - name: string, - args: StaticSiteArgs = {}, - opts: ComponentResourceOptions = {}, - ) { - super(__pulumiType, name, args, opts); - - const self = this; - const { sitePath, environment, indexPage } = prepare(args); - const outputPath = $dev - ? path.join($cli.paths.platform, "functions", "empty-site") - : buildApp(self, name, args.build, sitePath, environment); - const worker = createRouter(); - - this.server = worker; - - this.registerOutputs({ - _hint: $dev ? undefined : this.url, - _dev: { - environment, - command: "npm run dev", - directory: sitePath, - autostart: true, - }, - _metadata: { - mode: $dev ? "placeholder" : "deployed", - path: sitePath, - environment, - url: this.url, - }, - }); - - function createRouter() { - return new Worker( - `${name}Router`, - { - handler: path.join( - $cli.paths.platform, - "functions", - "cf-static-site-router-worker-experimental", - ), - environment: environment.apply((e) => ({ - ...e, - INDEX_PAGE: indexPage, - ...(args.errorPage ? { ERROR_PAGE: args.errorPage } : {}), - })), - url: true, - dev: false, - domain: args.domain, - assets: { - directory: outputPath, - }, - }, - { parent: self }, - ); - } - } - - /** - * The URL of the website. - * - * If the `domain` is set, this is the URL with the custom domain. - * Otherwise, it's the auto-generated worker URL. - */ - public get url() { - return this.server.url; - } - - /** - * The underlying [resources](/docs/components/#nodes) this component creates. - */ - public get nodes() { - return { - /** - * The worker that serves the requests. - */ - server: this.server, - }; - } - - /** @internal */ - public getSSTLink() { - return { - properties: { - url: this.url, - }, - }; - } -} - -const __pulumiType = "sst:cloudflare:StaticSite"; -// @ts-expect-error -StaticSite.__pulumiType = __pulumiType; +// Re-export from the main cloudflare module for backwards compatibility +// sst.cloudflare.x.StaticSite still works but is deprecated +export { StaticSiteV2 as StaticSite } from "../static-site-v2.js"; +export type { StaticSiteV2Args as StaticSiteArgs } from "../static-site-v2.js"; diff --git a/platform/src/components/cloudflare/helpers/compatibility.ts b/platform/src/components/cloudflare/helpers/compatibility.ts new file mode 100644 index 0000000000..2eea85e59c --- /dev/null +++ b/platform/src/components/cloudflare/helpers/compatibility.ts @@ -0,0 +1,44 @@ +import { all, output } from "@pulumi/pulumi"; +import type { Input } from "../../input.js"; + +export const DEFAULT_COMPATIBILITY_DATE = "2025-05-05"; +export const DEFAULT_COMPATIBILITY_FLAGS = ["nodejs_compat"]; + +type CompatibilityArgs = { + compatibility?: Input<{ + date?: Input; + flags?: Input[]>; + }>; + transform?: { + worker?: + | { + compatibilityDate?: Input; + compatibilityFlags?: Input[]>; + } + | ((...args: any[]) => undefined); + }; +}; + +export function normalizeCompatibility(args?: CompatibilityArgs) { + const compatibility = output(args?.compatibility); + const workerTransform = + typeof args?.transform?.worker === "function" + ? undefined + : args?.transform?.worker; + return output({ + date: all([ + compatibility.apply((value) => value?.date), + workerTransform?.compatibilityDate, + ]).apply( + ([argValue, transformValue]) => + transformValue ?? argValue ?? DEFAULT_COMPATIBILITY_DATE, + ), + flags: all([ + compatibility.apply((value) => value?.flags), + workerTransform?.compatibilityFlags, + ]).apply( + ([argValue, transformValue]) => + transformValue ?? argValue ?? DEFAULT_COMPATIBILITY_FLAGS, + ), + }); +} diff --git a/platform/src/components/cloudflare/helpers/validation.ts b/platform/src/components/cloudflare/helpers/validation.ts new file mode 100644 index 0000000000..ba0ce32917 --- /dev/null +++ b/platform/src/components/cloudflare/helpers/validation.ts @@ -0,0 +1,74 @@ +import fs from "fs"; +import path from "path"; +import { VisibleError } from "../../error.js"; + +/** + * Validates that no wrangler configuration file exists in the site directory. + * SST manages wrangler configuration automatically and user-provided files can cause conflicts. + */ +export function validateNoWranglerFile(sitePath: string, componentName: string): void { + const wranglerFiles = ["wrangler.toml", "wrangler.json", "wrangler.jsonc"]; + + for (const file of wranglerFiles) { + const filePath = path.join(sitePath, file); + if (fs.existsSync(filePath)) { + throw new VisibleError( + [ + `Found ${file} in "${path.resolve(sitePath)}" for ${componentName}.`, + "", + "Remove it to avoid interfering with SST managed wrangler configurations:", + `https://sst.dev/docs/cloudflare/#cloudflare-vite-plugin`, + ].join("\n"), + ); + } + } +} + +/** + * Validates that the framework config file contains the required SST_WRANGLER_PATH configuration. + * This ensures linked resources work correctly in Cloudflare SSR sites. + */ +export function validateFrameworkConfig(input: { + sitePath: string; + configName: string; + componentName: string; +}): void { + const { sitePath, configName, componentName } = input; + + const extensions = [".ts", ".js", ".mjs"]; + const configDir = sitePath; + + // Find the config file + let configPath: string | undefined; + for (const ext of extensions) { + const candidate = path.join(configDir, `${configName}${ext}`); + if (fs.existsSync(candidate)) { + configPath = candidate; + break; + } + } + + if (!configPath) { + throw new VisibleError( + `Could not find config file for ${componentName} in "${path.resolve(configDir)}".\nExpected one of: ${extensions.map(e => `${configName}${e}`).join(", ")}.` + ); + } + + // Read and check for SST_WRANGLER_PATH pattern + const content = fs.readFileSync(configPath, "utf-8"); + const hasWranglerPath = /configPath\s*[:=]\s*process\.env\.SST_WRANGLER_PATH/.test(content); + + if (!hasWranglerPath) { + throw new VisibleError( + [ + `Missing required configuration for ${componentName}.`, + "", + `The Cloudflare adapter must be configured with:`, + ` configPath: process.env.SST_WRANGLER_PATH,`, + "", + `This is required for linked resources to work correctly:`, + `https://sst.dev/docs/cloudflare/#cloudflare-vite-plugin`, + ].join("\n") + ); + } +} diff --git a/platform/src/components/cloudflare/helpers/worker-builder.ts b/platform/src/components/cloudflare/helpers/worker-builder.ts index 0954f585d7..f81fc7fb31 100644 --- a/platform/src/components/cloudflare/helpers/worker-builder.ts +++ b/platform/src/components/cloudflare/helpers/worker-builder.ts @@ -18,12 +18,13 @@ export function workerBuilder( definition: Input, argsTransform?: Transform, opts?: ComponentResourceOptions, + accountId?: Input, ): WorkerBuilder { return output(definition).apply((definition) => { if (typeof definition === "string") { // Case 1: The definition is a handler const worker = new Worker( - ...transform(argsTransform, name, { handler: definition }, opts || {}), + ...transform(argsTransform, name, { accountId, handler: definition }, opts || {}), ); return { getWorker: () => worker, @@ -38,6 +39,7 @@ export function workerBuilder( argsTransform, name, { + accountId, ...definition, }, opts || {}, diff --git a/platform/src/components/cloudflare/helpers/wrangler.ts b/platform/src/components/cloudflare/helpers/wrangler.ts new file mode 100644 index 0000000000..3d63ce9329 --- /dev/null +++ b/platform/src/components/cloudflare/helpers/wrangler.ts @@ -0,0 +1,222 @@ +import fs from "fs"; +import path from "path"; + +export type WranglerCompatibility = { + date: string; + flags: string[]; +}; + +export type WranglerLinkInclude = { + type: string; + binding?: string; + properties?: Record; +}; + +export type WranglerLink = { + name: string; + include: WranglerLinkInclude[]; + properties?: Record; +}; + +export function createWranglerConfig(input: { + appName: string; + appStage: string; + name: string; + frameworkConfig?: Record; + compatibility: WranglerCompatibility; + environment?: Record; + links?: WranglerLink[]; + accountId?: string; +}) { + const config: Record = { + ...(input.frameworkConfig ?? {}), + name: sanitizeWranglerName(`sst-${input.appStage}-${input.name}`), + compatibility_date: input.compatibility.date, + compatibility_flags: input.compatibility.flags, + }; + + if (input.accountId) { + config.account_id = input.accountId; + } + + const vars: Record = { + ...(input.environment ?? {}), + SST_RESOURCE_App: JSON.stringify({ + name: input.appName, + stage: input.appStage, + }), + }; + const kvNamespaces: Record[] = []; + const r2Buckets: Record[] = []; + const d1Databases: Record[] = []; + const hyperdrives: Record[] = []; + const services: Record[] = []; + const queueProducers: Record[] = []; + const workflows: Record[] = []; + const rateLimits: Record[] = []; + let ai: Record | undefined; + let versionMetadata: Record | undefined; + + for (const link of input.links ?? []) { + const binding = link.include.find( + (item) => item.type === "cloudflare.binding", + ); + // Links without a native Cloudflare binding (Secret, sst.aws.*, custom + // Linkable, etc.) are surfaced as JSON-stringified vars so they match + // the `secret_text` deploy path handled in `worker.ts buildBindings`. + if (!binding) { + vars[`SST_RESOURCE_${link.name}`] = JSON.stringify(link.properties ?? {}); + continue; + } + + const properties = binding.properties ?? {}; + switch (binding.binding) { + case "aiBindings": + ai = { + binding: link.name, + remote: true, + }; + break; + case "kvNamespaceBindings": + kvNamespaces.push({ + binding: link.name, + id: stringValue(properties.namespaceId), + remote: true, + }); + break; + case "secretTextBindings": + case "plainTextBindings": + vars[link.name] = stringValue(properties.text); + break; + case "serviceBindings": + services.push({ + binding: link.name, + service: stringValue(properties.service), + remote: true, + }); + break; + case "queueBindings": + queueProducers.push({ + binding: link.name, + queue: stringValue(properties.queueName), + remote: true, + }); + break; + case "r2BucketBindings": + r2Buckets.push({ + binding: link.name, + bucket_name: stringValue(properties.bucketName), + remote: true, + }); + break; + case "d1DatabaseBindings": + d1Databases.push({ + binding: link.name, + database_id: stringValue(properties.id), + remote: true, + }); + break; + case "hyperdriveBindings": + hyperdrives.push({ + binding: link.name, + id: stringValue(properties.id), + }); + break; + case "versionMetadataBindings": + versionMetadata = { + binding: link.name, + }; + break; + case "workflowBindings": + workflows.push({ + binding: link.name, + name: stringValue(properties.workflowName), + class_name: stringValue(properties.className), + script_name: stringValue(properties.scriptName), + remote: true, + }); + break; + case "rateLimitBindings": + rateLimits.push({ + name: link.name, + namespace_id: stringValue(properties.namespaceId), + simple: properties.simple, + }); + break; + } + } + + if (Object.keys(vars).length > 0) { + config.vars = vars; + } + if (kvNamespaces.length > 0) { + config.kv_namespaces = kvNamespaces; + } + if (r2Buckets.length > 0) { + config.r2_buckets = r2Buckets; + } + if (d1Databases.length > 0) { + config.d1_databases = d1Databases; + } + if (hyperdrives.length > 0) { + config.hyperdrive = hyperdrives; + } + if (services.length > 0) { + config.services = services; + } + if (queueProducers.length > 0) { + config.queues = { + producers: queueProducers, + }; + } + if (ai) { + config.ai = ai; + } + if (versionMetadata) { + config.version_metadata = versionMetadata; + } + if (workflows.length > 0) { + config.workflows = workflows; + } + if (rateLimits.length > 0) { + config.rate_limits = rateLimits; + } + + return config; +} + +export function writeWranglerConfig(args: { + workDir: string; + stage: string; + name: string; + config: Record; +}) { + const wranglerPath = path.join( + args.workDir, + "wrangler", + args.stage, + `${args.name}.jsonc`, + ); + const contents = JSON.stringify(args.config, null, 2); + + fs.mkdirSync(path.dirname(wranglerPath), { recursive: true }); + if ( + !fs.existsSync(wranglerPath) || + fs.readFileSync(wranglerPath, "utf-8") !== contents + ) { + fs.writeFileSync(wranglerPath, contents); + } + + return wranglerPath; +} + +function stringValue(input: unknown) { + return typeof input === "string" ? input : ""; +} + +const wranglerNameRegex = /[^a-z0-9-]+/g; + +function sanitizeWranglerName(input: string) { + const value = input.toLowerCase().replaceAll(wranglerNameRegex, "-"); + return value.replace(/^-+|-+$/g, "") || "sst"; +} diff --git a/platform/src/components/cloudflare/hyperdrive.ts b/platform/src/components/cloudflare/hyperdrive.ts new file mode 100644 index 0000000000..e91e1c7b34 --- /dev/null +++ b/platform/src/components/cloudflare/hyperdrive.ts @@ -0,0 +1,402 @@ +import { ComponentResourceOptions, Input, output } from "@pulumi/pulumi"; +import * as cloudflare from "@pulumi/cloudflare"; +import { Component, Transform, transform } from "../component"; +import { Link } from "../link"; +import { binding } from "./binding"; +import { DEFAULT_ACCOUNT_ID } from "./account-id"; +import { DurationHours, toSeconds } from "../duration"; + +export interface HyperdriveGetArgs { + /** + * The ID of the existing Hyperdrive config. + */ + hyperdriveId: string; + /** + * The Cloudflare account ID the Hyperdrive config belongs to. + * Overrides the default account ID set via `CLOUDFLARE_DEFAULT_ACCOUNT_ID`. + * @internal + */ + accountId?: string; +} + +interface HyperdriveRef { + ref: true; + hyperdrive: cloudflare.HyperdriveConfig; +} + +export interface HyperdriveArgs { + /** + * Configure caching for SQL queries sent through Hyperdrive. + * + * :::tip + * Caching is enabled by default. Pass `false` to disable it. + * ::: + * + * @example + * Disable caching. + * ```js + * { + * caching: false + * } + * ``` + * + * Customize cache durations. + * ```js + * { + * caching: { + * maxAge: "30 minutes", + * staleWhileRevalidate: "30 seconds" + * } + * } + * ``` + */ + caching?: Input< + | false + | { + /** + * The maximum duration items should persist in the cache. Can be up to 1 hour. + * + * @default `"60 seconds"` + * + * @example + * ```js + * { + * caching: { + * maxAge: "30 minutes" + * } + * } + * ``` + */ + maxAge?: Input; + /** + * The duration the cache may serve a stale response while it's being revalidated. + * + * @default `"15 seconds"` + * + * @example + * ```js + * { + * caching: { + * staleWhileRevalidate: "30 seconds" + * } + * } + * ``` + */ + staleWhileRevalidate?: Input; + } + >; + /** + * Configure mTLS authentication when connecting to the origin database. + */ + mtls?: Input<{ + /** + * Define CA certificate ID obtained after uploading CA cert. + */ + caCertificateId?: Input; + /** + * Define mTLS certificate ID obtained after uploading client cert. + */ + mtlsCertificateId?: Input; + /** + * Set SSL mode to 'require', 'verify-ca', or 'verify-full' to verify the CA. + */ + sslmode?: Input; + }>; + /** + * The (soft) maximum number of connections the Hyperdrive is allowed to make to the origin database. + */ + connectionLimit?: Input; + /** + * The connection details for the origin database Hyperdrive connects to. + */ + origin: Input<{ + /** + * Defines the Client ID of the Access token to use when connecting to the origin database. + */ + accessClientId?: Input; + /** + * Defines the Client Secret of the Access Token to use when connecting to the origin database. The API never returns this write-only value. + */ + accessClientSecret?: Input; + /** + * Set the name of your origin database. + */ + database: Input; + /** + * Defines the host (hostname or IP) of your origin database. + */ + host: Input; + /** + * Set the password needed to access your origin database. The API never returns this write-only value. + */ + password: Input; + /** + * Defines the port of your origin database. Defaults to 5432 for PostgreSQL or 3306 for MySQL if not specified. + */ + port?: Input; + /** + * Specifies the URL scheme used to connect to your origin database. + */ + scheme: Input<"postgres" | "mysql">; + /** + * Set the user of your origin database. + */ + user: Input; + }>; + + /** + * The Cloudflare account ID to use for this Hyperdrive. + * Overrides the default account ID set via `CLOUDFLARE_DEFAULT_ACCOUNT_ID`. + * @internal + */ + accountId?: Input; + /** + * [Transform](/docs/components/#transform) how this component creates its underlying + * resources. + */ + transform?: { + /** + * Transform the Hyperdrive config resource. + */ + hyperdrive?: Transform; + }; +} + +/** + * The `Hyperdrive` component lets you add a [Cloudflare Hyperdrive](https://developers.cloudflare.com/hyperdrive/) to + * your app. + * + * Hyperdrive can connect Workers to PostgreSQL and MySQL databases. + * Set `origin.scheme` to `"postgres"` or `"mysql"`. + * + * @example + * + * #### PostgreSQL example + * + * ```ts title="sst.config.ts" + * const hyperdrive = new sst.cloudflare.Hyperdrive("PostgresDatabase", { + * origin: { + * database: "app", + * host: "db.example.com", + * password: "secret", + * scheme: "postgres", + * user: "postgres", + * }, + * }) + * ``` + * + * [Check out the PlanetScale](/docs/examples/#cloudflare-hyperdrive-planetscale) + * or the [AWS RDS Postgres](/docs/examples/#cloudflare-hyperdrive-with-aws-postgres) examples for a complete guide. + * + * #### MySQL example + * + * ```ts title="sst.config.ts" + * const hyperdrive = new sst.cloudflare.Hyperdrive("MySQLDatabase", { + * origin: { + * database: "app", + * host: "db.example.com", + * password: "secret", + * scheme: "mysql", + * user: "root", + * }, + * }) + * ``` + * + * #### Link to a worker + * + * You can link Hyperdrive to a worker. + * + * ```ts {3} title="sst.config.ts" + * new sst.cloudflare.Worker("MyWorker", { + * handler: "./index.ts", + * link: [hyperdrive], + * url: true, + * }) + * ``` + * + * Once linked, you can use the SDK to access the Hyperdrive binding in your worker. + * + * ```ts title="index.ts" {4} + * import postgres from "postgres" + * import { Resource } from "sst/resource" + * + * const sql = postgres(Resource.PostgresDatabase.connectionString) + * ``` + * + * It also works with MySQL: + * + * ```ts title="index.ts" {4} + * import mysql from "mysql2/promise" + * import { Resource } from "sst/resource" + * + * const db = await mysql.createConnection(Resource.MySQLDatabase.connectionString) + * ``` + */ +export class Hyperdrive extends Component implements Link.Linkable { + private hyperdrive: cloudflare.HyperdriveConfig; + + constructor( + name: string, + args: HyperdriveArgs, + opts?: ComponentResourceOptions, + ) { + super(__pulumiType, name, args, opts); + + if (args && "ref" in args) { + const ref = args as unknown as HyperdriveRef; + this.hyperdrive = ref.hyperdrive; + return; + } + + const parent = this; + + const origin = output(args.origin); + const caching = normalizeCaching(); + + this.hyperdrive = new cloudflare.HyperdriveConfig( + ...transform( + args.transform?.hyperdrive, + `${name}Hyperdrive`, + { + accountId: args.accountId ?? DEFAULT_ACCOUNT_ID, + caching, + mtls: args.mtls, + name: "", + origin, + originConnectionLimit: args.connectionLimit, + }, + { parent }, + ), + ); + + function normalizeCaching() { + if (args.caching === undefined) return undefined; + return output(args.caching).apply((c) => { + if (c === false) return { disabled: true }; + return { + maxAge: c.maxAge ? toSeconds(c.maxAge) : undefined, + staleWhileRevalidate: c.staleWhileRevalidate + ? toSeconds(c.staleWhileRevalidate) + : undefined, + }; + }); + } + } + + /** + * When you link Hyperdrive to a worker, the Hyperdrive binding will be available in the + * worker and you can use its `connectionString` to connect with a PostgreSQL or MySQL client. + * + * @example + * ```ts title="index.ts" {3} + * import postgres from "postgres" + * import { Resource } from "sst" + * + * const sql = postgres(Resource.PostgresDatabase.connectionString) + * ``` + * + * For MySQL: + * + * ```ts title="index.ts" {3} + * import mysql from "mysql2/promise" + * import { Resource } from "sst" + * + * const db = await mysql.createConnection(Resource.MySQLDatabase.connectionString) + * ``` + * + * @internal + */ + public getSSTLink() { + return { + properties: { + id: this.id, + }, + include: [ + binding({ + type: "hyperdriveBindings", + properties: { + id: this.id, + }, + }), + ], + }; + } + + /** + * The generated ID of the Hyperdrive config. + */ + public get id() { + // Pulumi returns "accountId/hyperdriveId" for imported resources. + return this.hyperdrive.id.apply((id) => + id.includes("/") ? id.split("/")[1] : id, + ); + } + + /** + * The generated name of the Hyperdrive config. + */ + public get name() { + return this.hyperdrive.name; + } + + /** + * The underlying [resources](/docs/components/#nodes) this component creates. + */ + public get nodes() { + return { + /** + * The Cloudflare Hyperdrive config. + */ + hyperdrive: this.hyperdrive, + }; + } + + /** + * Reference an existing Hyperdrive config with the given ID. This is useful when you + * create a Hyperdrive config in one stage and want to share it in another. + * + * :::tip + * You can use the `static get` method to share Hyperdrive configs across stages. + * ::: + * + * @param name The name of the component. + * @param args The arguments to get the Hyperdrive config. + * @param opts Resource options. + * + * @example + * Imagine you create a Hyperdrive config in the `dev` stage. And in your personal stage + * `frank`, instead of creating a new one, you want to share the same one from `dev`. + * + * ```ts title="sst.config.ts" + * const hyperdrive = $app.stage === "frank" + * ? sst.cloudflare.Hyperdrive.get("MyDatabase", { + * hyperdriveId: "a1b2c3d4e5f6", + * }) + * : new sst.cloudflare.Hyperdrive("MyDatabase", { ... }); + * ``` + */ + public static get( + name: string, + args: HyperdriveGetArgs, + opts?: ComponentResourceOptions, + ) { + const hyperdrive = cloudflare.HyperdriveConfig.get( + `${name}Hyperdrive`, + `${args.accountId ?? DEFAULT_ACCOUNT_ID}/${args.hyperdriveId}`, + undefined, + opts, + ); + return new Hyperdrive( + name, + { + ref: true, + hyperdrive, + } as unknown as HyperdriveArgs, + opts, + ); + } +} + +const __pulumiType = "sst:cloudflare:Hyperdrive"; +// @ts-expect-error +Hyperdrive.__pulumiType = __pulumiType; diff --git a/platform/src/components/cloudflare/index.ts b/platform/src/components/cloudflare/index.ts index f04b592115..94b562b9bc 100644 --- a/platform/src/components/cloudflare/index.ts +++ b/platform/src/components/cloudflare/index.ts @@ -2,12 +2,21 @@ export * from "./bucket"; export * from "./kv"; export * from "./d1"; export * from "./dns"; +export * from "./durable-object"; export * from "./static-site"; +export * from "./static-site-v2"; export * from "./worker"; export * from "./account-id"; export * from "./auth"; export * from "./queue"; export * from "./cron"; +export * from "./ai"; +export * from "./hyperdrive"; +export * from "./astro"; +export * from "./react-router"; +export * from "./tan-stack-start"; +export * from "./workflow"; +export * from "./rate-limit"; export { binding } from "./binding.js"; /** diff --git a/platform/src/components/cloudflare/kv.ts b/platform/src/components/cloudflare/kv.ts index 6cf3e8afb5..f0921dfaae 100644 --- a/platform/src/components/cloudflare/kv.ts +++ b/platform/src/components/cloudflare/kv.ts @@ -4,15 +4,22 @@ import { Component, Transform, transform } from "../component"; import { Link } from "../link"; import { binding } from "./binding"; import { DEFAULT_ACCOUNT_ID } from "./account-id"; +import type { Input } from "../input"; export interface KvArgs { + /** + * The Cloudflare account ID to use for this KV namespace. + * Overrides the default account ID set via `CLOUDFLARE_DEFAULT_ACCOUNT_ID`. + * @internal + */ + accountId?: Input; /** * [Transform](/docs/components/#transform) how this component creates its underlying * resources. */ transform?: { /** - * Transform the R2 KV namespace resource. + * Transform the KV namespace resource. */ namespace?: Transform; }; @@ -23,6 +30,12 @@ export interface KvGetArgs { * The ID of the existing KV namespace. */ namespaceId: string; + /** + * The Cloudflare account ID the namespace belongs to. + * Overrides the default account ID set via `CLOUDFLARE_DEFAULT_ACCOUNT_ID`. + * @internal + */ + accountId?: string; } interface KvRef { @@ -87,7 +100,7 @@ export class Kv extends Component implements Link.Linkable { `${name}Namespace`, { title: "", - accountId: DEFAULT_ACCOUNT_ID, + accountId: args?.accountId ?? DEFAULT_ACCOUNT_ID, }, { parent }, ), @@ -105,7 +118,7 @@ export class Kv extends Component implements Link.Linkable { * * @param name The name of the component. * @param args The arguments to get the KV namespace. - * @param opts? Resource options. + * @param opts Resource options. * * @example * Imagine you create a KV namespace in the `dev` stage. And in your personal stage `frank`, @@ -126,7 +139,7 @@ export class Kv extends Component implements Link.Linkable { ) { const namespace = cloudflare.WorkersKvNamespace.get( `${name}Namespace`, - `${DEFAULT_ACCOUNT_ID}/${args.namespaceId}`, + `${args.accountId ?? DEFAULT_ACCOUNT_ID}/${args.namespaceId}`, undefined, opts, ); diff --git a/platform/src/components/cloudflare/providers/dns-record.ts b/platform/src/components/cloudflare/providers/dns-record.ts index 641d448d5a..0348b99e49 100644 --- a/platform/src/components/cloudflare/providers/dns-record.ts +++ b/platform/src/components/cloudflare/providers/dns-record.ts @@ -13,6 +13,7 @@ export interface DnsRecordInputs { value: Input; }>; proxied?: Input; + accountId?: Input; } export interface DnsRecord { @@ -31,7 +32,7 @@ export class DnsRecord extends dynamic.Resource { { ...args, recordId: undefined, - accountId: DEFAULT_ACCOUNT_ID, + accountId: args.accountId ?? DEFAULT_ACCOUNT_ID, apiToken: $app.providers?.cloudflare?.apiToken || process.env.CLOUDFLARE_API_TOKEN!, diff --git a/platform/src/components/cloudflare/providers/worker-assets.ts b/platform/src/components/cloudflare/providers/worker-assets.ts index 400a8891d0..7a4b1170ea 100644 --- a/platform/src/components/cloudflare/providers/worker-assets.ts +++ b/platform/src/components/cloudflare/providers/worker-assets.ts @@ -8,6 +8,7 @@ export interface WorkerAssetsInputs { manifest: Input< Record >; + accountId?: Input; } export interface WorkerAssets { @@ -27,7 +28,7 @@ export class WorkerAssets extends dynamic.Resource { { ...args, jwt: undefined, - accountId: DEFAULT_ACCOUNT_ID, + accountId: args.accountId ?? DEFAULT_ACCOUNT_ID, apiToken: $app.providers?.cloudflare?.apiToken || process.env.CLOUDFLARE_API_TOKEN!, diff --git a/platform/src/components/cloudflare/providers/worker-placement.ts b/platform/src/components/cloudflare/providers/worker-placement.ts index d377fdc27a..6d01046571 100644 --- a/platform/src/components/cloudflare/providers/worker-placement.ts +++ b/platform/src/components/cloudflare/providers/worker-placement.ts @@ -5,6 +5,7 @@ import { DEFAULT_ACCOUNT_ID } from "../account-id.js"; interface Inputs { accountId: string; scriptName: string; + etag?: string; mode?: string; region?: string; host?: string; @@ -14,6 +15,7 @@ interface Inputs { export interface WorkerPlacementInputs { accountId?: Input; scriptName: Input; + etag?: Input; mode?: Input; region?: Input; host?: Input; diff --git a/platform/src/components/cloudflare/providers/worker-script.ts b/platform/src/components/cloudflare/providers/worker-script.ts index fe70855c01..047281a753 100644 --- a/platform/src/components/cloudflare/providers/worker-script.ts +++ b/platform/src/components/cloudflare/providers/worker-script.ts @@ -33,7 +33,7 @@ export class WorkerScript extends dynamic.Resource { `${name}.sst.cloudflare.WorkerScript`, { ...args, - accountId: DEFAULT_ACCOUNT_ID, + accountId: args.accountId ?? DEFAULT_ACCOUNT_ID, apiToken: $app.providers?.cloudflare?.apiToken || process.env.CLOUDFLARE_API_TOKEN!, diff --git a/platform/src/components/cloudflare/providers/worker-url.ts b/platform/src/components/cloudflare/providers/worker-url.ts index c89ca5d430..1a6efeecc7 100644 --- a/platform/src/components/cloudflare/providers/worker-url.ts +++ b/platform/src/components/cloudflare/providers/worker-url.ts @@ -5,6 +5,7 @@ interface Inputs { accountId: string; scriptName: string; enabled: boolean; + etag?: string; } interface Outputs { @@ -15,6 +16,7 @@ export interface WorkerUrlInputs { accountId: Input; scriptName: Input; enabled: Input; + etag?: Input; } export interface WorkerUrl { diff --git a/platform/src/components/cloudflare/providers/zone-lookup.ts b/platform/src/components/cloudflare/providers/zone-lookup.ts index be21a3ea48..8c79e0f710 100644 --- a/platform/src/components/cloudflare/providers/zone-lookup.ts +++ b/platform/src/components/cloudflare/providers/zone-lookup.ts @@ -43,6 +43,7 @@ class Provider implements dynamic.ResourceProvider { try { const qs = new URLSearchParams({ per_page: "50", + page: String(page), "account.id": inputs.accountId, }).toString(); const ret = await cfFetch<{ name: string; id: string }[]>( diff --git a/platform/src/components/cloudflare/queue-worker-subscriber.ts b/platform/src/components/cloudflare/queue-worker-subscriber.ts new file mode 100644 index 0000000000..7cc991e1b1 --- /dev/null +++ b/platform/src/components/cloudflare/queue-worker-subscriber.ts @@ -0,0 +1,192 @@ +import { ComponentResourceOptions, Input, output } from "@pulumi/pulumi"; +import * as cloudflare from "@pulumi/cloudflare"; +import { Component, Transform, transform } from "../component"; +import { + DurationMinutes, + DurationSeconds, + toMilliseconds, +} from "../duration"; +import { WorkerBuilder, workerBuilder } from "./helpers/worker-builder"; +import { WorkerArgs } from "./worker"; +import { DEFAULT_ACCOUNT_ID } from "./account-id"; + +export interface QueueWorkerSubscriberArgs { + /** + * The queue to use. + */ + queue: Input<{ + /** + * The ID of the queue. + */ + id: Input; + }>; + /** + * The subscriber worker. + */ + subscriber: Input; + /** + * The Cloudflare account ID to use for this subscriber and its consumer. + * Overrides the default account ID set via `CLOUDFLARE_DEFAULT_ACCOUNT_ID`. + * @internal + */ + accountId?: Input; + /** + * The dead letter queue to send messages that fail processing. + * + * When `dlq` is configured, `dlq.queue` is required. + */ + dlq?: { + /** + * The name of the dead letter queue. + */ + queue: Input; + /** + * The number of times the main queue will retry the message before sending it to the dead-letter queue. + * @default `3` + */ + retry?: Input; + /** + * The number of seconds to delay before making the message available for another attempt. + * @default `0 seconds` + */ + retryDelay?: Input; + }; + /** + * Maximum number of concurrent consumers that may consume from this Queue. + * @default `null` + */ + maxConcurrency?: Input; + /** + * The maximum number of messages to include in a batch. + * @default `10` + */ + batch?: { + /** + * The maximum number of events that will be processed together in a single invocation + * of the consumer function. + * + * Value must be between 1 and 100. + * + * :::note + * When `size` is set to a value greater than 10, `window` must be set to at least `1 second`. + * ::: + * + * @default `10` + */ + size?: Input; + /** + * The maximum amount of time to wait for collecting events before sending the batch to + * the consumer function, even if the batch size hasn't been reached. + * + * Value must be between 0 seconds and 60 seconds. + * @default `"5 seconds"` + */ + window?: Input; + }; + /** + * [Transform](/docs/components/#transform) how this component creates its underlying + * resources. + */ + transform?: { + /** + * Transform the Worker resource. + */ + worker?: Transform; + /** + * Transform the Consumer resource. + */ + consumer?: Transform; + }; +} + +/** + * The `QueueWorkerSubscriber` component is internally used by the `Queue` component to + * add a consumer to [Cloudflare Queues](https://developers.cloudflare.com/queues/). + * + * :::note + * This component is not intended to be created directly. + * ::: + * + * You'll find this component returned by `Queue.subscribe()`. + */ +export class QueueWorkerSubscriber extends Component { + private readonly _worker: WorkerBuilder; + private readonly consumer: cloudflare.QueueConsumer; + + constructor( + name: string, + args: QueueWorkerSubscriberArgs, + opts?: ComponentResourceOptions, + ) { + super(__pulumiType, name, args, opts); + + const self = this; + const queue = output(args.queue); + const accountId = args.accountId ?? DEFAULT_ACCOUNT_ID; + const worker = createWorker(); + const batchSize = output(args.batch?.size ?? 10); + const window = output(args.batch?.window ?? "5 seconds"); + const retryDelay = output(args.dlq?.retryDelay ?? "0 seconds"); + const consumer = createConsumer(); + + this._worker = worker; + this.consumer = consumer; + + function createWorker() { + return workerBuilder( + `${name}Function`, + args.subscriber, + args.transform?.worker, + { parent: self }, + accountId, + ); + } + + function createConsumer() { + return new cloudflare.QueueConsumer( + ...transform( + args.transform?.consumer, + `${name}Consumer`, + { + accountId: accountId, + deadLetterQueue: args.dlq?.queue, + queueId: queue.id, + scriptName: worker.script.scriptName, + settings: { + batchSize, + maxConcurrency: args.maxConcurrency, + maxRetries: args.dlq?.retry, + retryDelay: retryDelay.apply((v) => toMilliseconds(v)), + maxWaitTimeMs: window.apply((v) => toMilliseconds(v)), + }, + type: "worker", + }, + { parent: self }, + ), + ); + } + } + + /** + * The underlying [resources](/docs/components/#nodes) this component creates. + */ + public get nodes() { + const self = this; + return { + /** + * The Worker that'll process messages from the queue. + */ + get worker() { + return self._worker.apply((worker) => worker.getWorker()); + }, + /** + * The Cloudflare Queue Consumer. + */ + consumer: this.consumer, + }; + } +} + +const __pulumiType = "sst:cloudflare:QueueWorkerSubscriber"; +// @ts-expect-error +QueueWorkerSubscriber.__pulumiType = __pulumiType; diff --git a/platform/src/components/cloudflare/queue.ts b/platform/src/components/cloudflare/queue.ts index f62480286a..255c223659 100644 --- a/platform/src/components/cloudflare/queue.ts +++ b/platform/src/components/cloudflare/queue.ts @@ -1,11 +1,47 @@ -import { ComponentResourceOptions } from "@pulumi/pulumi"; +import { ComponentResourceOptions, Input } from "@pulumi/pulumi"; import * as cloudflare from "@pulumi/cloudflare"; import { Component, Transform, transform } from "../component"; import { Link } from "../link"; import { binding } from "./binding"; import { DEFAULT_ACCOUNT_ID } from "./account-id"; +import { WorkerArgs } from "./worker"; +import { VisibleError } from "../error"; +import { QueueWorkerSubscriber } from "./queue-worker-subscriber"; +import { DurationMinutes, DurationSeconds } from "../duration"; export interface QueueArgs { + /** + * The Cloudflare account ID to use for this Queue. + * Overrides the default account ID set via `CLOUDFLARE_DEFAULT_ACCOUNT_ID`. + * @internal + */ + accountId?: Input; + /** + * The dead letter queue to send messages that fail processing. + * + * When `dlq` is configured, `dlq.queue` is required. + */ + dlq?: { + /** + * The name of the dead letter queue. + */ + queue: Input; + /** + * The number of times the main queue will retry the message before sending it to the dead-letter queue. + * @default `3` + */ + retry?: Input; + /** + * The number of seconds to delay before making the message available for another attempt. + * @default `0 seconds` + */ + retryDelay?: Input; + }; + /** + * Maximum number of concurrent consumers that may consume from this Queue. + * @default `null` + */ + maxConcurrency?: Input; /** * [Transform](/docs/components/#transform) how this component creates its underlying * resources. @@ -18,17 +54,135 @@ export interface QueueArgs { }; } +export interface QueueSubscribeArgs { + /** + * The maximum number of messages to include in a batch. + * @default `10` + */ + batch?: { + /** + * The maximum number of events that will be processed together in a single invocation + * of the consumer function. + * + * Value must be between 1 and 100. + * + * :::note + * When `size` is set to a value greater than 10, `window` must be set to at least `1 second`. + * ::: + * + * @default `10` + * @example + * Set batch size to 1. This will process events individually. + * ```js + * { + * batch: { + * size: 1 + * } + * } + * ``` + */ + size?: Input; + /** + * The maximum amount of time to wait for collecting events before sending the batch to + * the consumer function, even if the batch size hasn't been reached. + * + * Value must be between 0 seconds and 60 seconds. + * @default `"5 seconds"` + * @example + * ```js + * { + * batch: { + * window: "5 seconds" + * } + * } + * ``` + */ + window?: Input; + }; + /** + * [Transform](/docs/components/#transform) how this component creates its underlying + * resources. + */ + transform?: { + /** + * Transform the Worker resource. + */ + worker?: Transform; + /** + * Transform the Consumer resource. + */ + consumer?: Transform; + }; +} + /** * The `Queue` component lets you add a [Cloudflare Queue](https://developers.cloudflare.com/queues/) to * your app. + * + * @example + * #### Create a Queue + * + * ```ts title="sst.config.ts" + * const queue = new sst.cloudflare.Queue("MyQueue"); + * ``` + * + * #### Subscribe to the Queue + * + * Create a worker file that exposes a default handler for queue messages: + * + * ```ts title="consumer.ts" + * export default { + * async queue(batch, env) { + * for (const message of batch.messages) { + * console.log("Processing message:", message.body); + * } + * }, + * }; + * ``` + * + * Subscribe to the queue with a consumer worker. + * + * ```ts title="sst.config.ts" + * queue.subscribe("consumer.ts"); + * ``` + * + * #### Link to the Queue + * + * You can link other workers to the queue. + * + * ```ts title="sst.config.ts" + * new sst.cloudflare.Worker("MyWorker", { + * handler: "producer.ts", + * link: [queue], + * url: true, + * }); + * ``` + * + * #### Subscribe with full worker props + * + * ```ts title="sst.config.ts" + * const bucket = new sst.cloudflare.Bucket("MyBucket"); + * + * queue.subscribe({ + * handler: "consumer.ts", + * link: [bucket], + * }); + * ``` */ export class Queue extends Component implements Link.Linkable { private queue: cloudflare.Queue; + private isSubscribed = false; + private constructorName: string; + private constructorArgs?: QueueArgs; + private constructorOpts?: ComponentResourceOptions; constructor(name: string, args?: QueueArgs, opts?: ComponentResourceOptions) { super(__pulumiType, name, args, opts); const parent = this; + this.constructorName = name; + this.constructorArgs = args; + this.constructorOpts = opts; const queue = create(); @@ -41,7 +195,7 @@ export class Queue extends Component implements Link.Linkable { `${name}Queue`, { queueName: "", - accountId: DEFAULT_ACCOUNT_ID, + accountId: args?.accountId ?? DEFAULT_ACCOUNT_ID, }, { parent }, ), @@ -49,6 +203,74 @@ export class Queue extends Component implements Link.Linkable { } } + /** + * Subscribe to the queue with a worker. + * + * @param subscriber The worker that'll process messages from the queue. + * @param args Configure the subscription. + * @param opts Component resource options. + * + * @example + * + * Subscribe to the queue with a worker file. + * + * ```ts title="sst.config.ts" + * queue.subscribe("consumer.ts"); + * ``` + * + * Pass in full worker props. + * + * ```ts title="sst.config.ts" + * const bucket = new sst.cloudflare.Bucket("MyBucket"); + * + * queue.subscribe({ + * handler: "consumer.ts", + * link: [bucket], + * }); + * ``` + * + * Configure batch settings. + * + * ```ts title="sst.config.ts" + * queue.subscribe("consumer.ts", { + * batch: { + * size: 10, + * window: "20 seconds", + * }, + * }); + * ``` + */ + public subscribe( + subscriber: Input, + args?: QueueSubscribeArgs, + opts?: ComponentResourceOptions, + ) { + if (this.isSubscribed) { + throw new VisibleError( + `Cannot subscribe to the "${this.constructorName}" queue multiple times. A Cloudflare Queue can only have one consumer.`, + ); + } + + this.isSubscribed = true; + + const parent = this; + const name = this.constructorName; + + return new QueueWorkerSubscriber( + `${name}Subscriber`, + { + queue: { id: this.queue.id }, + subscriber, + accountId: this.constructorArgs?.accountId, + dlq: this.constructorArgs?.dlq, + maxConcurrency: this.constructorArgs?.maxConcurrency, + batch: args?.batch, + transform: args?.transform, + }, + { ...opts, provider: this.constructorOpts?.provider }, + ); + } + getSSTLink() { return { properties: {}, diff --git a/platform/src/components/cloudflare/rate-limit.ts b/platform/src/components/cloudflare/rate-limit.ts new file mode 100644 index 0000000000..2af65e2aac --- /dev/null +++ b/platform/src/components/cloudflare/rate-limit.ts @@ -0,0 +1,178 @@ +import { + ComponentResourceOptions, + output, + type Input, + type Output, +} from "@pulumi/pulumi"; +import { Component } from "../component"; +import { Link } from "../link"; +import { binding } from "./binding"; +import { toSeconds } from "../duration"; +import { VisibleError } from "../error"; + +export interface RateLimitArgs { + /** + * A positive integer that uniquely defines this rate limiting namespace within your Cloudflare account. + */ + namespaceId: Input; + /** + * The number of allowed requests within the specified period of time. + */ + limit: Input; + /** + * The duration of the rate limit window. Must be either 10 seconds or 1 minute. + */ + period: Input<"10 seconds" | "1 minute">; +} + +/** + * The `RateLimit` component lets you add a [Cloudflare Rate Limit](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/) binding to + * your app. + * + * @example + * + * #### Minimal example + * + * ```ts title="sst.config.ts" + * const rateLimit = new sst.cloudflare.RateLimit("MyRateLimit", { + * namespaceId: 1001, + * limit: 100, + * period: "1 minute", + * }); + * ``` + * + * #### Link to a worker + * + * You can link RateLimit to a worker. + * + * ```ts {4} title="sst.config.ts" + * new sst.cloudflare.Worker("MyWorker", { + * handler: "./index.ts", + * url: true + * link: [rateLimit], + * }); + * ``` + * + * Once linked, you can use the SDK to interact with the RateLimit binding. + * + * ```ts title="index.ts" {7} + * import { Resource } from "sst/resource"; + * + * export default { + * async fetch(req, env): Promise { + * const url = new URL(req.url); + * + * const outcome = await Resource.MyRateLimit.limit({ key: url.pathname }); + * if (!outcome.success) { + * return new Response(`Rate limit exceeded for ${url.pathname}`, { status: 429 }); + * } + * + * return new Response("OK", { status: 200 }); + * } + * } + * ``` + */ +export class RateLimit extends Component implements Link.Linkable { + private _namespaceId: Output; + private _limit: Output; + private _period: Output; + + constructor( + name: string, + args: RateLimitArgs, + opts?: ComponentResourceOptions, + ) { + super(__pulumiType, name, args, opts); + + const namespaceId = normalizeNamespaceId(); + const limit = output(args.limit); + const period = normalizePeriod(); + + this._namespaceId = namespaceId; + this._limit = limit; + this._period = period; + + function normalizeNamespaceId() { + return output(args.namespaceId).apply((namespaceId) => { + if (!Number.isInteger(namespaceId) || namespaceId <= 0) { + throw new VisibleError( + "The `namespaceId` property must be a positive integer.", + ); + } + + return namespaceId.toString(); + }); + } + + function normalizePeriod() { + return output(args.period).apply(toSeconds); + } + } + + /** + * A unique identifier for the rate limit namespace. + */ + public get namespaceId() { + return this._namespaceId; + } + + /** + * The number of allowed requests within the specified period of time. + */ + public get limit() { + return this._limit; + } + + /** + * The duration of the rate limit window, in seconds. + */ + public get period() { + return this._period; + } + + /** + * When you link a RateLimit binding, it will be available to the worker and you can + * interact with it using its [API methods](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/). + * + * @example + * ```ts title="index.ts" + * import { Resource } from "sst/resource"; + * + * export default { + * async fetch(req, env): Promise { + * const url = new URL(req.url); + * + * const outcome = await Resource.MyRateLimit.limit({ key: url.pathname }); + * if (!outcome.success) { + * return new Response(`Rate limit exceeded for ${url.pathname}`, { status: 429 }); + * } + * + * return new Response("OK", { status: 200 }); + * } + * } + * ``` + * + * @internal + */ + getSSTLink() { + return { + properties: {}, + include: [ + binding({ + type: "rateLimitBindings", + properties: { + namespaceId: this._namespaceId, + simple: { + limit: this._limit, + period: this._period, + }, + }, + }), + ], + }; + } +} + +const __pulumiType = "sst:cloudflare:RateLimit"; +// @ts-expect-error +RateLimit.__pulumiType = __pulumiType; diff --git a/platform/src/components/cloudflare/react-router.ts b/platform/src/components/cloudflare/react-router.ts new file mode 100644 index 0000000000..f00f92f17f --- /dev/null +++ b/platform/src/components/cloudflare/react-router.ts @@ -0,0 +1,322 @@ +import path from "path"; +import { ComponentResourceOptions, Output } from "@pulumi/pulumi"; +import { VisibleError } from "../error.js"; +import { Plan, SsrSite, SsrSiteArgs } from "./ssr-site.js"; +import { existsAsync } from "../../util/fs.js"; +import { + validateFrameworkConfig, + validateNoWranglerFile, +} from "./helpers/validation.js"; + +export interface ReactRouterArgs extends SsrSiteArgs { + /** + * Configure how this component works in `sst dev`. + * + * :::note + * In `sst dev` your React Router app is run in dev mode; it's not deployed. + * ::: + * + * Instead of deploying your React Router app, this starts it in dev mode. It's run + * as a separate process in the `sst dev` multiplexer. Read more about + * [`sst dev`](/docs/reference/cli/#dev). + * + * To disable dev mode, pass in `false`. + */ + dev?: SsrSiteArgs["dev"]; + /** + * Path to the directory where your React Router app is located. This path is relative to your `sst.config.ts`. + * + * By default it assumes your React Router app is in the root of your SST app. + * @default `"."` + * + * @example + * + * If your React Router app is in a package in your monorepo. + * + * ```js + * { + * path: "packages/web" + * } + * ``` + */ + path?: SsrSiteArgs["path"]; + /** + * [Link resources](/docs/linking/) to your React Router app. This will: + * + * 1. Grant the permissions needed to access the resources. + * 2. Allow you to access it in your app using the [SDK](/docs/reference/sdk/). + * + * @example + * + * Takes a list of resources to link to the app. + * + * ```js + * { + * link: [bucket, stripeKey] + * } + * ``` + * + * You can access the linked resources in your React Router app. + * + * ```ts + * import { Resource } from "sst"; + * + * console.log(Resource.MyBucket.name); + * ``` + */ + link?: SsrSiteArgs["link"]; + /** + * Set environment variables in your React Router app. These are made available: + * + * 1. In `vite build`, they are loaded into the build. + * 2. At runtime as Worker bindings. + * 3. Locally while running `vite dev` through `sst dev`. + * + * :::tip + * You can also `link` resources to your React Router app and access them in a type-safe way with the [SDK](/docs/reference/sdk/). We recommend linking since it's more secure. + * ::: + * + * @example + * ```js + * { + * environment: { + * API_URL: api.url, + * PUBLIC_STRIPE_PUBLISHABLE_KEY: "pk_test_123" + * } + * } + * ``` + * + * You can access the environment variables in your React Router app as follows: + * + * ```ts + * export function loader({ context }: Route.LoaderArgs) { + * return { apiUrl: context.cloudflare.env.API_URL }; + * } + * ``` + */ + environment?: SsrSiteArgs["environment"]; + /** + * Set a custom domain for your React Router app. + * + * @example + * + * ```js + * { + * domain: "my-app.com" + * } + * ``` + * + * Redirect alternate domains to the main domain. + * + * ```js + * { + * domain: { + * name: "my-app.com", + * redirects: ["www.my-app.com"] + * } + * } + * ``` + * + * Or keep visitors on alternate domains with aliases. + * + * ```js + * { + * domain: { + * name: "app1.my-app.com", + * aliases: ["app2.my-app.com"] + * } + * } + * ``` + */ + domain?: SsrSiteArgs["domain"]; + /** + * The command used internally to build your React Router app. + * + * @default `"npm run build"` + * + * @example + * + * If you want to use a different build command. + * ```js + * { + * buildCommand: "yarn build" + * } + * ``` + */ + buildCommand?: SsrSiteArgs["buildCommand"]; +} + +/** + * The `ReactRouter` component lets you deploy a [React Router v7](https://reactrouter.com) app to Cloudflare. + * + * :::note + * Create a Cloudflare-compatible app with `npm create cloudflare@latest -- my-react-router-app --framework=react-router`. + * ::: + * + * @example + * + * #### Minimal example + * + * Deploy the React Router app that's in the project root. + * + * ```js title="sst.config.ts" + * new sst.cloudflare.ReactRouter("MyWeb"); + * ``` + * + * #### Change the path + * + * Deploys the React Router app in the `my-app/` directory. + * + * ```js {2} title="sst.config.ts" + * new sst.cloudflare.ReactRouter("MyWeb", { + * path: "my-app/" + * }); + * ``` + * + * #### Add a custom domain + * + * Set a custom domain for your React Router app. + * + * ```js {2} title="sst.config.ts" + * new sst.cloudflare.ReactRouter("MyWeb", { + * domain: "my-app.com" + * }); + * ``` + * + * #### Redirect www to apex domain + * + * Redirect `www.my-app.com` to `my-app.com`. + * + * ```js {4} title="sst.config.ts" + * new sst.cloudflare.ReactRouter("MyWeb", { + * domain: { + * name: "my-app.com", + * redirects: ["www.my-app.com"] + * } + * }); + * ``` + * + * #### Add domain aliases + * + * Allow visitors to use alternate domains without redirecting. + * + * ```js {4} title="sst.config.ts" + * new sst.cloudflare.ReactRouter("MyWeb", { + * domain: { + * name: "app1.my-app.com", + * aliases: ["app2.my-app.com"] + * } + * }); + * ``` + * + * #### Link resources + * + * [Link resources](/docs/linking/) to your React Router app. This will grant permissions + * to the resources and allow you to access it in your app. + * + * ```ts {4} title="sst.config.ts" + * const bucket = new sst.cloudflare.Bucket("MyBucket"); + * + * new sst.cloudflare.ReactRouter("MyWeb", { + * link: [bucket] + * }); + * ``` + * + * Add this to your `vite.config.ts` for SST to work correctly. + * + * ```ts title="vite.config.ts" + * import { reactRouter } from "@react-router/dev/vite"; + * import { cloudflare } from "@cloudflare/vite-plugin"; + * import { defineConfig } from "vite"; + * + * export default defineConfig({ + * plugins: [ + * cloudflare({ + * viteEnvironment: { name: "ssr" }, + * configPath: process.env.SST_WRANGLER_PATH, + * }), + * reactRouter(), + * ], + * }); + * ``` + * + * Use `sst/resource` for linked resources. + * + * ```ts title="app/routes/home.tsx" + * import { Resource } from "sst/resource"; + * + * export async function loader() { + * const files = await Resource.MyBucket.list(); + * return { files }; + * } + * ``` + * + * [Check out the full example](/docs/examples/#cloudflare-react-router). + */ +export class ReactRouter extends SsrSite { + constructor( + name: string, + args: ReactRouterArgs = {}, + opts: ComponentResourceOptions = {}, + ) { + super(__pulumiType, name, args, opts); + } + + protected validate(sitePath: string): void { + validateFrameworkConfig({ + sitePath, + configName: "vite.config", + componentName: "ReactRouter", + }); + validateNoWranglerFile(sitePath, "ReactRouter"); + } + + protected buildPlan(outputPath: Output): Output { + return outputPath.apply(async (outputPath) => { + const serverPath = path.join(outputPath, "build", "server", "index.js"); + const assetsPath = path.join(outputPath, "build", "client"); + + if (!(await existsAsync(serverPath))) { + throw new VisibleError( + [ + `React Router server bundle not found at:\n "${serverPath}".`, + "", + "Make sure the Cloudflare Vite plugin is configured in your `vite.config.ts`.", + "If your React Router project is entirely pre-rendered, use `sst.cloudflare.StaticSite` instead.", + ].join("\n"), + ); + } + + if (!(await existsAsync(assetsPath))) { + throw new VisibleError( + `React Router assets directory not found at:\n "${assetsPath}".`, + ); + } + + return { + server: "./build/server/index.js", + assets: "./build/client", + }; + }); + } + + protected buildWrangler(sitePath: string) { + return { + main: path.resolve(sitePath, "workers/app.ts"), + }; + } + + /** + * The URL of the React Router app. + * + * If the `domain` is set, this is the URL with the custom domain. + * Otherwise, it's the auto-generated Worker URL. + */ + public get url() { + return super.url; + } +} + +const __pulumiType = "sst:cloudflare:ReactRouter"; +// @ts-expect-error +ReactRouter.__pulumiType = __pulumiType; diff --git a/platform/src/components/cloudflare/remix.ts.old b/platform/src/components/cloudflare/remix.ts.old deleted file mode 100644 index 4591564334..0000000000 --- a/platform/src/components/cloudflare/remix.ts.old +++ /dev/null @@ -1,466 +0,0 @@ -import fs from "fs"; -import path from "path"; -import { ComponentResourceOptions, Output, all } from "@pulumi/pulumi"; -import { - SsrSiteArgs, - createKvStorage, - createRouter, - prepare, - validatePlan, -} from "./ssr-site.js"; -import { Component } from "../component.js"; -import { Hint } from "../hint.js"; -import { Link } from "../link.js"; -import { Kv } from "./kv.js"; -import { buildApp } from "../base/base-ssr-site.js"; -import { Worker } from "./worker.js"; -import { Plugin } from "esbuild"; -import { pathToRegexp } from "../../util/path-to-regex.js"; - -export interface RemixArgs extends SsrSiteArgs { - /** - * Configure how the Remix app assets are uploaded to S3. - * - * By default, this is set to the following. Read more about these options below. - * ```js - * { - * assets: { - * textEncoding: "utf-8", - * versionedFilesCacheHeader: "public,max-age=31536000,immutable", - * nonVersionedFilesCacheHeader: "public,max-age=0,s-maxage=86400,stale-while-revalidate=8640" - * } - * } - * ``` - */ - assets?: SsrSiteArgs["assets"]; - /** - * The command used internally to build your Remix app. - * - * @default Dynamically determined based on the presence of package manager lock files. If "yarn.lock" is found, defaults to "yarn run build". If "pnpm-lock.yaml" is found, defaults to "pnpm run build". If "bun.lockb" is found, defaults to "bun run build". If "package-lock.json" is found, defaults to "npm run build". - * - * @example - * - * If you want to use a different build command. - * ```js - * { - * buildCommand: "npm run custom-build" - * } - * ``` - */ - buildCommand?: SsrSiteArgs["buildCommand"]; - /** - * Set a custom domain for your Remix app. Supports domains hosted either on - * [Route 53](https://aws.amazon.com/route53/) or outside AWS. - * - * :::tip - * You can also migrate an externally hosted domain to Amazon Route 53 by - * [following this guide](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/MigratingDNS.html). - * ::: - * - * @example - * - * ```js - * { - * domain: "domain.com" - * } - * ``` - * - * Specify a `www.` version of the custom domain. - * - * ```js - * { - * domain: { - * name: "domain.com", - * redirects: ["www.domain.com"] - * } - * } - * ``` - */ - domain?: SsrSiteArgs["domain"]; - /** - * Set [environment variables](https://remix.run/docs/en/main/guides/envvars) in your Remix app. These are made available: - * - * 1. In `remix build`, they are loaded into `process.env`. - * 2. Locally while running `sst dev remix dev`. - * - * :::tip - * You can also `link` resources to your Remix app and access them in a type-safe way with the [SDK](/docs/reference/sdk/). We recommend linking since it's more secure. - * ::: - * - * @example - * ```js - * { - * environment: { - * API_URL: api.url, - * STRIPE_PUBLISHABLE_KEY: "pk_test_123" - * } - * } - * ``` - */ - environment?: SsrSiteArgs["environment"]; - /** - * [Link resources](/docs/linking/) to your Remix app. This will: - * - * 1. Grant the permissions needed to access the resources. - * 2. Allow you to access it in your site using the [SDK](/docs/reference/sdk/). - * - * @example - * - * Takes a list of resources to link to the function. - * - * ```js - * { - * link: [bucket, stripeKey] - * } - * ``` - */ - link?: SsrSiteArgs["link"]; - /** - * Path to the directory where your Remix app is located. This path is relative to your `sst.config.ts`. - * - * By default it assumes your Remix app is in the root of your SST app. - * @default `"."` - * - * @example - * - * If your Remix app is in a package in your monorepo. - * - * ```js - * { - * path: "packages/web" - * } - * ``` - */ - path?: SsrSiteArgs["path"]; -} - -/** - * The `Remix` component lets you deploy a [Remix](https://remix.run) app to AWS. - * - * @example - * - * #### Minimal example - * - * Deploy a Remix app that's in the project root. - * - * ```js - * new sst.aws.Remix("MyWeb"); - * ``` - * - * #### Change the path - * - * Deploys the Remix app in the `my-remix-app/` directory. - * - * ```js {2} - * new sst.aws.Remix("MyWeb", { - * path: "my-remix-app/" - * }); - * ``` - * - * #### Add a custom domain - * - * Set a custom domain for your Remix app. - * - * ```js {2} - * new sst.aws.Remix("MyWeb", { - * domain: "my-app.com" - * }); - * ``` - * - * #### Redirect www to apex domain - * - * Redirect `www.my-app.com` to `my-app.com`. - * - * ```js {4} - * new sst.aws.Remix("MyWeb", { - * domain: { - * name: "my-app.com", - * redirects: ["www.my-app.com"] - * } - * }); - * ``` - * - * #### Link resources - * - * [Link resources](/docs/linking/) to your Remix app. This will grant permissions - * to the resources and allow you to access it in your app. - * - * ```ts {4} - * const bucket = new sst.aws.Bucket("MyBucket"); - * - * new sst.aws.Remix("MyWeb", { - * link: [bucket] - * }); - * ``` - * - * You can use the [SDK](/docs/reference/sdk/) to access the linked resources - * in your Remix app. - * - * ```ts title="app/root.tsx" - * import { Resource } from "sst"; - * - * console.log(Resource.MyBucket.name); - * ``` - */ -export class Remix extends Component implements Link.Linkable { - private assets: Kv; - private router: Output; - private server: Output; - - constructor( - name: string, - args: RemixArgs = {}, - opts: ComponentResourceOptions = {}, - ) { - super(__pulumiType, name, args, opts); - - const parent = this; - const { sitePath } = prepare(args); - const isUsingVite = checkIsUsingVite(); - const storage = createKvStorage(parent, name, args); - const outputPath = $dev ? sitePath : buildApp(parent, name, args, sitePath); - const { buildMeta } = loadBuildOutput(); - const plan = buildPlan(); - const { router, server } = createRouter( - parent, - name, - args, - outputPath, - storage, - plan, - ); - - this.assets = storage; - this.router = router; - this.server = server; - if (!$dev) { - Hint.register(this.urn, this.url as Output); - } - this.registerOutputs({ - _metadata: { - mode: $dev ? "placeholder" : "deployed", - path: sitePath, - url: this.url, - }, - }); - - function checkIsUsingVite() { - return sitePath.apply( - (sitePath) => - fs.existsSync(path.join(sitePath, "vite.config.ts")) || - fs.existsSync(path.join(sitePath, "vite.config.js")), - ); - } - - function loadBuildOutput() { - return { - buildMeta: $dev ? loadBuildMetadataPlaceholder() : loadBuildMetadata(), - }; - } - - function loadBuildMetadata() { - return all([outputPath, isUsingVite]).apply( - ([outputPath, isUsingVite]) => { - // The path for all files that need to be in the "/" directory (static assets) - // is different when using Vite. These will be located in the "build/client" - // path of the output. It will be the "public" folder when using remix config. - const assetsPath = isUsingVite - ? path.join("build", "client") - : "public"; - const assetsVersionedSubDir = isUsingVite ? undefined : "build"; - - return { - assetsPath, - assetsVersionedSubDir, - // create 1 behaviour for each top level asset file/folder - staticRoutes: fs - .readdirSync(path.join(outputPath, assetsPath), { - withFileTypes: true, - }) - .map((item) => - item.isDirectory() ? `${item.name}/(.*)` : item.name, - ), - }; - }, - ); - } - - function loadBuildMetadataPlaceholder() { - return { - assetsPath: "placeholder", - assetsVersionedSubDir: undefined, - staticRoutes: [], - }; - } - - function buildPlan() { - return all([isUsingVite, outputPath, buildMeta]).apply( - ([isUsingVite, outputPath, buildMeta]) => { - return validatePlan({ - server: createServerLambdaBundle(isUsingVite, outputPath), - assets: { - copy: [ - { - from: buildMeta.assetsPath, - to: "", - cached: true, - versionedSubDir: buildMeta.assetsVersionedSubDir, - }, - ], - }, - routes: [ - { - regex: pathToRegexp(buildMeta.staticRoutes).source, - origin: "assets" as const, - }, - { - regex: pathToRegexp("(.*)").source, - origin: "server" as const, - }, - ], - }); - }, - ); - } - - function createServerLambdaBundle( - isUsingVite: boolean, - outputPath: string, - ) { - // Create a Lambda@Edge handler for the Remix server bundle. - // - // Note: Remix does perform their own internal ESBuild process, but it - // doesn't bundle 3rd party dependencies by default. In the interest of - // keeping deployments seamless for users we will create a server bundle - // with all dependencies included. We will still need to consider how to - // address any need for external dependencies, although I think we should - // possibly consider this at a later date. - - // In this path we are assuming that the Remix build only outputs the - // "core server build". We can safely assume this as we have guarded the - // remix.config.js to ensure it matches our expectations for the build - // configuration. - // We need to ensure that the "core server build" is wrapped with an - // appropriate Lambda@Edge handler. We will utilise an internal asset - // template to create this wrapper within the "core server build" output - // directory. - - // Ensure build directory exists - const buildPath = path.join(outputPath, "build"); - fs.mkdirSync(buildPath, { recursive: true }); - - // Copy the server lambda handler and pre-append the build injection based - // on the config file used. - const content = [ - // When using Vite config, the output build will be "server/index.js" - // and when using Remix config it will be `server.js`. - //isUsingVite - // ? `import * as remixServerBuild from "./server/index.js";` - // : `import * as remixServerBuild from "./index.js";`, - //`import { createRequestHandler } from "@remix-run/cloudflare";`, - //`import * as remixServerBuild from "./server";`, - //`import { createRequestHandler } from "@remix-run/cloudflare";`, - //`export default {`, - //` async fetch(request) {`, - //` const requestHandler = createRequestHandler(remixServerBuild);`, - //` return await requestHandler(request);`, - //` },`, - //`};`, - `import { createRequestHandler } from "@remix-run/cloudflare";`, - `import * as build from "./server/index.js";`, - `export default {`, - ` async fetch(request) {`, - ` console.log("fetch");`, - ` console.log("build", build);`, - ` console.log("build mode", build.mode);`, - ` const handleRequest = createRequestHandler(build);`, - ` console.log("handleRequest", handleRequest);`, - ` return await handleRequest(request);`, - ` },`, - `};`, - ].join("\n"); - fs.writeFileSync(path.join(buildPath, "server.ts"), content); - - const nodeBuiltInModulesPlugin: Plugin = { - name: "node:built-in:modules", - setup(build) { - build.onResolve({ filter: /^(util|stream)$/ }, ({ kind, path }) => { - // this plugin converts `require("node:*")` calls, those are the only ones that - // need updating (esm imports to "node:*" are totally valid), so here we tag with the - // node-buffer namespace only imports that are require calls - return kind === "require-call" - ? { path, namespace: "node-built-in-modules" } - : undefined; - }); - - // we convert the imports we tagged with the node-built-in-modules namespace so that instead of `require("node:*")` - // they import from `export * from "node:*";` - build.onLoad( - { filter: /.*/, namespace: "node-built-in-modules" }, - ({ path }) => { - return { - contents: `export * from 'node:${path}'`, - loader: "js", - }; - }, - ); - }, - }; - - return { - handler: path.join(buildPath, "server.ts"), - build: { - esbuild: { - define: { - process: JSON.stringify({ - env: { - //NODE_ENV: "production", - NODE_ENV: "development", - }, - }), - }, - plugins: [nodeBuiltInModulesPlugin], - }, - }, - }; - } - } - - /** - * The URL of the Remix app. - * - * If the `domain` is set, this is the URL with the custom domain. - * Otherwise, it's the auto-generated CloudFront URL. - */ - public get url() { - return this.router.url; - } - - /** - * The underlying [resources](/docs/components/#nodes) this component creates. - */ - public get nodes() { - return { - /** - * The AWS Lambda server function that renders the site. - */ - server: this.server, - /** - * The Amazon S3 Bucket that stores the assets. - */ - assets: this.assets, - }; - } - - /** @internal */ - public getSSTLink() { - return { - properties: { - url: this.url, - }, - }; - } -} -const __pulumiType = "sst:cloudflare:Remix"; -// @ts-expect-error -Remix.__pulumiType = __pulumiType; diff --git a/platform/src/components/cloudflare/ssr-site.ts b/platform/src/components/cloudflare/ssr-site.ts index f25495ce32..67a7412b23 100644 --- a/platform/src/components/cloudflare/ssr-site.ts +++ b/platform/src/components/cloudflare/ssr-site.ts @@ -6,7 +6,14 @@ import { Component, transform, type Transform } from "../component.js"; import { VisibleError } from "../error.js"; import { BaseSsrSiteArgs, buildApp } from "../base/base-ssr-site.js"; import { Worker, WorkerArgs } from "./worker.js"; +import { normalizeCompatibility } from "./helpers/compatibility.js"; +import { + createWranglerConfig, + writeWranglerConfig, +} from "./helpers/wrangler.js"; import { Link } from "../link.js"; +import { URL_UNAVAILABLE } from "../aws/linkable.js"; +import { DEFAULT_ACCOUNT_ID } from "./account-id.js"; export type Plan = { server: string; @@ -14,7 +21,13 @@ export type Plan = { }; export interface SsrSiteArgs extends BaseSsrSiteArgs { - domain?: Input; + domain?: WorkerArgs["domain"]; + /** + * The Cloudflare account ID to use for this SsrSite. + * Overrides the default account ID set via `CLOUDFLARE_DEFAULT_ACCOUNT_ID`. + * @internal + */ + accountId?: Input; /** * [Transform](/docs/components#transform) how this component creates its underlying * resources. @@ -28,13 +41,18 @@ export interface SsrSiteArgs extends BaseSsrSiteArgs { } export abstract class SsrSite extends Component implements Link.Linkable { - private server: Worker; + private server?: Worker; + private devUrl?: Output; - protected abstract buildPlan( - outputPath: Output, - name: string, - args: SsrSiteArgs, - ): Output; + protected validate(_sitePath: string): void {} + + protected abstract buildPlan(outputPath: Output): Output; + + protected buildWrangler( + _sitePath: string, + ): Input> | undefined> { + return undefined; + } constructor( type: string, @@ -46,46 +64,172 @@ export abstract class SsrSite extends Component implements Link.Linkable { const self = this; const sitePath = normalizeSitePath(); - const outputPath = $dev ? sitePath : buildApp(self, name, args, sitePath); - const plan = validatePlan(this.buildPlan(outputPath, name, args)); + const compatibility = resolveCompatibility(); + const frameworkConfig = resolveFrameworkConfig(); + const wranglerConfig = resolveWranglerConfig(); + const dev = normalizeDev(); + this.validate(sitePath); + + if (dev.enabled) { + this.devUrl = dev.url; + this.registerOutputs({ + _dev: dev.outputs, + _metadata: { + mode: "placeholder", + path: sitePath, + }, + }); + return; + } + + const outputPath = buildApp( + self, + name, + args, + sitePath, + undefined, + resolveBuildEnvironment(), + ); + const plan = this.buildPlan(outputPath); const worker = createWorker(); this.server = worker; this.registerOutputs({ - _hint: $dev ? undefined : this.url, - _dev: { - environment: args.environment, - command: "npm run dev", - directory: sitePath, - autostart: true, - links: output(args.link || []) - .apply(Link.build) - .apply((links) => links.map((link) => link.name)), - }, + _hint: this.url, _metadata: { - mode: $dev ? "placeholder" : "deployed", + mode: "deployed", path: sitePath, }, }); - function normalizeSitePath() { - return output(args.path).apply((sitePath) => { - if (!sitePath) return "."; - - if (!fs.existsSync(sitePath)) { - throw new VisibleError( - `Site directory not found at "${path.resolve( - sitePath, - )}". Please check the path setting in your configuration.`, - ); - } - return sitePath; + function normalizeDev() { + const enabled = $dev && args.dev !== false; + const devArgs = args.dev || {}; + + return { + enabled, + url: output(devArgs.url ?? URL_UNAVAILABLE), + outputs: { + title: devArgs.title, + environment: args.environment, + cloudflare: enabled + ? { + path: resolveDevWranglerPath(), + } + : undefined, + command: output(devArgs.command ?? "npm run dev"), + autostart: output(devArgs.autostart ?? true), + directory: devArgs.directory ?? sitePath, + links: output(args.link || []) + .apply(Link.build) + .apply((links) => links.map((link) => link.name)), + }, + }; + } + + function resolveFrameworkConfig() { + return output(self.buildWrangler(sitePath)).apply((config) => config ?? {}); + } + + function resolveCompatibility() { + const [, workerArgs] = transform( + args.transform?.server, + `${name}Worker`, + { + environment: args.environment, + link: args.link, + accountId: args.accountId, + url: true, + dev: false, + domain: args.domain, + handler: output(""), + assets: { + directory: output(""), + }, + }, + { parent: self }, + ); + return normalizeCompatibility(workerArgs); + } + + function resolveBuildEnvironment() { + return resolveBuildWranglerPath().apply((wranglerPath) => ({ + SST_WRANGLER_PATH: wranglerPath, + })); + } + + function resolveDevWranglerPath() { + return wranglerConfig.apply((config) => { + return writeWranglerConfig({ + workDir: $cli.paths.work, + stage: $app.stage, + name, + config, + }); + }); + } + + function resolveWranglerConfig() { + return all([ + output(args.environment ?? {}), + resolveLinkBindings(), + compatibility, + frameworkConfig, + args.accountId, + ]).apply(([environment, links, compatibility, frameworkConfig, accountId]) => { + return createWranglerConfig({ + appName: $app.name, + appStage: $app.stage, + name, + frameworkConfig, + compatibility, + environment, + links, + accountId: accountId ?? DEFAULT_ACCOUNT_ID, + }); + }); + } + + function resolveBuildWranglerPath() { + return wranglerConfig.apply((config) => { + return writeWranglerConfig({ + workDir: $cli.paths.work, + stage: $app.stage, + name, + config, + }); + }); + } + + function resolveLinkBindings() { + return output(args.link ?? []).apply((links) => { + const linkBindings = links.filter(Link.isLinkable).map((link) => + output({ + urn: link.urn, + link: link.getSSTLink(), + }).apply(({ urn, link }) => ({ + name: urn.split("::").at(-1)!, + include: link.include ?? [], + properties: link.properties, + })), + ); + return linkBindings.length > 0 ? all(linkBindings) : []; }); } - function validatePlan(plan: Output) { - return plan; + function normalizeSitePath() { + const sitePath = args.path ?? "."; + + if (!fs.existsSync(sitePath)) { + throw new VisibleError( + `Site directory not found at "${path.resolve( + sitePath, + )}". Please check the path setting in your configuration.`, + ); + } + + return sitePath; } function createWorker() { @@ -94,14 +238,15 @@ export abstract class SsrSite extends Component implements Link.Linkable { args.transform?.server, `${name}Worker`, { - handler: all([outputPath, plan.server]).apply( - ([outputPath, server]) => path.join(outputPath, server), - ), environment: args.environment, link: args.link, + accountId: args.accountId, url: true, dev: false, domain: args.domain, + handler: all([outputPath, plan.server]).apply( + ([outputPath, server]) => path.join(outputPath, server), + ), assets: { directory: all([outputPath, plan.assets]).apply( ([outputPath, assets]) => path.join(outputPath, assets), @@ -115,13 +260,14 @@ export abstract class SsrSite extends Component implements Link.Linkable { } /** - * The URL of the Remix app. + * The URL of the site. * * If the `domain` is set, this is the URL with the custom domain. - * Otherwise, it's the auto-generated CloudFront URL. + * Otherwise, it's the auto-generated Worker URL. */ public get url() { - return this.server.url; + if (this.server) return this.server.url; + return this.devUrl!; } /** diff --git a/platform/src/components/cloudflare/static-site-v2.ts b/platform/src/components/cloudflare/static-site-v2.ts new file mode 100644 index 0000000000..f64e54a48e --- /dev/null +++ b/platform/src/components/cloudflare/static-site-v2.ts @@ -0,0 +1,428 @@ +import path from "path"; +import { ComponentResourceOptions, output } from "@pulumi/pulumi"; +import { Component, transform, type Transform } from "../component.js"; +import { Link } from "../link.js"; +import { Input } from "../input.js"; +import { URL_UNAVAILABLE } from "../aws/linkable.js"; +import { Worker, WorkerArgs } from "./worker.js"; +import { + BaseStaticSiteArgs, + buildApp, + prepare, +} from "../base/base-static-site.js"; +import type { BaseSiteDev } from "../base/base-site.js"; +import type { Prettify } from "../component.js"; + +export interface StaticSiteV2Args extends Omit { + /** + * Configure how this component works in `sst dev`. + * + * :::note + * In `sst dev` your static site is run in dev mode; it's not deployed. + * ::: + * + * Instead of deploying your static site, this starts it in dev mode. It's run + * as a separate process in the `sst dev` multiplexer. Read more about + * [`sst dev`](/docs/reference/cli/#dev). + * + * To disable dev mode, pass in `false`. + * + * @example + * Use a custom dev command. + * ```js + * { + * dev: { + * command: "yarn dev" + * } + * } + * ``` + */ + dev?: false | Prettify; + /** + * Path to the directory where your static site is located. By default this assumes your static site is in the root of your SST app. + * + * This directory will be uploaded as static assets. The path is relative to your `sst.config.ts`. + * + * :::note + * If the `build` options are specified, `build.output` will be uploaded as static assets instead. + * ::: + * + * If you are using a static site generator, like Vite, you'll need to configure the `build` options. When these are set, the `build.output` directory will be uploaded as static assets instead. + * + * @default `"."` + * + * @example + * + * Change where your static site is located. + * + * ```js + * { + * path: "packages/web" + * } + * ``` + */ + path?: BaseStaticSiteArgs["path"]; + /** + * Configure if your static site needs to be built. This is useful if you are using a static site generator. + * + * The `build.output` directory will be uploaded as static assets. + * + * @example + * For a Vite project using npm this might look like this. + * + * ```js + * { + * build: { + * command: "npm run build", + * output: "dist" + * } + * } + * ``` + */ + build?: BaseStaticSiteArgs["build"]; + /** + * Set a custom domain for your static site. Supports domains hosted on Cloudflare. + * + * :::tip + * You can migrate an externally hosted domain to Cloudflare by + * [following this guide](https://developers.cloudflare.com/dns/zone-setups/full-setup/setup/). + * ::: + * + * @example + * + * ```js + * { + * domain: "domain.com" + * } + * ``` + * + * Redirect alternate domains to the main domain. + * + * ```js + * { + * domain: { + * name: "domain.com", + * redirects: ["www.domain.com"] + * } + * } + * ``` + * + * Or keep visitors on alternate domains with aliases. + * + * ```js + * { + * domain: { + * name: "app1.domain.com", + * aliases: ["app2.domain.com"] + * } + * } + * ``` + */ + domain?: WorkerArgs["domain"]; + /** + * Configure trailing slash behavior for HTML pages. + * + * - `"auto"`: Individual files served without slash, folder indexes with slash + * - `"force"`: All HTML pages served with trailing slash + * - `"drop"`: All HTML pages served without trailing slash + * + * @default `"auto"` + * + * @example + * + * #### Force trailing slashes + * + * ```js + * { + * trailingSlash: "force" + * } + * ``` + */ + trailingSlash?: "auto" | "force" | "drop"; + /** + * Configure the response when a request does not match a static asset. + * + * - `"single-page-application"`: Serve `index.html` for unmatched routes (SPA mode) + * - `"404"`: Serve the nearest `404.html` file with a `404` status + * + * @default `"single-page-application"` + */ + notFound?: Input<"single-page-application" | "404">; + /** @deprecated */ + indexPage?: string; + /** @deprecated */ + errorPage?: Input; + /** + * [Transform](/docs/components#transform) how this component creates its underlying + * resources. + */ + transform?: { + /** + * Transform the Worker component used for serving the static site. + */ + server?: Transform; + }; + /** + * The Cloudflare account ID to use for this StaticSiteV2 and its worker. + * Overrides the default account ID set via `CLOUDFLARE_DEFAULT_ACCOUNT_ID`. + * @internal + */ + accountId?: Input; +} + +/** + * The `StaticSiteV2` component lets you deploy a static website to Cloudflare. It uses [Cloudflare Workers](https://developers.cloudflare.com/workers/) with [static assets](https://developers.cloudflare.com/workers/static-assets/) to serve your files. + * + * It can also `build` your site by running your static site generator, like [Vite](https://vitejs.dev) and uploading the build output as static assets. + * + * @example + * + * #### Minimal example + * + * Simply uploads the current directory as a static site. + * + * ```js + * new sst.cloudflare.StaticSiteV2("MyWeb"); + * ``` + * + * #### Change the path + * + * Change the `path` that should be uploaded. + * + * ```js + * new sst.cloudflare.StaticSiteV2("MyWeb", { + * path: "path/to/site" + * }); + * ``` + * + * #### Deploy a Vite SPA + * + * Use [Vite](https://vitejs.dev) to deploy a React/Vue/Svelte/etc. SPA by specifying the `build` config. + * + * ```js + * new sst.cloudflare.StaticSiteV2("MyWeb", { + * build: { + * command: "npm run build", + * output: "dist" + * }, + * notFound: "single-page-application" + * }); + * ``` + * + * #### Deploy a Jekyll site + * + * Use [Jekyll](https://jekyllrb.com) to deploy a static site. + * + * ```js + * new sst.cloudflare.StaticSiteV2("MyWeb", { + * build: { + * command: "bundle exec jekyll build", + * output: "_site" + * } + * }); + * ``` + * + * #### Add a custom domain + * + * Set a custom domain for your site. + * + * ```js {2} + * new sst.cloudflare.StaticSiteV2("MyWeb", { + * domain: "my-app.com" + * }); + * ``` + * + * #### Redirect www to apex domain + * + * Redirect `www.my-app.com` to `my-app.com`. + * + * ```js {4} + * new sst.cloudflare.StaticSiteV2("MyWeb", { + * domain: { + * name: "my-app.com", + * redirects: ["www.my-app.com"] + * } + * }); + * ``` + * + * #### Add domain aliases + * + * Allow visitors to use alternate domains without redirecting. + * + * ```js {4} + * new sst.cloudflare.StaticSiteV2("MyWeb", { + * domain: { + * name: "app1.my-app.com", + * aliases: ["app2.my-app.com"] + * } + * }); + * ``` + * + * #### Set environment variables + * + * Set `environment` variables for the build process of your static site. These will be used locally and on deploy. + * + * For some static site generators like Vite, [environment variables](https://vitejs.dev/guide/env-and-mode) prefixed with `VITE_` can be accessed in the browser. + * + * ```ts {5-7} + * const bucket = new sst.cloudflare.Bucket("MyBucket"); + * + * new sst.cloudflare.StaticSiteV2("MyWeb", { + * environment: { + * BUCKET_NAME: bucket.name, + * // Accessible in the browser + * VITE_STRIPE_PUBLISHABLE_KEY: "pk_test_123" + * }, + * build: { + * command: "npm run build", + * output: "dist" + * } + * }); + * ``` + */ +export class StaticSiteV2 extends Component implements Link.Linkable { + private server?: Worker; + private devUrl = output(URL_UNAVAILABLE); + + constructor( + name: string, + args: StaticSiteV2Args = {}, + opts: ComponentResourceOptions = {}, + ) { + super(__pulumiType, name, args, opts); + + const self = this; + const { sitePath, environment, indexPage } = prepare(args); + const dev = normalizeDev(); + + if (dev.enabled) { + this.devUrl = dev.url; + this.registerOutputs({ + _hint: undefined, + _dev: dev.outputs, + _metadata: { + mode: "placeholder", + path: sitePath, + environment, + url: this.url, + }, + }); + return; + } + + function normalizeDev() { + const enabled = $dev && args.dev !== false; + const devArgs = args.dev || {}; + + return { + enabled, + url: output(devArgs.url ?? URL_UNAVAILABLE), + outputs: { + title: devArgs.title, + environment, + command: output(devArgs.command ?? "npm run dev"), + autostart: output(devArgs.autostart ?? true), + directory: output(devArgs.directory ?? sitePath), + }, + }; + } + + const htmlHandling = normalizeHtmlHandling(); + const notFound = normalizeNotFound(); + const outputPath = buildApp(self, name, args.build, sitePath, environment); + const worker = createRouter(); + + this.server = worker; + + this.registerOutputs({ + _hint: this.url, + _dev: dev.outputs, + _metadata: { + mode: "deployed", + path: sitePath, + environment, + url: this.url, + }, + }); + + function createRouter() { + return new Worker( + ...transform( + args.transform?.server, + `${name}Router`, + { + accountId: args.accountId, + handler: path.join( + $cli.paths.platform, + "functions", + "cf-static-site-router-worker-experimental", + ), + environment: environment.apply((e) => ({ + ...e, + ...(args.indexPage || args.errorPage + ? { INDEX_PAGE: indexPage } + : {}), + ...(args.errorPage ? { ERROR_PAGE: args.errorPage } : {}), + })), + url: true, + dev: false, + domain: args.domain, + assets: { + directory: outputPath, + htmlHandling, + notFoundHandling: notFound, + }, + }, + { parent: self }, + ), + ); + } + + function normalizeHtmlHandling() { + return args.trailingSlash === "force" + ? "force-trailing-slash" + : args.trailingSlash === "drop" + ? "drop-trailing-slash" + : "auto-trailing-slash"; + } + + function normalizeNotFound() { + return args.notFound === "404" ? "404-page" : "single-page-application"; + } + } + + /** + * The URL of the website. + * + * If the `domain` is set, this is the URL with the custom domain. + * Otherwise, it's the auto-generated worker URL. + */ + public get url() { + return this.server?.url ?? this.devUrl; + } + + /** + * The underlying [resources](/docs/components/#nodes) this component creates. + */ + public get nodes() { + return { + /** + * The worker that serves the requests. + */ + server: this.server, + }; + } + + /** @internal */ + public getSSTLink() { + return { + properties: { + url: this.url, + }, + }; + } +} + +const __pulumiType = "sst:cloudflare:StaticSite"; +// @ts-expect-error +StaticSiteV2.__pulumiType = __pulumiType; diff --git a/platform/src/components/cloudflare/static-site.ts b/platform/src/components/cloudflare/static-site.ts index 6efd65fa24..a4e588715c 100644 --- a/platform/src/components/cloudflare/static-site.ts +++ b/platform/src/components/cloudflare/static-site.ts @@ -1,14 +1,13 @@ import fs from "fs"; import path from "path"; import crypto from "crypto"; -import { ComponentResourceOptions, all, output } from "@pulumi/pulumi"; +import { ComponentResourceOptions, all, output, type Input } from "@pulumi/pulumi"; import { Kv, KvArgs } from "./kv.js"; -import { Component, Transform, transform } from "../component.js"; +import { Component, Prettify, Transform, transform } from "../component.js"; import { Link } from "../link.js"; -import { Input } from "../input.js"; import { globSync } from "glob"; import { KvData } from "./providers/kv-data.js"; -import { Worker } from "./worker.js"; +import { Worker, WorkerArgs } from "./worker.js"; import { getContentType } from "../base/base-site.js"; import { BaseStaticSiteArgs, @@ -84,7 +83,7 @@ export interface StaticSiteArgs extends BaseStaticSiteArgs { * ``` * @default `Object` */ - assets?: Input; + assets?: Prettify; /** * Set a custom domain for your static site. Supports domains hosted on Cloudflare. * @@ -100,8 +99,30 @@ export interface StaticSiteArgs extends BaseStaticSiteArgs { * domain: "domain.com" * } * ``` + * + * Redirect alternate domains to the main domain. + * + * ```js + * { + * domain: { + * name: "domain.com", + * redirects: ["www.domain.com"] + * } + * } + * ``` + * + * Or keep visitors on alternate domains with aliases. + * + * ```js + * { + * domain: { + * name: "app1.domain.com", + * aliases: ["app2.domain.com"] + * } + * } + * ``` */ - domain?: Input; + domain?: WorkerArgs["domain"]; /** * [Transform](/docs/components#transform) how this component creates its underlying * resources. @@ -112,9 +133,21 @@ export interface StaticSiteArgs extends BaseStaticSiteArgs { */ assets?: Transform; }; + /** + * The Cloudflare account ID to use for this StaticSite and its resources. + * Overrides the default account ID set via `CLOUDFLARE_DEFAULT_ACCOUNT_ID`. + * @internal + */ + accountId?: Input; } /** + * The `StaticSite` component has been deprecated. Use [`StaticSiteV2`](/docs/component/cloudflare/static-site-v2) instead. + * + * :::caution + * This component has been deprecated. + * ::: + * * The `StaticSite` component lets you deploy a static website to Cloudflare. It uses [Cloudflare KV storage](https://developers.cloudflare.com/kv/) to store your files and [Cloudflare Workers](https://developers.cloudflare.com/workers/) to serve them. * * It can also `build` your site by running your static site generator, like [Vite](https://vitejs.dev) and uploading the build output to Cloudflare KV. @@ -126,7 +159,7 @@ export interface StaticSiteArgs extends BaseStaticSiteArgs { * Simply uploads the current directory as a static site. * * ```js - * new sst.aws.StaticSite("MyWeb"); + * new sst.cloudflare.StaticSite("MyWeb"); * ``` * * #### Change the path @@ -134,7 +167,7 @@ export interface StaticSiteArgs extends BaseStaticSiteArgs { * Change the `path` that should be uploaded. * * ```js - * new sst.aws.StaticSite("MyWeb", { + * new sst.cloudflare.StaticSite("MyWeb", { * path: "path/to/site" * }); * ``` @@ -144,7 +177,7 @@ export interface StaticSiteArgs extends BaseStaticSiteArgs { * Use [Vite](https://vitejs.dev) to deploy a React/Vue/Svelte/etc. SPA by specifying the `build` config. * * ```js - * new sst.aws.StaticSite("MyWeb", { + * new sst.cloudflare.StaticSite("MyWeb", { * build: { * command: "npm run build", * output: "dist" @@ -157,7 +190,7 @@ export interface StaticSiteArgs extends BaseStaticSiteArgs { * Use [Jekyll](https://jekyllrb.com) to deploy a static site. * * ```js - * new sst.aws.StaticSite("MyWeb", { + * new sst.cloudflare.StaticSite("MyWeb", { * errorPage: "404.html", * build: { * command: "bundle exec jekyll build", @@ -166,39 +199,12 @@ export interface StaticSiteArgs extends BaseStaticSiteArgs { * }); * ``` * - * #### Deploy a Gatsby site - * - * Use [Gatsby](https://www.gatsbyjs.com) to deploy a static site. - * - * ```js - * new sst.aws.StaticSite("MyWeb", { - * errorPage: "404.html", - * build: { - * command: "npm run build", - * output: "public" - * } - * }); - * ``` - * - * #### Deploy an Angular SPA - * - * Use [Angular](https://angular.dev) to deploy a SPA. - * - * ```js - * new sst.aws.StaticSite("MyWeb", { - * build: { - * command: "ng build --output-path dist", - * output: "dist" - * } - * }); - * ``` - * * #### Add a custom domain * * Set a custom domain for your site. * * ```js {2} - * new sst.aws.StaticSite("MyWeb", { + * new sst.cloudflare.StaticSite("MyWeb", { * domain: "my-app.com" * }); * ``` @@ -208,7 +214,7 @@ export interface StaticSiteArgs extends BaseStaticSiteArgs { * Redirect `www.my-app.com` to `my-app.com`. * * ```js {4} - * new sst.aws.StaticSite("MyWeb", { + * new sst.cloudflare.StaticSite("MyWeb", { * domain: { * name: "my-app.com", * redirects: ["www.my-app.com"] @@ -227,11 +233,11 @@ export interface StaticSiteArgs extends BaseStaticSiteArgs { * For some static site generators like Vite, [environment variables](https://vitejs.dev/guide/env-and-mode) prefixed with `VITE_` can be accessed in the browser. * * ```ts {5-7} - * const bucket = new sst.aws.Bucket("MyBucket"); + * const kv = new sst.cloudflare.Kv("MyKv"); * - * new sst.aws.StaticSite("MyWeb", { + * new sst.cloudflare.StaticSite("MyWeb", { * environment: { - * BUCKET_NAME: bucket.name, + * KV_NAMESPACE: kv.id, * // Accessible in the browser * VITE_STRIPE_PUBLISHABLE_KEY: "pk_test_123" * }, @@ -241,6 +247,8 @@ export interface StaticSiteArgs extends BaseStaticSiteArgs { * } * }); * ``` + * + * @deprecated Use [`StaticSiteV2`](/docs/component/cloudflare/static-site-v2) instead. */ export class StaticSite extends Component implements Link.Linkable { private assets: Kv; @@ -285,7 +293,9 @@ export class StaticSite extends Component implements Link.Linkable { ...transform( args.transform?.assets, `${name}Assets`, - {}, + { + accountId: args.accountId, + }, { parent, retainOnDelete: false, @@ -357,7 +367,7 @@ export class StaticSite extends Component implements Link.Linkable { return new KvData( `${name}AssetFiles`, { - accountId: DEFAULT_ACCOUNT_ID, + accountId: args.accountId ?? DEFAULT_ACCOUNT_ID, namespaceId: storage.id, entries: assetManifest.apply((manifest) => manifest.map((m) => ({ @@ -377,6 +387,7 @@ export class StaticSite extends Component implements Link.Linkable { return new Worker( `${name}Router`, { + accountId: args.accountId, handler: path.join( $cli.paths.platform, "functions", diff --git a/platform/src/components/cloudflare/tan-stack-start.ts b/platform/src/components/cloudflare/tan-stack-start.ts new file mode 100644 index 0000000000..c3c6285397 --- /dev/null +++ b/platform/src/components/cloudflare/tan-stack-start.ts @@ -0,0 +1,408 @@ +import fs from "fs/promises"; +import path from "path"; +import { ComponentResourceOptions, Output } from "@pulumi/pulumi"; +import { VisibleError } from "../error.js"; +import { Plan, SsrSite, SsrSiteArgs } from "./ssr-site.js"; +import { existsAsync } from "../../util/fs.js"; +import { + validateFrameworkConfig, + validateNoWranglerFile, +} from "./helpers/validation.js"; + +export interface TanStackStartArgs extends SsrSiteArgs { + /** + * Configure how this component works in `sst dev`. + * + * :::note + * In `sst dev` your TanStack Start app is run in dev mode; it's not deployed. + * ::: + * + * Instead of deploying your TanStack Start app, this starts it in dev mode. It's run + * as a separate process in the `sst dev` multiplexer. Read more about + * [`sst dev`](/docs/reference/cli/#dev). + * + * To disable dev mode, pass in `false`. + */ + dev?: SsrSiteArgs["dev"]; + /** + * Path to the directory where your TanStack Start app is located. This path is relative to your `sst.config.ts`. + * + * By default it assumes your TanStack Start app is in the root of your SST app. + * @default `"."` + * + * @example + * + * If your TanStack Start app is in a package in your monorepo. + * + * ```js + * { + * path: "packages/web" + * } + * ``` + */ + path?: SsrSiteArgs["path"]; + /** + * [Link resources](/docs/linking/) to your TanStack Start app. This will: + * + * 1. Grant the permissions needed to access the resources. + * 2. Allow you to access it in your app using the [SDK](/docs/reference/sdk/). + * + * @example + * + * Takes a list of resources to link to the app. + * + * ```js + * { + * link: [bucket, stripeKey] + * } + * ``` + * + * You can access the linked resources in your TanStack Start app. + * + * ```ts + * import { Resource } from "sst"; + * + * console.log(Resource.MyBucket.name); + * ``` + */ + link?: SsrSiteArgs["link"]; + /** + * Set environment variables in your TanStack Start app. These are made available: + * + * 1. In `vite build`, they are loaded into the build. + * 2. At runtime as Worker bindings. + * 3. Locally while running `vite dev` through `sst dev`. + * + * :::tip + * You can also `link` resources to your TanStack Start app and access them in a type-safe way with the [SDK](/docs/reference/sdk/). We recommend linking since it's more secure. + * ::: + * + * @example + * ```js + * { + * environment: { + * API_URL: api.url, + * PUBLIC_STRIPE_PUBLISHABLE_KEY: "pk_test_123" + * } + * } + * ``` + * + * You can access the environment variables in your TanStack Start app as follows: + * + * ```ts + * import { env } from "cloudflare:workers"; + * + * const apiUrl = env.API_URL; + * ``` + */ + environment?: SsrSiteArgs["environment"]; + /** + * Set a custom domain for your TanStack Start app. + * + * @example + * + * ```js + * { + * domain: "my-app.com" + * } + * ``` + * + * Redirect alternate domains to the main domain. + * + * ```js + * { + * domain: { + * name: "my-app.com", + * redirects: ["www.my-app.com"] + * } + * } + * ``` + * + * Or keep visitors on alternate domains with aliases. + * + * ```js + * { + * domain: { + * name: "app1.my-app.com", + * aliases: ["app2.my-app.com"] + * } + * } + * ``` + */ + domain?: SsrSiteArgs["domain"]; + /** + * The command used internally to build your TanStack Start app. + * + * @default `"npm run build"` + * + * @example + * + * If you want to use a different build command. + * ```js + * { + * buildCommand: "yarn build" + * } + * ``` + */ + buildCommand?: SsrSiteArgs["buildCommand"]; +} + +/** + * The `TanStackStart` component lets you deploy a [TanStack Start](https://tanstack.com/start/latest) app to Cloudflare. + * + * :::note + * Create a Cloudflare-compatible app with `bunx @tanstack/cli@latest create --deployment cloudflare`. + * ::: + * + * @example + * + * #### Minimal example + * + * Deploy the TanStack Start app that's in the project root. + * + * ```js title="sst.config.ts" + * new sst.cloudflare.TanStackStart("MyWeb"); + * ``` + * + * #### Change the path + * + * Deploys the TanStack Start app in the `my-app/` directory. + * + * ```js {2} title="sst.config.ts" + * new sst.cloudflare.TanStackStart("MyWeb", { + * path: "my-app/" + * }); + * ``` + * + * #### Add a custom domain + * + * Set a custom domain for your TanStack Start app. + * + * ```js {2} title="sst.config.ts" + * new sst.cloudflare.TanStackStart("MyWeb", { + * domain: "my-app.com" + * }); + * ``` + * + * #### Redirect www to apex domain + * + * Redirect `www.my-app.com` to `my-app.com`. + * + * ```js {4} title="sst.config.ts" + * new sst.cloudflare.TanStackStart("MyWeb", { + * domain: { + * name: "my-app.com", + * redirects: ["www.my-app.com"] + * } + * }); + * ``` + * + * #### Add domain aliases + * + * Allow visitors to use alternate domains without redirecting. + * + * ```js {4} title="sst.config.ts" + * new sst.cloudflare.TanStackStart("MyWeb", { + * domain: { + * name: "app1.my-app.com", + * aliases: ["app2.my-app.com"] + * } + * }); + * ``` + * + * #### Link resources + * + * [Link resources](/docs/linking/) to your TanStack Start app. This will grant permissions + * to the resources and allow you to access it in your app. + * + * ```ts {4} title="sst.config.ts" + * const bucket = new sst.cloudflare.Bucket("MyBucket"); + * + * new sst.cloudflare.TanStackStart("MyWeb", { + * link: [bucket] + * }); + * ``` + * + * Add this to your `vite.config.ts` for SST to work correctly. + * + * ```ts title="vite.config.ts" + * import { defineConfig } from 'vite' + * import { tanstackStart } from '@tanstack/react-start/plugin/vite' + * import { cloudflare } from '@cloudflare/vite-plugin' + * + * export default defineConfig({ + * plugins: [ + * cloudflare({ + * viteEnvironment: { name: 'ssr' }, + * configPath: process.env.SST_WRANGLER_PATH, + * }), + * tanstackStart(), + * ], + * }) + * ``` + * + * Use `sst/resource` for linked resources. + * + * ```ts title="src/routes/api.ts" + * import { Resource } from "sst/resource"; + * + * const files = await Resource.MyBucket.list(); + * ``` + * + * [Check out the full example](/docs/examples/#cloudflare-tanstack-start). + */ +export class TanStackStart extends SsrSite { + constructor( + name: string, + args: TanStackStartArgs = {}, + opts: ComponentResourceOptions = {}, + ) { + super(__pulumiType, name, args, opts); + } + + protected validate(sitePath: string): void { + validateFrameworkConfig({ + sitePath, + configName: "vite.config", + componentName: "TanStackStart", + }); + validateNoWranglerFile(sitePath, "TanStackStart"); + } + + protected buildPlan(outputPath: Output): Output { + return outputPath.apply(async (outputPath) => { + const distPlan = await resolveDistPlan(); + if (distPlan) return distPlan; + + const legacyPlan = await resolveLegacyPlan(); + if (legacyPlan) return legacyPlan; + + throw new VisibleError( + [ + `TanStack Start build output not found in "${path.resolve(outputPath)}".`, + "", + "Expected one of:", + " - dist/server/wrangler.json and dist/client", + " - dist/server/index.js and dist/client", + " - .output/server/index.mjs and .output/public", + "", + "Use the Cloudflare deployment adapter in TanStack Start, or if your project is entirely pre-rendered, use `sst.cloudflare.StaticSite` instead.", + ].join("\n"), + ); + + async function resolveDistPlan() { + const wranglerPath = path.join( + outputPath, + "dist", + "server", + "wrangler.json", + ); + if (await existsAsync(wranglerPath)) { + const wrangler = JSON.parse(await fs.readFile(wranglerPath, "utf-8")) as { + main?: string; + assets?: { + directory?: string; + }; + }; + if (!wrangler.main) { + throw new VisibleError( + `TanStack Start build output at "${path.resolve(wranglerPath)}" is missing the Worker entry in \`main\`.`, + ); + } + + const serverPath = path.resolve(outputPath, "dist", "server", wrangler.main); + const assetsPath = path.resolve( + outputPath, + "dist", + "server", + wrangler.assets?.directory ?? "../client", + ); + + if (!(await existsAsync(serverPath))) { + throw new VisibleError( + `TanStack Start server bundle not found at:\n "${serverPath}".`, + ); + } + if (!(await existsAsync(assetsPath))) { + throw new VisibleError( + `TanStack Start assets directory not found at:\n "${assetsPath}".`, + ); + } + + return { + server: toPlanPath(serverPath), + assets: toPlanPath(assetsPath), + }; + } + + for (const serverFile of ["index.js", "index.mjs"]) { + const serverPath = path.join( + outputPath, + "dist", + "server", + serverFile, + ); + const assetsPath = path.join(outputPath, "dist", "client"); + if ( + (await existsAsync(serverPath)) && + (await existsAsync(assetsPath)) + ) { + return { + server: toPlanPath(serverPath), + assets: toPlanPath(assetsPath), + }; + } + } + } + + async function resolveLegacyPlan() { + const serverPath = path.join( + outputPath, + ".output", + "server", + "index.mjs", + ); + const assetsPath = path.join(outputPath, ".output", "public"); + + if (!(await existsAsync(serverPath))) return; + if (!(await existsAsync(assetsPath))) { + throw new VisibleError( + `TanStack Start assets directory not found at:\n "${path.resolve( + assetsPath, + )}".`, + ); + } + + return { + server: toPlanPath(serverPath), + assets: toPlanPath(assetsPath), + }; + } + + function toPlanPath(filePath: string) { + const relativePath = path.relative(outputPath, filePath); + return `./${relativePath.split(path.sep).join("/")}`; + } + }); + } + + protected buildWrangler() { + return { + main: "@tanstack/react-start/server-entry", + }; + } + + /** + * The URL of the TanStack Start app. + * + * If the `domain` is set, this is the URL with the custom domain. + * Otherwise, it's the auto-generated Worker URL. + */ + public get url() { + return super.url; + } +} + +const __pulumiType = "sst:cloudflare:TanStackStart"; +// @ts-expect-error +TanStackStart.__pulumiType = __pulumiType; diff --git a/platform/src/components/cloudflare/worker.ts b/platform/src/components/cloudflare/worker.ts index c60afcf4b3..91becd003a 100644 --- a/platform/src/components/cloudflare/worker.ts +++ b/platform/src/components/cloudflare/worker.ts @@ -11,7 +11,7 @@ import { import * as cf from "@pulumi/cloudflare"; import type { Loader } from "esbuild"; import type { EsbuildOptions } from "../esbuild.js"; -import { Component, Transform, transform } from "../component"; +import { Component, Prettify, Transform, transform } from "../component"; import { WorkerUrl } from "./providers/worker-url.js"; import { WorkerPlacement } from "./providers/worker-placement.js"; import { Link } from "../link.js"; @@ -22,12 +22,102 @@ import { Permission } from "../aws/permission.js"; import { binding } from "./binding.js"; import { DEFAULT_ACCOUNT_ID } from "./account-id.js"; import { rpc } from "../rpc/rpc.js"; -import { WorkerAssets } from "./providers/worker-assets"; -import { globSync } from "glob"; -import { VisibleError } from "../error"; import { getContentType } from "../base/base-site"; -import { physicalName } from "../naming"; +import { prefixName } from "../naming"; import { existsAsync } from "../../util/fs"; +import { normalizeCompatibility } from "./helpers/compatibility.js"; +import { cfFetch } from "./helpers/fetch.js"; + +export interface WorkerDurableObjectMigration { + /** + * A unique identifier for this migration. + */ + tag: Input; + /** + * New Durable Object classes backed by the legacy KV storage backend. + */ + newClasses?: Input[]>; + /** + * New Durable Object classes backed by the SQLite storage backend. + */ + newSqliteClasses?: Input[]>; + /** + * Durable Object classes to delete. + */ + deletedClasses?: Input[]>; + /** + * Durable Object classes to rename. + */ + renamedClasses?: Input< + Input<{ + from: Input; + to: Input; + }>[] + >; + /** + * Durable Object classes to transfer from another script. + */ + transferredClasses?: Input< + Input<{ + from: Input; + fromScript: Input; + to: Input; + }>[] + >; +} + +export interface WorkerDomainArgs { + /** + * The custom domain you want to use. + * + * @example + * ```js + * { + * domain: { + * name: "example.com" + * } + * } + * ``` + */ + name: Input; + /** + * Alternate domains to be used. Visitors to the alternate domains will be redirected to the + * main `name`. + * + * :::note + * Unlike the `aliases` option, this will redirect visitors back to the main `name`. + * ::: + * + * @example + * Use this to create a `www.` version of your domain and redirect visitors to the apex domain. + * ```js {4} + * { + * domain: { + * name: "domain.com", + * redirects: ["www.domain.com"] + * } + * } + * ``` + */ + redirects?: Input[]; + /** + * Alias domains that should be used. Unlike the `redirects` option, this keeps your visitors + * on this alias domain. + * + * @example + * So if your users visit `app2.domain.com`, they will stay on `app2.domain.com` in their + * browser. + * ```js {4} + * { + * domain: { + * name: "app1.domain.com", + * aliases: ["app2.domain.com"] + * } + * } + * ``` + */ + aliases?: Input[]; +} export interface WorkerArgs { /** @@ -64,8 +154,30 @@ export interface WorkerArgs { * domain: "domain.com" * } * ``` + * + * You can also redirect alternate domains to the main domain. + * + * ```js + * { + * domain: { + * name: "domain.com", + * redirects: ["www.domain.com"] + * } + * } + * ``` + * + * Or keep visitors on alternate domains with aliases. + * + * ```js + * { + * domain: { + * name: "app1.domain.com", + * aliases: ["app2.domain.com"] + * } + * } + * ``` */ - domain?: Input; + domain?: Input | Prettify; /** * Configure how your function is bundled. * @@ -127,6 +239,29 @@ export interface WorkerArgs { */ minify?: Input; }>; + /** + * Configure Cloudflare compatibility for the Worker. + */ + compatibility?: Input<{ + /** + * The Cloudflare compatibility date for the Worker. + * + * SST uses this for both the uploaded Worker and for deciding which native + * Node.js modules should stay external during bundling. + * + * @default `"2025-05-05"` + */ + date?: Input; + /** + * The Cloudflare compatibility flags for the Worker. + * + * SST uses this for both the uploaded Worker and for deciding which native + * Node.js modules should stay external during bundling. + * + * @default `["nodejs_compat"]` + */ + flags?: Input[]>; + }>; /** * [Link resources](/docs/linking/) to your worker. This will: * @@ -161,26 +296,63 @@ export interface WorkerArgs { */ environment?: Input>>; /** - * Upload [static assets](https://developers.cloudflare.com/workers/static-assets/) as - * part of the worker. + * Durable Object migrations for this worker. + * + * This follows Wrangler's top-level `migrations` config. Keep the full, + * ordered migration history here; SST reads the Worker's current migration + * tag and uploads the pending migrations as Cloudflare's `oldTag`, `newTag`, + * and `steps` payload. If there is no current tag, SST uploads the full + * history as the initial migration payload. If the current tag is not in the + * configured history, SST follows Wrangler and uploads all configured + * migrations with the current tag as `oldTag`. + * + * Each migration needs a unique `tag`. Add a migration when you create, + * rename, delete, or transfer a Durable Object class. Updating code for an + * existing class does not require one. * - * You can directly fetch and serve assets within your Worker code via the [assets - * binding](https://developers.cloudflare.com/workers/static-assets/binding/#binding). + * On the first deploy, add each new class with `newSqliteClasses`. For a + * rename, keep the original migration and append a new migration with + * `renamedClasses`. Do not declare the renamed class as a new class, or + * Cloudflare will reject the deploy or create a separate namespace. * * @example - * ```js + * ```ts * { - * assets: { - * directory: "./dist" - * } + * migrations: [{ + * tag: "v1", + * newSqliteClasses: ["Counter"] + * }] + * } + * ``` + * + * @example + * ```ts + * { + * migrations: [ + * { + * tag: "v1", + * newSqliteClasses: ["Counter"], + * }, + * { + * tag: "v2", + * renamedClasses: [{ from: "Counter", to: "CounterV2" }], + * }, + * ] * } * ``` */ + migrations?: Input[]>; + /** @internal */ assets?: Input<{ - /** - * The directory containing the assets. - */ directory: Input; + htmlHandling?: Input< + | "auto-trailing-slash" + | "force-trailing-slash" + | "drop-trailing-slash" + | "none" + >; + notFoundHandling?: Input<"404-page" | "single-page-application" | "none">; + runWorkerFirst?: Input[]>; }>; /** * Configure [placement](https://developers.cloudflare.com/workers/configuration/placement/) @@ -222,6 +394,12 @@ export interface WorkerArgs { */ worker?: Transform; }; + /** + * The Cloudflare account ID to use for this Worker and all its sub-resources. + * Overrides the default account ID set via `CLOUDFLARE_DEFAULT_ACCOUNT_ID`. + * @internal + */ + accountId?: Input; /** * @internal * Placehodler for future feature. @@ -293,37 +471,46 @@ export class Worker extends Component implements Link.Linkable { private script: cf.WorkersScript; private workerUrl: WorkerUrl; private workerPlacement?: WorkerPlacement; - private workerDomain?: cf.WorkerDomain; + private workerDomain?: cf.WorkersCustomDomain; constructor(name: string, args: WorkerArgs, opts?: ComponentResourceOptions) { super(__pulumiType, name, args, opts); const parent = this; + const accountId = args.accountId ?? DEFAULT_ACCOUNT_ID; const dev = normalizeDev(); const urlEnabled = normalizeUrl(); + const compatibility = normalizeCompatibility(args); + const domain = normalizeDomain(); const bindings = buildBindings(); const iamCredentials = createAwsCredentials(); - const buildInput = all([name, args.handler, args.build, dev]).apply( - async ([name, handler, build]) => { - return { - functionID: name, - links: {}, - handler, - runtime: "worker", - properties: { - accountID: DEFAULT_ACCOUNT_ID, - build, - }, - }; - }, - ); + const buildInput = all([ + name, + args.handler, + args.build, + compatibility, + ]).apply(async ([name, handler, build, compatibility]) => { + return { + functionID: name, + links: {}, + handler, + runtime: "worker", + properties: { + accountID: accountId, + build, + compatibility, + }, + }; + }); const build = buildHandler(); const script = createScript(); const workerUrl = createWorkersUrl(); const workerPlacement = createWorkerPlacement(); const workerDomain = createWorkersDomain(); + createAliases(); + createRedirects(); this.script = script; this.workerUrl = workerUrl; @@ -343,8 +530,8 @@ export class Worker extends Component implements Link.Linkable { }, ); this.registerOutputs({ - _live: all([name, args.handler, args.build, dev]).apply( - ([name, handler, build, dev]) => { + _live: all([name, args.handler, args.build, compatibility, dev]).apply( + ([name, handler, build, compatibility, dev]) => { if (!dev) return undefined; return { functionID: name, @@ -352,9 +539,10 @@ export class Worker extends Component implements Link.Linkable { handler, runtime: "worker", properties: { - accountID: DEFAULT_ACCOUNT_ID, + accountID: accountId, scriptName: script.scriptName, build, + compatibility, }, }; }, @@ -372,6 +560,28 @@ export class Worker extends Component implements Link.Linkable { return output(args.url).apply((v) => v ?? false); } + function normalizeDomain() { + if (!args.domain) return; + + if ( + typeof args.domain === "object" && + args.domain !== null && + "name" in args.domain + ) { + return { + name: args.domain.name, + aliases: args.domain.aliases ?? [], + redirects: args.domain.redirects ?? [], + }; + } + + return { + name: args.domain, + aliases: [], + redirects: [], + }; + } + function buildBindings() { const result = [ { @@ -396,20 +606,26 @@ export class Worker extends Component implements Link.Linkable { b ? { type: { + aiBindings: "ai", plainTextBindings: "plain_text", secretTextBindings: "secret_text", queueBindings: "queue", serviceBindings: "service", + durableObjectNamespaceBindings: "durable_object_namespace", kvNamespaceBindings: "kv_namespace", d1DatabaseBindings: "d1", r2BucketBindings: "r2_bucket", + hyperdriveBindings: "hyperdrive", + versionMetadataBindings: "version_metadata", + workflowBindings: "workflow", + rateLimitBindings: "ratelimit", }[b.binding], name, ...b.properties, } : { type: "secret_text", - name: name, + name: output(name).apply((name) => `SST_RESOURCE_${name}`), text: jsonStringify(item.properties), }, ); @@ -484,6 +700,92 @@ export class Worker extends Component implements Link.Linkable { } function createScript() { + // workers.dev URLs fail above 54 chars when previews are enabled + const scriptName = prefixName(54, `${name}Script`).toLowerCase(); + const durableObjectMigrationState = args.migrations + ? all([args.migrations, accountId]).apply( + async ([migrations, accountId]) => { + const latest = migrations.at(-1); + if (!latest) { + return { + migrationTag: undefined, + migrations: undefined, + }; + } + + let currentTag: string | undefined; + try { + const published = await cfFetch< + { + id?: string; + migration_tag?: string; + }[] + >(`/accounts/${accountId}/workers/scripts`); + currentTag = published.result.find( + (script) => script.id === scriptName, + )?.migration_tag; + } catch (error) { + const code = + typeof error === "object" && + error !== null && + "errors" in error && + Array.isArray(error.errors) && + typeof error.errors[0]?.code === "number" + ? error.errors[0].code + : undefined; + // workers.api.error.service_not_found + const serviceNotFound = code === 10090; + // workers.api.error.environment_not_found + const environmentNotFound = code === 10092; + + // Wrangler suppresses these not-found cases when reading the + // current migration tag; they mean this is the first deploy for + // the script/environment, so there is no remote tag yet. + if (!serviceNotFound && !environmentNotFound) { + throw error; + } + } + + if (currentTag) { + const foundIndex = migrations.findIndex( + (migration) => migration.tag === currentTag, + ); + + if (foundIndex === migrations.length - 1) { + return { + migrationTag: currentTag, + migrations: undefined, + }; + } + + const pending = + foundIndex === -1 + ? migrations + : migrations.slice(foundIndex + 1); + const steps = pending.map(({ tag: _tag, ...step }) => step); + + return { + migrationTag: currentTag, + migrations: { + oldTag: currentTag, + newTag: latest.tag, + steps, + }, + }; + } + + const steps = migrations.map(({ tag: _tag, ...step }) => step); + + return { + migrationTag: "", + migrations: { + newTag: latest.tag, + steps, + }, + }; + }, + ) + : undefined; const contentFilePath = build.apply((build) => path.join(build.out, build.handler), ); @@ -492,9 +794,9 @@ export class Worker extends Component implements Link.Linkable { args.transform?.worker as Transform, `${name}Script`, { - scriptName: physicalName(64, `${name}Script`).toLowerCase(), + scriptName, mainModule: "placeholder", - accountId: DEFAULT_ACCOUNT_ID, + accountId, contentFile: contentFilePath, contentSha256: contentFilePath.apply(async (p) => crypto @@ -502,27 +804,52 @@ export class Worker extends Component implements Link.Linkable { .update(await fs.readFile(p, "utf-8")) .digest("hex"), ), - compatibilityDate: "2025-05-05", - compatibilityFlags: ["nodejs_compat"], + compatibilityDate: compatibility.apply((value) => value.date), + compatibilityFlags: compatibility.apply((value) => value.flags), + ...(durableObjectMigrationState + ? { + migrations: durableObjectMigrationState.apply( + (state) => state.migrations, + ) as Input, + migrationTag: durableObjectMigrationState.apply( + (state) => state.migrationTag, + ) as Input, + } + : {}), assets: args.assets ? output(args.assets).apply(async (assets) => { - const directory = path.join( - $cli.paths.root, - assets.directory, - ); + const directory = path.isAbsolute(assets.directory) + ? assets.directory + : path.join($cli.paths.root, assets.directory); + let headers; + let redirects; try { headers = await fs.readFile( path.join(directory, "_headers"), "utf-8", ); } catch (e) {} + + try { + redirects = await fs.readFile( + path.join(directory, "_redirects"), + "utf-8", + ); + } catch (e) {} return { directory, - config: { headers }, + config: { + headers, + redirects, + htmlHandling: assets.htmlHandling, + notFoundHandling: assets.notFoundHandling, + runWorkerFirst: assets.runWorkerFirst, + }, }; }) : undefined, + bindings: all([args.environment, iamCredentials, bindings]).apply( ([environment, iamCredentials, bindings]) => [ ...bindings, @@ -565,9 +892,10 @@ export class Worker extends Component implements Link.Linkable { return new WorkerUrl( `${name}Url`, { - accountId: DEFAULT_ACCOUNT_ID, + accountId: accountId, scriptName: script.scriptName, enabled: urlEnabled, + etag: script.etag, }, { parent }, ); @@ -581,8 +909,11 @@ export class Worker extends Component implements Link.Linkable { return new WorkerPlacement( `${name}Placement`, { - accountId: DEFAULT_ACCOUNT_ID, + accountId: accountId, scriptName: script.scriptName, + // Reapply placement after each script update. Asset-backed SSR workers + // can rewrite script settings and reset placement back to the default. + etag: script.etag, ...args.placement, }, { parent }, @@ -590,13 +921,13 @@ export class Worker extends Component implements Link.Linkable { } function createWorkersDomain() { - if (!args.domain) return; + if (!domain) return; const zone = new ZoneLookup( `${name}ZoneLookup`, { - accountId: DEFAULT_ACCOUNT_ID, - domain: args.domain, + accountId: accountId, + domain: domain.name, }, { parent }, ); @@ -604,15 +935,90 @@ export class Worker extends Component implements Link.Linkable { return new cf.WorkersCustomDomain( `${name}Domain`, { - accountId: DEFAULT_ACCOUNT_ID, + accountId: accountId, service: script.scriptName, - hostname: args.domain, + hostname: domain.name, zoneId: zone.id, environment: "production", }, { parent }, ); } + + function createAliases() { + if (!domain) return; + + for (const [i, hostname] of domain.aliases.entries()) { + const zone = new ZoneLookup( + `${name}Alias${i}ZoneLookup`, + { + accountId, + domain: hostname, + }, + { parent }, + ); + + new cf.WorkersCustomDomain( + `${name}Alias${i}Domain`, + { + accountId, + service: script.scriptName, + hostname, + zoneId: zone.id, + environment: "production", + }, + { parent }, + ); + } + } + + function createRedirects() { + if (!domain) return; + + for (const [i, hostname] of domain.redirects.entries()) { + const resourceName = `${name}Redirect${i}`; + const zone = new ZoneLookup( + `${resourceName}ZoneLookup`, + { + accountId, + domain: hostname, + }, + { parent }, + ); + + new cf.DnsRecord( + `${resourceName}Record`, + { + zoneId: zone.id, + name: hostname, + type: "AAAA", + content: "100::", + proxied: true, + ttl: 1, + }, + { parent }, + ); + + new cf.PageRule( + `${resourceName}Rule`, + { + zoneId: zone.id, + target: interpolate`${hostname}/*`, + priority: i + 1, + status: "active", + actions: { + forwardingUrl: { + statusCode: 301, + url: output(domain.name).apply( + (domainName) => `https://${domainName}/$1`, + ), + }, + }, + }, + { parent }, + ); + } + } } /** diff --git a/platform/src/components/cloudflare/workflow.ts b/platform/src/components/cloudflare/workflow.ts new file mode 100644 index 0000000000..83868691d1 --- /dev/null +++ b/platform/src/components/cloudflare/workflow.ts @@ -0,0 +1,319 @@ +import { ComponentResourceOptions } from "@pulumi/pulumi"; +import * as cf from "@pulumi/cloudflare"; +import { Component, Transform, transform } from "../component"; +import { Link } from "../link"; +import { Input } from "../input"; +import { binding } from "./binding"; +import { DEFAULT_ACCOUNT_ID } from "./account-id"; +import { Worker, WorkerArgs } from "./worker"; + +export interface WorkflowArgs { + /** + * Path to the handler file that exports the class that extends `WorkflowEntrypoint`. + * + * The handler path is relative to the root of your repo or the `sst.config.ts`. + * + * @example + * + * ```js + * { + * handler: "src/workflow.ts" + * } + * ``` + */ + handler: Input; + /** + * The name of the class in your handler file that extends `WorkflowEntrypoint`. + * + * Cloudflare Workflows are defined as classes that extend the + * [`WorkflowEntrypoint`](https://developers.cloudflare.com/workflows/build/workers-api/#workflowentrypoint) + * class. You must specify the name of the class so SST can bind to it. + * + * :::caution + * The class must be exported as a named export, not as `export default`. + * ::: + * + * @example + * + * Given this handler file: + * + * ```ts title="src/workflow.ts" + * import { WorkflowEntrypoint } from "cloudflare:workers"; + * + * export class OrderProcessor extends WorkflowEntrypoint { + * async run(event, step) { + * // ... + * } + * } + * ``` + * + * You would set: + * + * ```js + * { + * className: "OrderProcessor" + * } + * ``` + */ + className: Input; + /** + * [Link resources](/docs/linking/) to your workflow. This will: + * + * 1. Make them available on `this.env` inside your workflow class. + * 2. Allow you to access them in your workflow using the [SDK](/docs/reference/sdk/). + * + * @example + * + * Takes a list of components to link to the workflow. + * + * ```js + * { + * link: [bucket, db] + * } + * ``` + */ + link?: Input; + /** + * Key-value pairs that are set as environment variables on the workflow's Worker. + * + * They can be accessed in your workflow through `this.env.`. + * + * @example + * + * ```js + * { + * environment: { + * DEBUG: "true" + * } + * } + * ``` + */ + environment?: Input>>; + /** + * The Cloudflare account ID to use for this Workflow. + * Overrides the default account ID set via `CLOUDFLARE_DEFAULT_ACCOUNT_ID`. + * @internal + */ + accountId?: Input; + /** + * [Transform](/docs/components/#transform) how this component creates its underlying + * resources. + */ + transform?: { + /** + * Transform the underlying Worker that hosts the workflow class. + */ + worker?: Transform; + /** + * Transform the Workflow resource. + */ + workflow?: Transform; + }; +} + +/** + * The `Workflow` component lets you add [Cloudflare Workflows](https://developers.cloudflare.com/workflows/) + * to your app. + * + * :::caution + * Workflow `console.log` output doesn't stream in real time during `sst dev`. + * ::: + * + * A Workflow is a durable, multi-step function that runs on Cloudflare Workers. You + * define it as a class that extends `WorkflowEntrypoint` and pass it to this component. + * + * @example + * #### Minimal example + * + * Define a workflow class: + * + * ```ts title="src/workflow.ts" + * import { WorkflowEntrypoint, WorkflowEvent, WorkflowStep } from "cloudflare:workers"; + * + * export class OrderProcessor extends WorkflowEntrypoint { + * async run(event: WorkflowEvent<{ orderId: string }>, step: WorkflowStep) { + * await step.do("validate", async () => {}); + * await step.sleep("cooldown", "10 seconds"); + * await step.do("charge", async () => {}); + * } + * } + * ``` + * + * Then register it with SST: + * + * ```ts title="sst.config.ts" + * const processor = new sst.cloudflare.Workflow("OrderProcessor", { + * handler: "src/workflow.ts", + * className: "OrderProcessor", + * }); + * ``` + * + * #### Trigger from a Worker + * + * Link the workflow to a Worker to get a native Cloudflare + * [`Workflow`](https://developers.cloudflare.com/workflows/build/workers-api/#workflow) + * binding: + * + * ```ts title="sst.config.ts" {4} + * new sst.cloudflare.Worker("Api", { + * handler: "src/api.ts", + * url: true, + * link: [processor], + * }); + * ``` + * + * Then invoke it from your worker code: + * + * ```ts title="src/api.ts" + * import { Resource } from "sst"; + * + * export default { + * async fetch(req: Request) { + * const instance = await Resource.OrderProcessor.create({ + * params: { orderId: "ord_123" }, + * }); + * return Response.json({ id: instance.id }); + * }, + * }; + * ``` + * + * #### Give the workflow access to other resources + * + * ```ts title="sst.config.ts" + * const bucket = new sst.cloudflare.Bucket("Orders"); + * + * const processor = new sst.cloudflare.Workflow("OrderProcessor", { + * handler: "src/workflow.ts", + * className: "OrderProcessor", + * link: [bucket], + * }); + * ``` + * + */ +export class Workflow extends Component implements Link.Linkable { + private worker: Worker; + private workflow: cf.Workflow; + + constructor( + name: string, + args: WorkflowArgs, + opts?: ComponentResourceOptions, + ) { + super(__pulumiType, name, args, opts); + + const parent = this; + + const worker = createWorker(); + const workflow = createWorkflow(); + + this.worker = worker; + this.workflow = workflow; + + function createWorker() { + return new Worker( + ...transform( + args.transform?.worker, + `${name}Script`, + { + handler: args.handler, + link: args.link, + environment: args.environment, + }, + { parent }, + ), + ); + } + + function createWorkflow() { + return new cf.Workflow( + ...transform( + args.transform?.workflow, + `${name}Workflow`, + { + accountId: args.accountId ?? DEFAULT_ACCOUNT_ID, + workflowName: "", + className: args.className, + scriptName: worker.nodes.worker.scriptName, + }, + { parent }, + ), + ); + } + } + + /** + * The name of the Cloudflare Workflow. + */ + public get workflowName() { + return this.workflow.workflowName; + } + + /** + * The name of the workflow class. + */ + public get className() { + return this.workflow.className; + } + + /** + * The name of the Worker script that hosts the workflow class. + */ + public get scriptName() { + return this.workflow.scriptName; + } + + /** + * The underlying [resources](/docs/components/#nodes) this component creates. + */ + public get nodes() { + return { + /** + * The Cloudflare Worker that hosts the workflow class. + */ + worker: this.worker, + /** + * The Cloudflare Workflow resource. + */ + workflow: this.workflow, + }; + } + + /** + * When you link a workflow to a worker, it automatically creates a + * [Workflow binding](https://developers.cloudflare.com/workflows/build/workers-api/#call-workflows-from-workers) + * on the worker. This lets you trigger, inspect, and manage workflow instances + * from your worker code. + * + * @example + * ```ts title="src/api.ts" + * import { Resource } from "sst"; + * + * const instance = await Resource.MyWorkflow.create({ params: { hello: "world" } }); + * ``` + * + * @internal + */ + getSSTLink() { + return { + properties: { + workflowName: this.workflow.workflowName, + className: this.workflow.className, + scriptName: this.workflow.scriptName, + }, + include: [ + binding({ + type: "workflowBindings", + properties: { + workflowName: this.workflow.workflowName, + className: this.workflow.className, + scriptName: this.workflow.scriptName, + }, + }), + ], + }; + } +} + +const __pulumiType = "sst:cloudflare:Workflow"; +// @ts-expect-error +Workflow.__pulumiType = __pulumiType; diff --git a/platform/src/components/component.ts b/platform/src/components/component.ts index bc02abe60a..6c8bd44fc7 100644 --- a/platform/src/components/component.ts +++ b/platform/src/components/component.ts @@ -160,6 +160,7 @@ export class Component extends ComponentResource { "aws:rds/proxyTarget:ProxyTarget", "aws:route53/record:Record", "aws:s3/bucketCorsConfigurationV2:BucketCorsConfigurationV2", + "aws:s3/bucketCorsConfiguration:BucketCorsConfiguration", "aws:s3/bucketNotification:BucketNotification", "aws:s3/bucketObject:BucketObject", "aws:s3/bucketObjectv2:BucketObjectv2", @@ -167,20 +168,28 @@ export class Component extends ComponentResource { "aws:s3/bucketPublicAccessBlock:BucketPublicAccessBlock", "aws:s3/bucketVersioningV2:BucketVersioningV2", "aws:s3/bucketLifecycleConfigurationV2:BucketLifecycleConfigurationV2", + "aws:s3/bucketLifecycleConfiguration:BucketLifecycleConfiguration", "aws:s3/bucketWebsiteConfigurationV2:BucketWebsiteConfigurationV2", + "aws:s3/bucketVersioning:BucketVersioning", + "aws:s3/bucketWebsiteConfiguration:BucketWebsiteConfiguration", "aws:secretsmanager/secretVersion:SecretVersion", + "aws:wafv2/webAclLoggingConfiguration:WebAclLoggingConfiguration", "aws:ses/domainIdentityVerification:DomainIdentityVerification", "aws:sesv2/configurationSetEventDestination:ConfigurationSetEventDestination", "aws:sesv2/emailIdentity:EmailIdentity", + "aws:sesv2/emailIdentityMailFromAttributes:EmailIdentityMailFromAttributes", "aws:sns/topicPolicy:TopicPolicy", "aws:sns/topicSubscription:TopicSubscription", "aws:sqs/queuePolicy:QueuePolicy", "aws:ssm/parameter:Parameter", "cloudflare:index/dnsRecord:DnsRecord", + "cloudflare:index/pageRule:PageRule", "cloudflare:index/workersCronTrigger:WorkersCronTrigger", "cloudflare:index/workersCustomDomain:WorkersCustomDomain", + "cloudflare:index/queueConsumer:QueueConsumer", "docker-build:index:Image", "vercel:index/dnsRecord:DnsRecord", + "aws:dsql/clusterPeering:ClusterPeering" ].includes(args.type) ) return; @@ -213,7 +222,13 @@ export class Component extends ComponentResource { "aws:cloudfront/keyValueStore:KeyValueStore": ["name", 64], "aws:cognito/identityPool:IdentityPool": ["identityPoolName", 128], "aws:cognito/userPool:UserPool": ["name", 128], + "aws:cognito/userPoolDomain:UserPoolDomain": [ + "domain", + 63, + { lower: true }, + ], "aws:dynamodb/table:Table": ["name", 255], + "aws:dsql/cluster:Cluster": ["tags", 255], "aws:ec2/keyPair:KeyPair": ["keyName", 255], "aws:ec2/eip:Eip": ["tags", 255], "aws:ec2/instance:Instance": ["tags", 255], @@ -224,6 +239,7 @@ export class Component extends ComponentResource { "aws:ec2/defaultSecurityGroup:DefaultSecurityGroup": ["tags", 255], "aws:ec2/subnet:Subnet": ["tags", 255], "aws:ec2/vpc:Vpc": ["tags", 255], + "aws:ec2/vpcEndpoint:VpcEndpoint": ["tags", 255], "aws:ecs/cluster:Cluster": ["name", 255], "aws:elasticache/parameterGroup:ParameterGroup": [ "name", @@ -277,13 +293,14 @@ export class Component extends ComponentResource { { lower: true }, ], "aws:rds/subnetGroup:SubnetGroup": ["name", 255, { lower: true }], - "aws:s3/bucketV2:BucketV2": ["bucket", 63, { lower: true }], + "aws:s3/bucket:Bucket": ["bucket", 63, { lower: true }], "aws:secretsmanager/secret:Secret": ["name", 512], "aws:sesv2/configurationSet:ConfigurationSet": [ "configurationSetName", 64, { lower: true }, ], + "aws:scheduler/schedule:Schedule": ["name", 64], "aws:sfn/stateMachine:StateMachine": ["name", 80], "aws:sns/topic:Topic": [ "name", @@ -305,12 +322,16 @@ export class Component extends ComponentResource { ), }, ], + "aws:wafv2/webAcl:WebAcl": ["name", 64], "cloudflare:index/d1Database:D1Database": [ "name", 64, { lower: true }, ], "cloudflare:index/r2Bucket:R2Bucket": ["name", 64, { lower: true }], + "aws:backup/vault:Vault": ["name", 50], + "aws:backup/plan:Plan": ["name", 50], + "aws:backup/selection:Selection": ["name", 50], "cloudflare:index/workersScript:WorkersScript": [ "scriptName", 64, @@ -322,6 +343,16 @@ export class Component extends ComponentResource { 64, { lower: true }, ], + "cloudflare:index/hyperdriveConfig:HyperdriveConfig": [ + "name", + 64, + { lower: true }, + ], + "cloudflare:index/workflow:Workflow": [ + "workflowName", + 64, + { lower: true }, + ], }; const rule = namingRules[args.type]; diff --git a/platform/src/components/dns.ts b/platform/src/components/dns.ts index d0b1385bc5..f6a6dc88f1 100644 --- a/platform/src/components/dns.ts +++ b/platform/src/components/dns.ts @@ -20,6 +20,10 @@ export interface Record { * The value of the record. */ value: Input; + /** + * The priority of the record. Only applies to `MX` records. + */ + priority?: Input; } export interface AliasRecord { diff --git a/platform/src/components/duration.ts b/platform/src/components/duration.ts index 1fec30eaf5..54b425d9f1 100644 --- a/platform/src/components/duration.ts +++ b/platform/src/components/duration.ts @@ -44,3 +44,24 @@ export function toSeconds( throw new Error(`Invalid duration ${duration}`); } + +export function toMilliseconds( + duration: Duration | DurationMinutes | DurationSeconds | DurationDays, +) { + return toSeconds(duration) * 1000; +} + +export function toDays(duration: Duration) { + const [count, unit] = duration.split(" "); + const countNum = parseInt(count); + const unitLower = unit.toLowerCase(); + + if (unitLower.startsWith("day")) { + return countNum; + } + + const DAYS_IN_SECONDS = 86400; + const seconds = toSeconds(duration); + const result = seconds / DAYS_IN_SECONDS; + return Math.ceil(result); +} diff --git a/platform/src/components/hint.ts b/platform/src/components/hint.ts deleted file mode 100644 index 900d802d22..0000000000 --- a/platform/src/components/hint.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Input, all } from "@pulumi/pulumi"; - -export module Hint { - let hints = {} as Record>; - export function reset() { - hints = {}; - } - - export function register(name: Input, hint: Input) { - all([name]).apply(([name]) => { - hints[name] = hint; - }); - } - - export function list() { - return hints; - } -} diff --git a/platform/src/components/link.ts b/platform/src/components/link.ts index a3b2b39e19..537b3b63ac 100644 --- a/platform/src/components/link.ts +++ b/platform/src/components/link.ts @@ -9,7 +9,11 @@ import { import { VisibleError } from "./error.js"; import { Linkable } from "./linkable.js"; -export module Link { +export namespace Link { + function normalizeType(type: string) { + return type.replaceAll(":", "."); + } + export interface Definition< Properties extends Record = Record, > { @@ -35,7 +39,7 @@ export module Link { target: target, include, properties: { - type: type.replaceAll(":", "."), + type: normalizeType(type), ...properties, }, }); @@ -115,7 +119,7 @@ export module Link { name: urn.split("::").at(-1)!, properties: { ...link.properties, - type: urn.split("::").at(-2), + type: normalizeType(urn.split("::").at(-2)!), }, })); }); @@ -144,7 +148,7 @@ export module Link { const name = urn.split("::").at(-1)!; const data = { ...properties, - type: urn.split("::").at(-2), + type: normalizeType(urn.split("::").at(-2)!), }; return [name, data]; }), diff --git a/platform/src/components/linkable.ts b/platform/src/components/linkable.ts index 30b9402c76..cca8e19c4c 100644 --- a/platform/src/components/linkable.ts +++ b/platform/src/components/linkable.ts @@ -1,4 +1,4 @@ -import { output } from "@pulumi/pulumi"; +import { Output, output } from "@pulumi/pulumi"; import { Link } from "./link"; import { Component } from "./component"; import { Input } from "./input"; @@ -171,6 +171,13 @@ export interface Definition< * ``` * * This overrides the built-in link and lets you create your own. + * + * #### Exposing links as env vars + * + * If you want to pass link env vars to compute not managed by SST, like an ECS task + * definition or a Kubernetes pod, use `Linkable.env()`. It returns an `Output` of + * `SST_RESOURCE_*` env vars that you can pass to any provider. + * [Check out an example](/docs/examples/#aws-linkable-env). */ export class Linkable> extends Component @@ -280,6 +287,31 @@ export class Linkable> return cb(this); }; } + + /** + * Convert an array of resources into `SST_RESOURCE_*` environment variables so + * that `Resource.MyResource` works at runtime inside containers or functions + * deployed through an external provider. + * + * @param links Array of linkable resources. + * + * @example + * + * If the provider accepts a flat `Record`, you can pass the + * output directly. + * + * ```ts title="sst.config.ts" + * const bucket = new sst.aws.Bucket("MyBucket"); + * + * new vercel.Project("BadDecision", { + * name: "bad-decision", + * environment: sst.Linkable.env([bucket]), + * }); + * ``` + */ + public static env(links: Input): Output> { + return Link.propertiesToEnv(Link.getProperties(links)); + } } /** diff --git a/platform/src/components/rpc/rpc.ts b/platform/src/components/rpc/rpc.ts index 6674674fb3..4ba02ffa07 100644 --- a/platform/src/components/rpc/rpc.ts +++ b/platform/src/components/rpc/rpc.ts @@ -1,7 +1,7 @@ import { dynamic } from "@pulumi/pulumi"; import http from "http"; -export module rpc { +export namespace rpc { export class MethodNotFoundError extends Error { constructor(public method: string) { super(`Method "${method}" not found`); diff --git a/platform/src/components/vercel/dns.ts b/platform/src/components/vercel/dns.ts index a7eda36113..aab4ea3c10 100644 --- a/platform/src/components/vercel/dns.ts +++ b/platform/src/components/vercel/dns.ts @@ -21,10 +21,10 @@ * * #### Configure provider * - * 1. To use this component, add the `@pulumiverse/vercel` provider to your app. + * 1. To use this component, add the `vercel` provider to your app. * * ```bash - * sst add @pulumiverse/vercel + * sst add vercel * ``` * * 2. If you don't already have a Vercel Access Token, [follow this guide](https://vercel.com/guides/how-do-i-use-a-vercel-api-access-token#creating-an-access-token) to create one. @@ -158,6 +158,7 @@ export function dns(args: DnsArgs) { type: record.type, name: recordName, value: record.value, + mxPriority: record.priority, teamId: DEFAULT_TEAM_ID, ttl: 60, }, diff --git a/platform/src/components/vercel/providers/dns-record.ts b/platform/src/components/vercel/providers/dns-record.ts index 4838031633..d38a6fd118 100644 --- a/platform/src/components/vercel/providers/dns-record.ts +++ b/platform/src/components/vercel/providers/dns-record.ts @@ -6,6 +6,7 @@ export interface DnsRecordInputs { type: Input; name: Input; value: Input; + mxPriority?: Input; } export interface DnsRecord { diff --git a/platform/src/config.ts b/platform/src/config.ts index c8297e4326..51c417ea71 100644 --- a/platform/src/config.ts +++ b/platform/src/config.ts @@ -273,19 +273,134 @@ export interface App { /** * Configure which directories should be watched for changes when running `sst dev`. - * By default, all directories are watched (except node_modules and hidden directories). + * By default, SST watches the project root recursively, except for hidden + * directories and `node_modules`. + * + * You can configure it with `paths` and `ignore`. + * + * :::caution + * Passing an array directly is supported for backward compatibility + * and will be deprecated in the next major version. + * ::: * * @example * ```ts * { - * watch: ["packages/www", "packages/api"] + * watch: { + * paths: ["packages/www", "packages/api"], + * ignore: [".env", "packages/api/generated"] + * } * } * ``` * - * This will only watch the `packages/www` and `packages/api` directories. - * The paths are relative to the project root. + * @example + * + * Watch all dictories except `*.egg-info`. + * ```ts + * { + * watch: { + * ignore: ["*.egg-info"] + * } + * } + * ``` */ - watch?: string[]; + watch?: string[] | AppWatch; + + /** + * Configure how your app's state is managed. + */ + state?: { + /** + * If set to `true`, running `sst remove` will fully remove all state associated + * with the stage once all resources have been successfully removed. + * + * :::tip + * Only enable this for ephemeral or development stages. + * ::: + * + * This removes the state files, secrets, update history, event logs, snapshots + * and the encryption passphrase. + * + * :::caution + * This is irreversible. Once the state encryption key is deleted, secrets and + * all state versions will be unrecoverable. + * ::: + * + * On AWS this requires `s3:DeleteObjectVersion` and `s3:ListBucketVersions` + * on the state bucket. See the [IAM credentials](/docs/iam-credentials) + * docs for the full policy. + * + * @default false + * + * @example + * + * ```ts + * { + * state: { + * purge: true + * } + * } + * ``` + */ + purge?: boolean; + /** + * If set to `true`, state files are gzip-compressed before being uploaded + * to your home provider. This reduces transfer size for large state files. + * + * Applies to the `aws` (S3) and `cloudflare` (R2) home providers. The + * `local` home provider ignores this setting. + * + * Reads remain backward compatible regardless of this setting, so it can + * be safely toggled on or off between deploys. + * + * @default false + * + * @example + * + * ```ts + * { + * state: { + * compress: true + * } + * } + * ``` + */ + compress?: boolean; + }; + + /** + * Configure type generation options. + * + * @example + * ```ts + * { + * types: { + * ignore: ["packages/docs", "services/legacy-python"] + * } + * } + * ``` + */ + types?: { + /** + * Directories to ignore when generating `sst-env.d.ts` and `sst.pyi` files. + * + * By default, type files are generated for all JavaScript and Python projects + * (except node_modules and hidden directories). + * + * This will skip generating type files inside the listed directories. + * The paths are relative to the project root. + * + * @example + * ```ts + * { + * types: { + * ignore: ["packages/docs", "services/legacy-python"] + * } + * } + * ``` + */ + ignore?: string[]; + }; } export interface AppInput { @@ -301,6 +416,26 @@ export interface AppInput { stage: string; } +export interface AppWatch { + /** + * Directories to recursively watch. + * + * The paths are relative to the project root. If omitted, SST watches the + * project root. Glob patterns are not supported here; use explicit + * directories. + */ + paths?: string[]; + /** + * Files or directories to ignore within the watched paths. + * + * Entries without a slash match names anywhere in the watched tree, like + * `.env` or `*.egg-info`. Entries with a slash match that subtree relative + * to the project root and can also point outside it, like + * `../external-package/dist`. + */ + ignore?: string[]; +} + export interface RunnerInput { /** * The stage the deployment will be run in. diff --git a/platform/src/global.d.ts b/platform/src/global.d.ts index 989505b9c7..3e7d663346 100644 --- a/platform/src/global.d.ts +++ b/platform/src/global.d.ts @@ -75,7 +75,7 @@ declare global { export const $linkable: typeof import("./components/link").Link.linkable; /** - * A convenience reference to the the [`util`](https://www.pulumi.com/docs/reference/pkg/nodejs/pulumi/pulumi/) module from Pulumi. + * A convenience reference to the [`util`](https://www.pulumi.com/docs/reference/pkg/nodejs/pulumi/pulumi/) module from Pulumi. * * This is useful for working with components. You can use these without importing or installing the Pulumi SDK. * @@ -322,7 +322,7 @@ declare global { * ```ts title="sst.config.ts" * $transform(sst.aws.Function, (args, opts, name) => { * // Set the default if it's not set by the component - * args.runtime ??= "nodejs20.x"; + * args.runtime ??= "nodejs24.x"; * }); * ``` * diff --git a/platform/src/runtime/worker/unenv.d.ts b/platform/src/runtime/worker/unenv.d.ts new file mode 100644 index 0000000000..48cd8b4564 --- /dev/null +++ b/platform/src/runtime/worker/unenv.d.ts @@ -0,0 +1,14 @@ +export interface CloudflareUnenvInput { + date?: string; + flags?: string[]; +} + +export interface CloudflareUnenvConfig { + alias: Record; + external: string[]; + polyfill: string[]; +} + +export function getCloudflareUnenvConfig( + input?: CloudflareUnenvInput, +): CloudflareUnenvConfig; diff --git a/platform/src/runtime/worker/unenv.mjs b/platform/src/runtime/worker/unenv.mjs new file mode 100644 index 0000000000..2a9544676f --- /dev/null +++ b/platform/src/runtime/worker/unenv.mjs @@ -0,0 +1,26 @@ +import { getCloudflarePreset } from "@cloudflare/unenv-preset"; +import { defineEnv } from "unenv"; +import { fileURLToPath } from "node:url"; + +export function getCloudflareUnenvConfig(input = {}) { + const { env } = defineEnv({ + npmShims: true, + presets: [ + getCloudflarePreset({ + compatibilityDate: input.date, + compatibilityFlags: input.flags, + }), + ], + }); + + return { + alias: env.alias, + external: env.external, + polyfill: env.polyfill, + }; +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const input = process.argv[2] ? JSON.parse(process.argv[2]) : {}; + process.stdout.write(JSON.stringify(getCloudflareUnenvConfig(input))); +} diff --git a/platform/src/scrap.ts b/platform/src/scrap.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/platform/src/shim/boot.js b/platform/src/shim/boot.js deleted file mode 100644 index 817f69b4ee..0000000000 --- a/platform/src/shim/boot.js +++ /dev/null @@ -1,3 +0,0 @@ -import { $config } from "../config"; - -export { $config as "$config" }; diff --git a/platform/src/shim/runtime.js b/platform/src/shim/runtime.js deleted file mode 100644 index bf6fce5052..0000000000 --- a/platform/src/shim/runtime.js +++ /dev/null @@ -1,8 +0,0 @@ -export const Resource = new Proxy($resource, { - get(target, prop) { - if (!(prop in target)) { - throw new Error(`"${prop}" is not linked`); - } - return target[prop]; - }, -}); diff --git a/platform/src/util/minify.ts b/platform/src/util/minify.ts new file mode 100644 index 0000000000..b9e399806e --- /dev/null +++ b/platform/src/util/minify.ts @@ -0,0 +1,10 @@ +import { transformSync } from "esbuild"; + +export function minify(strings: TemplateStringsArray, ...values: unknown[]) { + const code = String.raw(strings, ...values); + return transformSync(code, { + minifyWhitespace: true, + minifySyntax: true, + target: "es2018", + }).code; +} diff --git a/platform/src/util/package.ts b/platform/src/util/package.ts new file mode 100644 index 0000000000..2ef99faba7 --- /dev/null +++ b/platform/src/util/package.ts @@ -0,0 +1,29 @@ +import fs from "fs"; +import path from "path"; + +/** + * Gets the version of a package from the site's package.json. + * Checks both dependencies and devDependencies. + * + * @param sitePath - Path to the site directory containing package.json + * @param packageName - Name of the package to look up + * @returns The version string (e.g., "^6.0.0", "~5.2.0", "latest") or undefined if not found + */ +export function getPackageVersion( + sitePath: string, + packageName: string, +): string | undefined { + try { + const pkgPath = path.join(sitePath, "package.json"); + if (!fs.existsSync(pkgPath)) { + return undefined; + } + + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); + return ( + pkg.dependencies?.[packageName] ?? pkg.devDependencies?.[packageName] + ); + } catch { + return undefined; + } +} diff --git a/platform/src/util/path-to-regex.ts b/platform/src/util/path-to-regex.ts deleted file mode 100644 index 19ff577f1c..0000000000 --- a/platform/src/util/path-to-regex.ts +++ /dev/null @@ -1,625 +0,0 @@ -/** - * This file is adapted from pillarjs/path-to-regexp - Blake Embrey - * Source: https://github.com/pillarjs/path-to-regexp/blob/v6.2.1/src/index.ts - */ -/** - * Tokenizer results. - */ -interface LexToken { - type: - | "OPEN" - | "CLOSE" - | "PATTERN" - | "NAME" - | "CHAR" - | "ESCAPED_CHAR" - | "MODIFIER" - | "END"; - index: number; - value: string; -} - -/** - * Tokenize input string. - */ -function lexer(str: string): LexToken[] { - const tokens: LexToken[] = []; - let i = 0; - - while (i < str.length) { - const char = str[i]; - - if (char === "*" || char === "+" || char === "?") { - tokens.push({ type: "MODIFIER", index: i, value: str[i++] }); - continue; - } - - if (char === "\\") { - tokens.push({ type: "ESCAPED_CHAR", index: i++, value: str[i++] }); - continue; - } - - if (char === "{") { - tokens.push({ type: "OPEN", index: i, value: str[i++] }); - continue; - } - - if (char === "}") { - tokens.push({ type: "CLOSE", index: i, value: str[i++] }); - continue; - } - - if (char === ":") { - let name = ""; - let j = i + 1; - - while (j < str.length) { - const code = str.charCodeAt(j); - - if ( - // `0-9` - (code >= 48 && code <= 57) || - // `A-Z` - (code >= 65 && code <= 90) || - // `a-z` - (code >= 97 && code <= 122) || - // `_` - code === 95 - ) { - name += str[j++]; - continue; - } - - break; - } - - if (!name) throw new TypeError(`Missing parameter name at ${i}`); - - tokens.push({ type: "NAME", index: i, value: name }); - i = j; - continue; - } - - if (char === "(") { - let count = 1; - let pattern = ""; - let j = i + 1; - - if (str[j] === "?") { - throw new TypeError(`Pattern cannot start with "?" at ${j}`); - } - - while (j < str.length) { - if (str[j] === "\\") { - pattern += str[j++] + str[j++]; - continue; - } - - if (str[j] === ")") { - count--; - if (count === 0) { - j++; - break; - } - } else if (str[j] === "(") { - count++; - if (str[j + 1] !== "?") { - throw new TypeError(`Capturing groups are not allowed at ${j}`); - } - } - - pattern += str[j++]; - } - - if (count) throw new TypeError(`Unbalanced pattern at ${i}`); - if (!pattern) throw new TypeError(`Missing pattern at ${i}`); - - tokens.push({ type: "PATTERN", index: i, value: pattern }); - i = j; - continue; - } - - tokens.push({ type: "CHAR", index: i, value: str[i++] }); - } - - tokens.push({ type: "END", index: i, value: "" }); - - return tokens; -} - -export interface ParseOptions { - /** - * Set the default delimiter for repeat parameters. (default: `'/'`) - */ - delimiter?: string; - /** - * List of characters to automatically consider prefixes when parsing. - */ - prefixes?: string; -} - -/** - * Parse a string for the raw tokens. - */ -export function parse(str: string, options: ParseOptions = {}): Token[] { - const tokens = lexer(str); - const { prefixes = "./" } = options; - const defaultPattern = `[^${escapeString(options.delimiter || "/#?")}]+?`; - const result: Token[] = []; - let key = 0; - let i = 0; - let path = ""; - - const tryConsume = (type: LexToken["type"]): string | undefined => { - if (i < tokens.length && tokens[i].type === type) return tokens[i++].value; - }; - - const mustConsume = (type: LexToken["type"]): string => { - const value = tryConsume(type); - if (value !== undefined) return value; - const { type: nextType, index } = tokens[i]; - throw new TypeError(`Unexpected ${nextType} at ${index}, expected ${type}`); - }; - - const consumeText = (): string => { - let result = ""; - let value: string | undefined; - while ((value = tryConsume("CHAR") || tryConsume("ESCAPED_CHAR"))) { - result += value; - } - return result; - }; - - while (i < tokens.length) { - const char = tryConsume("CHAR"); - const name = tryConsume("NAME"); - const pattern = tryConsume("PATTERN"); - - if (name || pattern) { - let prefix = char || ""; - - if (prefixes.indexOf(prefix) === -1) { - path += prefix; - prefix = ""; - } - - if (path) { - result.push(path); - path = ""; - } - - result.push({ - name: name || key++, - prefix, - suffix: "", - pattern: pattern || defaultPattern, - modifier: tryConsume("MODIFIER") || "", - }); - continue; - } - - const value = char || tryConsume("ESCAPED_CHAR"); - if (value) { - path += value; - continue; - } - - if (path) { - result.push(path); - path = ""; - } - - const open = tryConsume("OPEN"); - if (open) { - const prefix = consumeText(); - const name = tryConsume("NAME") || ""; - const pattern = tryConsume("PATTERN") || ""; - const suffix = consumeText(); - - mustConsume("CLOSE"); - - result.push({ - name: name || (pattern ? key++ : ""), - pattern: name && !pattern ? defaultPattern : pattern, - prefix, - suffix, - modifier: tryConsume("MODIFIER") || "", - }); - continue; - } - - mustConsume("END"); - } - - return result; -} - -export interface TokensToFunctionOptions { - /** - * When `true` the regexp will be case sensitive. (default: `false`) - */ - sensitive?: boolean; - /** - * Function for encoding input strings for output. - */ - encode?: (value: string, token: Key) => string; - /** - * When `false` the function can produce an invalid (unmatched) path. (default: `true`) - */ - validate?: boolean; -} - -/** - * Compile a string to a template function for the path. - */ -export function compile

( - str: string, - options?: ParseOptions & TokensToFunctionOptions, -) { - return tokensToFunction

(parse(str, options), options); -} - -export type PathFunction

= (data?: P) => string; - -/** - * Expose a method for transforming tokens into the path function. - */ -export function tokensToFunction

( - tokens: Token[], - options: TokensToFunctionOptions = {}, -): PathFunction

{ - const reFlags = flags(options); - const { encode = (x: string) => x, validate = true } = options; - - // Compile all the tokens into regexps. - const matches = tokens.map((token) => { - if (typeof token === "object") { - return new RegExp(`^(?:${token.pattern})$`, reFlags); - } - }); - - return (data: Record | null | undefined) => { - let path = ""; - - for (let i = 0; i < tokens.length; i++) { - const token = tokens[i]; - - if (typeof token === "string") { - path += token; - continue; - } - - const value = data ? data[token.name] : undefined; - const optional = token.modifier === "?" || token.modifier === "*"; - const repeat = token.modifier === "*" || token.modifier === "+"; - - if (Array.isArray(value)) { - if (!repeat) { - throw new TypeError( - `Expected "${token.name}" to not repeat, but got an array`, - ); - } - - if (value.length === 0) { - if (optional) continue; - - throw new TypeError(`Expected "${token.name}" to not be empty`); - } - - for (let j = 0; j < value.length; j++) { - const segment = encode(value[j], token); - - if (validate && !(matches[i] as RegExp).test(segment)) { - throw new TypeError( - `Expected all "${token.name}" to match "${token.pattern}", but got "${segment}"`, - ); - } - - path += token.prefix + segment + token.suffix; - } - - continue; - } - - if (typeof value === "string" || typeof value === "number") { - const segment = encode(String(value), token); - - if (validate && !(matches[i] as RegExp).test(segment)) { - throw new TypeError( - `Expected "${token.name}" to match "${token.pattern}", but got "${segment}"`, - ); - } - - path += token.prefix + segment + token.suffix; - continue; - } - - if (optional) continue; - - const typeOfMessage = repeat ? "an array" : "a string"; - throw new TypeError(`Expected "${token.name}" to be ${typeOfMessage}`); - } - - return path; - }; -} - -export interface RegexpToFunctionOptions { - /** - * Function for decoding strings for params. - */ - decode?: (value: string, token: Key) => string; -} - -/** - * A match result contains data about the path match. - */ -export interface MatchResult

{ - path: string; - index: number; - params: P; -} - -/** - * A match is either `false` (no match) or a match result. - */ -export type Match

= false | MatchResult

; - -/** - * The match function takes a string and returns whether it matched the path. - */ -export type MatchFunction

= ( - path: string, -) => Match

; - -/** - * Create path match function from `path-to-regexp` spec. - */ -export function match

( - str: Path, - options?: ParseOptions & TokensToRegexpOptions & RegexpToFunctionOptions, -) { - const keys: Key[] = []; - const re = pathToRegexp(str, keys, options); - return regexpToFunction

(re, keys, options); -} - -/** - * Create a path match function from `path-to-regexp` output. - */ -export function regexpToFunction

( - re: RegExp, - keys: Key[], - options: RegexpToFunctionOptions = {}, -): MatchFunction

{ - const { decode = (x: string) => x } = options; - - return function (pathname: string) { - const m = re.exec(pathname); - if (!m) return false; - - const { 0: path, index } = m; - const params = Object.create(null); - - for (let i = 1; i < m.length; i++) { - if (m[i] === undefined) continue; - - const key = keys[i - 1]; - - if (key.modifier === "*" || key.modifier === "+") { - params[key.name] = m[i].split(key.prefix + key.suffix).map((value) => { - return decode(value, key); - }); - } else { - params[key.name] = decode(m[i], key); - } - } - - return { path, index, params }; - }; -} - -/** - * Escape a regular expression string. - */ -function escapeString(str: string) { - return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1"); -} - -/** - * Get the flags for a regexp from the options. - */ -function flags(options?: { sensitive?: boolean }) { - return options && options.sensitive ? "" : "i"; -} - -/** - * Metadata about a key. - */ -export interface Key { - name: string | number; - prefix: string; - suffix: string; - pattern: string; - modifier: string; -} - -/** - * A token is a string (nothing special) or key metadata (capture group). - */ -export type Token = string | Key; - -/** - * Pull out keys from a regexp. - */ -function regexpToRegexp(path: RegExp, keys?: Key[]): RegExp { - if (!keys) return path; - - const groupsRegex = /\((?:\?<(.*?)>)?(?!\?)/g; - - let index = 0; - let execResult = groupsRegex.exec(path.source); - while (execResult) { - keys.push({ - // Use parenthesized substring match if available, index otherwise - name: execResult[1] || index++, - prefix: "", - suffix: "", - modifier: "", - pattern: "", - }); - execResult = groupsRegex.exec(path.source); - } - - return path; -} - -/** - * Transform an array into a regexp. - */ -function arrayToRegexp( - paths: Array, - keys?: Key[], - options?: TokensToRegexpOptions & ParseOptions, -): RegExp { - const parts = paths.map((path) => pathToRegexp(path, keys, options).source); - return new RegExp(`(?:${parts.join("|")})`, flags(options)); -} - -/** - * Create a path regexp from string input. - */ -function stringToRegexp( - path: string, - keys?: Key[], - options?: TokensToRegexpOptions & ParseOptions, -) { - return tokensToRegexp(parse(path, options), keys, options); -} - -export interface TokensToRegexpOptions { - /** - * When `true` the regexp will be case sensitive. (default: `false`) - */ - sensitive?: boolean; - /** - * When `true` the regexp won't allow an optional trailing delimiter to match. (default: `false`) - */ - strict?: boolean; - /** - * When `true` the regexp will match to the end of the string. (default: `true`) - */ - end?: boolean; - /** - * When `true` the regexp will match from the beginning of the string. (default: `true`) - */ - start?: boolean; - /** - * Sets the final character for non-ending optimistic matches. (default: `/`) - */ - delimiter?: string; - /** - * List of characters that can also be "end" characters. - */ - endsWith?: string; - /** - * Encode path tokens for use in the `RegExp`. - */ - encode?: (value: string) => string; -} - -/** - * Expose a function for taking tokens and returning a RegExp. - */ -export function tokensToRegexp( - tokens: Token[], - keys?: Key[], - options: TokensToRegexpOptions = {}, -) { - const { - strict = false, - start = true, - end = true, - encode = (x: string) => x, - delimiter = "/#?", - endsWith = "", - } = options; - const endsWithRe = `[${escapeString(endsWith)}]|$`; - const delimiterRe = `[${escapeString(delimiter)}]`; - let route = start ? "^" : ""; - - // Iterate over the tokens and create our regexp string. - for (const token of tokens) { - if (typeof token === "string") { - route += escapeString(encode(token)); - } else { - const prefix = escapeString(encode(token.prefix)); - const suffix = escapeString(encode(token.suffix)); - - if (token.pattern) { - if (keys) keys.push(token); - - if (prefix || suffix) { - if (token.modifier === "+" || token.modifier === "*") { - const mod = token.modifier === "*" ? "?" : ""; - route += `(?:${prefix}((?:${token.pattern})(?:${suffix}${prefix}(?:${token.pattern}))*)${suffix})${mod}`; - } else { - route += `(?:${prefix}(${token.pattern})${suffix})${token.modifier}`; - } - } else { - if (token.modifier === "+" || token.modifier === "*") { - route += `((?:${token.pattern})${token.modifier})`; - } else { - route += `(${token.pattern})${token.modifier}`; - } - } - } else { - route += `(?:${prefix}${suffix})${token.modifier}`; - } - } - } - - if (end) { - if (!strict) route += `${delimiterRe}?`; - - route += !options.endsWith ? "$" : `(?=${endsWithRe})`; - } else { - const endToken = tokens[tokens.length - 1]; - const isEndDelimited = - typeof endToken === "string" - ? delimiterRe.indexOf(endToken[endToken.length - 1]) > -1 - : endToken === undefined; - - if (!strict) { - route += `(?:${delimiterRe}(?=${endsWithRe}))?`; - } - - if (!isEndDelimited) { - route += `(?=${delimiterRe}|${endsWithRe})`; - } - } - - return new RegExp(route, flags(options)); -} - -/** - * Supported `path-to-regexp` input types. - */ -export type Path = string | RegExp | Array; - -/** - * Normalize the given path string, returning a regular expression. - * - * An empty array can be passed in for the keys, which will hold the - * placeholder key descriptions. For example, using `/user/:id`, `keys` will - * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`. - */ -export function pathToRegexp( - path: Path, - keys?: Key[], - options?: TokensToRegexpOptions & ParseOptions, -) { - if (path instanceof RegExp) return regexpToRegexp(path, keys); - if (Array.isArray(path)) return arrayToRegexp(path, keys, options); - return stringToRegexp(path, keys, options); -} diff --git a/platform/support/bridge-task/.gitignore b/platform/support/bridge-task/.gitignore new file mode 100644 index 0000000000..567609b123 --- /dev/null +++ b/platform/support/bridge-task/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/platform/support/bridge-task/Dockerfile b/platform/support/bridge-task/Dockerfile index fc6a3132b5..f6afe255f8 100644 --- a/platform/support/bridge-task/Dockerfile +++ b/platform/support/bridge-task/Dockerfile @@ -1,13 +1,6 @@ -FROM golang:1.23.4 AS builder -WORKDIR /app -COPY go.mod go.sum ./ -RUN go mod download -COPY . . -ARG TARGETARCH -RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -o main - FROM alpine LABEL org.opencontainers.image.source=https://github.com/sst/sst WORKDIR /app -COPY --from=builder /app/main /app/main +ARG TARGETARCH +COPY ${TARGETARCH}/main /app/main CMD ["/app/main"] diff --git a/platform/test/ast/add.test.ts b/platform/test/ast/add.test.ts new file mode 100644 index 0000000000..b2151d1c33 --- /dev/null +++ b/platform/test/ast/add.test.ts @@ -0,0 +1,125 @@ +import { describe, it, expect } from "vitest"; +import { execFileSync } from "child_process"; +import fs from "fs"; +import path from "path"; +import os from "os"; + +const ADD_SCRIPT = path.resolve(__dirname, "../../src/ast/add.mjs"); +const PROVIDER = "grafana"; +const PKG = "@pulumiverse/grafana"; +const VERSION = "0.0.1"; + +function run(config: string) { + const tmp = path.join(os.tmpdir(), `sst-add-test-${Date.now()}.ts`); + fs.writeFileSync(tmp, config); + execFileSync("node", [ADD_SCRIPT, tmp, PROVIDER, VERSION, PKG]); + const result = fs.readFileSync(tmp, "utf-8"); + fs.unlinkSync(tmp); + return result; +} + +describe("add provider", () => { + it("method declaration", () => { + const result = run(`export default $config({ + app(input) { + return { + name: "my-app", + providers: {}, + }; + }, +});`); + expect(result).toContain(`${PROVIDER}: {`); + expect(result).toContain(`package: "${PKG}"`); + expect(result).toContain(`version: "${VERSION}"`); + }); + + it("arrow function with block body", () => { + const result = run(`export default $config({ + app: (input) => { + return { + name: "my-app", + providers: {}, + }; + }, +});`); + expect(result).toContain(`${PROVIDER}: {`); + expect(result).toContain(`package: "${PKG}"`); + expect(result).toContain(`version: "${VERSION}"`); + }); + + it("arrow function with concise body", () => { + const result = run(`export default $config({ + app: (input) => ({ + name: "my-app", + providers: {}, + }), +});`); + expect(result).toContain(`${PROVIDER}: {`); + expect(result).toContain(`package: "${PKG}"`); + expect(result).toContain(`version: "${VERSION}"`); + }); + + it("function expression", () => { + const result = run(`export default $config({ + app: function(input) { + return { + name: "my-app", + providers: {}, + }; + }, +});`); + expect(result).toContain(`${PROVIDER}: {`); + expect(result).toContain(`package: "${PKG}"`); + expect(result).toContain(`version: "${VERSION}"`); + }); + + it("adds providers key when missing", () => { + const result = run(`export default $config({ + app(input) { + return { + name: "my-app", + }; + }, +});`); + expect(result).toContain("providers"); + expect(result).toContain(`${PROVIDER}: {`); + expect(result).toContain(`package: "${PKG}"`); + expect(result).toContain(`version: "${VERSION}"`); + }); + + it("adds package to existing string provider", () => { + const config = `export default $config({ + app(input) { + return { + name: "my-app", + providers: { "${PROVIDER}": "0.0.0" }, + }; + }, +});`; + const result = run(config); + expect(result).toContain(`${PROVIDER}: {`); + expect(result).toContain(`package: "${PKG}"`); + expect(result).toContain(`version: "0.0.0"`); + }); + + it("adds package to existing object provider", () => { + const config = `export default $config({ + app(input) { + return { + name: "my-app", + providers: { + "${PROVIDER}": { + version: "0.0.0", + region: "us-east-1", + }, + }, + }; + }, +});`; + const result = run(config); + expect(result).toContain(`${PROVIDER}: {`); + expect(result).toContain(`package: "${PKG}"`); + expect(result).toContain(`version: "0.0.0"`); + expect(result).toContain(`region: "us-east-1"`); + }); +}); diff --git a/platform/test/components/alb.test.ts b/platform/test/components/alb.test.ts new file mode 100644 index 0000000000..8d1c0905ce --- /dev/null +++ b/platform/test/components/alb.test.ts @@ -0,0 +1,190 @@ +import { describe, beforeAll, it, expect } from "vitest"; +import "../../src/global.d.ts"; +import * as pulumi from "@pulumi/pulumi"; + +// @ts-ignore +global.$app = { + name: "app", + stage: "test", +}; +global.$util = pulumi; + +pulumi.runtime.setMocks( + { + newResource: function (args: pulumi.runtime.MockResourceArgs): { + id: string; + state: any; + } { + return { + id: args.inputs.name + "_id", + state: { + ...args.inputs, + arn: `arn:aws:elasticloadbalancing:us-east-1:123456789:${args.type}/${args.inputs.name}`, + dnsName: `${args.inputs.name}.us-east-1.elb.amazonaws.com`, + zoneId: "Z1234567890", + vpcId: "vpc-mock-id", + securityGroups: ["sg-mock-id"], + }, + }; + }, + call: function (args: pulumi.runtime.MockCallArgs) { + // Mock lb.getListener β€” return a fake ARN so Listener.get works + if (args.token === "aws:alb/getListener:getListener") { + return { + arn: `arn:aws:elasticloadbalancing:us-east-1:123456789:listener/app/mock/${args.inputs.port}`, + ...args.inputs, + }; + } + return args.inputs; + }, + }, + "project", + "stack", + false, +); + +describe("Alb", function () { + let Alb: typeof import("../../src/components/aws/alb").Alb; + + beforeAll(async function () { + Alb = (await import("../../src/components/aws/alb")).Alb; + }); + + describe("#constructor", () => { + it("creates load balancer with correct type", async () => { + const alb = new Alb("TestAlb", { + vpc: { + id: "vpc-123", + publicSubnets: ["subnet-a", "subnet-b"], + privateSubnets: ["subnet-c", "subnet-d"], + }, + listeners: [{ port: 80, protocol: "http" }], + }); + + pulumi.all([alb.arn]).apply(([arn]) => { + expect(arn).toBeDefined(); + }); + }); + + it("exposes url without domain", async () => { + const alb = new Alb("TestAlbUrl", { + vpc: { + id: "vpc-123", + publicSubnets: ["subnet-a", "subnet-b"], + privateSubnets: ["subnet-c", "subnet-d"], + }, + listeners: [{ port: 80, protocol: "http" }], + }); + + pulumi.all([alb.url]).apply(([url]) => { + expect(url).toMatch(/^http:\/\//); + }); + }); + + it("exposes security group ID", async () => { + const alb = new Alb("TestAlbSg", { + vpc: { + id: "vpc-123", + publicSubnets: ["subnet-a", "subnet-b"], + privateSubnets: ["subnet-c", "subnet-d"], + }, + listeners: [{ port: 80, protocol: "http" }], + }); + + pulumi.all([alb.securityGroupId]).apply(([sgId]) => { + expect(sgId).toBeDefined(); + }); + }); + + it("exposes nodes with loadBalancer, securityGroup, listeners", async () => { + const alb = new Alb("TestAlbNodes", { + vpc: { + id: "vpc-123", + publicSubnets: ["subnet-a", "subnet-b"], + privateSubnets: ["subnet-c", "subnet-d"], + }, + listeners: [{ port: 80, protocol: "http" }], + }); + + expect(alb.nodes.loadBalancer).toBeDefined(); + expect(alb.nodes.securityGroup).toBeDefined(); + expect(alb.nodes.listeners).toBeDefined(); + }); + }); + + describe("#getListener", () => { + it("throws for non-existent listener on non-ref ALB", async () => { + const alb = new Alb("TestAlbMissing", { + vpc: { + id: "vpc-123", + publicSubnets: ["subnet-a", "subnet-b"], + privateSubnets: ["subnet-c", "subnet-d"], + }, + listeners: [{ port: 80, protocol: "http" }], + }); + + expect(() => alb.getListener("https", 443)).toThrow( + /Listener "HTTPS:443" not found/, + ); + }); + + it("error message includes ALB name", async () => { + const alb = new Alb("MyNamedAlb", { + vpc: { + id: "vpc-123", + publicSubnets: ["subnet-a", "subnet-b"], + privateSubnets: ["subnet-c", "subnet-d"], + }, + listeners: [{ port: 80, protocol: "http" }], + }); + + expect(() => alb.getListener("https", 443)).toThrow(/MyNamedAlb/); + }); + }); + + describe(".get() reference", () => { + it("creates a referenced ALB from ARN", async () => { + const alb = Alb.get( + "RefAlb", + "arn:aws:elasticloadbalancing:us-east-1:123456789:loadbalancer/app/my-alb/abc123", + ); + + pulumi.all([alb.arn]).apply(([arn]) => { + expect(arn).toBeDefined(); + }); + }); + + it("lazy discovers listener via getListener on ref ALB", async () => { + const alb = Alb.get( + "RefAlbListener", + "arn:aws:elasticloadbalancing:us-east-1:123456789:loadbalancer/app/my-alb/abc123", + ); + + // Should not throw β€” lazy discovery kicks in + const listener = alb.getListener("http", 80); + expect(listener).toBeDefined(); + }); + + it("caches discovered listeners on subsequent calls", async () => { + const alb = Alb.get( + "RefAlbCache", + "arn:aws:elasticloadbalancing:us-east-1:123456789:loadbalancer/app/my-alb/abc123", + ); + + const listener1 = alb.getListener("http", 80); + const listener2 = alb.getListener("http", 80); + expect(listener1).toBe(listener2); + }); + + it("discovers different listeners independently", async () => { + const alb = Alb.get( + "RefAlbMulti", + "arn:aws:elasticloadbalancing:us-east-1:123456789:loadbalancer/app/my-alb/abc123", + ); + + const http = alb.getListener("http", 80); + const https = alb.getListener("https", 443); + expect(http).not.toBe(https); + }); + }); +}); diff --git a/platform/test/components/arn.test.ts b/platform/test/components/arn.test.ts new file mode 100644 index 0000000000..a484be221e --- /dev/null +++ b/platform/test/components/arn.test.ts @@ -0,0 +1,327 @@ +import { describe, it, expect } from "vitest"; +import { + parseFunctionArn, + splitQualifiedFunctionArn, + parseBucketArn, + parseTopicArn, + parseQueueArn, + parseDynamoArn, + parseDynamoStreamArn, + parseKinesisStreamArn, + parseEventBusArn, + parseRoleArn, + parseLambdaEdgeArn, + parseElasticSearch, + parseOpenSearch, + parseDsqlPublicEndpoint, + parseDsqlPrivateEndpoint, +} from "../../src/components/aws/helpers/arn"; + +describe("parseFunctionArn", () => { + it("parses unqualified ARN", () => { + expect( + parseFunctionArn( + "arn:aws:lambda:us-east-1:123456789012:function:my-func", + ), + ).toEqual({ functionName: "my-func" }); + }); + + it("parses qualified ARN (returns function name without qualifier)", () => { + expect( + parseFunctionArn( + "arn:aws:lambda:us-east-1:123456789012:function:my-func:live", + ), + ).toEqual({ functionName: "my-func" }); + }); + + it("throws on invalid ARN", () => { + expect(() => parseFunctionArn("not-an-arn")).toThrow(); + }); +}); + +describe("splitQualifiedFunctionArn", () => { + it("returns unqualified ARN as-is with no qualifier", () => { + const arn = "arn:aws:lambda:us-east-1:123456789012:function:my-func"; + expect(splitQualifiedFunctionArn(arn)).toEqual({ + unqualifiedArn: arn, + qualifier: undefined, + }); + }); + + it("splits qualified ARN with alias", () => { + expect( + splitQualifiedFunctionArn( + "arn:aws:lambda:us-east-1:123456789012:function:my-func:live", + ), + ).toEqual({ + unqualifiedArn: + "arn:aws:lambda:us-east-1:123456789012:function:my-func", + qualifier: "live", + }); + }); + + it("splits qualified ARN with version number", () => { + expect( + splitQualifiedFunctionArn( + "arn:aws:lambda:us-east-1:123456789012:function:my-func:42", + ), + ).toEqual({ + unqualifiedArn: + "arn:aws:lambda:us-east-1:123456789012:function:my-func", + qualifier: "42", + }); + }); + + it("splits qualified ARN with $LATEST", () => { + expect( + splitQualifiedFunctionArn( + "arn:aws:lambda:us-east-1:123456789012:function:my-func:$LATEST", + ), + ).toEqual({ + unqualifiedArn: + "arn:aws:lambda:us-east-1:123456789012:function:my-func", + qualifier: "$LATEST", + }); + }); +}); + +describe("parseBucketArn", () => { + it("parses valid S3 ARN", () => { + expect(parseBucketArn("arn:aws:s3:::my-bucket")).toEqual({ + bucketName: "my-bucket", + }); + }); + + it("throws on invalid ARN", () => { + expect(() => parseBucketArn("not-an-arn")).toThrow(); + }); +}); + +describe("parseTopicArn", () => { + it("parses valid SNS ARN", () => { + expect( + parseTopicArn("arn:aws:sns:us-east-1:123456789012:my-topic"), + ).toEqual({ topicName: "my-topic" }); + }); + + it("throws on invalid ARN", () => { + expect(() => parseTopicArn("not-an-arn")).toThrow(); + }); +}); + +describe("parseQueueArn", () => { + it("parses valid SQS ARN", () => { + const result = parseQueueArn( + "arn:aws:sqs:us-east-1:123456789012:my-queue", + ); + expect(result.queueName).toBe("my-queue"); + expect(result.queueUrl).toBe( + "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue", + ); + }); + + it("throws on invalid ARN", () => { + expect(() => parseQueueArn("not-an-arn")).toThrow(); + }); +}); + +describe("parseDynamoArn", () => { + it("parses valid DynamoDB ARN", () => { + expect( + parseDynamoArn( + "arn:aws:dynamodb:us-east-1:123456789012:table/MyTable", + ), + ).toEqual({ tableName: "MyTable" }); + }); + + it("throws on invalid ARN", () => { + expect(() => parseDynamoArn("not-an-arn")).toThrow(); + }); +}); + +describe("parseDynamoStreamArn", () => { + it("parses valid DynamoDB stream ARN", () => { + expect( + parseDynamoStreamArn( + "arn:aws:dynamodb:us-east-1:123456789012:table/MyTable/stream/2024-02-25T23:17:55.264", + ), + ).toEqual({ tableName: "MyTable" }); + }); + + it("throws on invalid ARN", () => { + expect(() => parseDynamoStreamArn("not-an-arn")).toThrow(); + }); + + it("throws on wrong service", () => { + expect(() => + parseDynamoStreamArn("arn:aws:kinesis:us-east-1:123456789012:stream/s"), + ).toThrow(); + }); +}); + +describe("parseKinesisStreamArn", () => { + it("parses valid Kinesis stream ARN", () => { + expect( + parseKinesisStreamArn( + "arn:aws:kinesis:us-east-1:123456789012:stream/MyStream", + ), + ).toEqual({ streamName: "MyStream" }); + }); + + it("throws on invalid ARN", () => { + expect(() => parseKinesisStreamArn("not-an-arn")).toThrow(); + }); + + it("throws on wrong service", () => { + expect(() => + parseKinesisStreamArn( + "arn:aws:dynamodb:us-east-1:123456789012:table/MyTable", + ), + ).toThrow(); + }); +}); + +describe("parseEventBusArn", () => { + it("parses valid EventBridge ARN", () => { + expect( + parseEventBusArn( + "arn:aws:events:us-east-1:123456789012:event-bus/my-bus", + ), + ).toEqual({ busName: "my-bus" }); + }); + + it("throws on invalid ARN", () => { + expect(() => parseEventBusArn("not-an-arn")).toThrow(); + }); +}); + +describe("parseRoleArn", () => { + it("parses valid IAM role ARN", () => { + expect( + parseRoleArn("arn:aws:iam::123456789012:role/MyRole"), + ).toEqual({ roleName: "MyRole" }); + }); + + it("throws on invalid ARN", () => { + expect(() => parseRoleArn("not-an-arn")).toThrow(); + }); +}); + +describe("parseLambdaEdgeArn", () => { + it("parses valid Lambda@Edge ARN", () => { + expect( + parseLambdaEdgeArn( + "arn:aws:lambda:us-east-1:123456789012:function:my-func:1", + ), + ).toEqual({ functionName: "my-func", region: "us-east-1", version: "1" }); + }); + + it("throws on wrong region", () => { + expect(() => + parseLambdaEdgeArn( + "arn:aws:lambda:eu-west-1:123456789012:function:my-func:1", + ), + ).toThrow(/us-east-1/); + }); + + it("throws on unqualified ARN (no version)", () => { + expect(() => + parseLambdaEdgeArn( + "arn:aws:lambda:us-east-1:123456789012:function:my-func", + ), + ).toThrow(/qualified/); + }); + + it("throws on $LATEST", () => { + expect(() => + parseLambdaEdgeArn( + "arn:aws:lambda:us-east-1:123456789012:function:my-func:$LATEST", + ), + ).toThrow(/qualified/); + }); +}); + +describe("parseElasticSearch", () => { + it("parses valid ElasticSearch domain ARN", () => { + expect( + parseElasticSearch( + "arn:aws:es:us-east-1:123456789012:domain/my-domain", + ), + ).toEqual({ tableName: "my-domain" }); + }); + + it("throws on invalid ARN", () => { + expect(() => parseElasticSearch("not-an-arn")).toThrow(); + }); +}); + +describe("parseOpenSearch", () => { + it("parses valid OpenSearch domain ARN", () => { + expect( + parseOpenSearch( + "arn:aws:opensearch:us-east-1:123456789012:domain/my-domain", + ), + ).toEqual({ tableName: "my-domain" }); + }); + + it("throws on invalid ARN", () => { + expect(() => parseOpenSearch("not-an-arn")).toThrow(); + }); +}); + +describe("parseDsqlPublicEndpoint", () => { + it("generates public endpoint from cluster ARN", () => { + expect( + parseDsqlPublicEndpoint( + "arn:aws:dsql:us-east-1:123456789012:cluster/abc123def456", + ), + ).toBe("abc123def456.dsql.us-east-1.on.aws"); + }); + + it("handles different regions", () => { + expect( + parseDsqlPublicEndpoint( + "arn:aws:dsql:eu-west-1:123456789012:cluster/mycluster", + ), + ).toBe("mycluster.dsql.eu-west-1.on.aws"); + }); + + it("throws on invalid ARN", () => { + expect(() => parseDsqlPublicEndpoint("not-an-arn")).toThrow(); + }); +}); + +describe("parseDsqlPrivateEndpoint", () => { + it("replaces wildcard with cluster ID", () => { + const clusterArn = + "arn:aws:dsql:us-east-1:123456789012:cluster/abc123def456"; + const dnsEntries = [ + { dnsName: "*.dsql-q73m.us-east-1.on.aws" }, + { dnsName: "vpce-123.dsql-q73m.us-east-1.on.aws" }, + ]; + expect(parseDsqlPrivateEndpoint(clusterArn, dnsEntries)).toBe( + "abc123def456.dsql-q73m.us-east-1.on.aws", + ); + }); + + it("uses first DNS entry when no wildcard", () => { + const clusterArn = + "arn:aws:dsql:us-east-1:123456789012:cluster/abc123def456"; + const dnsEntries = [{ dnsName: "vpce-123.dsql-q73m.us-east-1.on.aws" }]; + expect(parseDsqlPrivateEndpoint(clusterArn, dnsEntries)).toBe( + "vpce-123.dsql-q73m.us-east-1.on.aws", + ); + }); + + it("throws on invalid ARN", () => { + expect(() => + parseDsqlPrivateEndpoint("not-an-arn", [{ dnsName: "test" }]), + ).toThrow(); + }); + + it("throws on empty DNS entries", () => { + const clusterArn = + "arn:aws:dsql:us-east-1:123456789012:cluster/abc123def456"; + expect(() => parseDsqlPrivateEndpoint(clusterArn, [])).toThrow(); + }); +}); diff --git a/platform/test/components/cloudfront.test.ts b/platform/test/components/cloudfront.test.ts new file mode 100644 index 0000000000..61a9b2402e --- /dev/null +++ b/platform/test/components/cloudfront.test.ts @@ -0,0 +1,468 @@ +import { createRequire } from "node:module"; +import { readFileSync } from "node:fs"; +import vm from "node:vm"; +import { describe, expect, it } from "vitest"; + +import { CF_ROUTER_INJECTION } from "../../src/components/aws/router"; + +const require = createRequire(import.meta.url); +const routerSource = readFileSync( + new URL("../../src/components/aws/router.ts", import.meta.url), + "utf8", +); + +type CloudFrontField = { + value?: string; + multiValue?: { value: string }[]; +}; + +type CloudFrontRequest = { + uri: string; + headers: Record; + cookies: Record; + querystring: Record; + origin?: unknown; +}; + +type CloudFrontEvent = { + request: CloudFrontRequest; +}; + +type TestContext = vm.Context & { + Promise: PromiseConstructor; + Math: Math; + JSON: JSON; + RegExp: RegExpConstructor; + decodeURIComponent: typeof decodeURIComponent; + encodeURIComponent: typeof encodeURIComponent; + event: CloudFrontEvent; + cf: { + kvs: () => { + get: (key: string) => Promise; + }; + updateRequestOrigin: (origin: unknown) => void; + }; + require: NodeRequire; + __routeSite?: ( + kvNamespace: string, + metadata: Record, + ) => Promise; + __getRequestHeaderSize?: () => number; + __matchRoute?: (routes: string[]) => Promise; + __handler?: (event: CloudFrontEvent) => Promise; +}; + +type CreateContextInput = { + uri: string; + headers: Record; + cookies?: Record; + querystring?: Record; + kvGet?: (key: string) => Promise; + updateRequestOrigin?: (origin: unknown, event: CloudFrontEvent) => void; +}; + +function extractTemplateCode( + start: string, + end: string, + context: Record = {}, +) { + const startIndex = routerSource.indexOf(start); + const endIndex = routerSource.indexOf(end, startIndex); + + if (startIndex === -1 || endIndex === -1) { + throw new Error(`Failed to extract code between ${start} and ${end}`); + } + + return vm.runInNewContext(`\`${routerSource.slice(startIndex, endIndex)}\``, context); +} + +const MATCH_ROUTE_CODE = extractTemplateCode( + "async function matchRoute(routes) {", + "\n\n // Look up the route", +); + +const REQUEST_HANDLER_CODE = extractTemplateCode( + "async function handler(event) {\n ${userInjection}\n ${blockCloudfrontUrlInjection}\n ${CF_ROUTER_INJECTION}\n", + "\n}`,\n },\n { parent: self },\n );", + { + userInjection: "", + blockCloudfrontUrlInjection: "", + CF_ROUTER_INJECTION, + kvNamespace: "router", + }, +); + +function createContext(input: CreateContextInput) { + const event: CloudFrontEvent = { + request: { + uri: input.uri, + headers: input.headers, + cookies: input.cookies ?? {}, + querystring: input.querystring ?? {}, + }, + }; + + const context: TestContext = { + Promise, + Math, + JSON, + RegExp, + decodeURIComponent, + encodeURIComponent, + event, + cf: { + kvs: () => ({ + get: input.kvGet ?? (async () => { + throw new Error("missing"); + }), + }), + updateRequestOrigin(origin: any) { + input.updateRequestOrigin?.(origin, event); + }, + }, + require, + }; + + vm.createContext(context); + return { context, event }; +} + +function loadRouteSite(input: { + uri: string; + headers: Record; + cookies?: Record; + querystring?: Record; +}) { + const { context, event } = createContext(input); + + new vm.Script( + `${CF_ROUTER_INJECTION.replace( + /async function routeSite\(([^)]*)\)\{/, + "async function routeSite($1){globalThis.__getRequestHeaderSize=getRequestHeaderSize;", + )};globalThis.__routeSite = routeSite;`, + ).runInContext(context); + + return { + event, + routeSite: context.__routeSite!, + getRequestHeaderSize: () => context.__getRequestHeaderSize!(), + }; +} + +function loadRouteMatcher(input: { + uri: string; + headers: Record; + metadata: Record; +}) { + const { context } = createContext({ + ...input, + kvGet: async (key: string) => { + if (!key.endsWith(":metadata")) throw new Error("missing"); + const routeNs = key.slice(0, -":metadata".length); + const metadata = input.metadata[routeNs]; + if (!metadata) throw new Error("missing"); + return JSON.stringify(metadata); + }, + }); + + new vm.Script(`${MATCH_ROUTE_CODE};globalThis.__matchRoute = matchRoute;`).runInContext( + context, + ); + + return context.__matchRoute!; +} + +function loadHandler(input: { + uri: string; + headers: Record; + routes: string[]; + metadata: Record; + cookies?: Record; + querystring?: Record; +}) { + const { context, event } = createContext({ + ...input, + kvGet: async (key: string) => { + if (key === "router:routes") return JSON.stringify(input.routes); + if (!key.endsWith(":metadata")) throw new Error("missing"); + + const routeNs = key.slice(0, -":metadata".length); + const metadata = input.metadata[routeNs]; + if (!metadata) throw new Error("missing"); + return JSON.stringify(metadata); + }, + updateRequestOrigin(origin, currentEvent) { + currentEvent.request.origin = origin; + }, + }); + + new vm.Script(`${REQUEST_HANDLER_CODE} +} +globalThis.__handler = handler;`).runInContext( + context, + ); + + return { + event, + handler: context.__handler!, + }; +} + +async function selectRoute(requestUri: string, routePaths: string[]) { + const metadata = Object.fromEntries( + routePaths.map((path, index) => [`route${index}`, { path }]), + ); + + const matchRoute = loadRouteMatcher({ + uri: requestUri, + headers: { + host: { value: "example.com" }, + }, + metadata, + }); + + const match = await matchRoute( + routePaths.map((path, index) => `url,route${index},,${path}`), + ); + + return match?.metadata.path; +} + +async function pathMatches(requestUri: string, routePath: string) { + return (await selectRoute(requestUri, [routePath])) === routePath; +} + +describe("CloudFront router", () => { + describe("path matching", () => { + it("matches exact paths", async () => { + expect(await pathMatches("/api", "/api")).toBe(true); + expect(await pathMatches("/travel-plan", "/travel-plan")).toBe(true); + expect(await pathMatches("/", "/")).toBe(true); + }); + + it("matches paths followed by slashes", async () => { + expect(await pathMatches("/api/", "/api")).toBe(true); + expect(await pathMatches("/api/users", "/api")).toBe(true); + expect(await pathMatches("/api/users/123", "/api")).toBe(true); + }); + + it("matches nested paths", async () => { + expect(await pathMatches("/travel-plan/abc", "/travel-plan")).toBe( + true, + ); + expect(await pathMatches("/travel-plan/abc/def", "/travel-plan")).toBe( + true, + ); + expect(await pathMatches("/uploads/file.txt", "/uploads")).toBe(true); + }); + + it("treats root as catch-all", async () => { + expect(await pathMatches("/", "/")).toBe(true); + expect(await pathMatches("/anything", "/")).toBe(true); + expect(await pathMatches("/foo/bar", "/")).toBe(true); + expect(await pathMatches("/api", "/")).toBe(true); + }); + + it("does not match non-segment continuations", async () => { + expect(await pathMatches("/api-docs", "/api")).toBe(false); + expect(await pathMatches("/apiv2", "/api")).toBe(false); + expect(await pathMatches("/travel-plans", "/travel-plan")).toBe(false); + expect(await pathMatches("/travel-planning", "/travel-plan")).toBe( + false, + ); + }); + + it("does not match different paths", async () => { + expect(await pathMatches("/users", "/api")).toBe(false); + expect(await pathMatches("/v2/api", "/api")).toBe(false); + expect(await pathMatches("/files", "/uploads")).toBe(false); + }); + + it("does not match shorter paths", async () => { + expect(await pathMatches("/ap", "/api")).toBe(false); + expect(await pathMatches("/a", "/api")).toBe(false); + }); + + it("handles trailing slash routes", async () => { + expect(await pathMatches("/api/", "/api")).toBe(true); + expect(await pathMatches("/api/users", "/api/")).toBe(true); + expect(await pathMatches("/public/2025-11/image.jpg", "/public/")).toBe( + true, + ); + expect(await pathMatches("/public/", "/public/")).toBe(true); + expect(await pathMatches("/publicfile", "/public/")).toBe(false); + }); + + it("handles special characters", async () => { + expect(await pathMatches("/api/users-list", "/api")).toBe(true); + expect(await pathMatches("/api_v2", "/api")).toBe(false); + expect(await pathMatches("/api.json", "/api")).toBe(false); + }); + + it("handles deeply nested paths", async () => { + expect(await pathMatches("/a/b/c/d/e/f", "/a")).toBe(true); + expect(await pathMatches("/a/b/c/d/e/f", "/a/b")).toBe(true); + expect(await pathMatches("/a/b/c/d/e/f", "/a/b/c")).toBe(true); + }); + + it("handles real-world boundary cases", async () => { + expect(await pathMatches("/travel-plan/abc123", "/travel-plan")).toBe( + true, + ); + expect(await pathMatches("/travel-plans", "/travel-plan")).toBe(false); + expect(await pathMatches("/travel-plans/123", "/travel-plan")).toBe( + false, + ); + expect(await pathMatches("/v1/users", "/v1")).toBe(true); + expect(await pathMatches("/v1-beta", "/v1")).toBe(false); + expect(await pathMatches("/v1-beta/users", "/v1")).toBe(false); + expect(await pathMatches("/uploads/file.pdf", "/uploads")).toBe(true); + expect(await pathMatches("/uploads-backup", "/uploads")).toBe(false); + expect(await pathMatches("/uploads-backup/file.pdf", "/uploads")).toBe( + false, + ); + }); + + it("picks the longest matching path", async () => { + expect(await selectRoute("/api/users", ["/api", "/api-docs"])).toBe( + "/api", + ); + expect(await selectRoute("/api-docs/intro", ["/api", "/api-docs"])).toBe( + "/api-docs", + ); + expect( + await selectRoute("/api/v2/users", ["/api", "/api/v2", "/api/v2/users", "/api/v3"]), + ).toBe("/api/v2/users"); + }); + }); + + describe("header sizing", () => { + it("counts request headers and cookies", async () => { + const { getRequestHeaderSize, routeSite } = loadRouteSite({ + uri: "/", + headers: { + host: { value: "example.com" }, + accept: { value: "text/html" }, + }, + cookies: { + session: { value: "abc" }, + theme: { value: "dark" }, + }, + }); + + await routeSite("test", {}); + expect(getRequestHeaderSize()).toBe(71); + }); + + it("counts multi-value headers and cookies", async () => { + const { getRequestHeaderSize, routeSite } = loadRouteSite({ + uri: "/", + headers: { + accept: { + multiValue: [{ value: "text/html" }, { value: "application/json" }], + }, + }, + cookies: { + session: { + multiValue: [{ value: "abc" }, { value: "def" }], + }, + }, + }); + + await routeSite("test", {}); + expect(getRequestHeaderSize()).toBe(79); + }); + + it("returns 431 for oversized image requests", async () => { + const { routeSite } = loadRouteSite({ + uri: "/_next/image", + headers: { + host: { value: "example.com" }, + accept: { value: "image/webp" }, + }, + cookies: { + session: { value: "x".repeat(10000) }, + }, + }); + + const response = await routeSite("test", { + image: { route: "/_next/image", host: "image.example.com" }, + }); + + expect(response.statusCode).toBe(431); + expect(response.statusDescription).toBe("Request Header Fields Too Large"); + expect(response.body.data).toContain("Reduce cookie size"); + }); + + it("returns 431 for oversized server requests", async () => { + const { routeSite } = loadRouteSite({ + uri: "/", + headers: { + host: { value: "example.com" }, + }, + cookies: { + session: { value: "x".repeat(10000) }, + }, + }); + + const response = await routeSite("test", { + servers: [["server.example.com", 0, 0]], + origin: {}, + }); + + expect(response.statusCode).toBe(431); + expect(response.statusDescription).toBe("Request Header Fields Too Large"); + expect(response.body.data).toContain("Reduce cookie size"); + }); + + it("returns 431 for oversized image requests through handler", async () => { + const { event, handler } = loadHandler({ + uri: "/_next/image", + headers: { + host: { value: "example.com" }, + accept: { value: "image/webp" }, + }, + cookies: { + session: { value: "x".repeat(10000) }, + }, + routes: ["site,route0,,/"], + metadata: { + route0: { + image: { route: "/_next/image", host: "image.example.com" }, + }, + }, + }); + + const response = await handler(event); + + expect(response.statusCode).toBe(431); + expect(response.statusDescription).toBe("Request Header Fields Too Large"); + expect(response.body.data).toContain("Reduce cookie size"); + }); + + it("returns 431 for oversized server requests through handler", async () => { + const { event, handler } = loadHandler({ + uri: "/", + headers: { + host: { value: "example.com" }, + }, + cookies: { + session: { value: "x".repeat(10000) }, + }, + routes: ["site,route0,,/"], + metadata: { + route0: { + servers: [["server.example.com", 0, 0]], + origin: {}, + }, + }, + }); + + const response = await handler(event); + + expect(response.statusCode).toBe(431); + expect(response.statusDescription).toBe("Request Header Fields Too Large"); + expect(response.body.data).toContain("Reduce cookie size"); + }); + }); +}); diff --git a/platform/test/components/function-builder.test.ts b/platform/test/components/function-builder.test.ts new file mode 100644 index 0000000000..94c2c5f8cf --- /dev/null +++ b/platform/test/components/function-builder.test.ts @@ -0,0 +1,111 @@ +import * as pulumi from "@pulumi/pulumi"; +import { describe, expect, it } from "vitest"; +import { Function } from "../../src/components/aws/function"; +import { functionBuilder } from "../../src/components/aws/helpers/function-builder"; + +async function resolveOutputs(values: T) { + return await new Promise((resolve, reject) => { + pulumi.all(values as unknown as any[]).apply((result) => { + try { + resolve(result as unknown as T); + } catch (error) { + reject(error); + } + }); + }); +} + +function createMockFunction({ + durable, + publish, +}: { + durable: boolean; + publish: boolean; +}) { + const fn = Object.create(Function.prototype) as Function; + Object.assign(fn as any, { + durable, + function: pulumi.output({ + arn: "arn:aws:lambda:us-east-1:123456789012:function:my-func", + qualifiedArn: + "arn:aws:lambda:us-east-1:123456789012:function:my-func:$LATEST", + invokeArn: + "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:my-func/invocations", + qualifiedInvokeArn: + "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:my-func:$LATEST/invocations", + responseStreamingInvokeArn: + "arn:aws:apigateway:us-east-1:lambda:path/2021-11-15/functions/arn:aws:lambda:us-east-1:123456789012:function:my-func/response-streaming-invocations", + publish, + }), + }); + return fn; +} + +describe("Function targets", () => { + it("keeps arn raw and exposes qualified durable targets", async () => { + const fn = createMockFunction({ durable: true, publish: false }); + const [arn, targetArn, qualifier, targetInvokeArn, targetResponse] = + await resolveOutputs([ + fn.arn, + fn.targetArn, + fn.qualifier, + fn.targetInvokeArn, + fn.targetResponseStreamingInvokeArn, + ] as const); + + expect({ arn, targetArn, qualifier, targetInvokeArn, targetResponse }).toEqual( + { + arn: "arn:aws:lambda:us-east-1:123456789012:function:my-func", + targetArn: + "arn:aws:lambda:us-east-1:123456789012:function:my-func:$LATEST", + qualifier: "$LATEST", + targetInvokeArn: + "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:my-func:$LATEST/invocations", + targetResponse: + "arn:aws:apigateway:us-east-1:lambda:path/2021-11-15/functions/arn:aws:lambda:us-east-1:123456789012:function:my-func:$LATEST/response-streaming-invocations", + }, + ); + }); +}); + +describe("Function reset", () => { + it("clears the dev bridge cache before a new program run", () => { + const cache = (Function as any).devBridgeCode(); + cache.set("us-east-1:bridge", Promise.resolve({})); + + Function.reset(); + + expect(cache.size).toBe(0); + }); +}); + +describe("functionBuilder", () => { + it("normalizes raw qualified function arns", async () => { + const builder = functionBuilder( + "MyFunction", + "arn:aws:lambda:us-east-1:123456789012:function:my-func:live", + {}, + ); + const [arn, targetArn, qualifier, targetInvokeArn, targetResponse] = + await resolveOutputs([ + builder.arn, + builder.targetArn, + builder.qualifier, + builder.targetInvokeArn, + builder.targetResponseStreamingInvokeArn, + ] as const); + + expect({ arn, targetArn, qualifier, targetInvokeArn, targetResponse }).toEqual( + { + arn: "arn:aws:lambda:us-east-1:123456789012:function:my-func", + targetArn: + "arn:aws:lambda:us-east-1:123456789012:function:my-func:live", + qualifier: "live", + targetInvokeArn: + "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:my-func:live/invocations", + targetResponse: + "arn:aws:apigateway:us-east-1:lambda:path/2021-11-15/functions/arn:aws:lambda:us-east-1:123456789012:function:my-func:live/response-streaming-invocations", + }, + ); + }); +}); diff --git a/platform/test/components/link.test.ts b/platform/test/components/link.test.ts new file mode 100644 index 0000000000..7b7d45e0b8 --- /dev/null +++ b/platform/test/components/link.test.ts @@ -0,0 +1,64 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import * as pulumi from "@pulumi/pulumi"; + +// @ts-ignore +global.$app = { + name: "app", + stage: "test", +}; +// @ts-ignore +global.$util = pulumi; + +pulumi.runtime.setMocks( + { + newResource: function (args: pulumi.runtime.MockResourceArgs): { + id: string; + state: any; + } { + return { + id: args.name + "_id", + state: args.inputs, + }; + }, + call: function (args: pulumi.runtime.MockCallArgs) { + return args.inputs; + }, + }, + "project", + "stack", + false, +); + +describe("Link", () => { + let Secret: typeof import("../../src/components/secret").Secret; + let Link: typeof import("../../src/components/link").Link; + + function resolveOutput(value: pulumi.Output) { + return new Promise((resolve) => { + value.apply((resolved) => { + resolve(resolved); + return resolved; + }); + }); + } + + beforeAll(async () => { + Secret = (await import("../../src/components/secret")).Secret; + Link = (await import("../../src/components/link")).Link; + }); + + it("normalizes type in build output", async () => { + const secret = new Secret("MySecret", "test"); + const built = await resolveOutput(pulumi.output(Link.build([secret]))); + + expect(built[0].name).toBe("MySecret"); + expect(built[0].properties.type).toBe("sst.sst.Secret"); + }); + + it("normalizes type in env properties", async () => { + const secret = new Secret("MyOtherSecret", "test"); + const properties = await resolveOutput(Link.getProperties([secret])); + + expect(properties.MyOtherSecret.type).toBe("sst.sst.Secret"); + }); +}); diff --git a/platform/test/components/router-path-matching.test.ts b/platform/test/components/router-path-matching.test.ts deleted file mode 100644 index f220a43db9..0000000000 --- a/platform/test/components/router-path-matching.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { describe, it, expect } from "vitest"; - -/** - * Tests for Router path matching logic - * - * The Router CloudFront function uses path matching to route requests. - * This test verifies the path segment boundary logic to ensure routes - * only match at proper path boundaries. - */ -describe("Router path matching", () => { - /** - * Simulates the path matching logic from the CloudFront function - * This is extracted from platform/src/components/aws/router.ts - */ - function pathMatches(requestUri: string, routePath: string): boolean { - return ( - requestUri.startsWith(routePath) && - (requestUri === routePath || - routePath.endsWith("/") || - requestUri[routePath.length] === "/" || - routePath === "/") - ); - } - - describe("exact matches", () => { - it("should match exact path", () => { - expect(pathMatches("/api", "/api")).toBe(true); - expect(pathMatches("/travel-plan", "/travel-plan")).toBe(true); - expect(pathMatches("/", "/")).toBe(true); - }); - }); - - describe("path segment matches", () => { - it("should match when followed by slash", () => { - expect(pathMatches("/api/", "/api")).toBe(true); - expect(pathMatches("/api/users", "/api")).toBe(true); - expect(pathMatches("/api/users/123", "/api")).toBe(true); - }); - - it("should match nested paths", () => { - expect(pathMatches("/travel-plan/abc", "/travel-plan")).toBe(true); - expect(pathMatches("/travel-plan/abc/def", "/travel-plan")).toBe(true); - expect(pathMatches("/uploads/file.txt", "/uploads")).toBe(true); - }); - }); - - describe("root path catch-all", () => { - it("should match any path for root route", () => { - expect(pathMatches("/", "/")).toBe(true); - expect(pathMatches("/anything", "/")).toBe(true); - expect(pathMatches("/foo/bar", "/")).toBe(true); - expect(pathMatches("/api", "/")).toBe(true); - }); - }); - - describe("non-matches - path boundaries", () => { - it("should NOT match when path continues without slash", () => { - expect(pathMatches("/api-docs", "/api")).toBe(false); - expect(pathMatches("/apiv2", "/api")).toBe(false); - expect(pathMatches("/travel-plans", "/travel-plan")).toBe(false); - expect(pathMatches("/travel-planning", "/travel-plan")).toBe(false); - }); - - it("should NOT match different paths", () => { - expect(pathMatches("/users", "/api")).toBe(false); - expect(pathMatches("/v2/api", "/api")).toBe(false); - expect(pathMatches("/files", "/uploads")).toBe(false); - }); - - it("should NOT match shorter paths", () => { - expect(pathMatches("/ap", "/api")).toBe(false); - expect(pathMatches("/a", "/api")).toBe(false); - }); - }); - - describe("edge cases", () => { - it("should handle trailing slashes correctly", () => { - expect(pathMatches("/api/", "/api")).toBe(true); - expect(pathMatches("/api/users", "/api/")).toBe(true); - }); - - it("should handle special characters in paths", () => { - expect(pathMatches("/api/users-list", "/api")).toBe(true); - expect(pathMatches("/api_v2", "/api")).toBe(false); - expect(pathMatches("/api.json", "/api")).toBe(false); - }); - - it("should handle deeply nested paths", () => { - expect(pathMatches("/a/b/c/d/e/f", "/a")).toBe(true); - expect(pathMatches("/a/b/c/d/e/f", "/a/b")).toBe(true); - expect(pathMatches("/a/b/c/d/e/f", "/a/b/c")).toBe(true); - }); - }); - - describe("real-world scenarios", () => { - it("should correctly route travel-plan vs travel-plans", () => { - // This is the bug that was fixed - expect(pathMatches("/travel-plan/abc123", "/travel-plan")).toBe(true); - expect(pathMatches("/travel-plans", "/travel-plan")).toBe(false); - expect(pathMatches("/travel-plans/123", "/travel-plan")).toBe(false); - }); - - it("should correctly route API paths", () => { - expect(pathMatches("/v1/users", "/v1")).toBe(true); - expect(pathMatches("/v1-beta", "/v1")).toBe(false); - expect(pathMatches("/v1-beta/users", "/v1")).toBe(false); - }); - - it("should correctly route file uploads", () => { - expect(pathMatches("/uploads/file.pdf", "/uploads")).toBe(true); - expect(pathMatches("/uploads-backup", "/uploads")).toBe(false); - expect(pathMatches("/uploads-backup/file.pdf", "/uploads")).toBe(false); - }); - - it("should handle multiple similar routes", () => { - // When you have /api and /api-docs as different routes - // /api/users should only match /api - expect(pathMatches("/api/users", "/api")).toBe(true); - expect(pathMatches("/api/users", "/api-docs")).toBe(false); - - // /api-docs/intro should only match /api-docs - expect(pathMatches("/api-docs/intro", "/api")).toBe(false); - expect(pathMatches("/api-docs/intro", "/api-docs")).toBe(true); - }); - }); - - describe("trailing-slash routes", () => { - it("should match subpaths under trailing-slash route", () => { - expect(pathMatches("/public/2025-11/image.jpg", "/public/")).toBe(true); - expect(pathMatches("/public/", "/public/")).toBe(true); - }); - - it("should NOT match non-segment continuations", () => { - expect(pathMatches("/publicfile", "/public/")).toBe(false); - }); - }); - - describe("priority testing (longest match)", () => { - it("should support longest path matching", () => { - // When multiple routes could match, the Router picks the longest - // These tests verify our logic supports that behavior - const uri = "/api/v2/users"; - - expect(pathMatches(uri, "/api")).toBe(true); - expect(pathMatches(uri, "/api/v2")).toBe(true); - expect(pathMatches(uri, "/api/v2/users")).toBe(true); - expect(pathMatches(uri, "/api/v3")).toBe(false); - }); - }); -}); diff --git a/platform/test/components/router-waf.test.ts b/platform/test/components/router-waf.test.ts new file mode 100644 index 0000000000..5e8eb46cda --- /dev/null +++ b/platform/test/components/router-waf.test.ts @@ -0,0 +1,218 @@ +import { describe, beforeAll, beforeEach, it, expect } from "vitest"; +import * as pulumi from "@pulumi/pulumi"; + +// Suppress Pulumi "Trace events are unavailable" errors in test environment +process.on("unhandledRejection", (err: any) => { + if (err?.code === "ERR_TRACE_EVENTS_UNAVAILABLE") return; + throw err; +}); + +// @ts-ignore +global.$app = { + name: "app", + stage: "test", +}; +global.$util = pulumi; + +interface CreatedResource { + type: string; + name: string; + inputs: any; +} + +let createdResources: CreatedResource[] = []; + +pulumi.runtime.setMocks( + { + newResource: function (args: pulumi.runtime.MockResourceArgs): { + id: string; + state: any; + } { + createdResources.push({ + type: args.type, + name: args.name, + inputs: args.inputs, + }); + const arn = + args.type === "aws:wafv2/webAcl:WebAcl" + ? "arn:aws:wafv2:us-east-1:123456789012:global/webacl/test/abc-123" + : `arn:aws:mock:us-east-1:123456789012:${args.name}`; + return { + id: args.inputs.name + "_id", + state: { ...args.inputs, arn }, + }; + }, + call: function (args: pulumi.runtime.MockCallArgs) { + return args.inputs; + }, + }, + "project", + "stack", + false, +); + +const TYPES = { + webAcl: "aws:wafv2/webAcl:WebAcl", + logGroup: "aws:cloudwatch/logGroup:LogGroup", + loggingConfig: + "aws:wafv2/webAclLoggingConfiguration:WebAclLoggingConfiguration", +}; + +function findResources(type: string) { + return createdResources.filter((r) => r.type === type); +} + +// Flush event loop to let Pulumi .apply() chains settle without wall-clock delays +async function settle() { + for (let i = 0; i < 50; i++) { + await new Promise((resolve) => setImmediate(resolve)); + } +} + +describe("Router WAF Logging", function () { + let Router: typeof import("./../../src/components/aws/router").Router; + + beforeAll(async function () { + Router = (await import("./../../src/components/aws/router")).Router; + }); + + beforeEach(function () { + createdResources = []; + }); + + describe("no WAF configured", () => { + it("does not create WAF or logging resources", async () => { + new Router("NoWaf"); + await settle(); + expect(findResources(TYPES.webAcl)).toHaveLength(0); + expect(findResources(TYPES.logGroup)).toHaveLength(0); + expect(findResources(TYPES.loggingConfig)).toHaveLength(0); + }); + }); + + describe("WAF without logging", () => { + it("creates WebAcl but no logging resources", async () => { + new Router("WafNoLog", { waf: true }); + await settle(); + expect(findResources(TYPES.webAcl)).toHaveLength(1); + expect(findResources(TYPES.logGroup)).toHaveLength(0); + expect(findResources(TYPES.loggingConfig)).toHaveLength(0); + }); + }); + + describe("WAF with logging: true", () => { + it("creates WebAcl, LogGroup, and LoggingConfiguration", async () => { + new Router("WafLogTrue", { waf: { logging: true } }); + await settle(); + expect(findResources(TYPES.webAcl)).toHaveLength(1); + expect(findResources(TYPES.logGroup)).toHaveLength(1); + expect(findResources(TYPES.loggingConfig)).toHaveLength(1); + }); + + it("LogGroup name starts with aws-waf-logs-", async () => { + new Router("WafLogName", { waf: { logging: true } }); + await settle(); + const logGroups = findResources(TYPES.logGroup); + expect(logGroups[0].inputs.name).toMatch(/^aws-waf-logs-/); + }); + + it("LogGroup retention defaults to 30 days (1 month)", async () => { + new Router("WafLogRet", { waf: { logging: true } }); + await settle(); + const logGroups = findResources(TYPES.logGroup); + expect(logGroups[0].inputs.retentionInDays).toBe(30); + }); + + it("applies default PII redaction (queryString + cookie/authorization)", async () => { + new Router("WafLogRedact", { waf: { logging: true } }); + await settle(); + const configs = findResources(TYPES.loggingConfig); + expect(configs).toHaveLength(1); + const redacted = configs[0].inputs.redactedFields; + expect(redacted).toBeDefined(); + expect(redacted).toContainEqual({ queryString: {} }); + expect(redacted).toContainEqual({ + singleHeader: { name: "cookie" }, + }); + expect(redacted).toContainEqual({ + singleHeader: { name: "authorization" }, + }); + }); + }); + + describe("logging filter", () => { + it("include: 'blocked' sets DROP default with KEEP for BLOCK", async () => { + new Router("WafFiltered", { + waf: { logging: { include: "blocked" } }, + }); + await settle(); + const configs = findResources(TYPES.loggingConfig); + expect(configs).toHaveLength(1); + const filter = configs[0].inputs.loggingFilter; + expect(filter).toBeDefined(); + expect(filter.defaultBehavior).toBe("DROP"); + expect(filter.filters[0].behavior).toBe("KEEP"); + expect(filter.filters[0].conditions[0].actionCondition.action).toBe( + "BLOCK", + ); + }); + + it("include: 'all' does not set a logging filter", async () => { + new Router("WafAll", { + waf: { logging: { include: "all" } }, + }); + await settle(); + const configs = findResources(TYPES.loggingConfig); + expect(configs).toHaveLength(1); + expect(configs[0].inputs.loggingFilter).toBeUndefined(); + }); + }); + + describe("retention", () => { + it("custom retention is applied", async () => { + new Router("WafRet3m", { + waf: { logging: { retention: "3 months" } }, + }); + await settle(); + const logGroups = findResources(TYPES.logGroup); + expect(logGroups[0].inputs.retentionInDays).toBe(90); + }); + }); + + describe("redact", () => { + it("redact: false disables all redaction", async () => { + new Router("WafNoRedact", { + waf: { logging: { redact: false } }, + }); + await settle(); + const configs = findResources(TYPES.loggingConfig); + expect(configs).toHaveLength(1); + expect(configs[0].inputs.redactedFields).toBeUndefined(); + }); + + it("custom redact fields are applied", async () => { + new Router("WafCustomRedact", { + waf: { + logging: { + redact: { + uriPath: true, + method: true, + headers: ["x-api-key"], + }, + }, + }, + }); + await settle(); + const configs = findResources(TYPES.loggingConfig); + expect(configs).toHaveLength(1); + const redacted = configs[0].inputs.redactedFields; + expect(redacted).toContainEqual({ method: {} }); + expect(redacted).toContainEqual({ uriPath: {} }); + expect(redacted).toContainEqual({ + singleHeader: { name: "x-api-key" }, + }); + // queryString not set, should not be in the list + expect(redacted).not.toContainEqual({ queryString: {} }); + }); + }); +}); diff --git a/platform/test/components/service-alb.test.ts b/platform/test/components/service-alb.test.ts new file mode 100644 index 0000000000..2a08450708 --- /dev/null +++ b/platform/test/components/service-alb.test.ts @@ -0,0 +1,379 @@ +import { describe, beforeAll, it, expect } from "vitest"; +import "../../src/global.d.ts"; +import * as pulumi from "@pulumi/pulumi"; +import fs from "fs"; +import path from "path"; + +// Create temp directories for Docker context resolution in fargate.ts +const testRoot = path.join("/tmp", "sst-test-" + process.pid); +for (const dir of [".", "api", "worker"]) { + fs.mkdirSync(path.join(testRoot, dir), { recursive: true }); +} + +// @ts-ignore +global.$app = { + name: "app", + stage: "test", +}; +global.$util = pulumi; +// @ts-ignore β€” Service checks $dev to decide dev mode +global.$dev = false; +// @ts-ignore β€” Cluster reads $cli.state.version, fargate reads $cli.paths.root +global.$cli = { + state: { version: {} }, + paths: { root: testRoot }, +}; +// @ts-ignore β€” fargate.ts uses $jsonStringify for container definitions +global.$jsonStringify = JSON.stringify; +// Suppress SST_SERVER RPC calls during tests +process.env.SST_SERVER = "http://localhost:13557"; + +const createdResources: pulumi.runtime.MockResourceArgs[] = []; + +pulumi.runtime.setMocks( + { + newResource: function (args: pulumi.runtime.MockResourceArgs): { + id: string; + state: any; + } { + createdResources.push(args); + return { + id: args.inputs.name + "_id", + state: { + ...args.inputs, + arn: `arn:aws:mock:us-east-1:123456789:${args.type}/${args.inputs.name}`, + dnsName: `${args.inputs.name}.us-east-1.elb.amazonaws.com`, + zoneId: "Z1234567890", + name: args.inputs.name || args.name, + status: "ACTIVE", + tags: { + "sst:ref:version": "1", + "sst:ref:sg": "sg-mock-id", + "sst:ref:vpc-id": "vpc-123", + }, + }, + }; + }, + call: function (args: pulumi.runtime.MockCallArgs) { + if (args.token === "aws:alb/getListener:getListener") { + return { + arn: `arn:aws:elasticloadbalancing:us-east-1:123456789:listener/app/mock/${args.inputs.port}`, + ...args.inputs, + }; + } + if (args.token === "aws:index/getRegion:getRegion") { + return { name: "us-east-1", description: "US East (N. Virginia)" }; + } + return args.inputs; + }, + }, + "project", + "stack", + false, +); + +describe("Service with external ALB", function () { + let Service: typeof import("../../src/components/aws/service").Service; + let Alb: typeof import("../../src/components/aws/alb").Alb; + let Cluster: typeof import("../../src/components/aws/cluster").Cluster; + + // Shared fixtures + let cluster: InstanceType; + let alb: InstanceType; + + beforeAll(async function () { + Alb = (await import("../../src/components/aws/alb")).Alb; + Cluster = (await import("../../src/components/aws/cluster")).Cluster; + Service = (await import("../../src/components/aws/service")).Service; + + cluster = new Cluster("TestCluster", { + vpc: { + id: "vpc-123", + securityGroups: ["sg-123"], + containerSubnets: ["subnet-a", "subnet-b"], + loadBalancerSubnets: ["subnet-a", "subnet-b"], + }, + }); + + alb = new Alb("TestAlb", { + vpc: { + id: "vpc-123", + publicSubnets: ["subnet-a", "subnet-b"], + privateSubnets: ["subnet-c", "subnet-d"], + }, + listeners: [{ port: 80, protocol: "http" }], + }); + }); + + describe("detectAlbAttachment", () => { + it("creates service with external ALB (happy path)", async () => { + const service = new Service("AlbService", { + cluster, + image: { context: "." }, + loadBalancer: { + instance: alb, + rules: [ + { + listen: "80/http", + forward: "3000/http", + conditions: { path: "/api/*" }, + priority: 100, + }, + ], + }, + }); + + // Service should use the ALB's URL + pulumi.all([service.url]).apply(([url]) => { + expect(url).toBeDefined(); + }); + }); + + it("creates service with standard loadBalancer (no ALB instance)", async () => { + const service = new Service("InlineService", { + cluster, + image: { context: "." }, + loadBalancer: { + ports: [{ listen: "80/http", forward: "3000/http" }], + }, + }); + + // Should still create successfully with inline LB + expect(service).toBeDefined(); + }); + + it("ignores non-Alb instance and falls through to standard loadBalancer", async () => { + // A plain object with `instance` that is NOT an Alb should not + // trigger the external ALB path β€” it falls through to the standard + // loadBalancer handling (which requires `ports`). + const fakeAlb = { instance: { notAnAlb: true } }; + + const service = new Service("NonAlbService", { + cluster, + image: { context: "." }, + loadBalancer: { + ...(fakeAlb as any), + ports: [{ listen: "80/http", forward: "3000/http" }], + }, + }); + + expect(service).toBeDefined(); + }); + }); + + // Note: Empty rules, out-of-range priorities, duplicate priorities, and + // missing conditions now throw synchronously. Container name validation + // still fires inside .apply() (async Pulumi resolution) since container + // names are resolved asynchronously. + + describe("validation: priority", () => { + it("creates rules with valid priority", async () => { + const service = new Service("ValidPriorityService", { + cluster, + image: { context: "." }, + loadBalancer: { + instance: alb, + rules: [ + { + listen: "80/http", + forward: "3000/http", + conditions: { path: "/valid/*" }, + priority: 100, + }, + ], + }, + }); + + expect(service).toBeDefined(); + }); + + it("creates rules with multiple priorities on same listener", async () => { + const service = new Service("MultiPriorityService", { + cluster, + image: { context: "." }, + loadBalancer: { + instance: alb, + rules: [ + { + listen: "80/http", + forward: "3000/http", + conditions: { path: "/api/*" }, + priority: 100, + }, + { + listen: "80/http", + forward: "3000/http", + conditions: { path: "/health" }, + priority: 200, + }, + ], + }, + }); + + expect(service).toBeDefined(); + }); + }); + + describe("health check defaults", () => { + it("creates target group without explicit health config", async () => { + createdResources.length = 0; + + new Service("NoHealthService", { + cluster, + image: { context: "." }, + loadBalancer: { + instance: alb, + rules: [ + { + listen: "80/http", + forward: "3000/http", + conditions: { path: "/no-health/*" }, + priority: 300, + }, + ], + }, + }); + + // Target groups are now created synchronously (plain loop, no apply) + expect(true).toBe(true); // Construction didn't throw + }); + + it("creates target group with explicit health config", async () => { + const service = new Service("HealthService", { + cluster, + image: { context: "." }, + loadBalancer: { + instance: alb, + rules: [ + { + listen: "80/http", + forward: "3000/http", + conditions: { path: "/with-health/*" }, + priority: 400, + }, + ], + health: { + "3000/http": { + path: "/health", + interval: "30 seconds", + timeout: "5 seconds", + healthyThreshold: 3, + unhealthyThreshold: 2, + successCodes: "200-299", + }, + }, + }, + }); + + expect(service).toBeDefined(); + }); + }); + + describe("multiple containers", () => { + it("creates service with container field in rules", async () => { + const service = new Service("MultiContainerService", { + cluster, + containers: [ + { + name: "api", + image: { context: "./api" }, + }, + { + name: "worker", + image: { context: "./worker" }, + }, + ], + loadBalancer: { + instance: alb, + rules: [ + { + listen: "80/http", + forward: "3000/http", + container: "api", + conditions: { path: "/api/*" }, + priority: 500, + }, + ], + }, + }); + + expect(service).toBeDefined(); + }); + }); + + describe("VPC validation", () => { + it("verifies ALB and cluster VPCs can be compared", async () => { + const mismatchedAlb = new Alb("MismatchedAlb", { + vpc: { + id: "vpc-999", + publicSubnets: ["subnet-x"], + privateSubnets: ["subnet-y"], + }, + listeners: [{ port: 80, protocol: "http" }], + }); + + // Verify the ALB and cluster expose different VPC IDs. + // The actual VisibleError is thrown inside .apply() at deploy time + // and cannot be caught synchronously in unit tests. + const [albVpcId, clusterVpcId] = await new Promise<[string, string]>( + (resolve) => { + pulumi + .all([mismatchedAlb._vpc, pulumi.output("vpc-123")]) + .apply(([a, b]) => resolve([a, b])); + }, + ); + + expect(albVpcId).toBe("vpc-999"); + expect(clusterVpcId).toBe("vpc-123"); + expect(albVpcId).not.toBe(clusterVpcId); + }); + + it("passes when ALB and cluster share the same VPC", async () => { + // alb fixture uses vpc-123, same as the cluster + const service = new Service("SameVpcService", { + cluster, + image: { context: "." }, + loadBalancer: { + instance: alb, + rules: [ + { + listen: "80/http", + forward: "3000/http", + conditions: { path: "/same-vpc/*" }, + priority: 700, + }, + ], + }, + }); + + expect(service).toBeDefined(); + }); + }); + + describe("Alb.get() with Service", () => { + it("creates service attached to a referenced ALB", async () => { + const refAlb = Alb.get( + "RefAlbForService", + "arn:aws:elasticloadbalancing:us-east-1:123456789:loadbalancer/app/my-alb/abc123", + ); + + const service = new Service("RefAlbService", { + cluster, + image: { context: "." }, + loadBalancer: { + instance: refAlb, + rules: [ + { + listen: "80/http", + forward: "3000/http", + conditions: { path: "/ref/*" }, + priority: 600, + }, + ], + }, + }); + + expect(service).toBeDefined(); + }); + }); +}); diff --git a/platform/test/runtime/worker-unenv.test.ts b/platform/test/runtime/worker-unenv.test.ts new file mode 100644 index 0000000000..59381cce5e --- /dev/null +++ b/platform/test/runtime/worker-unenv.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import { getCloudflareUnenvConfig } from "../../src/runtime/worker/unenv"; + +describe("getCloudflareUnenvConfig", () => { + it("keeps http shimmed before native compat", () => { + const config = getCloudflareUnenvConfig({ + date: "2025-05-05", + flags: ["nodejs_compat"], + }); + + expect(config.external).not.toContain("http"); + expect(config.external).not.toContain("node:http"); + expect(config.external).not.toContain("https"); + expect(config.external).not.toContain("node:https"); + }); + + it("externalizes http client modules when enabled by flag", () => { + const config = getCloudflareUnenvConfig({ + date: "2025-05-05", + flags: ["nodejs_compat", "enable_nodejs_http_modules"], + }); + + expect(config.external).toContain("http"); + expect(config.external).toContain("node:http"); + expect(config.external).toContain("https"); + expect(config.external).toContain("node:https"); + expect(config.external).toContain("_http_agent"); + expect(config.external).toContain("node:_http_agent"); + expect(config.external).not.toContain("_http_server"); + expect(config.external).not.toContain("node:_http_server"); + }); + + it("externalizes http server modules once the compat date enables them", () => { + const config = getCloudflareUnenvConfig({ + date: "2025-09-01", + flags: ["nodejs_compat"], + }); + + expect(config.external).toContain("_http_server"); + expect(config.external).toContain("node:_http_server"); + }); + + it("lets disable flags win over compat date defaults", () => { + const config = getCloudflareUnenvConfig({ + date: "2026-02-05", + flags: ["nodejs_compat", "disable_nodejs_http_modules"], + }); + + expect(config.external).not.toContain("http"); + expect(config.external).not.toContain("node:http"); + expect(config.external).not.toContain("https"); + expect(config.external).not.toContain("node:https"); + expect(config.external).not.toContain("_http_server"); + expect(config.external).not.toContain("node:_http_server"); + }); +}); diff --git a/sdk/golang/resource/resource.go b/sdk/golang/resource/resource.go index 6a358aab43..edc3686a0d 100644 --- a/sdk/golang/resource/resource.go +++ b/sdk/golang/resource/resource.go @@ -13,24 +13,31 @@ import ( var resources map[string]any func init() { + loadFromDisk() + loadFromEnv() +} + +func loadFromDisk() { key, err := base64.StdEncoding.DecodeString(os.Getenv("SST_KEY")) if err != nil { - panic(err) + resources = make(map[string]any) + return } encryptedData, err := os.ReadFile(os.Getenv("SST_KEY_FILE")) if err != nil { resources = make(map[string]any) - keys() return } nonce := make([]byte, 12) block, err := aes.NewCipher(key) if err != nil { - panic(err) + resources = make(map[string]any) + return } aesGCM, err := cipher.NewGCM(block) if err != nil { - panic(err) + resources = make(map[string]any) + return } // Split the auth tag and ciphertext @@ -44,15 +51,15 @@ func init() { // Decrypt decrypted, err := aesGCM.Open(nil, nonce, ciphertextWithTag, nil) if err != nil { - panic(err) + resources = make(map[string]any) + return } // Parse JSON if err := json.Unmarshal(decrypted, &resources); err != nil { - panic(err) + resources = make(map[string]any) + return } - - keys() } var ErrNotFound = errors.New("not found") @@ -80,7 +87,7 @@ func get(input any, path ...string) (any, error) { return get(next, path[1:]...) } -func keys() { +func loadFromEnv() { for _, item := range os.Environ() { pair := strings.SplitN(item, "=", 2) key := pair[0] @@ -94,4 +101,14 @@ func keys() { resources[strings.TrimPrefix(key, "SST_RESOURCE_")] = result } } + + // Load consolidated resources JSON (used on Windows to avoid uppercasing) + if consolidated := os.Getenv("SST_RESOURCES_JSON"); consolidated != "" { + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(consolidated), &parsed); err == nil { + for k, v := range parsed { + resources[k] = v + } + } + } } diff --git a/sdk/golang/resource/resource_test.go b/sdk/golang/resource/resource_test.go new file mode 100644 index 0000000000..b5814f1bdb --- /dev/null +++ b/sdk/golang/resource/resource_test.go @@ -0,0 +1,105 @@ +package resource + +import ( + "os" + "testing" +) + +func resetResources() { + resources = make(map[string]any) +} + +func TestKeys_LoadsSSTResourcesJSON(t *testing.T) { + resetResources() + os.Setenv("SST_RESOURCES_JSON", `{"MyBucket":{"name":"my-bucket"},"App":{"name":"app","stage":"dev"}}`) + defer os.Unsetenv("SST_RESOURCES_JSON") + + loadFromEnv() + + val, err := Get("MyBucket", "name") + if err != nil { + t.Fatalf("expected MyBucket.name to exist: %v", err) + } + if val != "my-bucket" { + t.Fatalf("expected MyBucket.name = my-bucket, got %v", val) + } +} + +func TestKeys_MergesSSTResourcesJSONWithIndividualVars(t *testing.T) { + resetResources() + os.Setenv("SST_RESOURCE_MyTable", `{"name":"my-table"}`) + os.Setenv("SST_RESOURCES_JSON", `{"MyBucket":{"name":"my-bucket"}}`) + defer os.Unsetenv("SST_RESOURCE_MyTable") + defer os.Unsetenv("SST_RESOURCES_JSON") + + loadFromEnv() + + val1, err := Get("MyTable", "name") + if err != nil { + t.Fatalf("expected MyTable.name to exist: %v", err) + } + if val1 != "my-table" { + t.Fatalf("expected MyTable.name = my-table, got %v", val1) + } + + val2, err := Get("MyBucket", "name") + if err != nil { + t.Fatalf("expected MyBucket.name to exist: %v", err) + } + if val2 != "my-bucket" { + t.Fatalf("expected MyBucket.name = my-bucket, got %v", val2) + } +} + +func TestKeys_SSTResourcesJSONOverridesIndividualVars(t *testing.T) { + resetResources() + os.Setenv("SST_RESOURCE_MyBucket", `{"name":"from-env-var"}`) + os.Setenv("SST_RESOURCES_JSON", `{"MyBucket":{"name":"from-json"}}`) + defer os.Unsetenv("SST_RESOURCE_MyBucket") + defer os.Unsetenv("SST_RESOURCES_JSON") + + loadFromEnv() + + val, err := Get("MyBucket", "name") + if err != nil { + t.Fatalf("expected MyBucket.name to exist: %v", err) + } + if val != "from-json" { + t.Fatalf("expected MyBucket.name = from-json, got %v", val) + } +} + +func TestKeys_InvalidSSTResourcesJSONIgnored(t *testing.T) { + resetResources() + os.Setenv("SST_RESOURCES_JSON", `not-json`) + defer os.Unsetenv("SST_RESOURCES_JSON") + + loadFromEnv() + + _, err := Get("MyBucket") + if err == nil { + t.Fatal("expected MyBucket to not exist") + } +} + +func TestGet_AllowsEmptyPath(t *testing.T) { + resetResources() + resources["TopLevel"] = "value" + + val, err := Get("TopLevel") + if err != nil { + t.Fatalf("expected TopLevel to exist: %v", err) + } + if val != "value" { + t.Fatalf("expected TopLevel = value, got %v", val) + } +} + +func TestGet_ReturnsNotFound(t *testing.T) { + resetResources() + + _, err := Get("Missing") + if err != ErrNotFound { + t.Fatalf("expected ErrNotFound, got %v", err) + } +} diff --git a/sdk/js/package.json b/sdk/js/package.json index ca7243f7a8..3db973cfc5 100644 --- a/sdk/js/package.json +++ b/sdk/js/package.json @@ -12,6 +12,16 @@ "license": "MIT", "exports": { ".": "./dist/index.js", + "./resource": { + "types": "./dist/resource/index.d.ts", + "workerd": "./dist/resource/cloudflare.js", + "worker": "./dist/resource/cloudflare.js", + "default": "./dist/resource/node.js" + }, + "./resource/cloudflare": { + "types": "./dist/resource/cloudflare.d.ts", + "default": "./dist/resource/cloudflare.js" + }, "./auth": "./dist/auth/index.js", "./auth/adapter": "./dist/auth/adapter/index.js", "./event": "./dist/event/index.js", @@ -27,6 +37,7 @@ "release": "./scripts/release.ts" }, "devDependencies": { + "@aws/durable-execution-sdk-js-testing": "^1.1.1", "@tsconfig/node20": "20.1.4", "@types/aws-lambda": "^8.10.155", "@types/node": "22.10.0", @@ -45,10 +56,9 @@ }, "optionalDependencies": {}, "dependencies": { - "aws-sdk": "2.1692.0", + "@aws/durable-execution-sdk-js": "1.0.2", "aws4fetch": "1.0.18", "jose": "5.2.3", - "opencontrol": "0.0.6", "openid-client": "5.6.4" } } diff --git a/sdk/js/scripts/release.ts b/sdk/js/scripts/release.ts index caf804addf..c018aa4481 100755 --- a/sdk/js/scripts/release.ts +++ b/sdk/js/scripts/release.ts @@ -16,7 +16,6 @@ if (snapshot) { } console.log("publishing", nextPkg.version); -console.log(`npm dist-tag add ${pkg.name}@${pkg.version} ion`); await fs.rmdir("dist", { recursive: true }).catch(() => {}); await $`bun run build`; @@ -71,8 +70,6 @@ try { await Bun.write("package.json", JSON.stringify(nextPkg, null, 2)); await fs.cp("../../README.md", "README.md"); await $`npm publish --access public --tag ${tag}`; - if (!snapshot) - await $`npm dist-tag add ${nextPkg.name}@${nextPkg.version} ion`; } finally { await Bun.write("package.json", JSON.stringify(pkg, null, 2)); await fs.rmdir(tmp, { recursive: true }); diff --git a/sdk/js/src/auth/handler.ts b/sdk/js/src/auth/handler.ts index 107c16717b..c2a9db00eb 100644 --- a/sdk/js/src/auth/handler.ts +++ b/sdk/js/src/auth/handler.ts @@ -47,7 +47,7 @@ export type Prettify = { } & {}; import process from "node:process"; -import { Resource } from "../resource.js"; +import { Resource } from "../resource/index.js"; export const aws = awsHandle; diff --git a/sdk/js/src/auth/index.ts b/sdk/js/src/auth/index.ts index 03da5f00ca..f15ffdd195 100644 --- a/sdk/js/src/auth/index.ts +++ b/sdk/js/src/auth/index.ts @@ -5,7 +5,7 @@ export { Issuer } from "openid-client"; import { AuthHandler } from "./handler.js"; import { createSessionBuilder } from "./session.js"; -export module auth { +export namespace auth { export type Issuer = import("openid-client").Issuer; export const authorizer = AuthHandler; export const sessions = createSessionBuilder; diff --git a/sdk/js/src/auth/session.ts b/sdk/js/src/auth/session.ts index 9eff895460..4c19bcca96 100644 --- a/sdk/js/src/auth/session.ts +++ b/sdk/js/src/auth/session.ts @@ -1,5 +1,5 @@ import { SignJWT, importPKCS8, importSPKI, jwtVerify } from "jose"; -import { Resource } from "../resource.js"; +import { Resource } from "../resource/index.js"; import process from "node:process"; export type SessionBuilder = ReturnType; diff --git a/sdk/js/src/aws/auth.ts b/sdk/js/src/aws/auth.ts index ec227ed0c3..3ec7bf5f25 100644 --- a/sdk/js/src/aws/auth.ts +++ b/sdk/js/src/aws/auth.ts @@ -4,7 +4,7 @@ import { AuthHandler } from "../auth/handler.js"; import { SessionBuilder, createSessionBuilder } from "../auth/session.js"; import { handle, streamHandle } from "hono/aws-lambda"; -export module auth { +export namespace auth { export type Issuer = import("openid-client").Issuer; export function authorizer< Providers extends Record>, @@ -12,7 +12,7 @@ export module auth { >(...args: Parameters>) { const hono = AuthHandler(...args); return ( - args[0].stream ? streamHandle(hono) : handle(hono) + args[0].stream ? streamHandle(hono) : handle(hono) ) as Handler; } export const sessions = createSessionBuilder; diff --git a/sdk/js/src/aws/bus.ts b/sdk/js/src/aws/bus.ts index 2e870dffbc..ac34f4d311 100644 --- a/sdk/js/src/aws/bus.ts +++ b/sdk/js/src/aws/bus.ts @@ -1,16 +1,11 @@ -import { AwsOptions, client } from "../aws/client.js"; -import { Resource } from "../resource.js"; +import { awsFetch, type AwsOptions } from "./client.js"; +import { Resource } from "../resource/index.js"; import { event } from "../event/index.js"; import { EventBridgeEvent, EventBridgeHandler, Context } from "aws-lambda"; -export module bus { +export namespace bus { export type Name = Extract["name"]; - function url(region?: string, options?: AwsOptions) { - if (options?.region) region = options.region; - return `https://events.${region}.amazonaws.com/`; - } - export function subscriber( _events: Events | Events[], cb: ( @@ -45,8 +40,6 @@ export module bus { aws?: AwsOptions; }, ): Promise { - const c = await client(); - const u = url(c.region, options?.aws); const evt = typeof def === "string" ? { @@ -55,10 +48,11 @@ export module bus { metadata: options?.metadata || {}, } : await def.create(properties, options?.metadata); - const res = await c - .fetch(u, { + const res = await awsFetch( + "events", + "/", + { method: "POST", - aws: options?.aws, headers: { "X-Amz-Target": "AWSEvents.PutEvents", "Content-Type": "application/x-amz-json-1.1", @@ -76,7 +70,9 @@ export module bus { }, ], }), - }) + }, + options, + ) .catch((e) => { if (e instanceof Error) console.log("cause", e.cause); throw e; diff --git a/sdk/js/src/aws/client.ts b/sdk/js/src/aws/client.ts index e814c2860a..cfaed1061f 100644 --- a/sdk/js/src/aws/client.ts +++ b/sdk/js/src/aws/client.ts @@ -27,7 +27,25 @@ async function getCredentials(url: string): Promise { return credentials; } -export async function client(): Promise { +type AwsFetchOptions = Exclude[1], null | undefined>; + +export type AwsOptions = Exclude< + Parameters[1], + null | undefined +>["aws"]; + +export async function client(aws?: AwsOptions): Promise { + if (aws?.accessKeyId && aws.secretAccessKey) { + return new AwsClient({ + accessKeyId: aws.accessKeyId, + secretAccessKey: aws.secretAccessKey, + sessionToken: aws.sessionToken, + service: aws.service, + region: aws.region ?? process.env.AWS_REGION, + cache: aws.cache, + }); + } + if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) { return new AwsClient({ accessKeyId: process.env.AWS_ACCESS_KEY_ID, @@ -53,7 +71,16 @@ export async function client(): Promise { throw new Error("No AWS credentials found"); } -export type AwsOptions = Exclude< - Parameters[1], - null | undefined ->["aws"]; +export async function awsFetch( + service: string, + path: string, + init: Omit, + options?: { aws?: AwsOptions }, +) { + const c = await client(options?.aws); + const region = options?.aws?.region ?? c.region; + return c.fetch(`https://${service}.${region}.amazonaws.com${path}`, { + ...init, + aws: options?.aws, + }); +} diff --git a/sdk/js/src/aws/realtime.ts b/sdk/js/src/aws/realtime.ts index 5bf53653e2..69bda7b8fe 100644 --- a/sdk/js/src/aws/realtime.ts +++ b/sdk/js/src/aws/realtime.ts @@ -8,7 +8,7 @@ import { IoTCustomAuthorizerHandler, PolicyDocument } from "aws-lambda"; * import { realtime } from "sst/aws/realtime"; * ``` */ -export module realtime { +export namespace realtime { export interface AuthResult { /** * The principal ID of the authorized client. This could be a user ID, username, or diff --git a/sdk/js/src/aws/task.ts b/sdk/js/src/aws/task.ts index 45861cf129..77b7d01eb8 100644 --- a/sdk/js/src/aws/task.ts +++ b/sdk/js/src/aws/task.ts @@ -1,4 +1,4 @@ -import { AwsOptions, client } from "./client.js"; +import { awsFetch, type AwsOptions } from "./client.js"; /** * The `task` client SDK is available through the following. * @@ -11,12 +11,7 @@ import { AwsOptions, client } from "./client.js"; * [`RunTask`](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_RunTask.html) to * run a task. */ -export module task { - function url(region?: string, options?: AwsOptions) { - if (options?.region) region = options.region; - return `https://ecs.${region}.amazonaws.com/`; - } - +export namespace task { /** * The link data for the task. * @@ -157,11 +152,8 @@ export module task { task: string, options?: Options ): Promise { - const c = await client(); - const u = url(c.region, options?.aws); - const res = await c.fetch(u, { + const res = await awsFetch("ecs", "/", { method: "POST", - aws: options?.aws, headers: { "X-Amz-Target": "AmazonEC2ContainerServiceV20141113.DescribeTasks", "Content-Type": "application/x-amz-json-1.1", @@ -170,7 +162,7 @@ export module task { cluster: resource.cluster, tasks: [task], }), - }); + }, options); if (!res.ok) throw new DescribeError(res); const data = (await res.json()) as { @@ -231,11 +223,8 @@ export module task { environment?: Record, options?: RunOptions ): Promise { - const c = await client(); - const u = url(c.region, options?.aws); - const res = await c.fetch(u, { + const res = await awsFetch("ecs", "/", { method: "POST", - aws: options?.aws, headers: { "X-Amz-Target": "AmazonEC2ContainerServiceV20141113.RunTask", "Content-Type": "application/x-amz-json-1.1", @@ -273,7 +262,7 @@ export module task { })), }, }), - }); + }, options); if (!res.ok) throw new RunError(res); const data = (await res.json()) as { @@ -327,11 +316,8 @@ export module task { task: string, options?: Options ): Promise { - const c = await client(); - const u = url(c.region, options?.aws); - const res = await c.fetch(u, { + const res = await awsFetch("ecs", "/", { method: "POST", - aws: options?.aws, headers: { "X-Amz-Target": "AmazonEC2ContainerServiceV20141113.StopTask", "Content-Type": "application/x-amz-json-1.1", @@ -340,7 +326,7 @@ export module task { cluster: resource.cluster, task, }), - }); + }, options); if (!res.ok) throw new StopError(res); const data = (await res.json()) as { @@ -361,20 +347,17 @@ export module task { export class DescribeError extends Error { constructor(public readonly response: Response) { super("Failed to describe task"); - console.log(response); } } export class RunError extends Error { constructor(public readonly response: Response) { super("Failed to run task"); - console.log(response); } } export class StopError extends Error { constructor(public readonly response: Response) { super("Failed to stop task"); - console.log(response); } } } diff --git a/sdk/js/src/aws/workflow.ts b/sdk/js/src/aws/workflow.ts new file mode 100644 index 0000000000..01a37e9d07 --- /dev/null +++ b/sdk/js/src/aws/workflow.ts @@ -0,0 +1,881 @@ +import * as durable from "@aws/durable-execution-sdk-js"; +import { awsFetch, type AwsOptions } from "./client.js"; + +/** + * The `workflow` SDK is a thin wrapper around the + * [`@aws/durable-execution-sdk-js`](https://www.npmjs.com/package/@aws/durable-execution-sdk-js) + * package and the AWS Lambda durable execution APIs. + * + * SST also adds a few helpers on top, including `ctx.stepWithRollback()`, + * `ctx.rollbackAll()`, and `ctx.waitUntil()`. + * + * @example + * ```ts title="src/workflow.ts" + * import { workflow } from "sst/aws/workflow"; + * ``` + * + * @example + * Use `stepWithRollback()` and `rollbackAll()` to register compensating actions. + * + * ```ts title="src/workflow.ts" + * import { workflow } from "sst/aws/workflow"; + * + * export const handler = workflow.handler(async (_event, ctx) => { + * try { + * const order = await ctx.stepWithRollback("create-order", { + * run: async () => ({ orderId: "order_123" }), + * undo: async (error, result) => { + * await fetch(`https://example.com/orders/${result.orderId}`, { + * method: "DELETE", + * }); + * }, + * }); + * + * await ctx.step("charge-card", async () => { + * throw new Error("Card declined"); + * }); + * + * return order; + * } catch (error) { + * await ctx.rollbackAll(error); + * throw error; + * } + * }); + * ``` + * + * @example + * Use `waitUntil()` when you already know the exact time the workflow should resume. + * + * ```ts title="src/workflow.ts" + * import { workflow } from "sst/aws/workflow"; + * + * export const handler = workflow.handler( + * async (_event, ctx) => { + * const resumeAt = new Date(); + * resumeAt.setMinutes(resumeAt.getMinutes() + 10); + * + * await ctx.waitUntil("wait-for-follow-up", resumeAt); + * + * return ctx.step("send-follow-up", async () => { + * return { delivered: true }; + * }); + * }, + * ); + * ``` + */ +export namespace workflow { + export interface Context< + TLogger extends durable.DurableLogger = durable.DurableLogger, + > extends durable.DurableContext { + /** + * Execute a durable step and register a compensating rollback step if it succeeds. + * If `run` throws, nothing is added to the rollback stack for that step. + */ + stepWithRollback( + name: string, + handler: StepWithRollbackHandler, + config?: StepConfig, + ): durable.DurablePromise; + /** + * Wait until the provided time. Delays are rounded up to the nearest second. + */ + waitUntil(name: string, until: Date): durable.DurablePromise; + /** + * Execute all registered rollback steps in reverse order. + */ + rollbackAll(error: unknown): Promise; + } + + export type Handler< + TEvent = any, + TResult = any, + TLogger extends durable.DurableLogger = durable.DurableLogger, + > = (event: TEvent, context: Context) => Promise; + export type Config = durable.DurableExecutionConfig; + export type Duration = durable.Duration; + export type StepConfig = durable.StepConfig; + export type ExecutionStatus = + | "RUNNING" + | "SUCCEEDED" + | "FAILED" + | "TIMED_OUT" + | "STOPPED"; + + export interface Resource { + /** + * The name of the workflow function. + */ + name: string; + /** + * The version or alias qualifier to invoke. + * + * Linked `sst.aws.Workflow` resources include this automatically. + */ + qualifier: string; + } + + export interface Options { + /** + * Configure the options for the [aws4fetch](https://github.com/mhart/aws4fetch) + * [`AWSClient`](https://github.com/mhart/aws4fetch?tab=readme-ov-file#new-awsclientoptions) used internally by the SDK. + */ + aws?: AwsOptions; + } + + export interface StartResponse { + /** + * The ARN of the durable execution. + */ + arn?: string; + /** + * The HTTP status code from Lambda. + */ + statusCode: number; + /** + * The function version that was executed. + */ + version?: string; + } + + export interface Execution { + /** + * The ARN of the durable execution. + */ + arn: string; + /** + * The durable execution name. + */ + name: string; + /** + * The ARN of the workflow function. + */ + functionArn: string; + /** + * The current execution status. + */ + status: ExecutionStatus; + /** + * When the execution started. + */ + createdAt: Date; + /** + * When the execution ended, if it has finished. + */ + endedAt?: Date; + } + + export interface ListResponse { + /** + * The matching executions. + */ + executions: Execution[]; + } + + export interface DescribeResponse extends Execution { + /** + * The version that started the execution. + */ + version?: string; + } + + export interface StopResponse { + /** + * The ARN of the durable execution. + */ + arn: string; + /** + * The execution status after the stop call. + */ + status: "STOPPED"; + /** + * When the execution was stopped. + */ + stoppedAt?: Date; + } + + /** + * Create a durable workflow handler. + * + * @example + * ```ts title="src/workflow.ts" + * import { workflow } from "sst/aws/workflow"; + * + * export const handler = workflow.handler( + * async (_event, ctx) => { + * const user = await ctx.step("load-user", async () => { + * return { id: "user_123", email: "alice@example.com" }; + * }); + * + * await ctx.wait("pause-before-email", "1 minute"); + * + * return ctx.step("send-email", async () => { + * return { sent: true, userId: user.id }; + * }); + * }, + * ); + * ``` + */ + export function handler< + TEvent = any, + TResult = any, + TLogger extends durable.DurableLogger = durable.DurableLogger, + >(input: Handler, config?: Config) { + return durable.withDurableExecution( + (event: TEvent, context: durable.DurableContext) => + input(event, withRollback(context)), + config, + ); + } + + /** + * Start a new workflow execution. + * + * This is the equivalent to calling + * [`Invoke`](https://docs.aws.amazon.com/lambda/latest/api/API_Invoke.html) + * for a durable Lambda function, using the durable invocation flow described in + * [Invoking durable Lambda functions](https://docs.aws.amazon.com/lambda/latest/dg/durable-invoking.html). + */ + export async function start( + resource: Resource, + input: StartInput, + options?: Options, + ): Promise { + const query = new URLSearchParams({ + Qualifier: resource.qualifier, + }); + const response = await awsFetch( + "lambda", + `/2015-03-31/functions/${encodeURIComponent( + resource.name, + )}/invocations?${query.toString()}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Amz-Durable-Execution-Name": input.name, + "X-Amz-Invocation-Type": "Event", + }, + body: + input.payload === undefined + ? undefined + : JSON.stringify(input.payload), + }, + options, + ); + if (!response.ok) throw new StartError(response); + + return { + arn: response.headers.get("X-Amz-Durable-Execution-Arn") ?? undefined, + statusCode: response.status, + version: response.headers.get("X-Amz-Executed-Version") ?? undefined, + }; + } + + /** + * List workflow executions. + * + * The SDK returns only the first page of results. + */ + export async function list( + resource: Resource, + query: ListQuery, + options?: Options, + ): Promise { + const startedAfter = query.createdAt?.from?.toISOString(); + const startedBefore = query.createdAt?.to?.toISOString(); + const direction = query.createdAt?.order ?? "asc"; + const status = query.status; + + if (startedAfter && startedBefore && startedAfter > startedBefore) { + throw new TypeError( + "workflow.list createdAt.from must be before createdAt.to", + ); + } + if (direction !== "asc" && direction !== "desc") { + throw new TypeError( + `Unsupported workflow order direction '${direction}'`, + ); + } + + const params = new URLSearchParams({ + MaxItems: String(workflowListPageSize), + Qualifier: resource.qualifier, + }); + + if (Array.isArray(status)) { + throw new TypeError("workflow.list status must be a single status"); + } + if (status) params.append("Statuses", status); + if (startedAfter) params.set("StartedAfter", startedAfter); + if (startedBefore) params.set("StartedBefore", startedBefore); + if (direction === "desc") params.set("ReverseOrder", "true"); + + const response = await awsFetch( + "lambda", + `/2025-12-01/functions/${encodeURIComponent( + resource.name, + )}/durable-executions?${params.toString()}`, + { + method: "GET", + }, + options, + ); + if (!response.ok) throw new ListError(response); + + const data = (await response.json()) as Partial; + const executions = Array.isArray(data.DurableExecutions) + ? data.DurableExecutions + : []; + + return { + executions: executions.map(parseExecution), + }; + } + + /** + * Get the details for a single workflow execution. + */ + export async function describe( + arn: string, + options?: Options, + ): Promise { + const response = await awsFetch( + "lambda", + `/2025-12-01/durable-executions/${encodeURIComponent(arn)}`, + { + method: "GET", + }, + options, + ); + if (!response.ok) throw new DescribeError(response); + + const data = (await response.json()) as Partial; + + if ( + !data.DurableExecutionArn || + !data.DurableExecutionName || + !data.FunctionArn || + data.StartTimestamp === undefined || + data.Status === undefined + ) { + throw new DescribeError(response); + } + + const execution = parseExecution(data as DescribeInvocationResponse); + return { + ...execution, + version: data.Version, + }; + } + + /** + * Stop a running workflow execution. + */ + export async function stop( + arn: string, + input?: StopInput, + options?: Options, + ): Promise { + const response = await awsFetch( + "lambda", + `/2025-12-01/durable-executions/${encodeURIComponent(arn)}/stop`, + { + method: "POST", + headers: input?.error + ? { + "Content-Type": "application/json", + } + : undefined, + body: + input?.error === undefined + ? undefined + : JSON.stringify(normalizeError(input.error)), + }, + options, + ); + if (!response.ok) throw new StopError(response); + + const data = (await response.json()) as StopInvocationResponse; + return { + arn, + status: "STOPPED", + stoppedAt: + data.StopTimestamp === undefined + ? undefined + : parseTimestamp(data.StopTimestamp), + }; + } + + /** + * Send a successful result for a pending workflow callback. + * + * This is the equivalent to calling + * [`SendDurableExecutionCallbackSuccess`](https://docs.aws.amazon.com/lambda/latest/api/API_SendDurableExecutionCallbackSuccess.html). + */ + export async function succeed( + token: string, + input: SucceedInput = {}, + options?: Options, + ): Promise { + const response = await awsFetch( + "lambda", + `/2025-12-01/durable-execution-callbacks/${encodeURIComponent( + token, + )}/succeed`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: + input.payload === undefined + ? undefined + : JSON.stringify(input.payload), + }, + options, + ); + if (!response.ok) throw new SucceedError(response); + } + + /** + * Send a failure result for a pending workflow callback. + * + * This is the equivalent to calling + * [`SendDurableExecutionCallbackFailure`](https://docs.aws.amazon.com/lambda/latest/api/API_SendDurableExecutionCallbackFailure.html). + */ + export async function fail( + token: string, + input: FailInput, + options?: Options, + ): Promise { + const response = await awsFetch( + "lambda", + `/2025-12-01/durable-execution-callbacks/${encodeURIComponent( + token, + )}/fail`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(normalizeError(input.error)), + }, + options, + ); + if (!response.ok) throw new FailError(response); + } + + /** + * Send a heartbeat for a pending workflow callback. + * + * This is useful when the external system handling the callback is still doing + * work and needs to prevent the callback from timing out. + * + * This is the equivalent to calling + * [`SendDurableExecutionCallbackHeartbeat`](https://docs.aws.amazon.com/lambda/latest/api/API_SendDurableExecutionCallbackHeartbeat.html). + */ + export async function heartbeat( + token: string, + options?: Options, + ): Promise { + const response = await awsFetch( + "lambda", + `/2025-12-01/durable-execution-callbacks/${encodeURIComponent( + token, + )}/heartbeat`, + { + method: "POST", + }, + options, + ); + if (!response.ok) throw new HeartbeatError(response); + } + + export class StartError extends Error { + constructor(public readonly response: Response) { + super("Failed to start workflow"); + } + } + + export class ListError extends Error { + constructor(public readonly response: Response) { + super("Failed to list workflows"); + } + } + + export class DescribeError extends Error { + constructor(public readonly response: Response) { + super("Failed to describe workflow"); + } + } + + export class StopError extends Error { + constructor(public readonly response: Response) { + super("Failed to stop workflow"); + } + } + + export class SucceedError extends Error { + constructor(public readonly response: Response) { + super("Failed to succeed workflow callback"); + } + } + + export class FailError extends Error { + constructor(public readonly response: Response) { + super("Failed to fail workflow callback"); + } + } + + export class HeartbeatError extends Error { + constructor(public readonly response: Response) { + super("Failed to heartbeat workflow callback"); + } + } + + export class RollbackError extends Error { + constructor( + public readonly stepName: string, + public readonly originalError: unknown, + public readonly undoError: unknown, + ) { + super( + `Failed to rollback workflow step '${stepName}': ${ + undoError instanceof Error ? undoError.message : String(undoError) + }`, + ); + this.name = "RollbackError"; + } + } +} + +interface DurableError { + ErrorMessage?: string; + ErrorType?: string; + ErrorData?: string; + StackTrace?: string[]; +} + +const workflowListPageSize = 1000; + +const rollbackStateSymbol = Symbol("sst.workflow.rollback.state"); + +interface RollbackEntry< + TLogger extends durable.DurableLogger = durable.DurableLogger, +> { + name: string; + execute( + error: unknown, + context: durable.DurableContext, + ): Promise; +} + +interface RollbackState< + TLogger extends durable.DurableLogger = durable.DurableLogger, +> { + undoStack: RollbackEntry[]; +} + +type WrappedDurableContext< + TLogger extends durable.DurableLogger = durable.DurableLogger, +> = durable.DurableContext & { + [rollbackStateSymbol]?: RollbackState; +}; + +function normalizeError(error: unknown): DurableError { + function serializeErrorData(input: unknown) { + if (input === undefined) return undefined; + if (typeof input === "string") return input; + try { + return JSON.stringify(input); + } catch { + return String(input); + } + } + + function normalizeStack(input: unknown) { + if (typeof input === "string") { + return input.split("\n").map((line) => line.trim()); + } + if (Array.isArray(input)) return input.map(String); + return undefined; + } + + if (error === undefined) { + return { + ErrorMessage: "Callback failed", + ErrorType: "Error", + }; + } + + if (error instanceof Error) { + const { message, name, stack, ...rest } = error as Error & + Record; + return { + ErrorMessage: message, + ErrorType: name, + ErrorData: Object.keys(rest).length + ? serializeErrorData(rest) + : undefined, + StackTrace: normalizeStack(stack), + }; + } + + if (typeof error === "string") { + return { + ErrorMessage: error, + ErrorType: "Error", + }; + } + + if (error === null || typeof error !== "object") { + return { + ErrorMessage: String(error), + ErrorType: "Error", + }; + } + + const value = error as Record; + const { data, message, name, stack, type, ...rest } = value; + const hasKnownFields = + message !== undefined || + name !== undefined || + type !== undefined || + data !== undefined || + stack !== undefined; + + return { + ErrorMessage: typeof message === "string" ? message : "Callback failed", + ErrorType: + typeof type === "string" + ? type + : typeof name === "string" + ? name + : "Error", + ErrorData: + data !== undefined + ? serializeErrorData(data) + : Object.keys(rest).length + ? serializeErrorData(rest) + : hasKnownFields + ? undefined + : serializeErrorData(error), + StackTrace: normalizeStack(stack), + }; +} + +interface StepWithRollbackHandler< + TOutput = any, + TLogger extends durable.DurableLogger = durable.DurableLogger, +> { + /** + * The durable step to execute. + */ + run: durable.StepFunc; + /** + * Called during rollback with the original error, the step result, and step context. + */ + undo: ( + error: unknown, + value: TOutput, + context: Parameters>[0], + ) => Promise; +} + +interface StartInput { + /** + * The unique name for this workflow execution. + */ + name: string; + /** + * The event payload passed to the workflow handler. + */ + payload?: TPayload; +} + +interface SucceedInput { + /** + * The payload to resolve the callback with. + */ + payload?: TPayload; +} + +interface FailInput { + /** + * The error to reject the callback with. Supports an `Error`, a string, + * or an object with camelCase fields like `message`, `type`, `data`, and `stack`. + */ + error: unknown; +} + +interface StopInput { + /** + * The error to reject the callback with. Supports an `Error`, a string, + * or an object with camelCase fields like `message`, `type`, `data`, and `stack`. + */ + error?: unknown; +} + +interface ListQuery { + status?: workflow.ExecutionStatus; + createdAt?: { + from?: Date; + to?: Date; + order?: "asc" | "desc"; + }; +} + +interface ListInvocationExecution { + DurableExecutionArn: string; + DurableExecutionName: string; + FunctionArn: string; + Status: workflow.ExecutionStatus; + StartTimestamp: string | number; + EndTimestamp?: string | number; +} + +interface ListInvocationResponse { + DurableExecutions: ListInvocationExecution[]; + NextMarker?: string; +} + +interface DescribeInvocationResponse { + DurableExecutionArn: string; + DurableExecutionName: string; + FunctionArn: string; + InputPayload?: string; + Result?: string; + Error?: { + ErrorMessage?: string; + ErrorType?: string; + ErrorData?: string; + StackTrace?: string[]; + }; + StartTimestamp: string | number; + Status: workflow.ExecutionStatus; + EndTimestamp?: string | number; + Version?: string; + TraceHeader?: { + XAmznTraceId?: string; + }; +} + +interface StopInvocationResponse { + StopTimestamp?: string | number; +} + +function parseExecution( + execution: ListInvocationExecution | DescribeInvocationResponse, +): workflow.Execution { + return { + arn: execution.DurableExecutionArn, + name: execution.DurableExecutionName, + functionArn: execution.FunctionArn, + status: execution.Status, + createdAt: parseTimestamp(execution.StartTimestamp), + endedAt: + execution.EndTimestamp === undefined + ? undefined + : parseTimestamp(execution.EndTimestamp), + }; +} + +function parseTimestamp(timestamp: string | number): Date { + const value = + typeof timestamp === "number" ? timestamp : Number(timestamp); + + if (Number.isFinite(value)) { + return new Date(value < 1_000_000_000_000 ? value * 1000 : value); + } + + return new Date(timestamp); +} + +function resolveWaitUntilDuration(until: Date): durable.Duration { + const timestamp = until.getTime(); + if (!Number.isFinite(timestamp)) { + throw new TypeError("waitUntil requires a valid Date"); + } + + return { + seconds: Math.max(0, Math.ceil((timestamp - Date.now()) / 1000)), + }; +} + +function withRollback< + TLogger extends durable.DurableLogger = durable.DurableLogger, +>(context: durable.DurableContext): workflow.Context { + const wrapped = context as WrappedDurableContext; + if (wrapped[rollbackStateSymbol]) return wrapped as workflow.Context; + + const rollbackState: RollbackState = { undoStack: [] }; + + wrapped[rollbackStateSymbol] = rollbackState; + + Object.defineProperty(wrapped, "stepWithRollback", { + configurable: true, + enumerable: false, + writable: true, + value: function ( + name: string, + handler: StepWithRollbackHandler, + config?: durable.StepConfig, + ): durable.DurablePromise { + const undoConfig = + config?.retryStrategy || config?.semantics + ? { + retryStrategy: config.retryStrategy, + semantics: config.semantics, + } + : undefined; + + return new durable.DurablePromise(async () => { + const result = await context.step(name, handler.run, config); + + rollbackState.undoStack.push({ + name, + execute: async ( + error: unknown, + rollbackContext: durable.DurableContext, + ) => { + await rollbackContext.step( + `Undo '${name}'`, + (stepContext) => handler.undo(error, result, stepContext), + undoConfig, + ); + }, + }); + + return result; + }); + }, + }); + + Object.defineProperty(wrapped, "waitUntil", { + configurable: true, + enumerable: false, + writable: true, + value: (name: string, until: Date): durable.DurablePromise => + context.wait(name, resolveWaitUntilDuration(until)), + }); + + Object.defineProperty(wrapped, "rollbackAll", { + configurable: true, + enumerable: false, + writable: true, + value: async (error: unknown) => { + while (rollbackState.undoStack.length > 0) { + const rollbackStep = rollbackState.undoStack.pop(); + if (!rollbackStep) continue; + + try { + await rollbackStep.execute(error, context); + } catch (undoError) { + throw new workflow.RollbackError(rollbackStep.name, error, undoError); + } + } + }, + }); + + return wrapped as workflow.Context; +} diff --git a/sdk/js/src/event/index.ts b/sdk/js/src/event/index.ts index e52dfe698f..0f04a5a870 100644 --- a/sdk/js/src/event/index.ts +++ b/sdk/js/src/event/index.ts @@ -1,6 +1,6 @@ import { Prettify } from "../auth/handler.js"; -export module event { +export namespace event { export type Definition = { type: string; $input: any; diff --git a/sdk/js/src/index.ts b/sdk/js/src/index.ts index afed06f937..3bc20a128f 100644 --- a/sdk/js/src/index.ts +++ b/sdk/js/src/index.ts @@ -1,5 +1,5 @@ export * from "./realtime/index.js"; -export * from "./resource.js"; +export { Resource } from "./resource/index.js"; export * from "./vector/index.js"; import { format } from "util"; diff --git a/sdk/js/src/opencontrol.ts b/sdk/js/src/opencontrol.ts deleted file mode 100644 index 159052e68e..0000000000 --- a/sdk/js/src/opencontrol.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { tool } from "opencontrol/tool"; -import { client } from "./aws/client.js"; -import { Resource } from "./resource.js"; -import { z } from "zod"; -import AWS from "aws-sdk"; - -/** - * A list of OpenControl tools provided by SST. Currently, it includes tools that - * can: - * - * - Lists the resources in your SST app. - * - Access the resources in your AWS account. - * - * You can add this tool to your OpenControl server by passing it to the `tools` - * option when creating it. - * - * @example - * ```js title="src/server.ts" - * import { create } from "opencontrol"; - * import { tools } from "sst/opencontrol"; - * - * const app = create({ - * model: // ... - * tools: [...tools] - * }); - * ``` - */ -export const tools = [ - /* - tool({ - name: "sst", - description: "Get the resources in the current SST app", - async run() { - const c = await client(); - const stateBucket = await getStateBucket(); - if (!stateBucket) - throw new Error( - "Failed to find the SST state bucket in user's AWS account. Ask the user to make sure the AWS account has been bootstrapped with SST." - ); - - const state = await getStateFile(); - if (!state) - throw new Error( - "Failed to find the SST state file in user's AWS account." - ); - - const resources = state["checkpoint"]["latest"]["resources"]; - return resources - .filter( - (r: any) => - r.type !== "sst:sst:LinkRef" && - !r.type.startsWith("pulumi:provider:") - ) - .map((r: any) => ({ - urn: r.urn, - type: r.type, - id: r.id, - parent: r.parent, - })); - - async function getStateBucket() { - const res = await c.fetch(`https://ssm.${c.region}.amazonaws.com/`, { - method: "POST", - headers: { - "X-Amz-Target": "AmazonSSM.GetParameter", - "Content-Type": "application/x-amz-json-1.1", - }, - body: JSON.stringify({ - Name: "/sst/bootstrap", - }), - }); - if (!res.ok) return; - - const data = (await res.json()) as { - Parameter: { - Value: string; - }; - }; - return JSON.parse(data.Parameter.Value)["state"]; - } - - async function getStateFile() { - const res = await c.fetch( - `https://${stateBucket}.s3.${c.region}.amazonaws.com/app/${Resource.App.name}/${Resource.App.stage}.json`, - { - method: "GET", - } - ); - if (!res.ok) return; - return (await res.json()) as any; - } - }, - }), - */ - tool({ - name: "aws", - description: `This uses aws sdk v2 in javascript to execute aws commands - this is roughly how it works - \`\`\`js - import aws from "aws-sdk"; - aws[service][method](params) - \`\`\` - `, - args: z.object({ - service: z - .string() - .describe( - "name of the aws service in the format aws sdk v2 uses, like S3 or EC2", - ), - method: z - .string() - .describe("name of the aws method in the format aws sdk v2 uses"), - params: z.string().describe("params for the aws method in json format"), - }), - async run(input) { - const aws = await import("aws-sdk"); - /* @ts-expect-error */ - const service = aws.default[input.service]; - if (!service) { - throw new Error(`service aws[${input.service}] not found`); - } - const instance = new service(); - if (!instance[input.method]) { - throw new Error( - `method aws.${input.service}.${input.method} not found`, - ); - } - return await instance[input.method](JSON.parse(input.params)).promise(); - }, - }), - /* - tool({ - name: "aws_batch", - description: - "Make multiple calls to the AWS SDK for JavaScript v2 with the same command but different arguments. This tools takes an array of arguments, and will call the command with each argument in the array in parallel. Use this over the aws tool when you need to call the same command multiple times with different arguments.", - args: z.object({ - client: z.string().describe("Class name of the client to use"), - command: z.string().describe("Command to call on the client"), - args: z - .array( - z - .record(z.string(), z.any()) - .optional() - .describe("Arguments to pass to the command"), - ) - .describe( - "An array of arguments. Each argument will be passed to the command in parallel.", - ), - }), - async run(input) { - // @ts-ignore - const client = new AWS[input.client](); - return await Promise.all( - input.args.map((arg: any) => client[input.command](arg).promise()), - ); - }, - }), - */ -]; diff --git a/sdk/js/src/resource.ts b/sdk/js/src/resource.ts index 1a8d16649c..b4ca6468e1 100644 --- a/sdk/js/src/resource.ts +++ b/sdk/js/src/resource.ts @@ -1,113 +1 @@ -import { env } from "process"; -import { readFileSync } from "fs"; -import crypto from "crypto"; - -export interface Resource { - App: { - name: string; - stage: string; - }; -} - -const raw: Record = { - // @ts-expect-error, - ...globalThis.$SST_LINKS, -}; - -const environment = { - ...env, - ...globalThis.process?.env, -}; - -// Handle consolidated resources JSON (for Windows with many resources) -if (environment.SST_RESOURCES_JSON) { - try { - const allResources = JSON.parse(environment.SST_RESOURCES_JSON); - Object.assign(raw, allResources); - } catch (error) { - console.error("Failed to parse SST_RESOURCES_JSON:", error); - } -} - -// Handle individual SST_RESOURCE_ environment variables -for (const [key, value] of Object.entries(environment)) { - if (key.startsWith("SST_RESOURCE_") && value) { - raw[key.slice("SST_RESOURCE_".length)] = JSON.parse(value); - } -} - -// @ts-expect-error -if (env.SST_KEY_FILE && env.SST_KEY && !globalThis.SST_KEY_FILE_DATA) { - const key = Buffer.from(env.SST_KEY, "base64"); - const encryptedData = readFileSync(env.SST_KEY_FILE); - const nonce = Buffer.alloc(12, 0); - const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce); - const authTag = encryptedData.subarray(-16); - const actualCiphertext = encryptedData.subarray(0, -16); - decipher.setAuthTag(authTag); - let decrypted = decipher.update(actualCiphertext); - decrypted = Buffer.concat([decrypted, decipher.final()]); - const decryptedData = JSON.parse(decrypted.toString()); - Object.assign(raw, decryptedData); -} - -// @ts-expect-error -if (globalThis.SST_KEY_FILE_DATA) { - // @ts-expect-error - Object.assign(raw, globalThis.SST_KEY_FILE_DATA); -} -export function fromCloudflareEnv(input: any) { - for (let [key, value] of Object.entries(input)) { - if (typeof value === "string") { - try { - value = JSON.parse(value); - } catch {} - } - raw[key] = value; - if (key.startsWith("SST_RESOURCE_")) { - raw[key.replace("SST_RESOURCE_", "")] = value; - } - } -} - -export function wrapCloudflareHandler(handler: any) { - if (typeof handler === "function" && handler.hasOwnProperty("prototype")) { - return class extends handler { - constructor(ctx: any, env: any) { - fromCloudflareEnv(env); - super(ctx, env); - } - }; - } - - function wrap(fn: any) { - return function (req: any, env: any, ...rest: any[]) { - fromCloudflareEnv(env); - return fn(req, env, ...rest); - }; - } - - const result = {} as any; - for (const [key, value] of Object.entries(handler)) { - result[key] = wrap(value); - } - return result; -} - -export const Resource = new Proxy(raw, { - get(_target, prop: string) { - if (prop in raw) { - return raw[prop]; - } - if (!env.SST_RESOURCE_App) { - throw new Error( - "It does not look like SST links are active. If this is in local development and you are not starting this process through the multiplexer, wrap your command with `sst dev -- `", - ); - } - let msg = `"${prop}" is not linked in your sst.config.ts`; - if (env.AWS_LAMBDA_FUNCTION_NAME) { - msg += ` to ${env.AWS_LAMBDA_FUNCTION_NAME}`; - } - throw new Error(msg); - }, -}) as Resource; +export * from "./resource/index.js"; diff --git a/sdk/js/src/resource/cloudflare.ts b/sdk/js/src/resource/cloudflare.ts new file mode 100644 index 0000000000..2d84d0f060 --- /dev/null +++ b/sdk/js/src/resource/cloudflare.ts @@ -0,0 +1,40 @@ +import { env } from "cloudflare:workers"; + +import { createResource, loadResourceEnvironment } from "./shared.js"; +import type { Resource as BaseResource } from "./node.js"; + +function loadCloudflareResources() { + loadResourceEnvironment(env); +} + +export function wrapCloudflareHandler(handler: any) { + if (handler == null) { + return undefined; + } + + if (typeof handler === "function" && handler.hasOwnProperty("prototype")) { + return class extends handler { + constructor(ctx: any, env: any) { + loadResourceEnvironment(env); + super(ctx, env); + } + }; + } + + function wrap(fn: any) { + return function (req: any, env: any, ...rest: any[]) { + loadResourceEnvironment(env); + return fn(req, env, ...rest); + }; + } + + const result = {} as any; + for (const [key, value] of Object.entries(handler)) { + result[key] = wrap(value); + } + return result; +} + +// Keep an interface here so generated sst-env.d.ts can augment Resource. +export interface Resource extends BaseResource {} +export const Resource = createResource(loadCloudflareResources); diff --git a/sdk/js/src/resource/index.ts b/sdk/js/src/resource/index.ts new file mode 100644 index 0000000000..4fd3b5bb90 --- /dev/null +++ b/sdk/js/src/resource/index.ts @@ -0,0 +1 @@ +export * from "./node.js"; diff --git a/sdk/js/src/resource/node.ts b/sdk/js/src/resource/node.ts new file mode 100644 index 0000000000..15e372d3d5 --- /dev/null +++ b/sdk/js/src/resource/node.ts @@ -0,0 +1,56 @@ +import crypto from "crypto"; +import { readFileSync } from "fs"; +import { env } from "process"; + +import { + createResource, + loadResourceData, + loadResourceEnvironment, +} from "./shared.js"; +import type { Resource as BaseResource } from "./shared.js"; + +const state = globalThis as typeof globalThis & { + SST_KEY_FILE_DATA?: Record; +}; + +function loadNodeResources() { + const environment: Record = { + ...env, + ...globalThis.process?.env, + }; + + loadResourceEnvironment(environment); + + if (environment.SST_RESOURCES_JSON) { + try { + loadResourceData(JSON.parse(environment.SST_RESOURCES_JSON)); + } catch (error) { + console.error("Failed to parse SST_RESOURCES_JSON:", error); + } + } + + if ( + environment.SST_KEY_FILE && + environment.SST_KEY && + !state.SST_KEY_FILE_DATA + ) { + const key = Buffer.from(environment.SST_KEY, "base64"); + const encryptedData = readFileSync(environment.SST_KEY_FILE); + const nonce = Buffer.alloc(12, 0); + const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce); + const authTag = encryptedData.subarray(-16); + const actualCiphertext = encryptedData.subarray(0, -16); + decipher.setAuthTag(authTag); + let decrypted = decipher.update(actualCiphertext); + decrypted = Buffer.concat([decrypted, decipher.final()]); + loadResourceData(JSON.parse(decrypted.toString())); + } + + if (state.SST_KEY_FILE_DATA) { + loadResourceData(state.SST_KEY_FILE_DATA); + } +} + +// Keep an interface here so generated sst-env.d.ts can augment Resource. +export interface Resource extends BaseResource {} +export const Resource = createResource(loadNodeResources); diff --git a/sdk/js/src/resource/shared.ts b/sdk/js/src/resource/shared.ts new file mode 100644 index 0000000000..b948b8d522 --- /dev/null +++ b/sdk/js/src/resource/shared.ts @@ -0,0 +1,81 @@ +export interface Resource { + App: { + name: string; + stage: string; + }; +} + +const state = globalThis as typeof globalThis & { + __SST_RESOURCE_RAW__?: Record; + __SST_RESOURCE_ENVIRONMENT__?: Record; +}; + +const raw: Record = (state.__SST_RESOURCE_RAW__ ??= { + // @ts-expect-error + ...globalThis.$SST_LINKS, +}); + +const environment: Record = + (state.__SST_RESOURCE_ENVIRONMENT__ ??= {}); + +export function loadResourceEnvironment(input?: Record) { + for (let [key, value] of Object.entries(input ?? {})) { + if (typeof value === "string") { + environment[key] = value; + if (!key.startsWith("SST_RESOURCE_") || !value) { + continue; + } + raw[key.slice("SST_RESOURCE_".length)] = JSON.parse(value); + continue; + } + + raw[key] = value; + } +} + +export function loadResourceData(input?: Record) { + Object.assign(raw, input ?? {}); +} + +export function createResource(load: () => void) { + let loaded = false; + const loadData = () => { + if (loaded) return; + load(); + loaded = true; + }; + + return new Proxy(raw, { + get(_target, prop: string | symbol) { + loadData(); + if (prop in raw) { + return raw[prop as string]; + } + if (typeof prop !== "string") { + return undefined; + } + if (!environment.SST_RESOURCE_App && !raw.App) { + throw new Error( + "It does not look like SST links are active. If this is in local development and you are not starting this process through the multiplexer, wrap your command with `sst dev -- `", + ); + } + let msg = `"${prop}" is not linked in your sst.config.ts`; + if (environment.AWS_LAMBDA_FUNCTION_NAME) { + msg += ` to ${environment.AWS_LAMBDA_FUNCTION_NAME}`; + } + throw new Error(msg); + }, + has(_target, prop: string | symbol) { + loadData(); + return prop in raw; + }, + ownKeys() { + loadData(); + return Reflect.ownKeys(raw); + }, + getOwnPropertyDescriptor(_target, prop: string | symbol) { + loadData(); + return Object.getOwnPropertyDescriptor(raw, prop); + }, + }) as T; +} diff --git a/sdk/js/src/resource/workers.d.ts b/sdk/js/src/resource/workers.d.ts new file mode 100644 index 0000000000..995273c58b --- /dev/null +++ b/sdk/js/src/resource/workers.d.ts @@ -0,0 +1,3 @@ +declare module "cloudflare:workers" { + export const env: Record; +} diff --git a/sdk/js/src/vector/index.ts b/sdk/js/src/vector/index.ts index 9f25d9974f..7c0d67489c 100644 --- a/sdk/js/src/vector/index.ts +++ b/sdk/js/src/vector/index.ts @@ -1,4 +1,4 @@ -import { Resource } from "../resource.js"; +import { Resource } from "../resource/index.js"; import { client } from "../aws/client.js"; export interface PutEvent { diff --git a/sdk/js/test/resource.test.ts b/sdk/js/test/resource.test.ts new file mode 100644 index 0000000000..6f2404fb66 --- /dev/null +++ b/sdk/js/test/resource.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from "bun:test"; +import { + createResource, + loadResourceData, + loadResourceEnvironment, +} from "../src/resource/shared.ts"; + +describe("resource environment", () => { + it("loads SST_RESOURCE string values", () => { + const name = "TestStringResource"; + const resource = createResource(() => {}); + + loadResourceEnvironment({ + SST_RESOURCE_App: JSON.stringify({ name: "app", stage: "dev" }), + [`SST_RESOURCE_${name}`]: JSON.stringify({ value: "linked" }), + }); + + expect(resource.App).toEqual({ name: "app", stage: "dev" }); + expect((resource as any)[name]).toEqual({ value: "linked" }); + expect(`SST_RESOURCE_${name}` in resource).toBe(false); + }); + + it("ignores plain string values", () => { + const resource = createResource(() => {}); + const plain = "TestPlainString"; + const json = "TestPlainJsonString"; + + loadResourceEnvironment({ + [plain]: "hello", + [json]: JSON.stringify({ value: "not-linked" }), + }); + + expect(plain in resource).toBe(false); + expect(json in resource).toBe(false); + }); + + it("loads non-string bindings directly", () => { + const resource = createResource(() => {}); + const binding = "TestBinding"; + const value = { get: () => "ok" }; + + loadResourceEnvironment({ + [binding]: value, + }); + + expect((resource as any)[binding]).toBe(value); + }); + + it("loads resources once through createResource", () => { + const name = "TestLoadedOnce"; + let loads = 0; + const resource = createResource(() => { + loads += 1; + loadResourceEnvironment({ + [`SST_RESOURCE_${name}`]: JSON.stringify({ value: "once" }), + }); + }); + + expect((resource as any)[name]).toEqual({ value: "once" }); + expect((resource as any)[name]).toEqual({ value: "once" }); + expect(loads).toBe(1); + }); + + it("loads consolidated resources JSON", () => { + const resource = createResource(() => {}); + + loadResourceData({ + MyBucket: { name: "my-bucket" }, + App: { name: "app", stage: "dev" }, + }); + + expect((resource as any).MyBucket).toEqual({ name: "my-bucket" }); + expect(resource.App).toEqual({ name: "app", stage: "dev" }); + }); + + it("consolidated JSON overrides individual env vars", () => { + const resource = createResource(() => {}); + + loadResourceEnvironment({ + SST_RESOURCE_MyBucket: JSON.stringify({ name: "from-env" }), + SST_RESOURCE_App: JSON.stringify({ name: "app", stage: "dev" }), + }); + + loadResourceData({ + MyBucket: { name: "from-json" }, + }); + + expect((resource as any).MyBucket).toEqual({ name: "from-json" }); + }); + + it("loads SST_RESOURCES_JSON via node resource module", async () => { + // Clear global state so the import re-initializes + delete (globalThis as any).__SST_RESOURCE_RAW__; + delete (globalThis as any).__SST_RESOURCE_ENVIRONMENT__; + + process.env.SST_RESOURCES_JSON = JSON.stringify({ + MyBucket: { name: "my-bucket" }, + App: { name: "app", stage: "dev" }, + }); + + const mod = await import("../src/resource/node.ts"); + + expect((mod.Resource as any).MyBucket).toEqual({ name: "my-bucket" }); + expect(mod.Resource.App).toEqual({ name: "app", stage: "dev" }); + + delete process.env.SST_RESOURCES_JSON; + }); +}); diff --git a/sdk/js/test/workflow.test.ts b/sdk/js/test/workflow.test.ts new file mode 100644 index 0000000000..236c3e9076 --- /dev/null +++ b/sdk/js/test/workflow.test.ts @@ -0,0 +1,540 @@ +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "bun:test"; +import { + ExecutionStatus, + LocalDurableTestRunner, +} from "@aws/durable-execution-sdk-js-testing"; +import { aws } from "../src/aws/client.ts"; +import { workflow } from "../src/aws/workflow.ts"; + +function jsonResponse(body: unknown, init: ResponseInit = {}) { + return new Response(JSON.stringify(body), { + headers: { + "Content-Type": "application/json", + }, + status: 200, + ...init, + }); +} + +describe("workflow client", () => { + const originalFetch = aws.fetch; + + beforeEach(() => { + aws.fetch = originalFetch; + }); + + afterEach(() => { + aws.fetch = originalFetch; + }); + + it("starts a workflow execution", async () => { + aws.fetch = async (service, path, init) => { + expect(service).toBe("lambda"); + expect(path).toBe( + "/2015-03-31/functions/my-workflow/invocations?Qualifier=live", + ); + expect(init.method).toBe("POST"); + expect(init.headers).toEqual({ + "Content-Type": "application/json", + "X-Amz-Durable-Execution-Name": "job-123", + "X-Amz-Invocation-Type": "Event", + }); + expect(JSON.parse(String(init.body))).toEqual({ hello: "world" }); + + return new Response(undefined, { + status: 202, + headers: { + "X-Amz-Durable-Execution-Arn": "arn:workflow:live/job-123", + "X-Amz-Executed-Version": "12", + }, + }); + }; + + const result = await workflow.start( + { name: "my-workflow", qualifier: "live" }, + { + name: "job-123", + payload: { hello: "world" }, + }, + ); + + expect(result).toEqual({ + arn: "arn:workflow:live/job-123", + statusCode: 202, + version: "12", + }); + expect("response" in result).toBe(false); + }); + + it("lists first-page executions", async () => { + const requests: { + service: string; + path: string; + init: RequestInit; + }[] = []; + + aws.fetch = async (service, path, init) => { + requests.push({ service, path, init }); + const url = new URL(`https://example.com${path}`); + const status = url.searchParams.getAll("Statuses")[0]; + + if (status === "FAILED") { + return jsonResponse({ + DurableExecutions: [ + { + DurableExecutionArn: "arn:failed-2", + DurableExecutionName: "failed-2", + FunctionArn: "arn:function:live", + Status: "FAILED", + StartTimestamp: "2026-04-03T00:00:00.000Z", + EndTimestamp: "2026-04-03T00:05:00.000Z", + }, + { + DurableExecutionArn: "arn:failed-1", + DurableExecutionName: "failed-1", + FunctionArn: "arn:function:live", + Status: "FAILED", + StartTimestamp: "2026-04-02T00:00:00.000Z", + EndTimestamp: "2026-04-02T00:05:00.000Z", + }, + ], + NextMarker: "ignored-page-2", + }); + } + + throw new Error(`Unexpected request: ${path}`); + }; + + const result = await workflow.list( + { name: "my-workflow", qualifier: "live" }, + { + status: "FAILED", + createdAt: { + from: new Date("2026-04-01T00:00:00.000Z"), + to: new Date("2026-04-04T00:00:00.000Z"), + order: "desc", + }, + }, + ); + + expect(requests).toHaveLength(1); + expect(requests.map((request) => request.service)).toEqual(["lambda"]); + + const firstUrl = new URL(`https://example.com${requests[0].path}`); + expect(firstUrl.searchParams.get("Qualifier")).toBe("live"); + expect(firstUrl.searchParams.get("MaxItems")).toBe("1000"); + expect(firstUrl.searchParams.get("StartedAfter")).toBe( + "2026-04-01T00:00:00.000Z", + ); + expect(firstUrl.searchParams.get("StartedBefore")).toBe( + "2026-04-04T00:00:00.000Z", + ); + expect(firstUrl.searchParams.get("ReverseOrder")).toBe("true"); + expect(firstUrl.searchParams.getAll("Statuses")).toEqual(["FAILED"]); + expect(requests[0].init.method).toBe("GET"); + + expect(firstUrl.searchParams.get("Marker")).toBeNull(); + + expect(result.executions.map((execution) => execution.arn)).toEqual([ + "arn:failed-2", + "arn:failed-1", + ]); + expect(result.executions[0]?.status).toBe("FAILED"); + expect(result.executions[0]?.endedAt?.toISOString()).toBe( + "2026-04-03T00:05:00.000Z", + ); + expect("response" in result).toBe(false); + }); + + it("rejects multiple statuses", async () => { + let calls = 0; + + aws.fetch = async () => { + calls += 1; + return jsonResponse({ DurableExecutions: [] }); + }; + + await expect( + workflow.list( + { name: "my-workflow", qualifier: "live" }, + { status: ["RUNNING", "FAILED"] as never }, + ), + ).rejects.toThrow("workflow.list status must be a single status"); + + expect(calls).toBe(0); + }); + + it("describes a workflow execution", async () => { + aws.fetch = async (service, path, init) => { + expect(service).toBe("lambda"); + expect(path).toBe( + "/2025-12-01/durable-executions/arn%3Aworkflow%3Alive%2Fexecution-123", + ); + expect(init.method).toBe("GET"); + + return jsonResponse({ + DurableExecutionArn: "arn:workflow:live/execution-123", + DurableExecutionName: "execution-123", + FunctionArn: "arn:function:live", + Status: "RUNNING", + StartTimestamp: 1775556000, + Version: "12", + }); + }; + + const result = await workflow.describe("arn:workflow:live/execution-123"); + + expect(result.arn).toBe("arn:workflow:live/execution-123"); + expect(result.name).toBe("execution-123"); + expect(result.status).toBe("RUNNING"); + expect(result.createdAt.toISOString()).toBe("2026-04-07T10:00:00.000Z"); + expect(result.version).toBe("12"); + expect("response" in result).toBe(false); + }); + + it("stops a workflow execution with a normalized error payload", async () => { + aws.fetch = async (service, path, init) => { + expect(service).toBe("lambda"); + expect(path).toBe( + "/2025-12-01/durable-executions/arn%3Aworkflow%3Alive%2Fexecution-123/stop", + ); + expect(init.method).toBe("POST"); + expect(init.headers).toEqual({ + "Content-Type": "application/json", + }); + expect(JSON.parse(String(init.body))).toEqual({ + ErrorMessage: "Cancelled by user", + ErrorType: "Cancelled", + ErrorData: JSON.stringify({ reason: "user" }), + }); + + return jsonResponse({ + StopTimestamp: "1775559600", + }); + }; + + const result = await workflow.stop("arn:workflow:live/execution-123", { + error: { + message: "Cancelled by user", + type: "Cancelled", + data: { reason: "user" }, + }, + }); + + expect(result).toEqual({ + arn: "arn:workflow:live/execution-123", + status: "STOPPED", + stoppedAt: new Date("2026-04-07T11:00:00.000Z"), + }); + expect("response" in result).toBe(false); + }); + + it("succeeds a workflow callback", async () => { + aws.fetch = async (service, path, init) => { + expect(service).toBe("lambda"); + expect(path).toBe( + "/2025-12-01/durable-execution-callbacks/callback-token/succeed", + ); + expect(init.method).toBe("POST"); + expect(init.headers).toEqual({ + "Content-Type": "application/json", + }); + expect(JSON.parse(String(init.body))).toEqual({ ok: true }); + + return new Response(undefined, { status: 200 }); + }; + + const result = await workflow.succeed("callback-token", { + payload: { ok: true }, + }); + + expect(result).toBeUndefined(); + }); + + it("fails a workflow callback", async () => { + aws.fetch = async (service, path, init) => { + expect(service).toBe("lambda"); + expect(path).toBe( + "/2025-12-01/durable-execution-callbacks/callback-token/fail", + ); + expect(init.method).toBe("POST"); + expect(init.headers).toEqual({ + "Content-Type": "application/json", + }); + expect(JSON.parse(String(init.body))).toEqual({ + ErrorMessage: "boom", + ErrorType: "Error", + }); + + return new Response(undefined, { status: 200 }); + }; + + const result = await workflow.fail("callback-token", { + error: "boom", + }); + + expect(result).toBeUndefined(); + }); + + it("heartbeats a workflow callback", async () => { + aws.fetch = async (service, path, init) => { + expect(service).toBe("lambda"); + expect(path).toBe( + "/2025-12-01/durable-execution-callbacks/callback-token/heartbeat", + ); + expect(init.method).toBe("POST"); + + return new Response(undefined, { status: 200 }); + }; + + const result = await workflow.heartbeat("callback-token"); + + expect(result).toBeUndefined(); + }); +}); + +describe("workflow rollback runner", () => { + beforeAll(async () => { + await LocalDurableTestRunner.setupTestEnvironment({ skipTime: true }); + }); + + afterAll(async () => { + await LocalDurableTestRunner.teardownTestEnvironment(); + }); + + it("waitUntil wraps a named wait", async () => { + const fixedNow = 1_700_000_000_000; + const originalNow = Date.now; + Date.now = () => fixedNow; + + try { + const handler = workflow.handler(async (_event, ctx) => { + await ctx.waitUntil("pause", new Date(fixedNow + 1_500)); + }); + + const runner = new LocalDurableTestRunner({ handlerFunction: handler }); + const execution = await runner.run(); + + expect(execution.getStatus()).toBe(ExecutionStatus.SUCCEEDED); + + const wait = await runner.getOperation("pause").waitForData(); + expect(wait.getWaitDetails()?.waitSeconds).toBe(2); + } finally { + Date.now = originalNow; + } + }); + + it("does not register rollback when run fails", async () => { + const calls: string[] = []; + + const handler = workflow.handler(async (_event, ctx) => { + try { + await ctx.stepWithRollback( + "step-a", + { + run: async () => { + calls.push("run:step-a"); + throw new Error("boom"); + }, + undo: async () => { + calls.push("undo:step-a"); + }, + }, + { + retryStrategy: () => ({ shouldRetry: false }), + }, + ); + } catch (error) { + await ctx.rollbackAll(error); + throw error; + } + }); + + const runner = new LocalDurableTestRunner({ handlerFunction: handler }); + const execution = await runner.run(); + + expect(execution.getStatus()).toBe(ExecutionStatus.FAILED); + expect(calls).toEqual(["run:step-a"]); + }); + + it("rebuilds rollback stack after wait replay", async () => { + const calls: string[] = []; + + const handler = workflow.handler(async (_event, ctx) => { + try { + await ctx.stepWithRollback("step-a", { + run: async () => { + calls.push("run:step-a"); + return "result-a"; + }, + undo: async (_error, value) => { + calls.push(`undo:${value}`); + }, + }); + + await ctx.wait("pause", { seconds: 1 }); + + await ctx.stepWithRollback("step-b", { + run: async () => { + calls.push("run:step-b"); + return "result-b"; + }, + undo: async (_error, value) => { + calls.push(`undo:${value}`); + }, + }); + + await ctx.step("fail", async () => { + throw new Error("boom"); + }); + } catch (error) { + await ctx.rollbackAll(error); + throw error; + } + }); + + const runner = new LocalDurableTestRunner({ handlerFunction: handler }); + const execution = await runner.run(); + + expect(execution.getStatus()).toBe(ExecutionStatus.FAILED); + expect(calls).toEqual([ + "run:step-a", + "run:step-b", + "undo:result-b", + "undo:result-a", + ]); + + const wait = await runner.getOperation("pause").waitForData(); + expect(wait.getWaitDetails()?.waitSeconds).toBe(1); + expect(runner.getOperation("Undo 'step-b'").getStatus()).toBeDefined(); + expect(runner.getOperation("Undo 'step-a'").getStatus()).toBeDefined(); + }); + + it("inherits step retry config for rollback steps", async () => { + const calls: string[] = []; + let undoAttempts = 0; + + const handler = workflow.handler(async (_event, ctx) => { + try { + await ctx.stepWithRollback( + "step-a", + { + run: async () => { + calls.push("run:step-a"); + return "result-a"; + }, + undo: async () => { + undoAttempts++; + calls.push(`undo-attempt:${undoAttempts}`); + if (undoAttempts === 1) { + throw new Error("retry undo once"); + } + }, + }, + { + retryStrategy: (_error, attempt) => ({ + shouldRetry: attempt < 2, + delay: { seconds: 1 }, + }), + }, + ); + + await ctx.step("fail", async () => { + throw new Error("boom"); + }); + } catch (error) { + await ctx.rollbackAll(error); + throw error; + } + }); + + const runner = new LocalDurableTestRunner({ handlerFunction: handler }); + const execution = await runner.run(); + + expect(execution.getStatus()).toBe(ExecutionStatus.FAILED); + expect(calls).toEqual([ + "run:step-a", + "undo-attempt:1", + "undo-attempt:2", + ]); + expect(runner.getOperation("Undo 'step-a'").getStepDetails()?.attempt).toBe(2); + }); + + it("stops rollback on undo failure", async () => { + const calls: string[] = []; + + const handler = workflow.handler(async (_event, ctx) => { + try { + await ctx.stepWithRollback("step-a", { + run: async () => { + calls.push("run:step-a"); + return "result-a"; + }, + undo: async () => { + calls.push("undo:step-a"); + }, + }); + + await ctx.stepWithRollback("step-b", { + run: async () => { + calls.push("run:step-b"); + return "result-b"; + }, + undo: async () => { + calls.push("undo:step-b"); + throw new Error("undo failed"); + }, + }); + + await ctx.step("fail", async () => { + throw new Error("boom"); + }); + } catch (error) { + await ctx.rollbackAll(error); + throw error; + } + }); + + const runner = new LocalDurableTestRunner({ handlerFunction: handler }); + const execution = await runner.run(); + + expect(execution.getStatus()).toBe(ExecutionStatus.FAILED); + expect(calls.slice(0, 2)).toEqual(["run:step-a", "run:step-b"]); + expect(calls.slice(2).every((call) => call === "undo:step-b")).toBe(true); + expect(calls).not.toContain("undo:step-a"); + expect(execution.getError().errorType).toBe("RollbackError"); + expect(execution.getError().errorMessage).toContain("step-b"); + }); + + it("allows rollbackAll to be called twice", async () => { + const calls: string[] = []; + + const handler = workflow.handler(async (_event, ctx) => { + try { + await ctx.stepWithRollback("step-a", { + run: async () => { + calls.push("run:step-a"); + return "result-a"; + }, + undo: async () => { + calls.push("undo:step-a"); + }, + }); + + await ctx.step("fail", async () => { + throw new Error("boom"); + }); + } catch (error) { + await ctx.rollbackAll(error); + await ctx.rollbackAll(error); + throw error; + } + }); + + const runner = new LocalDurableTestRunner({ handlerFunction: handler }); + const execution = await runner.run(); + + expect(execution.getStatus()).toBe(ExecutionStatus.FAILED); + expect(calls).toEqual(["run:step-a", "undo:step-a"]); + }); +}); diff --git a/sdk/js/tsconfig.json b/sdk/js/tsconfig.json index 8d5054ba85..16d6c6ba15 100644 --- a/sdk/js/tsconfig.json +++ b/sdk/js/tsconfig.json @@ -13,5 +13,5 @@ "lib": ["ES2022", "DOM"], "types": ["@types/node"] }, - "exclude": ["scripts", "dist"] + "exclude": ["scripts", "dist", "test"] } diff --git a/sdk/python/README.md b/sdk/python/README.md index 40697bc1dc..c65944ee48 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -1,11 +1,74 @@ -# SST SDK +# SST Python SDK -Similar the to the JS SDK, the Python SDK provides a way to access resources in your app. +The Python SDK for [SST](https://sst.dev) lets you access linked resources in your Python Lambda functions. + +## Installation + +```bash +pip install sst-sdk +``` + +Or with uv: + +```bash +uv add sst-sdk +``` + +## Migrating from the Git dependency + +If you were previously installing the SDK from GitHub: + +```toml +# Before +[project] +dependencies = ["sst"] + +[tool.uv.sources] +sst = { git = "https://github.com/anomalyco/sst", subdirectory = "sdk/python" } +``` + +Update your `pyproject.toml` to use the PyPI package instead: + +```toml +# After +[project] +dependencies = ["sst-sdk"] +``` + +That's it β€” remove the `[tool.uv.sources]` entry for `sst` and replace the dependency name. No code changes needed; `from sst import Resource` works the same way. ## Usage +Use `Resource` to access any resource linked to your function in `sst.config.ts`: + ```python from sst import Resource -print(Resource.MyBucket.name) +# Access linked resources by name +bucket_name = Resource.MyBucket.name +table_name = Resource.MyTable.name +``` + +Resources are defined and linked in your `sst.config.ts`: + +```ts +const bucket = new sst.aws.Bucket("MyBucket"); + +new sst.aws.Function("MyFunction", { + handler: "handler.main", + link: [bucket], +}); ``` + +The SDK reads resource bindings from encrypted environment variables set by SST at deploy time. In `sst dev`, resources are available automatically through the local development bridge. + +## Supported Python Versions + +- Python 3.9+ + +## Links + +- [SST Documentation](https://sst.dev/docs/) +- [SDK Reference](https://sst.dev/docs/reference/sdk/#python) +- [Python Examples](https://github.com/anomalyco/sst/tree/dev/examples/aws-python) +- [GitHub](https://github.com/anomalyco/sst) diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index 588534d3fa..d973e742ac 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -1,11 +1,45 @@ [project] -name = "sst" +name = "sst-sdk" version = "0.2.0" -description = "SST SDK" +description = "Python SDK for SST β€” access linked resources in your SST app" readme = "README.md" +license = "MIT" requires-python = ">=3.9" -dependencies = ["cryptography>=43.0.3"] +keywords = ["sst", "serverless", "aws", "lambda", "infrastructure"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Libraries", +] +dependencies = ["pycryptodomex==3.20.0"] + +[project.urls] +Homepage = "https://sst.dev" +Documentation = "https://sst.dev/docs/reference/sdk/#python" +Repository = "https://github.com/anomalyco/sst" +Issues = "https://github.com/anomalyco/sst/issues" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" + +[tool.hatch.build] +ignore-vcs = true + +[tool.hatch.build.targets.wheel] +packages = ["src/sst"] + +[tool.hatch.build.targets.sdist] +include = ["src/sst", "pyproject.toml", "README.md"] + +[dependency-groups] +dev = [ + "pytest>=8.4.2", +] diff --git a/sdk/python/src/sst/__init__.py b/sdk/python/src/sst/__init__.py index fc40174611..766c3d82de 100644 --- a/sdk/python/src/sst/__init__.py +++ b/sdk/python/src/sst/__init__.py @@ -2,7 +2,7 @@ import json import base64 from typing import Dict, Any -from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from Cryptodome.Cipher import AES raw: Dict[str, Any] = {} @@ -16,6 +16,13 @@ if key.startswith("SST_RESOURCE_") and value: raw[key[len("SST_RESOURCE_") :]] = json.loads(value) +# Load consolidated resources JSON (used on Windows to avoid uppercasing) +if "SST_RESOURCES_JSON" in os.environ: + try: + raw.update(json.loads(os.environ["SST_RESOURCES_JSON"])) + except json.JSONDecodeError: + pass + # Check if SST_KEY_FILE and SST_KEY are in environment variables # and SST_KEY_FILE_DATA is not already set in globals() if ( @@ -30,21 +37,16 @@ with open(os.environ["SST_KEY_FILE"], "rb") as f: encryptedData = f.read() - # Create a nonce of 12 zero bytes (as per your original code) + # Create a nonce of 12 zero bytes nonce = bytes(12) # Extract the authentication tag and the actual ciphertext authTag = encryptedData[-16:] actualCiphertext = encryptedData[:-16] - # Concatenate the ciphertext and authTag as required by AESGCM - ciphertext_with_tag = actualCiphertext + authTag - - # Create an AESGCM cipher object with the key - aesgcm = AESGCM(key) - - # Decrypt the ciphertext - plaintext = aesgcm.decrypt(nonce, ciphertext_with_tag, associated_data=None) + # Create AES-GCM cipher and decrypt using PyCryptodomex + cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) + plaintext = cipher.decrypt_and_verify(actualCiphertext, authTag) # Parse the decrypted plaintext as JSON decryptedData = json.loads(plaintext.decode("utf-8")) @@ -90,7 +92,7 @@ def __getattr__(self, prop): if hasattr(raw, prop): return getattr(raw, prop) - if "SST_RESOURCE_App" not in os.environ: + if "SST_RESOURCE_App" not in os.environ and "SST_RESOURCES_JSON" not in os.environ: raise Exception( "It does not look like SST links are active. If this is in local development and you are not starting this process through the multiplexer, wrap your command with `sst dev -- `" ) diff --git a/sdk/python/tests/test_resource.py b/sdk/python/tests/test_resource.py new file mode 100644 index 0000000000..48d383e2e2 --- /dev/null +++ b/sdk/python/tests/test_resource.py @@ -0,0 +1,64 @@ +import json +import os +import importlib +import pytest + +import sst + + +class TestSSTResourcesJSON: + def test_loads_resources_from_json(self, monkeypatch): + monkeypatch.setenv( + "SST_RESOURCES_JSON", + json.dumps({"MyBucket": {"name": "my-bucket"}, "App": {"name": "app", "stage": "dev"}}), + ) + importlib.reload(sst) + + assert sst.Resource.MyBucket.name == "my-bucket" + + def test_merges_with_individual_vars(self, monkeypatch): + monkeypatch.setenv("SST_RESOURCE_MyTable", json.dumps({"name": "my-table"})) + monkeypatch.setenv( + "SST_RESOURCES_JSON", + json.dumps({"MyBucket": {"name": "my-bucket"}, "App": {"name": "app", "stage": "dev"}}), + ) + importlib.reload(sst) + + assert sst.Resource.MyTable.name == "my-table" + assert sst.Resource.MyBucket.name == "my-bucket" + + def test_json_overrides_individual_vars(self, monkeypatch): + monkeypatch.setenv("SST_RESOURCE_MyBucket", json.dumps({"name": "from-env-var"})) + monkeypatch.setenv( + "SST_RESOURCES_JSON", + json.dumps({"MyBucket": {"name": "from-json"}, "App": {"name": "app", "stage": "dev"}}), + ) + importlib.reload(sst) + + assert sst.Resource.MyBucket.name == "from-json" + + def test_invalid_json_is_ignored(self, monkeypatch): + monkeypatch.setenv("SST_RESOURCES_JSON", "not-json") + importlib.reload(sst) + + with pytest.raises(Exception, match='"MyBucket" is not linked'): + _ = sst.Resource.MyBucket + + def test_links_not_active_without_app_or_json(self, monkeypatch): + monkeypatch.delenv("SST_RESOURCE_App", raising=False) + monkeypatch.delenv("SST_RESOURCES_JSON", raising=False) + importlib.reload(sst) + + with pytest.raises(Exception, match="It does not look like SST links are active"): + _ = sst.Resource.MyBucket + + def test_no_links_active_error_with_json(self, monkeypatch): + monkeypatch.delenv("SST_RESOURCE_App", raising=False) + monkeypatch.setenv( + "SST_RESOURCES_JSON", + json.dumps({"MyBucket": {"name": "my-bucket"}, "App": {"name": "app", "stage": "dev"}}), + ) + importlib.reload(sst) + + # Should not raise "links not active" + assert sst.Resource.MyBucket.name == "my-bucket" diff --git a/sdk/python/uv.lock b/sdk/python/uv.lock index dcfcb48cc0..6eec0e9bcc 100644 --- a/sdk/python/uv.lock +++ b/sdk/python/uv.lock @@ -1,5 +1,10 @@ version = 1 +revision = 3 requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] [[package]] name = "cffi" @@ -8,66 +13,75 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, - { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, - { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, - { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, - { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, - { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, - { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, - { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, - { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, - { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, - { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, - { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, - { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, - { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, - { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, - { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, - { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220 }, - { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605 }, - { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, - { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 }, - { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 }, - { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635 }, - { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218 }, - { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 }, - { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 }, - { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 }, - { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820 }, - { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220, upload-time = "2024-09-04T20:45:01.577Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605, upload-time = "2024-09-04T20:45:03.837Z" }, + { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910, upload-time = "2024-09-04T20:45:05.315Z" }, + { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200, upload-time = "2024-09-04T20:45:06.903Z" }, + { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565, upload-time = "2024-09-04T20:45:08.975Z" }, + { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635, upload-time = "2024-09-04T20:45:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218, upload-time = "2024-09-04T20:45:12.366Z" }, + { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486, upload-time = "2024-09-04T20:45:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911, upload-time = "2024-09-04T20:45:15.696Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632, upload-time = "2024-09-04T20:45:17.284Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820, upload-time = "2024-09-04T20:45:18.762Z" }, + { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290, upload-time = "2024-09-04T20:45:20.226Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] @@ -77,52 +91,229 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", size = 686989 } +sdist = { url = "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", size = 686989, upload-time = "2024-10-18T15:58:32.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/f3/01fdf26701a26f4b4dbc337a26883ad5bccaa6f1bbbdd29cd89e22f18a1c/cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", size = 6225303, upload-time = "2024-10-18T15:57:36.753Z" }, + { url = "https://files.pythonhosted.org/packages/a3/01/4896f3d1b392025d4fcbecf40fdea92d3df8662123f6835d0af828d148fd/cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", size = 3760905, upload-time = "2024-10-18T15:57:39.166Z" }, + { url = "https://files.pythonhosted.org/packages/0a/be/f9a1f673f0ed4b7f6c643164e513dbad28dd4f2dcdf5715004f172ef24b6/cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", size = 3977271, upload-time = "2024-10-18T15:57:41.227Z" }, + { url = "https://files.pythonhosted.org/packages/4e/49/80c3a7b5514d1b416d7350830e8c422a4d667b6d9b16a9392ebfd4a5388a/cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", size = 3746606, upload-time = "2024-10-18T15:57:42.903Z" }, + { url = "https://files.pythonhosted.org/packages/0e/16/a28ddf78ac6e7e3f25ebcef69ab15c2c6be5ff9743dd0709a69a4f968472/cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", size = 3986484, upload-time = "2024-10-18T15:57:45.434Z" }, + { url = "https://files.pythonhosted.org/packages/01/f5/69ae8da70c19864a32b0315049866c4d411cce423ec169993d0434218762/cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", size = 3852131, upload-time = "2024-10-18T15:57:47.267Z" }, + { url = "https://files.pythonhosted.org/packages/fd/db/e74911d95c040f9afd3612b1f732e52b3e517cb80de8bf183be0b7d413c6/cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", size = 4075647, upload-time = "2024-10-18T15:57:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/56/48/7b6b190f1462818b324e674fa20d1d5ef3e24f2328675b9b16189cbf0b3c/cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", size = 2623873, upload-time = "2024-10-18T15:57:51.822Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b1/0ebff61a004f7f89e7b65ca95f2f2375679d43d0290672f7713ee3162aff/cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", size = 3068039, upload-time = "2024-10-18T15:57:54.426Z" }, + { url = "https://files.pythonhosted.org/packages/30/d5/c8b32c047e2e81dd172138f772e81d852c51f0f2ad2ae8a24f1122e9e9a7/cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", size = 6222984, upload-time = "2024-10-18T15:57:56.174Z" }, + { url = "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", size = 3762968, upload-time = "2024-10-18T15:57:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", size = 3977754, upload-time = "2024-10-18T15:58:00.683Z" }, + { url = "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", size = 3749458, upload-time = "2024-10-18T15:58:02.225Z" }, + { url = "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", size = 3988220, upload-time = "2024-10-18T15:58:04.331Z" }, + { url = "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", size = 3853898, upload-time = "2024-10-18T15:58:06.113Z" }, + { url = "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", size = 4076592, upload-time = "2024-10-18T15:58:08.673Z" }, + { url = "https://files.pythonhosted.org/packages/81/1e/ffcc41b3cebd64ca90b28fd58141c5f68c83d48563c88333ab660e002cd3/cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", size = 2623145, upload-time = "2024-10-18T15:58:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/87/5c/3dab83cc4aba1f4b0e733e3f0c3e7d4386440d660ba5b1e3ff995feb734d/cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", size = 3068026, upload-time = "2024-10-18T15:58:11.916Z" }, + { url = "https://files.pythonhosted.org/packages/6f/db/d8b8a039483f25fc3b70c90bc8f3e1d4497a99358d610c5067bf3bd4f0af/cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c", size = 3144545, upload-time = "2024-10-18T15:58:13.572Z" }, + { url = "https://files.pythonhosted.org/packages/93/90/116edd5f8ec23b2dc879f7a42443e073cdad22950d3c8ee834e3b8124543/cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3", size = 3679828, upload-time = "2024-10-18T15:58:15.254Z" }, + { url = "https://files.pythonhosted.org/packages/d8/32/1e1d78b316aa22c0ba6493cc271c1c309969e5aa5c22c830a1d7ce3471e6/cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83", size = 3908132, upload-time = "2024-10-18T15:58:16.943Z" }, + { url = "https://files.pythonhosted.org/packages/91/bb/cd2c13be3332e7af3cdf16154147952d39075b9f61ea5e6b5241bf4bf436/cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7", size = 2988811, upload-time = "2024-10-18T15:58:19.674Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/ff7c76afdc4f5933b5e99092528d4783d3d1b131960fc8b31eb38e076ca8/cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664", size = 3146844, upload-time = "2024-10-18T15:58:21.595Z" }, + { url = "https://files.pythonhosted.org/packages/d7/29/a233efb3e98b13d9175dcb3c3146988ec990896c8fa07e8467cce27d5a80/cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08", size = 3681997, upload-time = "2024-10-18T15:58:24.08Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cf/c9eea7791b961f279fb6db86c3355cfad29a73141f46427af71852b23b95/cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa", size = 3905208, upload-time = "2024-10-18T15:58:25.699Z" }, + { url = "https://files.pythonhosted.org/packages/21/ea/6c38ca546d5b6dab3874c2b8fc6b1739baac29bacdea31a8c6c0513b3cfa/cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff", size = 2989787, upload-time = "2024-10-18T15:58:27.521Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/f3/01fdf26701a26f4b4dbc337a26883ad5bccaa6f1bbbdd29cd89e22f18a1c/cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", size = 6225303 }, - { url = "https://files.pythonhosted.org/packages/a3/01/4896f3d1b392025d4fcbecf40fdea92d3df8662123f6835d0af828d148fd/cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", size = 3760905 }, - { url = "https://files.pythonhosted.org/packages/0a/be/f9a1f673f0ed4b7f6c643164e513dbad28dd4f2dcdf5715004f172ef24b6/cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", size = 3977271 }, - { url = "https://files.pythonhosted.org/packages/4e/49/80c3a7b5514d1b416d7350830e8c422a4d667b6d9b16a9392ebfd4a5388a/cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", size = 3746606 }, - { url = "https://files.pythonhosted.org/packages/0e/16/a28ddf78ac6e7e3f25ebcef69ab15c2c6be5ff9743dd0709a69a4f968472/cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", size = 3986484 }, - { url = "https://files.pythonhosted.org/packages/01/f5/69ae8da70c19864a32b0315049866c4d411cce423ec169993d0434218762/cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", size = 3852131 }, - { url = "https://files.pythonhosted.org/packages/fd/db/e74911d95c040f9afd3612b1f732e52b3e517cb80de8bf183be0b7d413c6/cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", size = 4075647 }, - { url = "https://files.pythonhosted.org/packages/56/48/7b6b190f1462818b324e674fa20d1d5ef3e24f2328675b9b16189cbf0b3c/cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", size = 2623873 }, - { url = "https://files.pythonhosted.org/packages/eb/b1/0ebff61a004f7f89e7b65ca95f2f2375679d43d0290672f7713ee3162aff/cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", size = 3068039 }, - { url = "https://files.pythonhosted.org/packages/30/d5/c8b32c047e2e81dd172138f772e81d852c51f0f2ad2ae8a24f1122e9e9a7/cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", size = 6222984 }, - { url = "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", size = 3762968 }, - { url = "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", size = 3977754 }, - { url = "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", size = 3749458 }, - { url = "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", size = 3988220 }, - { url = "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", size = 3853898 }, - { url = "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", size = 4076592 }, - { url = "https://files.pythonhosted.org/packages/81/1e/ffcc41b3cebd64ca90b28fd58141c5f68c83d48563c88333ab660e002cd3/cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", size = 2623145 }, - { url = "https://files.pythonhosted.org/packages/87/5c/3dab83cc4aba1f4b0e733e3f0c3e7d4386440d660ba5b1e3ff995feb734d/cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", size = 3068026 }, - { url = "https://files.pythonhosted.org/packages/6f/db/d8b8a039483f25fc3b70c90bc8f3e1d4497a99358d610c5067bf3bd4f0af/cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c", size = 3144545 }, - { url = "https://files.pythonhosted.org/packages/93/90/116edd5f8ec23b2dc879f7a42443e073cdad22950d3c8ee834e3b8124543/cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3", size = 3679828 }, - { url = "https://files.pythonhosted.org/packages/d8/32/1e1d78b316aa22c0ba6493cc271c1c309969e5aa5c22c830a1d7ce3471e6/cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83", size = 3908132 }, - { url = "https://files.pythonhosted.org/packages/91/bb/cd2c13be3332e7af3cdf16154147952d39075b9f61ea5e6b5241bf4bf436/cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7", size = 2988811 }, - { url = "https://files.pythonhosted.org/packages/cc/fc/ff7c76afdc4f5933b5e99092528d4783d3d1b131960fc8b31eb38e076ca8/cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664", size = 3146844 }, - { url = "https://files.pythonhosted.org/packages/d7/29/a233efb3e98b13d9175dcb3c3146988ec990896c8fa07e8467cce27d5a80/cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08", size = 3681997 }, - { url = "https://files.pythonhosted.org/packages/c0/cf/c9eea7791b961f279fb6db86c3355cfad29a73141f46427af71852b23b95/cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa", size = 3905208 }, - { url = "https://files.pythonhosted.org/packages/21/ea/6c38ca546d5b6dab3874c2b8fc6b1739baac29bacdea31a8c6c0513b3cfa/cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff", size = 2989787 }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pycparser" version = "2.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] name = "sst" -version = "0.1.0" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "cryptography" }, ] +[package.dev-dependencies] +dev = [ + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + [package.metadata] requires-dist = [{ name = "cryptography", specifier = ">=43.0.3" }] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.4.2" }] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] diff --git a/sdk/rust/Cargo.lock b/sdk/rust/Cargo.lock index ee4725af94..ba100cde2f 100644 --- a/sdk/rust/Cargo.lock +++ b/sdk/rust/Cargo.lock @@ -43,6 +43,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + [[package]] name = "cfg-if" version = "1.0.0" @@ -88,6 +94,99 @@ dependencies = [ "cipher", ] +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -109,6 +208,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "ghash" version = "0.5.1" @@ -136,9 +247,30 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "memchr" @@ -146,12 +278,53 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "opaque-debug" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "polyval" version = "0.6.2" @@ -182,13 +355,41 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", ] [[package]] @@ -197,6 +398,27 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + [[package]] name = "serde" version = "1.0.217" @@ -229,6 +451,43 @@ dependencies = [ "serde", ] +[[package]] +name = "serial_test" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "sst_sdk" version = "0.1.0" @@ -237,6 +496,8 @@ dependencies = [ "base64", "serde", "serde_json", + "serial_test", + "tempfile", "thiserror", ] @@ -257,6 +518,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -310,3 +584,33 @@ name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" diff --git a/sdk/rust/Cargo.toml b/sdk/rust/Cargo.toml index 57ec981107..8bebb55028 100644 --- a/sdk/rust/Cargo.toml +++ b/sdk/rust/Cargo.toml @@ -16,3 +16,8 @@ base64 = "0.21.5" serde = "1.0.217" serde_json = "1.0" thiserror = "1.0" + +[dev-dependencies] +serde = { version = "1.0.217", features = ["derive"] } +tempfile = "3.8" +serial_test = "3.0" diff --git a/sdk/rust/src/lib.rs b/sdk/rust/src/lib.rs index 8c4fc81700..635b616bf4 100644 --- a/sdk/rust/src/lib.rs +++ b/sdk/rust/src/lib.rs @@ -30,25 +30,29 @@ pub struct Resource { impl Resource { pub fn init() -> Result { - let key = BASE64_STANDARD.decode(env::var("SST_KEY")?)?; - let encrypted_data = std::fs::read(env::var("SST_KEY_FILE")?)?; + let mut resources: HashMap = HashMap::new(); - let nonce = GenericArray::from_slice(&[0u8; 12]); - let cipher = Aes256Gcm::new(GenericArray::from_slice(&key)); + if let (Ok(sst_key), Ok(sst_key_file)) = (env::var("SST_KEY"), env::var("SST_KEY_FILE")) { + let key = BASE64_STANDARD.decode(sst_key)?; + let encrypted_data = std::fs::read(sst_key_file)?; + + let nonce = GenericArray::from_slice(&[0u8; 12]); + let cipher = Aes256Gcm::new(GenericArray::from_slice(&key)); - let auth_tag_start = encrypted_data.len() - 16; - let actual_ciphertext = &encrypted_data[..auth_tag_start]; - let auth_tag = &encrypted_data[auth_tag_start..]; + let auth_tag_start = encrypted_data.len() - 16; + let actual_ciphertext = &encrypted_data[..auth_tag_start]; + let auth_tag = &encrypted_data[auth_tag_start..]; - let mut ciphertext_with_tag = Vec::with_capacity(encrypted_data.len()); - ciphertext_with_tag.extend_from_slice(actual_ciphertext); - ciphertext_with_tag.extend_from_slice(auth_tag); + let mut ciphertext_with_tag = Vec::with_capacity(encrypted_data.len()); + ciphertext_with_tag.extend_from_slice(actual_ciphertext); + ciphertext_with_tag.extend_from_slice(auth_tag); - let decrypted = cipher - .decrypt(nonce, ciphertext_with_tag.as_ref()) - .map_err(|e| ResourceError::DecryptionError(e.to_string()))?; + let decrypted = cipher + .decrypt(nonce, ciphertext_with_tag.as_ref()) + .map_err(|e| ResourceError::DecryptionError(e.to_string()))?; - let mut resources: HashMap = serde_json::from_slice(&decrypted)?; + resources = serde_json::from_slice(&decrypted)?; + } for (key, value) in env::vars() { if key.starts_with("SST_RESOURCE_") { @@ -57,6 +61,13 @@ impl Resource { } } + // Load consolidated resources JSON (used on Windows to avoid uppercasing) + if let Ok(consolidated) = env::var("SST_RESOURCES_JSON") { + if let Ok(parsed) = serde_json::from_str::>(&consolidated) { + resources.extend(parsed); + } + } + Ok(Self { resources }) } @@ -70,3 +81,234 @@ impl Resource { self.resources } } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use serial_test::serial; + use std::env; + use std::fs::File; + use std::io::Write; + use tempfile::TempDir; + + // Helper to clear all SST-related environment variables + fn clear_env_vars() { + env::remove_var("SST_KEY"); + env::remove_var("SST_KEY_FILE"); + env::remove_var("SST_RESOURCES_JSON"); + + let sst_resource_vars: Vec = env::vars() + .filter(|(key, _)| key.starts_with("SST_RESOURCE_")) + .map(|(key, _)| key) + .collect(); + + for var in sst_resource_vars { + env::remove_var(&var); + } + } + + // Helper to create an encrypted file for testing + fn create_encrypted_file(data: &HashMap) -> (TempDir, String, String) { + let temp_dir = TempDir::new().unwrap(); + let file_path = temp_dir.path().join("encrypted.bin"); + + let key = [0u8; 32]; + let key_base64 = BASE64_STANDARD.encode(&key); + + let json_data = serde_json::to_vec(data).unwrap(); + let nonce = GenericArray::from_slice(&[0u8; 12]); + let cipher = Aes256Gcm::new(GenericArray::from_slice(&key)); + + let ciphertext = cipher.encrypt(nonce, json_data.as_ref()).unwrap(); + + let mut file = File::create(&file_path).unwrap(); + file.write_all(&ciphertext).unwrap(); + + (temp_dir, file_path.to_str().unwrap().to_string(), key_base64) + } + + #[test] + #[serial] + fn test_init_without_sst_key_vars() { + clear_env_vars(); + + let resource = Resource::init(); + assert!(resource.is_ok()); + let resource = resource.unwrap(); + assert_eq!(resource.resources.len(), 0); + } + + #[test] + #[serial] + fn test_init_with_sst_resource_env_vars() { + clear_env_vars(); + + env::set_var("SST_RESOURCE_MyBucket", r#"{"name":"my-bucket","type":"aws.s3.Bucket"}"#); + env::set_var("SST_RESOURCE_MyTable", r#"{"name":"my-table","type":"aws.dynamodb.Table"}"#); + + let resource = Resource::init().unwrap(); + + assert_eq!(resource.resources.len(), 2); + assert!(resource.resources.contains_key("MyBucket")); + assert!(resource.resources.contains_key("MyTable")); + } + + #[test] + #[serial] + fn test_init_with_encrypted_file() { + clear_env_vars(); + + let mut data = HashMap::new(); + data.insert("EncryptedBucket".to_string(), json!({"name": "encrypted-bucket", "type": "aws.s3.Bucket"})); + data.insert("EncryptedQueue".to_string(), json!({"name": "encrypted-queue", "type": "aws.sqs.Queue"})); + + let (_temp_dir, file_path, key_base64) = create_encrypted_file(&data); + + env::set_var("SST_KEY", &key_base64); + env::set_var("SST_KEY_FILE", &file_path); + + let resource = Resource::init().unwrap(); + + assert_eq!(resource.resources.len(), 2); + assert!(resource.resources.contains_key("EncryptedBucket")); + assert!(resource.resources.contains_key("EncryptedQueue")); + } + + #[test] + #[serial] + fn test_init_with_both_encrypted_and_env_vars() { + clear_env_vars(); + + let mut encrypted_data = HashMap::new(); + encrypted_data.insert("EncryptedResource".to_string(), json!({"name": "encrypted", "type": "aws.s3.Bucket"})); + + let (_temp_dir, file_path, key_base64) = create_encrypted_file(&encrypted_data); + + env::set_var("SST_KEY", &key_base64); + env::set_var("SST_KEY_FILE", &file_path); + env::set_var("SST_RESOURCE_EnvResource", r#"{"name":"env-resource","type":"aws.dynamodb.Table"}"#); + + let resource = Resource::init().unwrap(); + + assert_eq!(resource.resources.len(), 2); + assert!(resource.resources.contains_key("EncryptedResource")); + assert!(resource.resources.contains_key("EnvResource")); + } + + #[test] + #[serial] + fn test_get_existing_resource() { + clear_env_vars(); + + env::set_var("SST_RESOURCE_TestBucket", r#"{"name":"test-bucket","arn":"arn:aws:s3:::test-bucket"}"#); + + let resource = Resource::init().unwrap(); + + #[derive(serde::Deserialize, Debug, PartialEq)] + struct BucketInfo { + name: String, + arn: String, + } + + let bucket: BucketInfo = resource.get("TestBucket").unwrap(); + assert_eq!(bucket.name, "test-bucket"); + assert_eq!(bucket.arn, "arn:aws:s3:::test-bucket"); + } + + #[test] + #[serial] + fn test_get_nonexistent_resource() { + clear_env_vars(); + + let resource = Resource::init().unwrap(); + + let result: Result = resource.get("NonExistent"); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), ResourceError::NotFound)); + } + + #[test] + #[serial] + fn test_init_with_only_sst_key() { + clear_env_vars(); + + env::set_var("SST_KEY", "dGVzdA=="); // base64 "test" + + let resource = Resource::init(); + assert!(resource.is_ok()); + } + + #[test] + #[serial] + fn test_into_inner() { + clear_env_vars(); + + env::set_var("SST_RESOURCE_Resource1", r#"{"value":"test1"}"#); + env::set_var("SST_RESOURCE_Resource2", r#"{"value":"test2"}"#); + + let resource = Resource::init().unwrap(); + let inner = resource.into_inner(); + + assert_eq!(inner.len(), 2); + assert!(inner.contains_key("Resource1")); + assert!(inner.contains_key("Resource2")); + } + + #[test] + #[serial] + fn test_init_with_sst_resources_json() { + clear_env_vars(); + + env::set_var("SST_RESOURCES_JSON", r#"{"MyBucket":{"name":"my-bucket"},"App":{"name":"app","stage":"dev"}}"#); + + let resource = Resource::init().unwrap(); + + assert_eq!(resource.resources.len(), 2); + assert!(resource.resources.contains_key("MyBucket")); + assert!(resource.resources.contains_key("App")); + } + + #[test] + #[serial] + fn test_init_with_sst_resources_json_and_env_vars() { + clear_env_vars(); + + env::set_var("SST_RESOURCE_MyTable", r#"{"name":"my-table"}"#); + env::set_var("SST_RESOURCES_JSON", r#"{"MyBucket":{"name":"my-bucket"},"App":{"name":"app","stage":"dev"}}"#); + + let resource = Resource::init().unwrap(); + + assert_eq!(resource.resources.len(), 3); + assert!(resource.resources.contains_key("MyTable")); + assert!(resource.resources.contains_key("MyBucket")); + assert!(resource.resources.contains_key("App")); + } + + #[test] + #[serial] + fn test_sst_resources_json_overrides_env_vars() { + clear_env_vars(); + + env::set_var("SST_RESOURCE_MyBucket", r#"{"name":"from-env-var"}"#); + env::set_var("SST_RESOURCES_JSON", r#"{"MyBucket":{"name":"from-json"},"App":{"name":"app","stage":"dev"}}"#); + + let resource = Resource::init().unwrap(); + + let bucket = resource.resources.get("MyBucket").unwrap(); + assert_eq!(bucket["name"], "from-json"); + } + + #[test] + #[serial] + fn test_invalid_sst_resources_json_ignored() { + clear_env_vars(); + + env::set_var("SST_RESOURCES_JSON", "not-json"); + + let resource = Resource::init(); + assert!(resource.is_ok()); + let resource = resource.unwrap(); + assert_eq!(resource.resources.len(), 0); + } +} \ No newline at end of file diff --git a/www/.gitignore b/www/.gitignore index e38acb5df9..b6cac83de8 100644 --- a/www/.gitignore +++ b/www/.gitignore @@ -21,14 +21,13 @@ pnpm-debug.log* .DS_Store # typedoc output -common-errors-doc.json components-doc.json examples-doc.json cli-doc.json sdk-doc.json # generated docs -src/content/docs/docs/common-errors.mdx src/content/docs/docs/component/ src/content/docs/docs/reference/ !src/content/docs/docs/reference/sdk.mdx +src/content/docs/docs/examples/ diff --git a/www/astro.config.mjs b/www/astro.config.mjs index d07da81c8b..6fa4bd0487 100644 --- a/www/astro.config.mjs +++ b/www/astro.config.mjs @@ -8,8 +8,9 @@ import rehypeAutolinkHeadings from "rehype-autolink-headings"; const sidebar = [ { label: "Intro", slug: "docs" }, - { label: "Workflow", slug: "docs/workflow" }, - { label: "Enterprise", slug: "docs/enterprise" }, + { label: "Basics", slug: "docs/basics" }, + { label: "Examples", slug: "docs/examples" }, + { label: "Changelog", slug: "docs/changelog" }, { label: "Get Started", collapsed: true, @@ -60,9 +61,13 @@ const sidebar = [ label: "How to", collapsed: true, items: [ + { label: "Cloudflare", slug: "docs/cloudflare" }, + { label: "PlanetScale", slug: "docs/planetscale" }, + { label: "Policy Packs", slug: "docs/policy-packs" }, { label: "AWS Accounts", slug: "docs/aws-accounts" }, { label: "IAM Credentials", slug: "docs/iam-credentials" }, { label: "Migrate From v2", slug: "docs/migrate-from-v2" }, + { label: "Migrate From v3", slug: "docs/migrate-from-v3" }, { label: "Custom Domains", slug: "docs/custom-domains" }, { label: "Import Resources", slug: "docs/import-resources" }, { label: "Set up a Monorepo", slug: "docs/set-up-a-monorepo" }, @@ -70,7 +75,7 @@ const sidebar = [ { label: "Share Across Stages", slug: "docs/share-across-stages" }, { label: "Reference Resources", slug: "docs/reference-resources" }, { label: "Environment Variables", slug: "docs/environment-variables" }, - { label: "Policy Packs", slug: "docs/policy-packs" }, + { label: "Upgrade AWS Databases", slug: "docs/upgrade-aws-databases" }, ], }, { @@ -84,18 +89,21 @@ const sidebar = [ "docs/component/aws/bus", "docs/component/aws/vpc", "docs/component/aws/task", - "docs/component/aws/cron", + { + label: "Cron", + slug: "docs/component/aws/cron-v2", + }, "docs/component/aws/auth", "docs/component/aws/nuxt", + "docs/component/aws/dsql", "docs/component/aws/astro", "docs/component/aws/redis", "docs/component/aws/email", "docs/component/aws/react", "docs/component/aws/mysql", "docs/component/aws/remix", - "docs/component/aws/nextjs", "docs/component/aws/queue", - "docs/component/aws/vector", + "docs/component/aws/nextjs", "docs/component/aws/aurora", "docs/component/aws/router", "docs/component/aws/analog", @@ -103,6 +111,7 @@ const sidebar = [ "docs/component/aws/cluster", "docs/component/aws/service", "docs/component/aws/dynamo", + "docs/component/aws/workflow", "docs/component/aws/realtime", "docs/component/aws/sns-topic", "docs/component/aws/function", @@ -112,7 +121,6 @@ const sidebar = [ "docs/component/aws/static-site", "docs/component/aws/solid-start", "docs/component/aws/open-search", - "docs/component/aws/opencontrol", "docs/component/aws/tan-stack-start", "docs/component/aws/kinesis-stream", "docs/component/aws/apigatewayv1", @@ -125,6 +133,7 @@ const sidebar = [ label: "Internal", collapsed: true, items: [ + "docs/component/aws/alb", "docs/component/aws/cdn", "docs/component/aws/app-sync-resolver", "docs/component/aws/app-sync-function", @@ -197,11 +206,14 @@ const sidebar = [ label: "Deprecated", collapsed: true, items: [ + { label: "Cron", slug: "docs/component/aws/cron" }, + { label: "OpenControl", slug: "docs/component/aws/opencontrol" }, { label: "Vpc.v1", slug: "docs/component/aws/vpc-v1" }, { label: "Redis.v1", slug: "docs/component/aws/redis-v1" }, { label: "Service.v1", slug: "docs/component/aws/service-v1" }, { label: "Cluster.v1", slug: "docs/component/aws/cluster-v1" }, { label: "Postgres.v1", slug: "docs/component/aws/postgres-v1" }, + { label: "Vector", slug: "docs/component/aws/vector" }, ], }, ], @@ -210,10 +222,32 @@ const sidebar = [ label: "Cloudflare", collapsed: true, items: [ - "docs/component/cloudflare/kv", + "docs/component/cloudflare/ai", "docs/component/cloudflare/d1", + "docs/component/cloudflare/kv", + "docs/component/cloudflare/cron", + "docs/component/cloudflare/astro", + "docs/component/cloudflare/queue", "docs/component/cloudflare/worker", "docs/component/cloudflare/bucket", + "docs/component/cloudflare/workflow", + "docs/component/cloudflare/rate-limit", + { label: "StaticSite", slug: "docs/component/cloudflare/static-site-v2" }, + "docs/component/cloudflare/hyperdrive", + "docs/component/cloudflare/react-router", + "docs/component/cloudflare/tan-stack-start", + { + label: "Internal", + collapsed: true, + items: ["docs/component/cloudflare/queue-worker-subscriber"], + }, + { + label: "Deprecated", + collapsed: true, + items: [ + { label: "StaticSite", slug: "docs/component/cloudflare/static-site" }, + ], + }, ], }, { @@ -253,8 +287,6 @@ const sidebar = [ "docs/component/experimental/dev-command", ], }, - { label: "Examples", slug: "docs/examples" }, - { label: "Common Errors", slug: "docs/common-errors" }, ]; if (import.meta.env.DEV) { @@ -279,7 +311,9 @@ export default defineConfig({ "/install": "https://raw.githubusercontent.com/sst/sst/dev/install", "/discord": "https://discord.gg/sst", "/guide": "https://guide.sst.dev", + "/docs/workflow": "/docs/basics", "/docs/start/aws/container": "/docs/start/aws/express", + "/docs/common-errors": "/docs/component/aws/svelte-kit/#assets", }, integrations: [ sitemap({ @@ -292,7 +326,7 @@ export default defineConfig({ dark: "./src/assets/logo-dark.svg", replacesTitle: true, }, - lastUpdated: true, + lastUpdated: !process.env.CI, favicon: "/favicon.svg", pagination: false, markdown: { @@ -328,6 +362,7 @@ export default defineConfig({ Header: "./src/components/Header.astro", Footer: "./src/components/Footer.astro", PageTitle: "./src/components/PageTitle.astro", + PageSidebar: "./src/components/PageSidebar.astro", MobileMenuFooter: "./src/components/MobileMenuFooter.astro", }, head: [ diff --git a/www/config.ts b/www/config.ts index f564e9f425..61447be3a3 100644 --- a/www/config.ts +++ b/www/config.ts @@ -22,10 +22,14 @@ export default { name: "Jay", twitter: "https://x.com/jayair", }, + "vimtor": { + name: "vimtor", + twitter: "https://x.com/vimtor", + }, }, email: "hello@sst.dev", sst: "https://sst.dev", - github: "https://github.com/sst/sst", + github: "https://github.com/anomalyco/sst", discord: "https://sst.dev/discord", twitter: "https://x.com/SST_dev", youtube: "https://www.youtube.com/c/sst-dev", diff --git a/www/generate.ts b/www/generate.ts index b9359386cd..6d500cddc7 100644 --- a/www/generate.ts +++ b/www/generate.ts @@ -34,17 +34,17 @@ type CliCommand = { children: CliCommand[]; }; -type CommonError = { - code: string; - message: string; - long: string[]; -}; - const cmd = process.argv[2]; const linkHashes = new Map< TypeDoc.DeclarationReflection, Map >(); +const externalTypeDocLinks = new Map([ + [ + "@aws/durable-execution-sdk-js:DurableContext", + "[the AWS Durable Execution SDK docs](https://docs.aws.amazon.com/durable-functions/sdk-reference/)", + ], +]); function useLinkHashes(module: TypeDoc.DeclarationReflection) { const v = linkHashes.get(module) ?? new Map(); @@ -55,8 +55,10 @@ function useLinkHashes(module: TypeDoc.DeclarationReflection) { configureLogger(); patchCode(); if (!cmd || cmd === "components") { - const components = await buildComponents(); - const sdks = await buildSdk(); + const [components, sdks] = await Promise.all([ + buildComponents(), + buildSdk(), + ]); for (const component of components) { const sourceFile = component.sources![0].fileName; @@ -76,13 +78,14 @@ if (!cmd || cmd === "components") { ) { await generateLinkableDoc(component); } else { + const componentProvider = component.name.split("/")[1]; const sdkName = component.name.split("/")[2]; const sdk = sdks.find( (s) => // ie. vector s.name === sdkName || // ie. aws/realtime - s.name === `aws/${sdkName}` + (componentProvider === "aws" && s.name === `aws/${sdkName}`) ); const sdkNamespace = sdk && useModuleOrNamespace(sdk); // Handle SDK modules are namespaced (ie. aws/realtime) @@ -91,8 +94,11 @@ if (!cmd || cmd === "components") { } } if (!cmd || cmd === "cli") await generateCliDoc(); -if (!cmd || cmd === "common-errors") await generateCommonErrorsDoc(); -if (!cmd || cmd === "examples") await generateExamplesDocs(); +if (!cmd || cmd === "examples") { + await generateExamplesDocs(); + await generateIndividualExampleDocs(); +} +if (!cmd || cmd === "changelog") await generateChangelog(); restoreCode(); function generateCliDoc() { @@ -313,96 +319,133 @@ function generateCliDoc() { } } -function generateCommonErrorsDoc() { - const content = fs.readFileSync("common-errors-doc.json"); - const json = JSON.parse(content.toString()) as CommonError[]; - const outputFilePath = `src/content/docs/docs/common-errors.mdx`; +async function generateChangelog() { + console.info(`Generating changelog...`); + + const REPO = "sst/sst"; + const MIN_MAJOR = 4; + const PER_PAGE = 100; + const outputFilePath = `src/data/changelog.json`; + + type GithubRelease = { + tag_name: string; + published_at: string; + created_at: string; + html_url: string; + body: string | null; + draft: boolean; + prerelease: boolean; + }; + + type ChangelogEntry = { + tag: string; + publishedAt: string; + url: string; + body: string; + }; + + const headers: Record = { + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "sst-docs-changelog-generator", + }; + const token = process.env.GITHUB_TOKEN; + if (token) headers.Authorization = `Bearer ${token}`; + + const releases: GithubRelease[] = []; + for (let page = 1; ; page++) { + const url = `https://api.github.com/repos/${REPO}/releases?per_page=${PER_PAGE}&page=${page}`; + const res = await fetch(url, { headers }); + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error( + `GitHub API ${res.status} ${res.statusText} on ${url}: ${text}` + ); + } + const batch = (await res.json()) as GithubRelease[]; + releases.push(...batch); + if (batch.length < PER_PAGE) break; + + // Stop early once we've gone past the v4 cutoff to save calls. + const passedCutoff = batch.every((r) => { + const major = parseMajor(r.tag_name); + return major !== null && major < MIN_MAJOR; + }); + if (passedCutoff) break; + } + console.debug(` - fetched ${releases.length} total releases`); + + const filtered = releases + .filter((r) => !r.draft) + .filter((r) => { + const major = parseMajor(r.tag_name); + return major !== null && major >= MIN_MAJOR; + }) + .sort( + (a, b) => + new Date(b.published_at).getTime() - + new Date(a.published_at).getTime() + ); + console.debug(` - ${filtered.length} releases match v${MIN_MAJOR}+`); - fs.writeFileSync( - outputFilePath, - [ - renderHeader( - "Common Errors", - "A list of CLI error messages and how to fix them." - ), - renderSourceMessage("cmd/sst/main.go"), - renderImports(outputFilePath), - renderBodyBegin(), - renderCommonErrorsAbout(), - renderCommonErrorsErrors(), - renderBodyEnd(), - ] - .flat() - .join("\n") - ); + const entries: ChangelogEntry[] = filtered.map((r) => ({ + tag: r.tag_name, + publishedAt: r.published_at, + url: r.html_url, + body: cleanBody(r.body), + })); - function renderCommonErrorsAbout() { - return [ - "Below is a collection of common errors you might encounter when using SST.", - "", - ":::tip", - "The error messages in the CLI link to this doc.", - ":::", - "", - "The error messages and descriptions in this doc are auto-generated from the CLI.", - "", - ]; - } + fs.mkdirSync(path.dirname(outputFilePath), { recursive: true }); + fs.writeFileSync(outputFilePath, JSON.stringify(entries, null, 2) + "\n"); - function renderCommonErrorsErrors() { - const lines: string[] = []; + function parseMajor(tag: string): number | null { + const m = /^v(\d+)\./.exec(tag); + return m ? Number(m[1]) : null; + } - for (const error of json) { - console.debug(` - command ${error.code}`); - lines.push( - ``, - `---`, - ``, - `## ${error.code}`, - ``, - `> ${error.message}`, - ``, - ...error.long + function cleanBody(body: string | null): string { + if (!body) return ""; + let text = body.replace(/\r\n/g, "\n"); + + // Strip the trailing "## Changelog" section heading and any blank lines + // immediately preceding it, but keep the bullets that follow. + text = text.replace(/\n*^##\s+Changelog\s*$/im, ""); + + const lines = text.split("\n").map((line) => { + // Match a leading bullet, an optional [hash](url) or bare hash, + // an optional ":" or whitespace separator, then the subject. + // Handles all observed formats: + // * abc1234 subject + // * abc1234: subject + // * [abc1234](https://...): subject + // * [abc1234](https://...) subject + const m = line.match( + /^(\s*[-*]\s+)(?:\[[0-9a-f]{7,40}\](?:\([^)]+\))?|[0-9a-f]{7,40})[:\s]\s*(.*)$/i ); - } - return lines; - } + if (m) return `${m[1]}${m[2]}`; + return line; + }); - function renderCliDescription(description: CliCommand["description"]) { - return description.long ?? description.short; - } + let result = lines.join("\n"); - function renderCliArgName(prop: CliCommand["args"][number]) { - return `${prop.name}${prop.required ? "" : "?"}`; - } + // Strip trailing "(@username)" author attributions from each line. + result = result.replace(/[ \t]*\(@[\w-]+\)[ \t]*$/gm, ""); - function renderCliCommandUsage(command: CliCommand) { - const parts: string[] = []; + result = result.trim(); - parts.push(command.name); - command.args.forEach((arg) => - arg.required ? parts.push(`<${arg.name}>`) : parts.push(`[${arg.name}]`) + // Linkify GitHub PR/issue references (#1234) so they remain clickable. + // Skip refs that are already inside a markdown link `[#1234]` or part of + // a URL path (`/issues/1234#anchor` etc). + result = result.replace( + /(^|[^\[\/\w])#(\d{2,})(?!\])/g, + (_, prefix, num) => + `${prefix}[#${num}](https://github.com/${REPO}/issues/${num})` ); - return parts.join(" "); - } - function renderCliFlagType(type: CliCommand["flags"][number]["type"]) { - if (type.startsWith("[") && type.endsWith("]")) { - return type - .substring(1, type.length - 1) - .split(",") - .map((t: string) => - [ - ``, - `${t}`, - ``, - ].join("") - ) - .join(` | `); - } + // Unwrap parentheses around inline PR/issue links: "([#1234](url))" β†’ "[#1234](url)". + result = result.replace(/\(\[#(\d+)\]\(([^)]+)\)\)/g, "[#$1]($2)"); - if (type === "bool") return `boolean`; - return `${type}`; + return result; } } @@ -421,7 +464,7 @@ async function generateExamplesDocs() { return [ ``, `---`, - renderTdComment(module.children![0].comment?.summary!), + renderExampleComment(module.children![0].comment?.summary!), ...renderRunFunction(module), ``, `View the [full example](${config.github}/tree/dev/examples/${ @@ -453,6 +496,95 @@ async function generateExamplesDocs() { } function renderRunFunction(module: TypeDoc.DeclarationReflection) { + const lines = fs + .readFileSync(path.join(`../examples`, module.sources![0].fileName)) + .toString() + .replace(/\t/g, " ") + .split("\n"); + return [ + '```ts title="sst.config.ts"', + ...extractRunSnippet(lines), + "```", + ]; + } +} + +async function generateIndividualExampleDocs() { + const HANDLER_EXTENSIONS: Record = { + ".ts": "ts", + ".js": "js", + ".py": "python", + ".go": "go", + ".rs": "rs", + }; + + function resolveHandlerFiles( + runBody: string, + exampleDir: string + ): { relPath: string; content: string; lang: string }[] { + const strings = [...runBody.matchAll(/"([^"]+)"/g)].map((m) => m[1]); + const seen = new Set(); + const results: { relPath: string; content: string; lang: string }[] = []; + + for (const str of strings) { + if (str.includes(" ") || str.includes(":") || str.includes("*")) continue; + + const candidates: string[] = []; + const ext = path.extname(str); + if (ext in HANDLER_EXTENSIONS) { + candidates.push(str); + } else { + const lastDot = str.lastIndexOf("."); + if (lastDot > 0) { + const base = str.substring(0, lastDot); + for (const ext of Object.keys(HANDLER_EXTENSIONS)) { + candidates.push(base + ext); + } + } + } + + for (const candidate of candidates) { + const normalized = candidate.replace(/^\.\//, ""); + if (seen.has(normalized)) continue; + const fullPath = path.join(exampleDir, normalized); + try { + if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) { + seen.add(normalized); + const fileExt = path.extname(normalized); + results.push({ + relPath: normalized, + content: fs.readFileSync(fullPath).toString().trimEnd(), + lang: HANDLER_EXTENSIONS[fileExt] || "ts", + }); + break; + } + } catch { + // skip unresolvable paths + } + } + } + + return results; + } + const modules = await buildExamples(); + const outputDir = `src/content/docs/docs/examples`; + fs.mkdirSync(outputDir, { recursive: true }); + + for (const module of modules) { + const dirName = module.name.split("/")[0]; + const comment = module.children![0].comment!; + const commentText = renderExampleComment(comment.summary); + + // Extract title from the ## heading in the comment + const titleMatch = commentText.match(/^##\s+(.+)/m); + if (!titleMatch) continue; + const title = titleMatch[1].trim(); + + // Description is everything after the ## heading + const description = commentText + .replace(/^##\s+.+\n*/m, "") + .trim(); + const lines = fs .readFileSync(path.join(`../examples`, module.sources![0].fileName)) .toString() @@ -460,14 +592,90 @@ async function generateExamplesDocs() { .split("\n"); const start = lines.indexOf(" async run() {"); const end = lines.lastIndexOf(" },"); - return [ + const codeBlock = [ '```ts title="sst.config.ts"', - ...lines.slice(start + 1, end).map((l) => l.substring(4)), + ...extractRunSnippet(lines), "```", ]; + + // Detect handler files referenced in sst.config.ts + const exampleDir = path.join(`../examples`, dirName); + const runBody = lines.slice(start + 1, end).join("\n"); + const handlerFiles = resolveHandlerFiles(runBody, exampleDir) + .filter(({ relPath }) => !description.includes(`title="${relPath}"`)); + + const outputFilePath = path.join(outputDir, `${dirName}.mdx`); + console.info(`Generating individual example page: ${dirName}...`); + fs.writeFileSync( + outputFilePath, + [ + `---`, + `title: "${title}"`, + `description: "${description.split("\n\n")[0].replace(/\n/g, " ").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/`/g, "").replace(/"/g, '\\"')}"`, + `---`, + ``, + `{/* DO NOT EDIT. AUTO-GENERATED FROM "examples/${dirName}" */}`, + ``, + `:::tip`, + `This page is best viewed through the site search or through the _AI_.`, + `:::`, + ``, + description, + ``, + ...codeBlock, + ...handlerFiles.flatMap(({ relPath, content, lang }) => [ + ``, + `\`\`\`${lang} title="${relPath}"`, + content, + `\`\`\``, + ]), + ``, + `View the [full example](${config.github}/tree/dev/examples/${dirName}).`, + ``, + ].join("\n") + ); } } +function extractRunSnippet(lines: string[]) { + const start = lines.indexOf(" async run() {"); + const end = lines.lastIndexOf(" },"); + return stripTopLevelReturnObject( + lines.slice(start + 1, end).map((l) => l.substring(4)) + ); +} + +function stripTopLevelReturnObject(lines: string[]) { + const start = lines.findIndex((line) => /^return\s*\{/.test(line)); + if (start === -1) return trimTrailingBlankLines(lines); + + const end = findReturnObjectEnd(lines, start); + if (end === -1) return trimTrailingBlankLines(lines); + + const before = trimTrailingBlankLines(lines.slice(0, start)); + return trimTrailingBlankLines([...before, ...lines.slice(end + 1)]); +} + +function findReturnObjectEnd(lines: string[], start: number) { + let depth = 0; + + for (let i = start; i < lines.length; i++) { + for (const char of lines[i]) { + if (char === "{") depth++; + if (char === "}") depth--; + } + if (depth === 0 && /}\s*;?\s*$/.test(lines[i])) return i; + } + + return -1; +} + +function trimTrailingBlankLines(lines: string[]) { + const result = [...lines]; + while (result.length && result[result.length - 1].trim() === "") result.pop(); + return result; +} + async function generateGlobalConfigDoc( module: TypeDoc.DeclarationReflection, iamEditComponent: TypeDoc.DeclarationReflection @@ -606,7 +814,7 @@ async function generateComponentDoc( const className = useClassName(component); const fullClassName = `${useClassProviderNamespace(component)}.${className}`; const matchRet = component.name.match(/-(v\d+)$/); - const version = matchRet ? `.${matchRet[1]}` : ""; + const version = matchRet && !className.toLowerCase().endsWith(matchRet[1]) ? `.${matchRet[1]}` : ""; // Remove leading `components/` // module.name = "components/aws/bucket" @@ -641,28 +849,14 @@ async function generateComponentDoc( const lines = [ ...renderLinks(component), ...renderCloudflareBindings(component), - ...(["realtime", "task"].includes(sdk?.name!) + ...(["realtime", "task", "workflow"].includes(sdk?.name!) ? renderAbout(useModuleComment(sdk!)) : []), - ...(() => { - if (!["opencontrol"].includes(sdk?.name!)) return []; - for (const variable of sdk!.children!) { - if (variable.name === "tools") { - // @ts-expect-error - variable.type = { - type: "reference", - name: "Tools", - package: "opencontrol", - }; - } - } - return renderVariables(sdk!); - })(), ...(sdk ? renderFunctions( sdk, useModuleFunctions(sdk), - ["realtime", "task"].includes(sdk.name) + ["realtime", "task", "workflow"].includes(sdk.name) ? { prefix: sdk.name } : undefined ) @@ -726,6 +920,20 @@ function renderTdComment(parts: TypeDoc.CommentDisplayPart[]) { return parts.map((part) => part.text).join(""); } +function renderExampleComment(parts: TypeDoc.CommentDisplayPart[]) { + return stripSstConfigReturns(renderTdComment(parts)); +} + +function stripSstConfigReturns(markdown: string) { + return markdown.replace( + /```([^\n]*\btitle=(["'])sst\.config\.ts\2[^\n]*)\n([\s\S]*?)```/g, + (_, meta: string, _quote: string, code: string) => { + const stripped = stripTopLevelReturnObject(code.split("\n")).join("\n"); + return `\`\`\`${meta}\n${stripped}\n\`\`\``; + } + ); +} + function renderBodyEnd() { return [""]; } @@ -760,6 +968,7 @@ function renderType( if (type.type === "literal") return renderLiteralType(type); if (type.type === "templateLiteral") return renderTemplateLiteralType(type); if (type.type === "union") return renderUnionType(type); + if (type.type === "indexedAccess") return renderIndexedAccessType(type); if (type.type === "array") return renderArrayType(type); if (type.type === "tuple") return renderTupleType(type); if (type.type === "reference" && type.package === "typescript") { @@ -786,9 +995,6 @@ function renderType( if (type.type === "reference" && type.package === "esbuild") { return renderEsbuildType(type); } - if (type.type === "reference" && type.package === "opencontrol") { - return renderOpencontrolType(type); - } if ( // when bun is installed globally, package is `bun-types` (type.type === "reference" && type.package === "bun-types") || @@ -797,18 +1003,30 @@ function renderType( ) { return renderBunShellType(type); } + if (type.type === "reference") { + return renderReferenceType(type); + } if (type.type === "reflection" && type.declaration.signatures) { return renderCallbackType(type); } if (type.type === "reflection" && type.declaration.children?.length) { return renderObjectType(type); } + if (type.type === "unknown") { + return renderUnknownType(type as TypeDoc.SomeType & { name?: string }); + } // @ts-expect-error delete type._project; console.log(type); throw new Error(`Unsupported type "${type.type}"`); } + function renderUnknownType(type: TypeDoc.SomeType & { name?: string }) { + return `${(type.name ?? "unknown") + .replace(/&/g, "&") + .replace(//g, ">")}`; + } function renderIntrisicType(type: TypeDoc.IntrinsicType) { return `${type.name}`; } @@ -822,6 +1040,9 @@ function renderType( if (type.value === true || type.value === false) { return `${type.value}`; } + if (typeof type.value !== "string") { + return `${String(type.value)}`; + } // String value // ie. // { @@ -863,6 +1084,9 @@ function renderType( const tail = type.tail[0][1].replace("{", "\\{").replace("}", "\\}"); return `${head}$\\{${type.tail[0][0].name}\\}${tail}`; } + function renderIndexedAccessType(type: TypeDoc.IndexedAccessType) { + return `${renderSomeType(type.objectType)}[${renderSomeType(type.indexType)}]`; + } function renderUnionType(type: TypeDoc.UnionType) { return type.types .map((t) => renderSomeType(t)) @@ -887,6 +1111,18 @@ function renderType( `>`, ].join(""); } + function renderReferenceType(type: TypeDoc.ReferenceType) { + return [ + `${type.name}`, + ...(type.typeArguments?.length + ? [ + `<`, + type.typeArguments.map((t) => renderSomeType(t)).join(", "), + `>`, + ] + : []), + ].join(""); + } function renderSstComponentType(type: TypeDoc.ReferenceType) { if (type.name === "Transform") { const renderedType = renderSomeType(type.typeArguments?.[0]!); @@ -959,8 +1195,10 @@ function renderType( } // types in different doc - const fileName = (type.reflection as TypeDoc.DeclarationReflection) - ?.sources?.[0].fileName; + const fileName = + (type.reflection as TypeDoc.DeclarationReflection)?.sources?.[0].fileName || + // Some local helper types only carry a ReflectionSymbolId target. + ((type as any)._target?.fileName as string | undefined); if (fileName?.startsWith("platform/src/components/")) { const docHash = type.name.endsWith("Args") ? `#${type.name.toLowerCase()}` @@ -992,7 +1230,20 @@ function renderType( return `[${ type.name }](#${type.name.toLowerCase()})`; - } else if (type.name === "T") { + } + const fileName = + (type.reflection as TypeDoc.DeclarationReflection)?.sources?.[0].fileName || + ((type as any)._target?.fileName as string | undefined); + if ( + fileName?.startsWith("sdk/js/src/") || + fileName?.includes("/sdk/js/src/") + ) { + return renderReferenceType(type); + } + if (type.refersToTypeParameter || (module.children ?? []).find((c) => c.name === type.name)) { + return renderReferenceType(type); + } + if (type.name === "T") { return `string`; } @@ -1080,6 +1331,7 @@ function renderType( DistributionCustomErrorResponse: "cloudfront/distribution", DistributionDefaultCacheBehavior: "cloudfront/distribution", DistributionOrderedCacheBehavior: "cloudfront/distribution", + PolicyDocument: "iam/getpolicydocument", }[type.name]; if (!link) { // @ts-expect-error @@ -1164,9 +1416,6 @@ function renderType( const hash = type.name === "Loader" ? `#loader` : "#build"; return `[${type.name}](https://esbuild.github.io/api/${hash})`; } - function renderOpencontrolType(type: TypeDoc.ReferenceType) { - return `[${type.name}](https://opencontrol.ai/)`; - } function renderBunShellType(type: TypeDoc.ReferenceType) { return `[Bun Shell](https://bun.sh/docs/runtime/shell)`; } @@ -1602,7 +1851,8 @@ function renderInterfacesAtH2Level( const interfaces = useModuleInterfaces(module) .filter((c) => !c.comment?.modifierTags.has("@internal")) .filter((c) => !c.comment?.blockTags.find((t) => t.tag === "@deprecated")) - .filter((c) => !opts.filter || opts.filter(c)); + .filter((c) => !opts.filter || opts.filter(c)) + .filter((c) => c.children?.length); for (const int of interfaces) { console.debug(` - interface ${int.name}`); @@ -1613,6 +1863,7 @@ function renderInterfacesAtH2Level( if (int.comment?.summary) { lines.push(``, renderTdComment(int.comment?.summary!)); } + lines.push(...renderInterfaceInheritedApiSummary(int)); // props for (const prop of useInterfaceProps(int)) { @@ -1708,27 +1959,39 @@ function renderInterfacesAtH3Level(module: TypeDoc.DeclarationReflection) { ``, `**Type** ${renderType(module, int)}`, ``, + ...renderInterfaceInheritedApiInline(i), ...renderNestedTypeList(module, int), ``, ``, // nested props (ie. `.domain`, `.transform`) ...useNestedTypes(int.type!, int.name).flatMap( - ({ depth, prefix, subType }) => [ - `${renderName(subType)}`, - ``, - `

`, - ``, - `**Type** ${renderType(module, subType)}`, - ``, - `
`, - ...renderDefaultTag(module, subType), - ...renderDescription(subType), - ``, - ...renderExamples(subType), - ``, - ] + ({ depth, prefix, subType }) => { + return subType.kind === TypeDoc.ReflectionKind.Method + ? renderMethod(module, subType, { + methodTitle: `${renderName( + subType + )}`, + parametersTitle: `**Parameters**`, + }) + : [ + `${renderName(subType)}`, + ``, + `
`, + ``, + `**Type** ${renderType(module, subType)}`, + ``, + `
`, + ...renderDefaultTag(module, subType), + ...renderDescription(subType), + ``, + ...renderExamples(subType), + `
`, + ]; + } ) ); } @@ -1867,6 +2130,38 @@ function renderSignature(signature: TypeDoc.SignatureReflection) { .join(", "); return `${signature.name}(${parameters})`; } +function renderInterfaceInheritedApiInline(int: TypeDoc.DeclarationReflection) { + const links = renderExternalExtendedTypeLinks(int); + if (!links.length) return []; + + return [ + ``, + `Only showing custom SDK methods here. For the full API, see ${links.join(", ")}.`, + ``, + ]; +} +function renderInterfaceInheritedApiSummary(int: TypeDoc.DeclarationReflection) { + const links = renderExternalExtendedTypeLinks(int); + if (!links.length) return []; + + return [ + ``, + `Only showing custom SDK methods here. For the full API, see ${links.join(", ")}.`, + ]; +} +function renderExternalExtendedTypeLinks(int: TypeDoc.DeclarationReflection) { + return (int.extendedTypes ?? []) + .filter( + (type): type is TypeDoc.ReferenceType => + type.type === "reference" && Boolean(type.package) + ) + .map((type) => { + const link = externalTypeDocLinks.get(`${type.package}:${type.name}`); + if (!link) return undefined; + return link; + }) + .filter((type): type is string => Boolean(type)); +} function renderJsonParseReviverType() { return `[JSON.parse reviver](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#reviver)`; } @@ -1981,6 +2276,7 @@ function useInterfaceProps(i: TypeDoc.DeclarationReflection) { if (!i.children?.length) throw new Error(`Interface ${i.name} has no props`); return i.children + .filter((c) => !c.flags.isExternal) .filter((c) => !c.comment?.modifierTags.has("@internal")) .filter((c) => !c.comment?.blockTags.find((t) => t.tag === "@deprecated")); } @@ -2008,7 +2304,9 @@ function useNestedTypes( } if (type.type === "reflection" && type.declaration.children?.length) { return type.declaration - .children!.filter((c) => !c.comment?.modifierTags.has("@internal")) + .children! + .filter((c) => !c.flags.isExternal) + .filter((c) => !c.comment?.modifierTags.has("@internal")) .filter((c) => !c.comment?.blockTags.find((t) => t.tag === "@deprecated")) .flatMap((subType) => [ { prefix, subType, depth }, @@ -2186,6 +2484,7 @@ async function buildComponents() { "../platform/src/components/aws/cognito-user-pool.ts", "../platform/src/components/aws/cognito-user-pool-client.ts", "../platform/src/components/aws/cron.ts", + "../platform/src/components/aws/cron-v2.ts", "../platform/src/components/aws/dynamo.ts", "../platform/src/components/aws/dynamo-lambda-subscriber.ts", "../platform/src/components/aws/efs.ts", @@ -2199,6 +2498,7 @@ async function buildComponents() { "../platform/src/components/aws/astro.ts", "../platform/src/components/aws/nextjs.ts", "../platform/src/components/aws/nuxt.ts", + "../platform/src/components/aws/dsql.ts", "../platform/src/components/aws/realtime.ts", "../platform/src/components/aws/realtime-lambda-subscriber.ts", "../platform/src/components/aws/react.ts", @@ -2224,11 +2524,25 @@ async function buildComponents() { "../platform/src/components/aws/task.ts", "../platform/src/components/aws/vpc.ts", "../platform/src/components/aws/vpc-v1.ts", - "../platform/src/components/cloudflare/worker.ts", + "../platform/src/components/aws/workflow.ts", + "../platform/src/components/cloudflare/ai.ts", + "../platform/src/components/cloudflare/astro.ts", "../platform/src/components/cloudflare/bucket.ts", + "../platform/src/components/cloudflare/cron.ts", "../platform/src/components/cloudflare/d1.ts", + "../platform/src/components/cloudflare/hyperdrive.ts", "../platform/src/components/cloudflare/kv.ts", + "../platform/src/components/cloudflare/queue.ts", + "../platform/src/components/cloudflare/queue-worker-subscriber.ts", + "../platform/src/components/cloudflare/react-router.ts", + "../platform/src/components/cloudflare/worker.ts", + "../platform/src/components/cloudflare/workflow.ts", + "../platform/src/components/cloudflare/rate-limit.ts", + "../platform/src/components/cloudflare/static-site.ts", + "../platform/src/components/cloudflare/static-site-v2.ts", + "../platform/src/components/cloudflare/tan-stack-start.ts", // internal + "../platform/src/components/aws/alb.ts", "../platform/src/components/aws/cdn.ts", "../platform/src/components/aws/dns.ts", "../platform/src/components/aws/iam-edit.ts", @@ -2280,7 +2594,7 @@ async function buildComponents() { })(); // Generate JSON (generated for debugging purposes) - await app.generateJson(project, "components-doc.json"); + if (process.env.DEBUG) await app.generateJson(project, "components-doc.json"); return project.getChildrenByKind(TypeDoc.ReflectionKind.Module); } @@ -2297,8 +2611,8 @@ async function buildSdk() { entryPoints: [ "../sdk/js/src/aws/realtime.ts", "../sdk/js/src/aws/task.ts", + "../sdk/js/src/aws/workflow.ts", "../sdk/js/src/vector/index.ts", - "../sdk/js/src/opencontrol.ts", ], tsconfig: "../sdk/js/tsconfig.json", }); @@ -2307,7 +2621,7 @@ async function buildSdk() { if (!project) throw new Error("Failed to convert project"); // Generate JSON (generated for debugging purposes) - await app.generateJson(project, "sdk-doc.json"); + if (process.env.DEBUG) await app.generateJson(project, "sdk-doc.json"); return project.getChildrenByKind(TypeDoc.ReflectionKind.Module); } @@ -2329,7 +2643,7 @@ async function buildExamples() { if (!project) throw new Error("Failed to convert project"); // Generate JSON (generated for debugging purposes) - await app.generateJson(project, "examples-doc.json"); + if (process.env.DEBUG) await app.generateJson(project, "examples-doc.json"); return project.children!.filter( (c) => diff --git a/www/package.json b/www/package.json index ab1031fa0d..7a9b3ef6dd 100644 --- a/www/package.json +++ b/www/package.json @@ -8,13 +8,12 @@ "build": "bun generate && astro build", "preview": "astro preview", "astro": "astro", - "generate": "bun generate-cli-json && bun generate-errors-json && tsx generate.ts", - "generate-components": "tsx generate.ts components", - "generate-examples": "tsx generate.ts examples", - "generate-cli": "bun generate-cli-json && tsx generate.ts cli", - "generate-cli-json": "go run ../cmd/sst introspect > cli-doc.json", - "generate-errors": "bun generate-errors-json && tsx generate.ts common-errors", - "generate-errors-json": "go run ../cmd/sst common-errors > common-errors-doc.json" + "generate": "bun generate-cli-json && bun ./generate.ts", + "generate-changelog": "bun ./generate.ts changelog", + "generate-components": "bun ./generate.ts components", + "generate-examples": "bun ./generate.ts examples", + "generate-cli": "bun generate-cli-json && bun ./generate.ts cli", + "generate-cli-json": "go run ../cmd/sst introspect > cli-doc.json" }, "dependencies": { "@astro-community/astro-embed-youtube": "^0.5.3", @@ -35,7 +34,6 @@ }, "devDependencies": { "@types/node": "^20.10.5", - "tsx": "^4.7.0", "typedoc": "0.25.7", "typescript": "5.3.3" } diff --git a/www/public/testimonials/marv.webp b/www/public/testimonials/marv.webp new file mode 100644 index 0000000000..c3ee077eba Binary files /dev/null and b/www/public/testimonials/marv.webp differ diff --git a/www/public/testimonials/rodrigo.webp b/www/public/testimonials/rodrigo.webp new file mode 100644 index 0000000000..bdce8b21a4 Binary files /dev/null and b/www/public/testimonials/rodrigo.webp differ diff --git a/www/public/testimonials/sockthedev.webp b/www/public/testimonials/sockthedev.webp new file mode 100644 index 0000000000..d7dae9a0cd Binary files /dev/null and b/www/public/testimonials/sockthedev.webp differ diff --git a/www/public/testimonials/waterbear.webp b/www/public/testimonials/waterbear.webp new file mode 100644 index 0000000000..bbeb566e47 Binary files /dev/null and b/www/public/testimonials/waterbear.webp differ diff --git a/www/public/testimonials/zac.webp b/www/public/testimonials/zac.webp new file mode 100644 index 0000000000..cccdc96118 Binary files /dev/null and b/www/public/testimonials/zac.webp differ diff --git a/www/src/assets/docs/workflow/editor-autocomplete.png b/www/src/assets/docs/basics/editor-autocomplete.png similarity index 100% rename from www/src/assets/docs/workflow/editor-autocomplete.png rename to www/src/assets/docs/basics/editor-autocomplete.png diff --git a/www/src/assets/docs/workflow/editor-help.png b/www/src/assets/docs/basics/editor-help.png similarity index 100% rename from www/src/assets/docs/workflow/editor-help.png rename to www/src/assets/docs/basics/editor-help.png diff --git a/www/src/assets/docs/workflow/editor-typecheck.png b/www/src/assets/docs/basics/editor-typecheck.png similarity index 100% rename from www/src/assets/docs/workflow/editor-typecheck.png rename to www/src/assets/docs/basics/editor-typecheck.png diff --git a/www/src/assets/docs/workflow/sst-console-autodeploy.png b/www/src/assets/docs/basics/sst-console-autodeploy.png similarity index 100% rename from www/src/assets/docs/workflow/sst-console-autodeploy.png rename to www/src/assets/docs/basics/sst-console-autodeploy.png diff --git a/www/src/components/Changelog.astro b/www/src/components/Changelog.astro new file mode 100644 index 0000000000..1911b8f9ce --- /dev/null +++ b/www/src/components/Changelog.astro @@ -0,0 +1,152 @@ +--- +import { createMarkdownProcessor } from "@astrojs/markdown-remark"; +import data from "../data/changelog.json"; + +type Entry = { + tag: string; + publishedAt: string; + url: string; + body: string; +}; + +const entries = data as Entry[]; + +const processor = await createMarkdownProcessor(); + +const dateFormatter = new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "long", + day: "numeric", +}); + +function formatTag(tag: string): string { + return tag.replace(/^v/, ""); +} + +const rendered = await Promise.all( + entries.map(async (entry) => { + const html = entry.body + ? (await processor.render(entry.body)).code + : ""; + return { + ...entry, + html, + label: formatTag(entry.tag), + anchor: formatTag(entry.tag), + formattedDate: dateFormatter.format(new Date(entry.publishedAt)), + }; + }), +); + +const releases = rendered.map((entry, index) => ({ + ...entry, + showDate: entry.formattedDate !== rendered[index - 1]?.formattedDate, +})); +--- + +
    + {releases.map((entry) => ( +
  • +
    +

    + {entry.label} +

    + +
    +
    + {entry.html + ? + :

    No release notes.

    + } +
    +
  • + ))} +
+ + diff --git a/www/src/components/Footer.astro b/www/src/components/Footer.astro index 824685db4d..84d6c7d82f 100644 --- a/www/src/components/Footer.astro +++ b/www/src/components/Footer.astro @@ -33,9 +33,6 @@ else if (slug === "docs/reference/global") { else if (slug === "docs/reference/cli") { editUrl = new URL("cmd/sst/main.go", editLink); } -else if (slug === "docs/common-errors") { - editUrl = new URL("pkg/project/stack.go", editLink); -} else if (slug === "docs/examples") { editUrl = new URL("examples", editLink); } diff --git a/www/src/components/PageSidebar.astro b/www/src/components/PageSidebar.astro new file mode 100644 index 0000000000..7ebfe3ec24 --- /dev/null +++ b/www/src/components/PageSidebar.astro @@ -0,0 +1,48 @@ +--- +import Default from "@astrojs/starlight/components/PageSidebar.astro"; +import changelog from "../data/changelog.json"; + +type Release = { + tag: string; +}; + +function isMinorOrMajor(tag: string): boolean { + return /^v\d+\.\d+\.0$/.test(tag); +} + +function formatTag(tag: string): string { + return tag.replace(/^v/, ""); +} + +const slug = Astro.url.pathname.replace(/^\//, "").replace(/\/$/, ""); +const route = Astro.locals.starlightRoute; +const isChangelog = slug === "docs/changelog"; + +if (isChangelog && route.toc) { + route.toc = { + ...route.toc, + items: (changelog as Release[]) + .filter((release) => isMinorOrMajor(release.tag)) + .map((release) => ({ + depth: 2, + slug: formatTag(release.tag), + text: formatTag(release.tag), + children: [], + })), + }; +} +--- + +
+ +
+ + diff --git a/www/src/components/TestimonialWall.astro b/www/src/components/TestimonialWall.astro new file mode 100644 index 0000000000..bf0d9cae1c --- /dev/null +++ b/www/src/components/TestimonialWall.astro @@ -0,0 +1,103 @@ +--- +interface Testimonial { + name: string; + avatar: string; + text: string; + url?: string; +} + +interface Props { + testimonials: Testimonial[]; +} + +const { testimonials } = Astro.props; +--- + +
+ {testimonials.map((t) => { + const Tag = t.url ? 'a' : 'div'; + return ( + +

{t.text}

+
+ {t.name} + {t.name} +
+
+ ); + })} +
+ + diff --git a/www/src/content/docs/blog/sst-v3.mdx b/www/src/content/docs/blog/sst-v3.mdx index a761bb8999..8a60eed229 100644 --- a/www/src/content/docs/blog/sst-v3.mdx +++ b/www/src/content/docs/blog/sst-v3.mdx @@ -116,7 +116,7 @@ These are great for getting started with SST in general. But we also recommend c - **Docs** - Start with the [What is SST](/docs/) and [Workflow](/docs/workflow/) docs. These give you a good overview and lead you to other docs that expand on the various concepts. + Start with the [What is SST](/docs/) and [Basics](/docs/basics/) docs. These give you a good overview and lead you to other docs that expand on the various concepts. - **Guide** diff --git a/www/src/content/docs/docs/all-providers.mdx b/www/src/content/docs/docs/all-providers.mdx index 1fae18779d..0d092eac34 100644 --- a/www/src/content/docs/docs/all-providers.mdx +++ b/www/src/content/docs/docs/all-providers.mdx @@ -198,7 +198,7 @@ If you want SST to support a Terraform provider or update a version, you can **s | [MongoDB Atlas](https://www.pulumi.com/registry/packages/mongodbatlas) | `mongodbatlas` | | [MSSQL](https://www.pulumi.com/registry/packages/mssql) | `@pulumiverse/mssql` | | [MySQL](https://www.pulumi.com/registry/packages/mysql) | `mysql` | -| [Neon](https://github.com/kislerdm/terraform-provider-neon) | `neon` | +| [Neon](https://www.pulumi.com/registry/packages/neon) | `neon` | | [New Relic](https://www.pulumi.com/registry/packages/newrelic) | `newrelic` | | [NGINX Ingress Controller](https://www.pulumi.com/registry/packages/kubernetes-ingress-nginx/) | `kubernetes-ingress-nginx` | | [ngrok](https://www.pulumi.com/registry/packages/ngrok) | `@pierskarsenbarg/ngrok` | @@ -240,6 +240,7 @@ If you want SST to support a Terraform provider or update a version, you can **s | [Statuscake](https://www.pulumi.com/registry/packages/statuscake) | `@pulumiverse/statuscake` | | [Strata Cloud Manager](https://www.pulumi.com/registry/packages/scm) | `scm` | | [Stripe](https://github.com/georgegebbett/pulumi-stripe) | `stripe` | +| [Stripe Official](https://github.com/stripe/terraform-provider-stripe) | `stripe-official` | | [StrongDM](https://www.pulumi.com/registry/packages/sdm/) | `@pierskarsenbarg/sdm` | | [Sumo Logic](https://www.pulumi.com/registry/packages/sumologic) | `sumologic` | | [Supabase](https://github.com/sst/pulumi-supabase) | `supabase` | @@ -253,7 +254,7 @@ If you want SST to support a Terraform provider or update a version, you can **s | [Unifi](https://www.pulumi.com/registry/packages/unifi) | `@pulumiverse/unifi` | | [Upstash](https://www.pulumi.com/registry/packages/upstash) | `@upstash/pulumi` | | [Venafi](https://www.pulumi.com/registry/packages/venafi) | `venafi` | -| [Vercel](https://www.pulumi.com/registry/packages/vercel) | `@pulumiverse/vercel` | +| [Vercel](https://www.pulumi.com/registry/packages/vercel) | `vercel` | | [VMware vSphere](https://www.pulumi.com/registry/packages/vsphere) | `vsphere` | | [Volcengine](https://www.pulumi.com/registry/packages/volcengine) | `@volcengine/pulumi` | | [vSphere](https://www.pulumi.com/registry/packages/vsphere) | `vsphere` | diff --git a/www/src/content/docs/docs/basics.mdx b/www/src/content/docs/docs/basics.mdx new file mode 100644 index 0000000000..1d21515cc1 --- /dev/null +++ b/www/src/content/docs/docs/basics.mdx @@ -0,0 +1,396 @@ +--- +title: Basics +description: The basics of building apps with SST. +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; +import VideoAside from "../../../components/VideoAside.astro"; + +The main difference between working on SST versus any other framework is that everything related to your app is all **defined in code**. + +1. SST **automatically manages** the resources in AWS (or any provider) defined in your app. +2. You don't need to **make any manual changes** to them in your cloud provider's console. + +This idea of _automating everything_ can feel unfamiliar at first. So let's go through the basics and look at some core concepts. + +--- + +## Setup + +Before you start working on your app, there are a couple of things we recommend setting up. + +Starting with your code editor. + +--- + +### Editor + +SST apps are configured through a file called `sst.config.ts`. It's a TypeScript file and it can work with your editor to type check and autocomplete your code. It can also show you inline help. + + + + ![Editor typecheck](../../../assets/docs/basics/editor-typecheck.png) + + + ![Editor autocomplete](../../../assets/docs/basics/editor-autocomplete.png) + + + ![Editor help](../../../assets/docs/basics/editor-help.png) + + + +Most modern editors; VS Code and Neovim included, should do the above automatically. But you should start by making sure that your editor has been set up. + +--- + +### Credentials + +SST apps are deployed to your infrastructure. So whether you are deploying to AWS, or Cloudflare, or any other cloud provider, make sure you have their credentials configured locally. + +Learn more about how to [configure your AWS credentials](/docs/iam-credentials/). + +--- + +### Console + +SST also comes with a [Console](/docs/console/). It shows you all your apps, the resources in them, lets you configure _git push to deploy_, and also send you alerts for when there are any issues. + +While it is optional, we recommend creating a free account and linking it to your AWS account. Learn more about the [SST Console](/docs/console/). + +--- + +## sst.config.ts + +Now that you are ready to work on your app and your `sst.config.ts`, let's take a look at what it means to _configure everything in code_. + + +--- + +### IaC + +Infrastructure as Code or _IaC_ is a process of automating the management of infrastructure through code. Rather than doing it manually through a console or user interface. + +:::tip +You won't need to use the AWS Console to configure your SST app. +::: + +Say your app has a Function and an S3 bucket, you would define that in your `sst.config.ts`. + +```ts title="sst.config.ts" +const bucket = new sst.aws.Bucket("MyBucket"); + +new sst.aws.Function("MyFunction", { + handler: "index.handler" +}); +``` + +You won't need to go to the Lambda and S3 parts of the AWS Console. SST will do the work for you. + +In the above snippets, `sst.aws.Function` and `sst.aws.Bucket` are called Components. Learn more about [Components](/docs/components/). + +--- + +### Resources + +The reason this works is because when SST deploys the above app, it'll convert it into a set of commands. These then call AWS with your credentials to create the underlying resources. So the above components get transformed into a list of low level resources in AWS. + +:::tip +You are not directly responsible for the low level resources that SST creates. +::: + +If you log in to your AWS Console you can see what gets created internally. While these might look a little intimidating, they are all managed by SST and you are not directly responsible for them. + +SST will create, track, and remove all the low level resources defined in your app. + +--- + +#### Exceptions + +There are some exceptions to this. You might have resources that are not defined in your SST config. These could include the following resources: + +1. **Previously created** + + You might've previously created some resources by hand that you would like to use in your new SST app. You can import these resources into your app. Moving forward, SST will manage them for you. Learn more about [importing resources](/docs/import-resources/). + +2. **Externally managed** + + You might have resources that are managed by a different team. In this case, you don't want SST to manage them. You simply want to reference them in your app. Learn more about [referencing resources](/docs/reference-resources/). + +3. **Shared across stages** + + If you are creating preview environments, you might not want to make copies of certain resources, like your database. You might want to share these across stages. Learn more about [sharing across stages](/docs/share-across-stages/). + +--- + +### Linking + +Let's say you wanted your function from the above example to upload a file to the S3 bucket, you'd need to hardcode the name of the bucket in your API. + + + +SST avoids this by allowing you to **link resources** together. + +```ts title="sst.config.ts" {3} +new sst.aws.Function("MyFunction", { + handler: "index.handler", + link: [bucket] +}); +``` + +Now in your function you can access the bucket using SST's [SDK](/docs/reference/sdk/). + +```ts title="index.ts" "Resource.MyBucket.name" +import { Resource } from "sst"; + +console.log(Resource.MyBucket.name); +``` + +There's a difference between the two snippets above. One is your **infrastructure code** and the other is your **runtime code**. One is run while creating your app, while the other runs when your users use your app. + +:::tip +You can access your infrastructure in your runtime using the SST SDK. +::: + +The _link_ allows you to access your **infrastructure** in your **runtime code**. Learn more about [resource linking](/docs/linking/). + +--- + +### State + +When you make a change to your `sst.config.ts`, like we did above. SST only deploys the changes. + +```diff lang="ts" title="sst.config.ts" +new sst.aws.Function("MyFunction", { + handler: "index.handler", ++ link: [bucket] +}); +``` + +It does this by maintaining a _state_ of your app. The state is a tree of all the resources in your app and all their properties. + +The state is stored in a file locally and backed up to a bucket in your AWS (or Cloudflare) account. + +:::tip +You can view the state of your app and its history in the SST Console. +::: + +A word of caution, if for some reason you delete your state locally and in your provider, SST won't be able to manage the resources anymore. To SST this app won't exist anymore. + +:::danger +Do not delete the bucket that stores your app's state. +::: + +To fix this, you'll have to manually re-import all those resources back into your app. Learn more about [how state works](/docs/state/). + +--- + +#### Out of sync + +We mentioned above that you are not responsible for the low level resources that SST creates. But this isn't just a point of convenience; it's something you should not do. + +:::caution +Do not manually make changes to the low level resources that SST creates. +::: + +The reason for this is that, SST only applies the diffs when your `sst.config.ts` changes. So if you manually change the resources, it'll be out of sync with your state. + +You can fix some of this by running [`sst refresh`](reference/cli/#refresh) but in general you should avoid doing this. + +--- + +## App + +So now that we know how IaC works, a lot of the workflow and concepts will begin to make sense. Starting with the key parts of an app. + +--- + +### Name + +Every app has a name. The name is used as a namespace. It allows SST to deploy multiple apps to the same cloud provider account, while isolating the resources in an app. + +If you change the name of your app in your `sst.config.ts`, SST will create a completely new set of resources for it. It **does not** rename the resources. + +:::caution +To rename an app, you'll need to remove the resources from the old one and deploy to the new one. +::: + +So if you: + +1. Create an app with the name `my-sst-app` in your `sst.config.ts` and deploy it. +2. Rename the app in your `sst.config.ts` to `my-new-sst-app` and deploy again. + +You will now have two apps in your AWS account called `my-sst-app` and `my-new-sst-app`. + +If you want to rename your app, you'll need to [remove](/docs/basics/#remove) the old app first and then deploy a new one with the new name. + +--- + +### Stage + +An app can have multiple stages. A stage is like an _environment_, it's a separate version of your app. For example, you might have a dev stage, a production stage, or a personal stage. + +It's useful to have multiple versions of your app because it lets you make changes and test in one version while your users continue to use the other. + +You create a new stage by deploying to it with the `--stage ` CLI option. The stage name is used as a namespace to create a new version of your app. It's similar to how the app name is used as a namespace. + +:::caution +To rename a stage, you'll need to [remove](/docs/basics/#remove) the resources from the old one and deploy to the new one. +::: + +Similar to app names, stages cannot be renamed. So if you wanted to rename a `development` stage to `dev`; you'll need to first remove `development` and then deploy `dev`. + +--- + +#### Personal stages + +By default, if no stage is passed in, SST creates a stage using the username in your computer. This is called a **personal stage**. Personal stages are typically used in _dev_ mode and every developer on your team should use their own personal stage. + +We'll look at this in detail below. + +--- + +### Region + +Most resources that are created in AWS (and many other providers) belong to a specific region. So when you deploy your app, it's deployed to a specific region. + +:::caution +To switch regions, you'll need to [remove](/docs/basics/#remove) the resources from one region and deploy to the new one. +::: + +For AWS, the region comes from your AWS credentials but it can be specified in the `sst.config.ts`. + +```ts title="sst.config.ts" {5-7} +export default $config({ + app(input) { + return { + name: "my-sst-app", + providers: { + aws: { region: "us-west-2" } + } + }; + } +}); +``` + +Similar to the app and stage, if you want to switch regions; you'll need to remove your app in the old region and deploy it to the new one. + +--- + +## Commands + +Now with the above background let's look at the workflow of building an SST app. + +Let's say you've created an app by running. + +```bash +sst init +``` + +--- + +### Dev + +To start with, you'll run your app in dev. + +```bash +sst dev +``` + +This deploys your app to your _personal_ stage in _dev mode_. It brings up a multiplexer that deploys your app, runs your functions, creates a tunnel, and starts your frontend and container services. + + + +It deploys your app a little differently and is optimized for local development. + +1. It runs the functions in your app [_Live_](/docs/live/) by deploying a **_stub_ version**. These proxy any requests to your local machine. +2. It **does not deploy** your frontends or container services. Instead, it starts them locally. +3. It also creates a [_tunnel_](/docs/reference/cli#tunnel) that allows them to connect to any resources that are deployed in a VPC. + +:::note +Only use `sst dev` in your personal stage. +::: + +For this reason we recommend only using your personal stage for local development. And instead deploying to a separate stage when you want to share your app with your users. + +Learn more about [`sst dev`](/docs/reference/cli/#dev). + +--- + +### Deploy + +Once you are ready to go to production you can run. + +```bash +sst deploy --stage production +``` + +You can use any stage name for production here. + +--- + +### Remove + +If you want to remove your app and all the resources in it, you can run. + +```bash +sst remove --stage +``` + +You want to be careful while running this command because it permanently removes all the resources from your AWS (or cloud provider) account. + +:::caution +Be careful while running `sst remove` since it permanently removes all your resources. +::: + +To prevent accidental removal, our template `sst.config.ts` comes with the following. + +```ts title="sst.config.ts" +removal: input?.stage === "production" ? "retain" : "remove", +``` + +This is telling SST that if the stage is called `production` then on remove, retain critical resources like buckets and databases. This should avoid any accidental data loss. + + + +Learn more about [removal policies](/docs/reference/config/#removal). + +--- + +## With a team + +This workflow really shines when working with a team. Let's look at what it looks like with a basic git workflow. + + + +1. Every developer on the team uses `sst dev` to work in their own isolated personal stage. +2. You commit your changes to a branch called `dev`. +3. Any changes to the `dev` branch are auto-deployed using `sst deploy --stage dev`. +4. Your team tests changes made to the `dev` stage of your app. +5. If they look good, `dev` is merged into a branch called `production`. +6. And any changes to the `production` branch are auto-deployed to the `production` stage with `sst deploy --stage production`. + +In this setup, you have a separate stage per developer, a _dev_ stage for testing, and a _production_ stage. + +--- + +### Autodeploy + +To have a branch automatically deploy to a stage when commits are pushed to it, you need to configure GitHub Actions. + +![SST Console Autodeploy](../../../assets/docs/basics/sst-console-autodeploy.png) + +Or you can connect your repo to the SST Console and it'll auto-deploy your app for you. Learn more about [Autodeploy](/docs/console/#autodeploy). + +--- + +### PR environments + +You can also set it up to create preview environments. + +So when a pull request (say PR#12) is created, you auto-deploy a new stage using `sst deploy --stage pr-12`. And once the PR is merged, the preview environment or stage gets removed using `sst remove --stage pr-12`. + +Just like above, you can configure this using GitHub Actions or let the SST Console do it for you. + +--- + +And there you have it. You are now ready to build apps the _SST way_. diff --git a/www/src/content/docs/docs/changelog.mdx b/www/src/content/docs/docs/changelog.mdx new file mode 100644 index 0000000000..86a0e67e80 --- /dev/null +++ b/www/src/content/docs/docs/changelog.mdx @@ -0,0 +1,10 @@ +--- +title: Changelog +description: Release notes for SST. +--- + +import Changelog from "../../../components/Changelog.astro"; + +Release notes for SST. For older versions or the full commit history, see the [GitHub releases](https://github.com/sst/sst/releases). + + diff --git a/www/src/content/docs/docs/cloudflare.mdx b/www/src/content/docs/docs/cloudflare.mdx new file mode 100644 index 0000000000..a2baf83870 --- /dev/null +++ b/www/src/content/docs/docs/cloudflare.mdx @@ -0,0 +1,203 @@ +--- +title: Cloudflare +description: Learn how to use SST with Cloudflare +--- + +[Cloudflare](https://cloudflare.com) lets you deploy apps with Workers and connect services like D1, R2, and DNS. This guide covers how to set it up with SST. + +--- + +## Install + +Add the Cloudflare provider to your SST app. Learn more about [providers](/docs/providers). + +```bash +sst add cloudflare +``` + +This adds the provider to your `sst.config.ts`. + +```ts title="sst.config.ts" {3} +{ + providers: { + cloudflare: "5.37.1", + }, +} +``` + +If Cloudflare should store your app state, set [`home`](/docs/reference/config/#home) to `"cloudflare"`. This is useful in setups where you plan to use Cloudflare as you main cloud provider. + +--- + +## Credentials + +You can create an account token in the Cloudflare dashboard under [Manage Account > API Tokens](https://dash.cloudflare.com/?to=/:account/api-tokens). + +Start with the **Edit Cloudflare Workers** template and add these permissions: + +- *Account - D1 - Edit* +- *Zone - DNS - Edit* + +:::tip +If your app uses other Cloudflare products, add the permissions those features need. +::: + +Give the token access to the account your application will be deploying to. If you are using Cloudflare DNS with SST, include the zones that SST should update. + +Set `CLOUDFLARE_DEFAULT_ACCOUNT_ID` to the Cloudflare account ID that SST should use. If you leave it unset, SST falls back to the first account that Cloudflare returns for that token. + +Then set these variables in your shell, `.env`, or CI environment before you deploy: + +```bash +export CLOUDFLARE_API_TOKEN=aaaaaaaa_aaaaaaaaaaaa_aaaaaaaa +export CLOUDFLARE_DEFAULT_ACCOUNT_ID=aaaaaaaa_aaaaaaaaaaaa_aaaaaaaa +``` + +--- + +### Deploying into Multiple Cloudflare Accounts + +You can deploy cloudflare components into multiple Cloudflare accounts by specifying the accountId argument and providing a different Cloudflare Provider. + +```ts +// define a provider with another api token for a different account +const provider = new cloudflare.Provider("AnotherProvider", { apiToken: "bbbbbbbb_bbbbbbbbbbbb_bbbbbbbb" }); + +// specify resource in another account +const sstWorker = new sst.cloudflare.Worker( + "MyWorker", + { handler: "index.ts", accountId: "bbbbbbbb-bbbbbbbbbbbb-bbbbbbbb" }, + { provider }, +); +``` + +:::caution +Do not link any resources which are deployed into different Cloudflare accounts. +::: + +--- + +## Components + +SST includes Cloudflare components for Workers, storage, queues, cron jobs, AI bindings, and more. + +### Worker + +Create a Cloudflare Worker and enable a URL so it can handle HTTP requests. + +```ts title="sst.config.ts" +const worker = new sst.cloudflare.Worker("MyWorker", { + handler: "index.ts", + url: true, +}); + +return { + url: worker.url, +}; +``` + +Use the [`Worker`](/docs/component/cloudflare/worker/) component to build APIs and edge handlers on Cloudflare. + + +### Storage + +Create Cloudflare storage resources and link them to your Worker. For example, here's a D1 database. + +```ts title="sst.config.ts" +const db = new sst.cloudflare.D1("MyDatabase"); + +new sst.cloudflare.Worker("MyWorker", { + handler: "index.ts", + link: [db], + url: true, +}); +``` + +Then access it in your handler through `Resource`. + +```ts title="index.ts" +import { Resource } from "sst"; + +export default { + async fetch() { + const row = await Resource.MyDatabase.prepare( + "SELECT id FROM todo ORDER BY id DESC LIMIT 1", + ).first(); + + return Response.json(row); + }, +}; +``` + +The same pattern works with [`Bucket`](/docs/component/cloudflare/bucket/) for R2 and [`Kv`](/docs/component/cloudflare/kv/) for KV namespaces. + + +### Queue + +Use [`Queue`](/docs/component/cloudflare/queue/) for async work. + +```ts title="sst.config.ts" +const queue = new sst.cloudflare.Queue("MyQueue"); + +queue.subscribe("consumer.ts"); + +const producer = new sst.cloudflare.Worker("Producer", { + handler: "producer.ts", + link: [queue], + url: true, +}); +``` + +For scheduled work, use [`Cron`](/docs/component/cloudflare/cron/) to run a worker on a cron expression. + +### More components + +Browse the component docs for [`Worker`](/docs/component/cloudflare/worker/), [`Astro`](/docs/component/cloudflare/astro/), [`Bucket`](/docs/component/cloudflare/bucket/), [`D1`](/docs/component/cloudflare/d1/), [`Kv`](/docs/component/cloudflare/kv/), [`Queue`](/docs/component/cloudflare/queue/), [`Cron`](/docs/component/cloudflare/cron/), [`Ai`](/docs/component/cloudflare/ai/), [`Workflow`](/docs/component/cloudflare/workflow/), and [`RateLimit`](/docs/component/cloudflare/rate-limit/). + +If you are using Cloudflare DNS with SST, use [`sst.cloudflare.dns`](/docs/component/cloudflare/dns/) with [custom domains](/docs/custom-domains/). + +--- + +## Cloudflare Vite plugin + +Cloudflare SSR components like [Astro](/docs/component/cloudflare/astro/) or [TanStack Start](/docs/component/cloudflare/tanstack-start/) need the Cloudflare Vite plugin to work correctly. + +:::caution +Do not include any Wrangler configuration files (`wrangler.toml`, `wrangler.json`) in your project. SST manages these for you and will generate them as needed. +::: + +You need to configure the Vite plugin to use the SST-managed Wrangler config. + +```ts title="vite.config.ts" +import { defineConfig } from "vite"; +import { cloudflare } from "@cloudflare/vite-plugin"; + +export default defineConfig({ + plugins: [ + cloudflare({ + configPath: process.env.SST_WRANGLER_CONFIG, + }), + ], +}); +``` + +This environment variable is set by SST and ensures the plugin uses the generated Wrangler configuration. + +:::tip +There is an [open PR](https://github.com/cloudflare/workers-sdk/pull/13587) on the Cloudflare Workers SDK that will add automatic detection of the SST-managed Wrangler config. Once merged, you won't need to explicitly set `configPath`. +::: + +--- + +## Examples + +Check out the full examples: + +- [Cloudflare Workers with SST](/docs/start/cloudflare/worker/) +- [Hono on Cloudflare with SST](/docs/start/cloudflare/hono/) +- [tRPC on Cloudflare with SST](/docs/start/cloudflare/trpc/) +- [Astro on Cloudflare](https://github.com/sst/sst/tree/dev/examples/cloudflare-astro) +- [Cloudflare D1](https://github.com/sst/sst/tree/dev/examples/cloudflare-d1) +- [Cloudflare KV](https://github.com/sst/sst/tree/dev/examples/cloudflare-kv) +- [Cloudflare Queue](https://github.com/sst/sst/tree/dev/examples/cloudflare-queue) +- [Cloudflare Cron](https://github.com/sst/sst/tree/dev/examples/cloudflare-cron) diff --git a/www/src/content/docs/docs/custom-domains.mdx b/www/src/content/docs/docs/custom-domains.mdx index a072d48d80..ca906c6194 100644 --- a/www/src/content/docs/docs/custom-domains.mdx +++ b/www/src/content/docs/docs/custom-domains.mdx @@ -167,7 +167,7 @@ If your domains are hosted on [Vercel](https://vercel.com), you'll need to do th 1. [Add the Vercel provider to your app](/docs/component/vercel/dns/#configure-provider). ```bash - sst add @pulumiverse/vercel + sst add vercel ``` 2. Set the **`VERCEL_API_TOKEN`** in your environment. You might also need to set the `VERCEL_TEAM_ID` if the domain belongs to a team. @@ -206,7 +206,7 @@ If your domains are hosted on [Cloudflare](https://developers.cloudflare.com/dns export CLOUDFLARE_DEFAULT_ACCOUNT_ID=aaaaaaaa_aaaaaaaaaaaa_aaaaaaaa ``` - To get your API tokens, head to the [API Tokens section](https://dash.cloudflare.com/profile/api-tokens) of your Cloudflare dashboard and create one with the **Edit zone DNS** policy. + To get your API tokens, head to the [API Tokens section](https://dash.cloudflare.com/?to=/:account/api-tokens) of your Cloudflare dashboard and create one with the **Edit zone DNS** policy. The Cloudflare providers need these credentials to deploy your app in the first place, which means they can't be set using the `sst secret` CLI. diff --git a/www/src/content/docs/docs/enterprise.mdx b/www/src/content/docs/docs/enterprise.mdx deleted file mode 100644 index 6ce6301b06..0000000000 --- a/www/src/content/docs/docs/enterprise.mdx +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: Enterprise -description: Everything you need to use SST at your enterprise. ---- - -import config from '../../../../config.ts'; - -SST is great for larger teams that have complex policy, security, and compliance requirements. - -1. It runs completely on your infrastructure. -2. It's designed to work with enterprise requirements by default. -3. You can transform any component if you have custom needs. -4. You can always drop down and use Pulumi or Terraform within SST if you need to. - -Feel free to open an issue on GitHub if you are having any problems with enterprise requirements. - ---- - -## Support - -While we support our community through GitHub and Discord, your team might need dedicated support. We're happy to: - -- Set up a shared Slack or Discord channel with your team. -- Fix any critical issues your team is running into. -- Do a call with your team and answer any questions. -- Prioritize any feature requests from your team. -- Share details about the SST roadmap. -- And provide support SLAs. - -**Contact us** for further details about our enterprise plans. diff --git a/www/src/content/docs/docs/examples.mdx b/www/src/content/docs/docs/examples.mdx deleted file mode 100644 index f8f685e5c5..0000000000 --- a/www/src/content/docs/docs/examples.mdx +++ /dev/null @@ -1,4732 +0,0 @@ ---- -title: Examples -description: A collection of example apps for reference. ---- - -{/* DO NOT EDIT. AUTO-GENERATED FROM "examples/" */} - -import { Tabs, TabItem } from '@astrojs/starlight/components'; -import VideoAside from '../../../../src/components/VideoAside.astro'; -import Segment from '../../../../src/components/tsdoc/Segment.astro'; -import Section from '../../../../src/components/tsdoc/Section.astro'; -import NestedTitle from '../../../../src/components/tsdoc/NestedTitle.astro'; -import InlineSection from '../../../../src/components/tsdoc/InlineSection.astro'; - -Below is a collection of example SST apps. These are available in the [`examples/`](https://github.com/sst/sst/tree/dev/examples) directory of the repo. - -:::tip -This doc is best viewed through the site search or through the _AI_. -::: - -The descriptions for these examples are generated using the comments in the `sst.config.ts` of the app. - -#### Contributing -To contribute an example or to edit one, submit a PR to the [repo](https://github.com/sst/sst). -Make sure to document the `sst.config.ts` in your example. - - ---- -## API Gateway auth - -Enable IAM and JWT authorizers for API Gateway routes. -```ts title="sst.config.ts" -const api = new sst.aws.ApiGatewayV2("MyApi", { - domain: { - name: "api.ion.sst.sh", - path: "v1", - }, -}); -api.route("GET /", { - handler: "route.handler", -}); -api.route("GET /foo", "route.handler", { auth: { iam: true } }); -api.route("GET /bar", "route.handler", { - auth: { - jwt: { - issuer: - "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Rq4d8zILG", - audiences: ["user@example.com"], - }, - }, -}); -api.route("$default", "route.handler"); - -return { - api: api.url, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-apig-auth). - - ---- -## AWS Astro container with Redis - -Creates a hit counter app with Astro and Redis. - -This deploys Astro as a Fargate service to ECS and it's linked to Redis. - -```ts title="sst.config.ts" {3} -new sst.aws.Service("MyService", { - cluster, - link: [redis], - loadBalancer: { - ports: [{ listen: "80/http", forward: "3000/http" }], - }, - dev: { - command: "npm run dev", - }, -}); -``` - -Since our Redis cluster is in a VPC, we’ll need a tunnel to connect to it from our local -machine. - -```bash "sudo" -sudo npx sst tunnel install -``` - -This needs _sudo_ to create a network interface on your machine. You’ll only need to do this -once on your machine. - -To start your app locally run. - -```bash -npx sst dev -``` - -Now if you go to `http://localhost:4321` you’ll see a counter update as you refresh the page. - -Finally, you can deploy it by adding the `Dockerfile` that's included in this example and -running `npx sst deploy --stage production`. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { bastion: true }); -const redis = new sst.aws.Redis("MyRedis", { vpc }); -const cluster = new sst.aws.Cluster("MyCluster", { vpc }); - -new sst.aws.Service("MyService", { - cluster, - link: [redis], - loadBalancer: { - ports: [{ listen: "80/http", forward: "4321/http" }], - }, - dev: { - command: "npm run dev", - }, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-astro-redis). - - ---- -## AWS Astro streaming - -Follows the [Astro Streaming](https://docs.astro.build/en/recipes/streaming-improve-page-performance/) guide to create an app that streams HTML. - -The `responseMode` in the [`astro-sst`](https://www.npmjs.com/package/astro-sst) adapter -is set to enable streaming. - - -```ts title="astro.config.mjs" -adapter: aws({ - responseMode: "stream" -}) -``` - -Now any components that return promises will be streamed. - -```astro title="src/components/Friends.astro" ---- -import type { Character } from "./character"; - -const friends: Character[] = await new Promise((resolve) => setTimeout(() => { - setTimeout(() => { - resolve( - [ - { name: "Patrick Star", image: "patrick.png" }, - { name: "Sandy Cheeks", image: "sandy.png" }, - { name: "Squidward Tentacles", image: "squidward.png" }, - { name: "Mr. Krabs", image: "mr-krabs.png" }, - ] - ); - }, 3000); -})); ---- -
- {friends.map((friend) => ( -
- {friend.name} -

{friend.name}

-
- ))} -
-``` - -You should see the _friends_ section load after a 3 second delay. - -:::note -Safari handles streaming differently than other browsers. -::: - -Safari uses a [different heuristic](https://bugs.webkit.org/show_bug.cgi?id=252413) to -determine when to stream data. You need to render _enough_ initial HTML to trigger streaming. -This is typically only a problem for demo apps. - -There's nothing to configure for streaming in the `Astro` component. -```ts title="sst.config.ts" -new sst.aws.Astro("MyWeb"); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-astro-stream). - - ---- -## AWS Aurora local - -In this example, we connect to a locally running Postgres instance for dev. While -on deploy, we use RDS Aurora. - -We use the [`docker run`](https://docs.docker.com/reference/cli/docker/container/run/) CLI -to start a local container with Postgres. You don't have to use Docker, you can use -Postgres.app or any other way to run Postgres locally. - -```bash -docker run \ - --rm \ - -p 5432:5432 \ - -v $(pwd)/.sst/storage/postgres:/var/lib/postgresql/data \ - -e POSTGRES_USER=postgres \ - -e POSTGRES_PASSWORD=password \ - -e POSTGRES_DB=local \ - postgres:16.4 -``` - -The data is saved to the `.sst/storage` directory. So if you restart the dev server, the -data will still be there. - -We then configure the `dev` property of the `Aurora` component with the settings for the -local Postgres instance. - -```ts title="sst.config.ts" -dev: { - username: "postgres", - password: "password", - database: "local", - port: 5432, -} -``` - -By providing the `dev` prop for Postgres, SST will use the local Postgres instance and -not deploy a new RDS database when running `sst dev`. - -It also allows us to access the database through a Resource `link` without having to -conditionally check if we are running locally. - -```ts title="index.ts" -const pool = new Pool({ - host: Resource.MyPostgres.host, - port: Resource.MyPostgres.port, - user: Resource.MyPostgres.username, - password: Resource.MyPostgres.password, - database: Resource.MyPostgres.database, -}); -``` - -The above will work in both `sst dev` and `sst deploy`. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { nat: "ec2" }); - -const database = new sst.aws.Aurora("MyPostgres", { - engine: "postgres", - dev: { - username: "postgres", - password: "password", - database: "local", - host: "localhost", - port: 5432, - }, - vpc, -}); - -new sst.aws.Function("MyFunction", { - vpc, - url: true, - link: [database], - handler: "index.handler", -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-aurora-local). - - ---- -## AWS Aurora MySQL - -In this example, we deploy a Aurora MySQL database. - -```ts title="sst.config.ts" -const mysql = new sst.aws.Aurora("MyDatabase", { - engine: "mysql", - vpc, -}); -``` - -And link it to a Lambda function. - -```ts title="sst.config.ts" {4} -new sst.aws.Function("MyApp", { - handler: "index.handler", - link: [mysql], - url: true, - vpc, -}); -``` - -Now in the function we can access the database. - -```ts title="index.ts" -const connection = await mysql.createConnection({ - database: Resource.MyDatabase.database, - host: Resource.MyDatabase.host, - port: Resource.MyDatabase.port, - user: Resource.MyDatabase.username, - password: Resource.MyDatabase.password, -}); -``` - -We also enable the `bastion` option for the VPC. This allows us to connect to the database -from our local machine with the `sst tunnel` CLI. - -```bash "sudo" -sudo npx sst tunnel install -``` - -This needs _sudo_ to create a network interface on your machine. You’ll only need to do this -once on your machine. - -Now you can run `npx sst dev` and you can connect to the database from your local machine. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { - nat: "ec2", - bastion: true, -}); -const mysql = new sst.aws.Aurora("MyDatabase", { - engine: "mysql", - vpc, -}); -new sst.aws.Function("MyApp", { - handler: "index.handler", - link: [mysql], - url: true, - vpc, -}); - -return { - host: mysql.host, - port: mysql.port, - username: mysql.username, - password: mysql.password, - database: mysql.database, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-aurora-mysql). - - ---- -## AWS Aurora Postgres - -In this example, we deploy a Aurora Postgres database. - -```ts title="sst.config.ts" -const postgres = new sst.aws.Aurora("MyDatabase", { - engine: "postgres", - vpc, -}); -``` - -And link it to a Lambda function. - -```ts title="sst.config.ts" {4} -new sst.aws.Function("MyApp", { - handler: "index.handler", - link: [postgres], - url: true, - vpc, -}); -``` - -In the function we use the [`postgres`](https://www.npmjs.com/package/postgres) package. - -```ts title="index.ts" -import postgres from "postgres"; -import { Resource } from "sst"; - -const sql = postgres({ - username: Resource.MyDatabase.username, - password: Resource.MyDatabase.password, - database: Resource.MyDatabase.database, - host: Resource.MyDatabase.host, - port: Resource.MyDatabase.port, -}); -``` - -We also enable the `bastion` option for the VPC. This allows us to connect to the database -from our local machine with the `sst tunnel` CLI. - -```bash "sudo" -sudo npx sst tunnel install -``` - -This needs _sudo_ to create a network interface on your machine. You’ll only need to do this -once on your machine. - -Now you can run `npx sst dev` and you can connect to the database from your local machine. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { - nat: "ec2", - bastion: true, -}); -const postgres = new sst.aws.Aurora("MyDatabase", { - engine: "postgres", - vpc, -}); -new sst.aws.Function("MyApp", { - handler: "index.handler", - link: [postgres], - url: true, - vpc, -}); - -return { - host: postgres.host, - port: postgres.port, - username: postgres.username, - password: postgres.password, - database: postgres.database, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-aurora-postgres). - - ---- -## AWS OpenAuth React SPA - -This is a full-stack monorepo app shows the OpenAuth flow for a single-page app -and an authenticated API. It has: - -- React SPA built with Vite and the `StaticSite` component in the `packages/web` - directory. - ```ts title="infra/web.ts" - export const web = new sst.aws.StaticSite("MyWeb", { - path: "packages/web", - build: { - output: "dist", - command: "npm run build", - }, - environment: { - VITE_API_URL: api.url, - VITE_AUTH_URL: auth.url, - }, - }); - ``` - -- API with Hono and the `Function` component in `packages/functions/src/api.ts`. - ```ts title="infra/api.ts" - export const api = new sst.aws.Function("MyApi", { - url: true, - link: [auth], - handler: "packages/functions/src/api.handler", - }); - ``` - -- OpenAuth with the `Auth` component in `packages/functions/src/auth.ts`. - ```ts title="infra/auth.ts" - export const auth = new sst.aws.Auth("MyAuth", { - issuer: "packages/functions/src/auth.handler", - }); - ``` - -The React frontend uses a `AuthContext` provider to manage the auth flow. - -```tsx title="packages/web/src/AuthContext.tsx" - - {children} - -``` - -Now in `App.tsx`, we can use the `useAuth` hook. - -```tsx title="packages/web/src/App.tsx" -const auth = useAuth(); - -return !auth.loaded ? ( -
Loading...
-) : ( -
- {auth.loggedIn ? ( -
-

- Logged in - {auth.userId && as {auth.userId}} -

-
- ) : ( - - )} -
-); -``` - -Once authenticated, we can call our authenticated API by passing in the access -token. - -```tsx title="packages/web/src/App.tsx" {3} -await fetch(`${import.meta.env.VITE_API_URL}me`, { - headers: { - Authorization: `Bearer ${await auth.getToken()}`, - }, -}); -``` - -The API uses the OpenAuth client to verify the token. - -```ts title="packages/functions/src/api.ts" {3} -const authHeader = c.req.header("Authorization"); -const token = authHeader.split(" ")[1]; -const verified = await client.verify(subjects, token); -``` - -The `sst.config.ts` dynamically imports all the `infra/` files. -```ts title="sst.config.ts" -await import("./infra/auth"); -await import("./infra/api"); -await import("./infra/web"); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-auth-react). - - ---- -## Bucket policy - -Create an S3 bucket and transform its bucket policy. -```ts title="sst.config.ts" -const bucket = new sst.aws.Bucket("MyBucket", { - transform: { - policy: (args) => { - // use sst.aws.iamEdit helper function to manipulate IAM policy - // containing Output values from components - args.policy = sst.aws.iamEdit(args.policy, (policy) => { - policy.Statement.push({ - Effect: "Allow", - Principal: { Service: "ses.amazonaws.com" }, - Action: "s3:PutObject", - Resource: $interpolate`arn:aws:s3:::${args.bucket}/*`, - }); - }); - }, - }, -}); - -return { - bucket: bucket.name, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-bucket-policy). - - ---- -## Bucket queue notifications - -Create an S3 bucket and subscribe to its events with an SQS queue. -```ts title="sst.config.ts" -const queue = new sst.aws.Queue("MyQueue"); -queue.subscribe("subscriber.handler"); - -const bucket = new sst.aws.Bucket("MyBucket"); -bucket.notify({ - notifications: [ - { - name: "MySubscriber", - queue, - events: ["s3:ObjectCreated:*"], - }, - ], -}); - -return { - bucket: bucket.name, - queue: queue.url, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-bucket-queue-subscriber). - - ---- -## Bucket notifications - -Create an S3 bucket and subscribe to its events with a function. -```ts title="sst.config.ts" -const bucket = new sst.aws.Bucket("MyBucket"); -bucket.notify({ - notifications: [ - { - name: "MySubscriber", - function: "subscriber.handler", - events: ["s3:ObjectCreated:*"], - }, - ], -}); - -return { - bucket: bucket.name, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-bucket-subscriber). - - ---- -## Bucket topic notifications - -Create an S3 bucket and subscribe to its events with an SNS topic. -```ts title="sst.config.ts" -const topic = new sst.aws.SnsTopic("MyTopic"); -topic.subscribe("MySubscriber", "subscriber.handler"); - -const bucket = new sst.aws.Bucket("MyBucket"); -bucket.notify({ - notifications: [ - { - name: "MySubscriber", - topic, - events: ["s3:ObjectCreated:*"], - }, - ], -}); - -return { - bucket: bucket.name, - topic: topic.name, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-bucket-topic-subscriber). - - ---- -## AWS Bun Elysia container - -Deploys a Bun [Elysia](https://elysiajs.com/) API to AWS. - -You can get started by running. - -```bash -bun create elysia aws-bun-elysia -cd aws-bun-elysia -bunx sst init -``` - -Now you can add a service. - -```ts title="sst.config.ts" -new sst.aws.Service("MyService", { - cluster, - loadBalancer: { - ports: [{ listen: "80/http", forward: "3000/http" }], - }, - dev: { - command: "bun dev", - }, -}); -``` - -Start your app locally. - -```bash -bun sst dev -``` - -This example lets you upload a file to S3 and then download it. - -```bash -curl -F file=@elysia.png http://localhost:3000/ -curl http://localhost:3000/latest -``` - -Finally, you can deploy it using `bun sst deploy --stage production`. -```ts title="sst.config.ts" -const bucket = new sst.aws.Bucket("MyBucket"); -const vpc = new sst.aws.Vpc("MyVpc"); - -const cluster = new sst.aws.Cluster("MyCluster", { vpc }); -new sst.aws.Service("MyService", { - cluster, - loadBalancer: { - ports: [{ listen: "80/http", forward: "3000/http" }], - }, - dev: { - command: "bun dev", - }, - link: [bucket], -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-bun-elysia). - - ---- -## AWS Bun Redis - -Creates a hit counter app with Bun and Redis. - -This deploys Bun as a Fargate service to ECS and it's linked to Redis. - -```ts title="sst.config.ts" {9} -new sst.aws.Service("MyService", { - cluster, - loadBalancer: { - ports: [{ listen: "80/http", forward: "3000/http" }], - }, - dev: { - command: "bun dev", - }, - link: [redis], -}); -``` - -We also have a couple of scripts. A `dev` script with a watcher and a `build` script -that used when we deploy to production. - -```json title="package.json" -{ - "scripts": { - "dev": "bun run --watch index.ts", - "build": "bun build --target bun index.ts" - }, -} -``` - -Since our Redis cluster is in a VPC, we’ll need a tunnel to connect to it from our local -machine. - -```bash "sudo" -sudo bun sst tunnel install -``` - -This needs _sudo_ to create a network interface on your machine. You’ll only need to do this -once on your machine. - -To start your app locally run. - -```bash -bun sst dev -``` - -Now if you go to `http://localhost:3000` you’ll see a counter update as you refresh the page. - -Finally, you can deploy it using `bun sst deploy --stage production` using a `Dockerfile` -that's included in the example. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { bastion: true }); -const redis = new sst.aws.Redis("MyRedis", { vpc }); -const cluster = new sst.aws.Cluster("MyCluster", { vpc }); - -new sst.aws.Service("MyService", { - cluster, - link: [redis], - loadBalancer: { - ports: [{ listen: "80/http", forward: "3000/http" }], - }, - dev: { - command: "bun dev", - }, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-bun-redis). - - ---- -## AWS Cluster custom autoscaling - -In this example, we'll create a cluster that can autoscales based on a custom -metric; in this case, the number of messages in a queue. - -We'll create a queue, and two functions that'll seed and purge the queue. We'll -also create two policies. - -One that scales it up. - -```ts title="sst.config.ts" -const scaleUpPolicy = new aws.appautoscaling.Policy("ScaleUpPolicy", { - serviceNamespace: service.nodes.autoScalingTarget.serviceNamespace, - scalableDimension: service.nodes.autoScalingTarget.scalableDimension, - resourceId: service.nodes.autoScalingTarget.resourceId, - policyType: "StepScaling", - stepScalingPolicyConfiguration: { - adjustmentType: "ChangeInCapacity", - cooldown: 5, - stepAdjustments: [ - { - metricIntervalLowerBound: "0", - scalingAdjustment: 1, - }, - ], - }, -}); -``` - -And one that scales it down. - -```ts title="sst.config.ts" -const scaleDownPolicy = new aws.appautoscaling.Policy("ScaleDownPolicy", { - serviceNamespace: service.nodes.autoScalingTarget.serviceNamespace, - scalableDimension: service.nodes.autoScalingTarget.scalableDimension, - resourceId: service.nodes.autoScalingTarget.resourceId, - policyType: "StepScaling", - stepScalingPolicyConfiguration: { - adjustmentType: "ChangeInCapacity", - cooldown: 5, - stepAdjustments: [ - { - metricIntervalUpperBound: "0", - scalingAdjustment: -1, - }, - ], - }, -}); -``` - -We'll add a CloudWatch metric alarm that triggers the scaling policies if the -queue depth exceeds 3 messages. - -```ts title="sst.config.ts" -new aws.cloudwatch.MetricAlarm("QueueDepthAlarm", { - comparisonOperator: "GreaterThanThreshold", - evaluationPeriods: 1, - metricName: "ApproximateNumberOfMessagesVisible", - namespace: "AWS/SQS", - period: 10, - statistic: "Average", - threshold: 3, - dimensions: { - QueueName: queue.nodes.queue.name, - }, - alarmDescription: "Scale up when queue depth exceeds 3 messages", - alarmActions: [scaleUpPolicy.arn], - okActions: [scaleDownPolicy.arn], -}); -``` - -To test this example, first deploy your app then: - -1. Invoke the `MyQueueSeeder` URL. This will cause the service to scale up to 5 - instances in a few minutes. -2. Then invoke the `MyQueuePurger` URL. This will cause the service to scale - down to 1 instance in a few minutes. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc"); - -// Create a queue and two functions to seed and purge the queue -const queue = new sst.aws.Queue("MyQueue"); -new sst.aws.Function("MyQueueSeeder", { - handler: "queue.seeder", - link: [queue], - url: true, -}); -new sst.aws.Function("MyQueuePurger", { - handler: "queue.purger", - link: [queue], - url: true, -}); - -// Create a cluster and disable default scaling on CPU and memory utilization -const cluster = new sst.aws.Cluster("MyCluster", { vpc }); -const service = new sst.aws.Service("MyService", { - cluster, - loadBalancer: { - ports: [{ listen: "80/http" }], - }, - scaling: { - min: 1, - max: 5, - cpuUtilization: false, - memoryUtilization: false, - }, -}); - -// Create a scale up policy that scales up by 1 instance at a time -const scaleUpPolicy = new aws.appautoscaling.Policy("ScaleUpPolicy", { - serviceNamespace: service.nodes.autoScalingTarget.serviceNamespace, - scalableDimension: service.nodes.autoScalingTarget.scalableDimension, - resourceId: service.nodes.autoScalingTarget.resourceId, - policyType: "StepScaling", - stepScalingPolicyConfiguration: { - adjustmentType: "ChangeInCapacity", - cooldown: 5, - stepAdjustments: [ - { - metricIntervalLowerBound: "0", - scalingAdjustment: 1, - }, - ], - }, -}); - -// Create a scale down policy that scales down by 1 instance at a time -const scaleDownPolicy = new aws.appautoscaling.Policy("ScaleDownPolicy", { - serviceNamespace: service.nodes.autoScalingTarget.serviceNamespace, - scalableDimension: service.nodes.autoScalingTarget.scalableDimension, - resourceId: service.nodes.autoScalingTarget.resourceId, - policyType: "StepScaling", - stepScalingPolicyConfiguration: { - adjustmentType: "ChangeInCapacity", - cooldown: 5, - stepAdjustments: [ - { - metricIntervalUpperBound: "0", - scalingAdjustment: -1, - }, - ], - }, -}); - -// Create an alarm that scales up when the queue depth exceeds 3 messages -// and scales down when the queue depth is less than 3 messages -new aws.cloudwatch.MetricAlarm("QueueDepthAlarm", { - comparisonOperator: "GreaterThanThreshold", - evaluationPeriods: 1, - metricName: "ApproximateNumberOfMessagesVisible", - namespace: "AWS/SQS", - period: 10, - statistic: "Average", - threshold: 3, - dimensions: { - QueueName: queue.nodes.queue.name, - }, - alarmDescription: "Scale up when queue depth exceeds 3 messages", - alarmActions: [scaleUpPolicy.arn], - okActions: [scaleDownPolicy.arn], -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-cluster-autoscaling). - - ---- -## AWS Cluster private service - -Adds a private load balancer to a service by setting the `loadBalancer.public` prop to -`false`. - -This allows you to create internal services that can only be accessed inside a VPC. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { bastion: true }); - -const cluster = new sst.aws.Cluster("MyCluster", { vpc }); -new sst.aws.Service("MyService", { - cluster, - loadBalancer: { - public: false, - ports: [{ listen: "80/http" }], - }, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-cluster-internal). - - ---- -## AWS Cluster Spot capacity - -This example, shows how to use the Fargate Spot capacity provider for your services. - -We have it set to use only Fargate Spot instances for all non-production stages. Learn more -about the [`capacity`](/docs/component/aws/cluster#capacity) prop. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc"); - -const cluster = new sst.aws.Cluster("MyCluster", { vpc }); -new sst.aws.Service("MyService", { - cluster, - loadBalancer: { - ports: [{ listen: "80/http" }], - }, - capacity: $app.stage === "production" ? undefined : "spot", -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-cluster-spot). - - ---- -## AWS Cluster with API Gateway - -Expose a service through API Gateway HTTP API using a VPC link. - -This is an alternative to using a load balancer. Since API Gateway is pay per request, it -works out a lot cheaper for services that don't get a lot of traffic. - -You need to specify which port in your service will be exposed through API Gateway. - -```ts title="sst.config.ts" {4} -const service = new sst.aws.Service("MyService", { - cluster, - serviceRegistry: { - port: 80, - }, -}); -``` - -A couple of things to note: - -1. Your API Gateway HTTP API also needs to be in the **same VPC** as the service. - -2. You also need to verify that your VPC's [**availability zones support VPC link**](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vpc-links.html#http-api-vpc-link-availability). - -3. Run `aws ec2 describe-availability-zones` to get a list of AZs for your - account. - -4. Only list the AZ ID's that support VPC link. - ```ts title="sst.config.ts" {4} - vpc: { - az: ["eu-west-3a", "eu-west-3c"] - } - ``` - If the VPC picks an AZ automatically that doesn't support VPC link, you'll get - the following error: - ``` - operation error ApiGatewayV2: BadRequestException: Subnet is in Availability - Zone 'euw3-az2' where service is not available - ``` -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { - // Pick at least two AZs that support VPC link - // az: ["eu-west-3a", "eu-west-3c"], -}); -const cluster = new sst.aws.Cluster("MyCluster", { vpc }); -const service = new sst.aws.Service("MyService", { - cluster, - serviceRegistry: { - port: 80, - }, -}); - -const api = new sst.aws.ApiGatewayV2("MyApi", { vpc }); -api.routePrivate("$default", service.nodes.cloudmapService.arn); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-cluster-vpclink). - - ---- -## Subscribe to queues with dead-letter queue - -Messages not processed successfully by the primary subscriber function will be sent to the dead-letter queue after the retry limit is reached. -```ts title="sst.config.ts" -// create dead letter queue -const dlq = new sst.aws.Queue("DeadLetterQueue"); -dlq.subscribe("subscriber.dlq"); - -// create main queue -const queue = new sst.aws.Queue("MyQueue", { - dlq: dlq.arn, -}); -queue.subscribe("subscriber.main"); - -const app = new sst.aws.Function("MyApp", { - handler: "publisher.handler", - link: [queue], - url: true, -}); - -return { - app: app.url, - queue: queue.url, - dlq: dlq.url, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-dead-letter-queue). - - ---- -## AWS Deno Redis - -Creates a hit counter app with Deno and Redis. - -This deploys Deno as a Fargate service to ECS and it's linked to Redis. - -```ts title="sst.config.ts" {3} -new sst.aws.Service("MyService", { - cluster, - link: [redis], - loadBalancer: { - ports: [{ listen: "80/http", forward: "8000/http" }], - }, - dev: { - command: "deno task dev", - }, -}); -``` - -Since our Redis cluster is in a VPC, we’ll need a tunnel to connect to it from our local -machine. - -```bash "sudo" -sudo sst tunnel install -``` - -This needs _sudo_ to create a network interface on your machine. You’ll only need to do this -once on your machine. - -To start your app locally run. - -```bash -sst dev -``` - -Now if you go to `http://localhost:8000` you’ll see a counter update as you refresh the page. - -Finally, you can deploy it using `sst deploy --stage production` using a `Dockerfile` -that's included in the example. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { bastion: true }); -const redis = new sst.aws.Redis("MyRedis", { vpc }); -const cluster = new sst.aws.Cluster("MyCluster", { vpc }); - -new sst.aws.Service("MyService", { - cluster, - link: [redis], - loadBalancer: { - ports: [{ listen: "80/http", forward: "8000/http" }], - }, - dev: { - command: "deno task dev", - }, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-deno-redis). - - ---- -## Drizzle migrations in CI/CD - -An example on how to run Drizzle migrations as a part of your CI/CD. - -Start by creating a function that runs migrations. - -```ts title="sst.config.ts" -const migrator = new sst.aws.Function("DatabaseMigrator", { - handler: "src/migrator.handler", - link: [rds], - vpc, - copyFiles: [ - { - from: "migrations", - to: "./migrations", - }, - ], -}); -``` - -Where `src/migrator.ts` looks like. - -```ts title="src/migrator.ts" -import { db } from "./drizzle"; -import { migrate } from "drizzle-orm/postgres-js/migrator"; - -export const handler = async (event: any) => { - await migrate(db, { - migrationsFolder: "./migrations", - }); -}; -``` - -And we can set it up to run on every deploy. - -```ts title="sst.config.ts" -if (!$dev){ - new aws.lambda.Invocation("DatabaseMigratorInvocation", { - input: Date.now().toString(), - functionName: migrator.name, - }); -} -``` - -We use the current time to make sure the function runs on every deploy. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { bastion: true, nat: "ec2" }); -const rds = new sst.aws.Postgres("MyPostgres", { vpc, proxy: true }); - -new sst.aws.Function("MyApi", { - vpc, - url: true, - link: [rds], - handler: "src/api.handler", -}); - -const migrator = new sst.aws.Function("DatabaseMigrator", { - handler: "src/migrator.handler", - link: [rds], - vpc, - copyFiles: [ - { - from: "migrations", - to: "./migrations", - }, - ], -}); - -if (!$dev) { - new aws.lambda.Invocation("DatabaseMigratorInvocation", { - input: Date.now().toString(), - functionName: migrator.name, - }); -} - -new sst.x.DevCommand("Studio", { - link: [rds], - dev: { - command: "npx drizzle-kit studio", - }, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-drizzle-migrations). - - ---- -## DynamoDB streams - -Create a DynamoDB table, enable streams, and subscribe to it with a function. -```ts title="sst.config.ts" -const table = new sst.aws.Dynamo("MyTable", { - fields: { - id: "string", - }, - primaryIndex: { hashKey: "id" }, - stream: "new-and-old-images", -}); -table.subscribe("MySubscriber", "subscriber.handler", { - filters: [ - { - dynamodb: { - NewImage: { - message: { - S: ["Hello"], - }, - }, - }, - }, - ], -}); - -const app = new sst.aws.Function("MyApp", { - handler: "publisher.handler", - link: [table], - url: true, -}); - -return { - app: app.url, - table: table.name, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-dynamo). - - ---- -## EC2 with Pulumi - -Use raw Pulumi resources to create an EC2 instance. -```ts title="sst.config.ts" -// Notice you don't need to import pulumi, it is already part of sst. -const securityGroup = new aws.ec2.SecurityGroup("web-secgrp", { - ingress: [ - { - protocol: "tcp", - fromPort: 80, - toPort: 80, - cidrBlocks: ["0.0.0.0/0"], - }, - ], -}); - -// Find the latest Ubuntu AMI -const ami = aws.ec2.getAmi({ - filters: [ - { - name: "name", - values: ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"], - }, - ], - mostRecent: true, - owners: ["099720109477"], // Canonical -}); - -// User data to set up a simple web server -const userData = `#!/bin/bash -ho "Hello, World!" > index.html -hup python3 -m http.server 80 &`; - -// Create an EC2 instance -const server = new aws.ec2.Instance("web-server", { - instanceType: "t2.micro", - ami: ami.then((ami) => ami.id), - userData: userData, - vpcSecurityGroupIds: [securityGroup.id], - associatePublicIpAddress: true, -}); - -return { - app: server.publicIp, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-ec2-pulumi). - - ---- -## AWS EFS with SQLite - -Mount an EFS file system to a function and write to a SQLite database. - -```js title="index.ts" -const db = sqlite3("/mnt/efs/mydb.sqlite"); -``` - -The file system is mounted to `/mnt/efs` in the function. - -:::note -Given the performance of EFS, it's not recommended to use it for databases. -::: - -This example is for demonstration purposes only. It's not recommended to use -EFS for databases in production. -```ts title="sst.config.ts" -// NAT Gateways are required for Lambda functions -const vpc = new sst.aws.Vpc("MyVpc", { nat: "managed" }); - -// Create an EFS file system to store the SQLite database -const efs = new sst.aws.Efs("MyEfs", { vpc }); - -// Create a Lambda function that queries the database -new sst.aws.Function("MyFunction", { - vpc, - url: true, - volume: { - efs, - path: "/mnt/efs", - }, - handler: "index.handler", - nodejs: { - install: ["better-sqlite3"], - }, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-efs-sqlite). - - ---- -## AWS EFS with SurrealDB - -We use the SurrealDB docker image to run a server in a container and use EFS as the file -system. - -```ts title="sst.config.ts" -const server = new sst.aws.Service("MyService", { - cluster, - architecture: "arm64", - image: "surrealdb/surrealdb:v2.0.2", - // ... - volumes: [ - { efs, path: "/data" }, - ], -}); -``` - -We then connect to the server from a Lambda function. - -```js title="index.ts" -const endpoint = `http://${Resource.MyConfig.host}:${Resource.MyConfig.port}`; - -const db = new Surreal(); -await db.connect(endpoint); -``` - -This uses the SurrealDB client to connect to the server. - -:::note -Given the performance of EFS, it's not recommended to use it for databases. -::: - -This example is for demonstration purposes only. It's not recommended to use -EFS for databases in production. -```ts title="sst.config.ts" -const { RandomPassword } = await import("@pulumi/random"); - -// SurrealDB Credentials -const PORT = 8080; -const NAMESPACE = "test"; -const DATABASE = "test"; -const USERNAME = "root"; -const PASSWORD = new RandomPassword("Password", { - length: 32, -}).result; - -// NAT Gateways are required for Lambda functions -const vpc = new sst.aws.Vpc("MyVpc", { nat: "managed" }); - -// Store SurrealDB data in EFS -const efs = new sst.aws.Efs("MyEfs", { vpc }); - -// Run SurrealDB server in a container -const cluster = new sst.aws.Cluster("MyCluster", { vpc }); -const server = new sst.aws.Service("MyService", { - cluster, - architecture: "arm64", - image: "surrealdb/surrealdb:v2.0.2", - command: [ - "start", - "--bind", - $interpolate`0.0.0.0:${PORT}`, - "--log", - "info", - "--user", - USERNAME, - "--pass", - PASSWORD, - "surrealkv://data/data.skv", - "--allow-scripting", - ], - volumes: [{ efs, path: "/data" }], -}); - -// Lambda client to connect to SurrealDB -const config = new sst.Linkable("MyConfig", { - properties: { - username: USERNAME, - password: PASSWORD, - namespace: NAMESPACE, - database: DATABASE, - port: PORT, - host: server.service, - }, -}); - -new sst.aws.Function("MyApp", { - handler: "index.handler", - link: [config], - url: true, - vpc, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-efs-surrealdb). - - ---- -## AWS EFS - -Mount an EFS file system to a function and a container. - -This allows both your function and the container to access the same file system. Here they -both update a counter that's stored in the file system. - -```js title="common.mjs" -await writeFile("/mnt/efs/counter", newValue.toString()); -``` - -The file system is mounted to `/mnt/efs` in both the function and the container. -```ts title="sst.config.ts" -// NAT Gateways are required for Lambda functions -const vpc = new sst.aws.Vpc("MyVpc", { nat: "managed" }); - -// Create an EFS file system to store a counter -const efs = new sst.aws.Efs("MyEfs", { vpc }); - -// Create a Lambda function that increments the counter -new sst.aws.Function("MyFunction", { - handler: "lambda.handler", - url: true, - vpc, - volume: { - efs, - path: "/mnt/efs", - }, -}); - -// Create a service that increments the same counter -const cluster = new sst.aws.Cluster("MyCluster", { vpc }); -new sst.aws.Service("MyService", { - cluster, - loadBalancer: { - ports: [{ listen: "80/http" }], - }, - volumes: [ - { - efs, - path: "/mnt/efs", - }, - ], -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-efs). - - ---- -## AWS Express Redis - -Creates a hit counter app with Express and Redis. - -This deploys Express as a Fargate service to ECS and it's linked to Redis. - -```ts title="sst.config.ts" {9} -new sst.aws.Service("MyService", { - cluster, - loadBalancer: { - ports: [{ listen: "80/http" }], - }, - dev: { - command: "node --watch index.mjs", - }, - link: [redis], -}); -``` - -Since our Redis cluster is in a VPC, we’ll need a tunnel to connect to it from our local -machine. - -```bash "sudo" -sudo npx sst tunnel install -``` - -This needs _sudo_ to create a network interface on your machine. You’ll only need to do this -once on your machine. - -To start your app locally run. - -```bash -npx sst dev -``` - -Now if you go to `http://localhost:80` you’ll see a counter update as you refresh the page. - -Finally, you can deploy it using `npx sst deploy --stage production` using a `Dockerfile` -that's included in the example. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { bastion: true }); -const redis = new sst.aws.Redis("MyRedis", { vpc }); -const cluster = new sst.aws.Cluster("MyCluster", { vpc }); - -new sst.aws.Service("MyService", { - cluster, - link: [redis], - loadBalancer: { - ports: [{ listen: "80/http" }], - }, - dev: { - command: "node --watch index.mjs", - }, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-express-redis). - - ---- -## FFmpeg in Lambda - -Uses [FFmpeg](https://ffmpeg.org/) to process videos. In this example, it takes a `clip.mp4` -and grabs a single frame from it. - -:::tip -You don't need to use a Lambda layer to use FFmpeg. -::: - -We use the [`ffmpeg-static`](https://www.npmjs.com/package/ffmpeg-static) package that -contains pre-built binaries for all architectures. - -```ts title="index.ts" -import ffmpeg from "ffmpeg-static"; -``` - -We can use this to spawn a child process and run FFmpeg. - -```ts title="index.ts" -spawnSync(ffmpeg, ffmpegParams, { stdio: "pipe" }); -``` - -We don't need a layer when we deploy this because SST will use the right binary for the -target Lambda architecture; including `arm64`. - -```json title="sst.config.ts" -{ - nodejs: { install: ["ffmpeg-static"] } -} -``` - -All this is handled by [`nodejs.install`](/docs/component/aws/function#nodejs-install). -```ts title="sst.config.ts" -const func = new sst.aws.Function("MyFunction", { - url: true, - memory: "2 GB", - timeout: "15 minutes", - handler: "index.handler", - copyFiles: [{ from: "clip.mp4" }], - nodejs: { install: ["ffmpeg-static"] }, -}); - -return { - url: func.url, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-ffmpeg). - - ---- -## AWS ApiGatewayV2 Go - -Uses [aws-lambda-go-api-proxy](https://github.com/awslabs/aws-lambda-go-api-proxy/tree/master) to allow you to run a Go API with API Gateway V2. - -:::tip -We use the `aws-lambda-go-api-proxy` package to handle the API Gateway V2 event. -::: - -So you write your Go function as you normally would and then use the package to handle the API Gateway V2 event. - -```go title="main.go" -import ( - "github.com/aws/aws-lambda-go/lambda" - "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" -) - -func router() *http.ServeMux { - mux := http.NewServeMux() - - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"message": "hello world"}`)) - }) - - return mux -} - -func main() { - lambda.Start(httpadapter.NewV2(router()).ProxyWithContext) -} -``` -```ts title="sst.config.ts" -const api = new sst.aws.ApiGatewayV2("GoApi"); - -api.route("$default", { - handler: "src/", - runtime: "go", -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-go-api-gateway-v2). - - ---- -## AWS Lambda Go S3 Presigned - -Generates a presigned URL for the linked S3 bucket in a Go Lambda function. - -Configure the S3 Client and the PresignedClient. - -```go title="main.go" -cfg, err := config.LoadDefaultConfig(context.TODO()) -if err != nil { - panic(err) -} - -client := s3.NewFromConfig(cfg) -presignedClient := s3.NewPresignClient(client) -``` - -Generate the presigned URL. - -```go title="main.go" -bucketName, err := resource.Get("Bucket", "name") -if err != nil { - panic(err) -} -url, err := presignedClient.PresignPutObject(context.TODO(), &s3.PutObjectInput{ - Bucket: aws.String(bucket.(string)), - Key: aws.String(key), -}) -``` -```ts title="sst.config.ts" -const bucket = new sst.aws.Bucket("Bucket"); - -const api = new sst.aws.ApiGatewayV2("Api"); - -api.route("GET /upload-url", { - handler: "src/", - runtime: "go", - link: [bucket], -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-go-lambda-bucket-presigned-url). - - ---- -## AWS Lambda Go DynamoDB - -An example on how to use a Go runtime Lambda with DynamoDB. - -You configure the DynamoDB client. - -```go title="src/main.go" -import ( - "github.com/sst/sst/v3/sdk/golang/resource" -) - -func main() { - cfg, err := config.LoadDefaultConfig(context.Background()) - if err != nil { - panic(err) - } - client := dynamodb.NewFromConfig(cfg) - - - tableName, err := resource.Get("Table", "name") - if err != nil { - panic(err) - } -} -``` - -And make a request to DynamoDB. - -```go title="src/main.go" -_, err = r.client.PutItem(ctx, &dynamodb.PutItemInput{ - TableName: tableName.(string), - Item: item, -}) -``` -```ts title="sst.config.ts" -const table = new sst.aws.Dynamo("Table", { - fields: { - PK: "string", - SK: "string", - }, - primaryIndex: { hashKey: "PK", rangeKey: "SK" }, -}); - -new sst.aws.Function("GoFunction", { - url: true, - runtime: "go", - handler: "./src", - link: [table], -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-go-lambda-dynamo). - - ---- -## AWS Hono container with Redis - -Creates a hit counter app with Hono and Redis. - -This deploys Hono API as a Fargate service to ECS and it's linked to Redis. - -```ts title="sst.config.ts" {2} -new sst.aws.Service("MyService", { - cluster, - link: [redis], - loadBalancer: { - ports: [{ listen: "80/http", forward: "3000/http" }], - }, - dev: { - command: "npm run dev", - }, -}); -``` - -Since our Redis cluster is in a VPC, we’ll need a tunnel to connect to it from our local -machine. - -```bash "sudo" -sudo npx sst tunnel install -``` - -This needs _sudo_ to create a network interface on your machine. You’ll only need to do this -once on your machine. - -To start your app locally run. - -```bash -npx sst dev -``` - -Now if you go to `http://localhost:3000` you’ll see a counter update as you refresh the page. - -Finally, you can deploy it by: - -1. Using the `Dockerfile` that's included in this example. - -2. This compiles our TypeScript file, so we'll need add the following to the `tsconfig.json`. - - ```diff lang="json" title="tsconfig.json" {4,6} - { - "compilerOptions": { - // ... - + "outDir": "./dist" - }, - + "exclude": ["node_modules"] - } - ``` - -3. Install TypeScript. - - ```bash - npm install typescript --save-dev - ``` - -3. And add a `build` script to our `package.json`. - - ```diff lang="json" title="package.json" - "scripts": { - // ... - + "build": "tsc" - } - ``` -And finally, running `npx sst deploy --stage production`. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { bastion: true }); -const redis = new sst.aws.Redis("MyRedis", { vpc }); -const cluster = new sst.aws.Cluster("MyCluster", { vpc }); - -new sst.aws.Service("MyService", { - cluster, - link: [redis], - loadBalancer: { - ports: [{ listen: "80/http", forward: "3000/http" }], - }, - dev: { - command: "npm run dev", - }, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-hono-redis). - - ---- -## AWS Hono streaming - -An example on how to enable streaming for Lambda functions using Hono. - -```ts title="sst.config.ts" -{ - streaming: true -} -``` - -While `sst dev` doesn't support streaming, we can conditionally enable it on deploy. - -```ts title="index.ts" -export const handler = process.env.SST_LIVE ? handle(app) : streamHandle(app); -``` - -This will return the standard handler for `sst dev`. - -:::note -Streaming is currently not supported in `sst dev`. -::: - -To test this in your terminal, use the `curl` command with the `--no-buffer` option. - -```bash "--no-buffer" -curl --no-buffer https://u3dyblk457ghskwbmzrbylpxoi0ayrbb.lambda-url.us-east-1.on.aws -``` - -Here we are using a Function URL directly because API Gateway doesn't support streaming. -```ts title="sst.config.ts" -const hono = new sst.aws.Function("Hono", { - url: true, - streaming: true, - timeout: "15 minutes", - handler: "index.handler", -}); -return { - api: hono.url, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-hono-steam). - - ---- -## IAM permissions boundaries - -Use permissions boundaries to set the maximum permissions for all IAM roles that'll be -created in your app. - -In this example, the Function has the `s3:ListAllMyBuckets` and `sqs:ListQueues` -permissions. However, we create a permissions boundary that only allows `s3:ListAllMyBuckets`. -And we apply it to all Roles in the app using the global -[`$transform`](/docs/reference/global/#transform). - -As a result, the Function is only allowed to list S3 buckets. If you open the deployed URL, -you'll see that the SQS list call fails. - -Learn more about [AWS IAM permissions boundaries](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html). -```ts title="sst.config.ts" -// Create a permission boundary -const permissionsBoundary = new aws.iam.Policy("MyPermissionsBoundary", { - policy: aws.iam.getPolicyDocumentOutput({ - statements: [ - { - actions: ["s3:ListAllMyBuckets"], - resources: ["*"], - }, - ], - }).json, -}); - -// Apply the boundary to all roles -$transform(aws.iam.Role, (args) => { - args.permissionsBoundary = permissionsBoundary; -}); - -// The boundary automatically applies to this Function's role -const app = new sst.aws.Function("MyApp", { - handler: "index.handler", - permissions: [ - { - actions: ["s3:ListAllMyBuckets", "sqs:ListQueues"], - resources: ["*"], - }, - ], - url: true, -}); - -return { - app: app.url, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-iam-permission-boundary). - - ---- -## Current AWS account - -You can use the `aws.getXXXXOutput()` provider functions to get info about the current -AWS account. -Learn more about [provider functions](/docs/providers/#functions). -```ts title="sst.config.ts" -return { - region: aws.getRegionOutput().name, - account: aws.getCallerIdentityOutput({}).accountId, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-info). - - ---- -## AWS JSX Email - -Uses [JSX Email](https://jsx.email) and the `Email` component to design and send emails. - -To test this example, change the `sst.config.ts` to use your own email address. - -```ts title="sst.config.ts" -sender: "email@example.com" -``` - -Then run. - -```bash -npm install -npx sst dev -``` - -You'll get an email from AWS asking you to confirm your email address. Click the link to -verify it. - -Next, go to the URL in the `sst dev` CLI output. You should now receive an email rendered -using JSX Email. - -```ts title="index.ts" -import { Template } from "./templates/email"; - -await render(Template({ - email: "spongebob@example.com", - name: "Spongebob Squarepants" -})) -``` - -Once you are ready to go to production, you can: - -- [Request production access](https://docs.aws.amazon.com/ses/latest/dg/request-production-access.html) for SES -- And [use your domain](/docs/component/aws/email/) to send emails -```ts title="sst.config.ts" -const email = new sst.aws.Email("MyEmail", { - sender: "email@example.com", -}); -const api = new sst.aws.Function("MyApi", { - handler: "index.handler", - link: [email], - url: true, -}); - -return { - api: api.url, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-jsx-email). - - ---- -## Kinesis streams - -Create a Kinesis stream, and subscribe to it with a function. -```ts title="sst.config.ts" -const stream = new sst.aws.KinesisStream("MyStream"); - -// Create a function subscribing to all events -stream.subscribe("AllSub", "subscriber.all"); - -// Create a function subscribing to events of `bar` type -stream.subscribe("FilteredSub", "subscriber.filtered", { - filters: [ - { - data: { - type: ["bar"], - }, - }, - ], -}); - -const app = new sst.aws.Function("MyApp", { - handler: "publisher.handler", - link: [stream], - url: true, -}); - -return { - app: app.url, - stream: stream.name, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-kinesis-stream). - - ---- -## AWS Lambda Go - -This example shows how to use the [`go`](https://golang.org/) runtime in your Lambda -functions. - -Our Go function is in the `src` directory and we point to it in our function. - -```ts title="sst.config.ts" {5} -new sst.aws.Function("MyFunction", { - url: true, - runtime: "go", - link: [bucket], - handler: "./src", -}); -``` - -We are also linking it to an S3 bucket. We can reference the bucket in our function. - -```go title="src/main.go" {2} -func handler() (string, error) { - bucket, err := resource.Get("MyBucket", "name") - if err != nil { - return "", err - } - return bucket.(string), nil -} -``` - -The `resource.Get` function is from the SST Go SDK. - -```go title="src/main.go" {2} -import ( - "github.com/sst/sst/v3/sdk/golang/resource" -) -``` - -The `sst dev` CLI also supports running your Go function [_Live_](/docs/live). -```ts title="sst.config.ts" -const bucket = new sst.aws.Bucket("MyBucket"); - -new sst.aws.Function("MyFunction", { - url: true, - runtime: "go", - link: [bucket], - handler: "./src", -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-lambda-golang). - - ---- -## AWS Lambda build hook - -In this example we hook into the Lambda function build process with -`hook.postbuild`. - -This is useful for modifying the generated Lambda function code before it's -uploaded to AWS. It can also be used for uploading the generated sourcemaps -to a service like Sentry. -```ts title="sst.config.ts" -new sst.aws.Function("MyFunction", { - url: true, - handler: "index.handler", - hook: { - async postbuild(dir) { - console.log(`postbuild ------- ${dir}`); - }, - }, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-lambda-hook). - - ---- -## AWS Lambda retry with queues - -An example on how to retry Lambda invocations using SQS queues. - -Create a SQS retry queue which will be set as the destination for the Lambda function. - -```ts title="src/retry.ts" -const retryQueue = new sst.aws.Queue("retryQueue"); - -const bus = new sst.aws.Bus("bus"); - -const busSubscriber = bus.subscribe("busSubscriber", { - handler: "src/bus-subscriber.handler", - environment: { - RETRIES: "2", // set the number of retries - }, - link: [retryQueue], // so the function can send messages to the retry queue -}); - -new aws.lambda.FunctionEventInvokeConfig("eventConfig", { - functionName: $resolve([busSubscriber.nodes.function.name]).apply( - ([name]) => name, - ), - maximumRetryAttempts: 2, // default is 2, must be between 0 and 2 - destinationConfig: { - onFailure: { - destination: retryQueue.arn, - }, - }, -}); -``` - -Create a bus subscriber which will publish messages to the bus. Include a DLQ for messages that continue to fail. - -```ts title="sst.config.ts" - -const dlq = new sst.aws.Queue("dlq"); - -retryQueue.subscribe({ - handler: "src/retry.handler", - link: [busSubscriber.nodes.function, retryQueue, dlq], - timeout: "30 seconds", - environment: { - RETRIER_QUEUE_URL: retryQueue.url, - }, - permissions: [ - { - actions: ["lambda:GetFunction", "lambda:InvokeFunction"], - resources: [ - $interpolate`arn:aws:lambda:${aws.getRegionOutput().name}:${ - aws.getCallerIdentityOutput().accountId - }:function:*`, - ], - }, - ], - transform: { - function: { - deadLetterConfig: { - targetArn: dlq.arn, - }, - }, - }, -}); -``` - - -The Retry function will read mesaages and send back to the queue to be retried with a backoff. - -```ts title="src/retry.ts" -export const handler: SQSHandler = async (evt) => { - for (const record of evt.Records) { - const parsed = JSON.parse(record.body); - console.log("body", parsed); - const functionName = parsed.requestContext.functionArn - .replace(":$LATEST", "") - .split(":") - .pop(); - if (parsed.responsePayload) { - const attempt = (parsed.requestPayload.attempts || 0) + 1; - - const info = await lambda.send( - new GetFunctionCommand({ - FunctionName: functionName, - }), - ); - const max = - Number.parseInt( - info.Configuration?.Environment?.Variables?.RETRIES || "", - ) || 0; - console.log("max retries", max); - if (attempt > max) { - console.log(`giving up after ${attempt} retries`); - // send to dlq - await sqs.send( - new SendMessageCommand({ - QueueUrl: Resource.dlq.url, - MessageBody: JSON.stringify({ - requestPayload: parsed.requestPayload, - requestContext: parsed.requestContext, - responsePayload: parsed.responsePayload, - }), - }), - ); - return; - } - const seconds = Math.min(Math.pow(2, attempt), 900); - console.log( - "delaying retry by ", - seconds, - "seconds for attempt", - attempt, - ); - parsed.requestPayload.attempts = attempt; - await sqs.send( - new SendMessageCommand({ - QueueUrl: Resource.retryQueue.url, - DelaySeconds: seconds, - MessageBody: JSON.stringify({ - requestPayload: parsed.requestPayload, - requestContext: parsed.requestContext, - }), - }), - ); - } - - if (!parsed.responsePayload) { - console.log("triggering function"); - try { - await lambda.send( - new InvokeCommand({ - InvocationType: "Event", - Payload: Buffer.from(JSON.stringify(parsed.requestPayload)), - FunctionName: functionName, - }), - ); - } catch (e) { - if (e instanceof ResourceNotFoundException) { - return; - } - throw e; - } - } - } -}; -``` -```ts title="sst.config.ts" -const dlq = new sst.aws.Queue("dlq"); - -const retryQueue = new sst.aws.Queue("retryQueue"); - -const bus = new sst.aws.Bus("bus"); - -const busSubscriber = bus.subscribe("busSubscriber", { - handler: "src/bus-subscriber.handler", - environment: { - RETRIES: "2", - }, - link: [retryQueue], // so the function can send messages to the queue -}); - -const publisher = new sst.aws.Function("publisher", { - handler: "src/publisher.handler", - link: [bus], - url: true, -}); - -new aws.lambda.FunctionEventInvokeConfig("eventConfig", { - functionName: $resolve([busSubscriber.nodes.function.name]).apply( - ([name]) => name, - ), - maximumRetryAttempts: 1, - destinationConfig: { - onFailure: { - destination: retryQueue.arn, - }, - }, -}); - -retryQueue.subscribe({ - handler: "src/retry.handler", - link: [busSubscriber.nodes.function, retryQueue, dlq], - timeout: "30 seconds", - environment: { - RETRIER_QUEUE_URL: retryQueue.url, - }, - permissions: [ - { - actions: ["lambda:GetFunction", "lambda:InvokeFunction"], - resources: [ - $interpolate`arn:aws:lambda:${aws.getRegionOutput().name}:${ - aws.getCallerIdentityOutput().accountId - }:function:*`, - ], - }, - ], - transform: { - function: { - deadLetterConfig: { - targetArn: dlq.arn, - }, - }, - }, -}); - -return { - publisher: publisher.url, - dlq: dlq.url, - retryQueue: retryQueue.url, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-lambda-retry-with-queues). - - ---- -## AWS Lamda Rust multiple-binaries - -This example shows how to deploy multiple binary rust project to AWS Lambda. - -SST relies on the work of [cargo lambda](https://cargo-lambda) to build and deploy Rust Lambda functions. - -What is special about the following file is that we are defining multiple binaries using the `[[bin]]` section in the `Cargo.toml` file. - -```toml title="Cargo.toml" {13,14,15,17,18,19} -[package] -name = "aws-lambda-rust-multi-bin" -version = "0.1.0" -edition = "2021" - -[dependencies] -lambda_runtime = "0.13.0" -serde = { version = "1.0.217", features = ["derive"] } -serde_json = "1.0.138" -tokio = { version = "1", features = ["macros"] } -# -- please note ommited dependencies -- - -[[bin]] -name = "push" -path = "src/push.rs" - -[[bin]] -name = "pop" -path = "src/pop.rs" -``` - -We then utilise the . syntax to specify the handler binary - -```ts title="sst.config.ts" {5,11} -new sst.aws.Function("push", { - url: true, - runtime: "rust", - link: [bucket], - handler: "./.push", -}); -new sst.aws.Function("pop", { - url: true, - runtime: "rust", - link: [bucket], - handler: "./.pop", -}); -``` -```ts title="sst.config.ts" -const bucket = new sst.aws.Bucket("Bucket"); -const push = new sst.aws.Function("push", { - runtime: "rust", - handler: "./.push", - url: true, - architecture: "arm64", - link: [bucket], -}); -const pop = new sst.aws.Function("pop", { - runtime: "rust", - handler: "./.pop", - url: true, - architecture: "arm64", - link: [bucket], -}); - -return { push_url: push.url, pop_url: pop.url }; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-lambda-rust-multiple-binaries). - - ---- -## AWS Lambda streaming - -An example on how to enable streaming for Lambda functions. - -```ts title="sst.config.ts" -{ - streaming: true -} -``` - -While `sst dev` doesn't support streaming, you can use the -[`lambda-stream`](https://github.com/astuyve/lambda-stream) package to test locally. - -```bash -npm install lambda-stream -``` - -Then, you can use the `streamifyResponse` function to wrap your handler: - -```ts title="index.ts" -import { APIGatewayProxyEventV2 } from "aws-lambda"; -import { streamifyResponse, ResponseStream } from "lambda-stream"; - -export const handler = streamifyResponse(myHandler); - -async function myHandler( - _event: APIGatewayProxyEventV2, - responseStream: ResponseStream -): Promise { - return new Promise((resolve, _reject) => { - responseStream.setContentType('text/plain') - responseStream.write('Hello') - setTimeout(() => { - responseStream.write(' World') - responseStream.end() - resolve() - }, 3000) - }) -} -``` - -When deployed, this will use the `awslambda.streamifyResponse`. - -:::note -Streaming is currently not supported in `sst dev`. -::: - -To test this in your terminal, use the `curl` command with the `--no-buffer` option. - -```bash "--no-buffer" -curl --no-buffer https://u3dyblk457ghskwbmzrbylpxoi0ayrbb.lambda-url.us-east-1.on.aws -``` - -Here we are using a Function URL directly because API Gateway doesn't support streaming. -```ts title="sst.config.ts" -const fn = new sst.aws.Function("MyFunction", { - url: true, - streaming: true, - timeout: "15 minutes", - handler: "index.handler", -}); - -return { - url: fn.url, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-lambda-stream). - - ---- -## AWS Lambda in a VPC - -You can use SST to locally work on Lambda functions that are in a VPC. To do so, you'll -need to enable `bastion` and `nat` on the `Vpc` component. - -```ts title="sst.config.ts" -new sst.aws.Vpc("MyVpc", { bastion: true, nat: "managed" }); -``` - -The NAT gateway is necessary to allow your Lambda function to connect to the internet. While, -the bastion host is necessary for your local machine to be able to tunnel to the VPC. - -You'll need to install the tunnel, if you haven't done this before. - -```bash "sudo" -sudo sst tunnel install -``` - -This needs _sudo_ to create the network interface on your machine. You'll only need to do -this once. - -Now you can run `sst dev`, your function can access resources in the VPC. For example, here -we are connecting to a Redis cluster. - -```ts title="index.ts" -const redis = new Cluster( - [{ host: Resource.MyRedis.host, port: Resource.MyRedis.port }], - { - dnsLookup: (address, callback) => callback(null, address), - redisOptions: { - tls: {}, - username: Resource.MyRedis.username, - password: Resource.MyRedis.password, - }, - } -); -``` - -The Redis cluster is in the same VPC as the function. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { bastion: true, nat: "managed" }); -const redis = new sst.aws.Redis("MyRedis", { vpc }); -const api = new sst.aws.Function("MyFunction", { - vpc, - url: true, - link: [redis], - handler: "index.handler" -}); - -return { - url: api.url, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-lambda-vpc). - - ---- -## AWS multi-region - -To deploy resources to multiple AWS regions, you can create a new provider for the region -you want to deploy to. - -```ts title="sst.config.ts" -const provider = new aws.Provider("MyProvider", { region: "us-west-2" }); -``` - -And then pass that in to the resource. - -```ts title="sst.config.ts" -new sst.aws.Function("MyFunction", { handler: "index.handler" }, { provider }); -``` - -If no provider is passed in, the default provider will be used. And if no region is -specified, the default region from your credentials will be used. -```ts title="sst.config.ts" -const east = new sst.aws.Function("MyEastFunction", { - url: true, - handler: "index.handler", -}); - -const provider = new aws.Provider("MyWestProvider", { region: "us-west-2" }); -const west = new sst.aws.Function( - "MyWestFunction", - { - url: true, - handler: "index.handler", - }, - { provider } -); - -return { - east: east.url, - west: west.url, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-multi-region). - - ---- -## AWS MySQL local - -In this example, we connect to a locally running MySQL instance for dev. While -on deploy, we use RDS. - -We use the [`docker run`](https://docs.docker.com/reference/cli/docker/container/run/) CLI -to start a local container with MySQL. You don't have to use Docker, you can use -any other way to run MySQL locally. - -```bash -docker run \ - --rm \ - -p 3306:3306 \ - -v $(pwd)/.sst/storage/mysql:/var/lib/mysql/data \ - -e MYSQL_ROOT_PASSWORD=password \ - -e MYSQL_DATABASE=local \ - mysql:8.0 -``` - -The data is saved to the `.sst/storage` directory. So if you restart the dev server, the -data will still be there. - -We then configure the `dev` property of the `Mysql` component with the settings for the -local MySQL instance. - -```ts title="sst.config.ts" -dev: { - username: "root", - password: "password", - database: "local", - host: "localhost", - port: 3306, -} -``` - -By providing the `dev` prop for Mysql, SST will use the local MySQL instance and -not deploy a new RDS database when running `sst dev`. - -It also allows us to access the database through a Resource `link` without having to -conditionally check if we are running locally. - -```ts title="index.ts" -const pool = new Pool({ - host: Resource.MyDatabase.host, - port: Resource.MyDatabase.port, - user: Resource.MyDatabase.username, - password: Resource.MyDatabase.password, - database: Resource.MyDatabase.database, -}); -``` - -The above will work in both `sst dev` and `sst deploy`. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { nat: "ec2" }); - -const mysql = new sst.aws.Mysql("MyDatabase", { - dev: { - username: "root", - password: "password", - database: "local", - host: "localhost", - port: 3306, - }, - vpc, -}); - -new sst.aws.Function("MyFunction", { - vpc, - url: true, - link: [mysql], - handler: "index.handler", -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-mysql-local). - - ---- -## AWS MySQL - -In this example, we deploy an RDS MySQL database. - -```ts title="sst.config.ts" -const mysql = new sst.aws.Mysql("MyDatabase", { - vpc, -}); -``` - -And link it to a Lambda function. - -```ts title="sst.config.ts" {3} -new sst.aws.Function("MyApp", { - handler: "index.handler", - link: [mysql], - url: true, - vpc, -}); -``` - -Now in the function we can access the database. - -```ts title="index.ts" -const connection = await mysql.createConnection({ - database: Resource.MyDatabase.database, - host: Resource.MyDatabase.host, - port: Resource.MyDatabase.port, - user: Resource.MyDatabase.username, - password: Resource.MyDatabase.password, -}); -``` - -We also enable the `bastion` option for the VPC. This allows us to connect to -the database from our local machine with the `sst tunnel` CLI. - -```bash "sudo" -sudo npx sst tunnel install -``` - -This needs _sudo_ to create a network interface on your machine. You’ll only -need to do this once on your machine. - -Now you can run `npx sst dev` and you can connect to the database from your local machine. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { nat: "ec2", bastion: true }); -const mysql = new sst.aws.Mysql("MyDatabase", { - vpc, -}); -const app = new sst.aws.Function("MyApp", { - handler: "index.handler", - link: [mysql], - url: true, - vpc, -}); - -return { - app: app.url, - host: mysql.host, - port: mysql.port, - username: mysql.username, - password: mysql.password, - database: mysql.database, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-mysql). - - ---- -## AWS NestJS with Redis - -Creates a hit counter app with NestJS and Redis. - -:::note -You need Node 22.12 or higher for this example to work. -::: - -Also make sure you have Node 22.12. Or set the `--experimental-require-module` flag. -This'll allow NestJS to import the SST SDK. - -This deploys NestJS as a Fargate service to ECS and it's linked to Redis. - -```ts title="sst.config.ts" {3} -new sst.aws.Service("MyService", { - cluster, - link: [redis], - loadBalancer: { - ports: [{ listen: "80/http", forward: "3000/http" }], - }, - dev: { - command: "npm run start:dev", - }, -}); -``` - -Since our Redis cluster is in a VPC, we’ll need a tunnel to connect to it from our local -machine. - -```bash "sudo" -sudo npx sst tunnel install -``` - -This needs _sudo_ to create a network interface on your machine. You’ll only need to do this -once on your machine. - -To start your app locally run. - -```bash -npx sst dev -``` - -Now if you go to `http://localhost:3000` you’ll see a counter update as you refresh the page. - -Finally, you can deploy it using `npx sst deploy --stage production` using a `Dockerfile` -that's included in the example. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc('MyVpc', { bastion: true }); -const redis = new sst.aws.Redis('MyRedis', { vpc }); -const cluster = new sst.aws.Cluster('MyCluster', { vpc }); - -new sst.aws.Service('MyService', { - cluster, - link: [redis], - loadBalancer: { - ports: [{ listen: '80/http', forward: '3000/http' }], - }, - dev: { - command: 'npm run start:dev', - }, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-nestjs-redis). - - ---- -## AWS Next.js add behavior - -Here's how to add additional routes or cache behaviors to the CDN of a Next.js app deployed -with OpenNext to AWS. - -Specify the path pattern that you want to forward to your new origin. For example, to forward -all requests to the `/blog` path to a different origin. - -```ts title="sst.config.ts" -pathPattern: "/blog/*" -``` - -And then specify the domain of the new origin. - -```ts title="sst.config.ts" -domainName: "blog.example.com" -``` - -We use this to `transform` our site's CDN and add the additional behaviors. -```ts title="sst.config.ts" -const blogOrigin = { - // The domain of the new origin - domainName: "blog.example.com", - originId: "blogCustomOrigin", - customOriginConfig: { - httpPort: 80, - httpsPort: 443, - originSslProtocols: ["TLSv1.2"], - // If HTTPS is supported - originProtocolPolicy: "https-only", - }, -}; - -const cacheBehavior = { - // The path to forward to the new origin - pathPattern: "/blog/*", - targetOriginId: blogOrigin.originId, - viewerProtocolPolicy: "redirect-to-https", - allowedMethods: ["GET", "HEAD", "OPTIONS"], - cachedMethods: ["GET", "HEAD"], - forwardedValues: { - queryString: true, - cookies: { - forward: "all", - }, - }, -}; - -new sst.aws.Nextjs("MyWeb", { - transform: { - cdn: (options: sst.aws.CdnArgs) => { - options.origins = $resolve(options.origins).apply(val => [...val, blogOrigin]); - - options.orderedCacheBehaviors = $resolve( - options.orderedCacheBehaviors || [] - ).apply(val => [...val, cacheBehavior]); - }, - }, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-nextjs-add-behavior). - - ---- -## AWS Next.js basic auth - -Deploys a simple Next.js app and adds basic auth to it. - -This is useful for dev environments where you want to share your app your team but ensure -that it's not publicly accessible. - -:::tip -You can use this for all the SSR sites, like Astro, Remix, SvelteKit, etc. -::: - -This works by injecting some code into a CloudFront function that checks the basic auth -header and matches it against the `USERNAME` and `PASSWORD` secrets. - -```ts title="sst.config.ts" -{ - injection: $interpolate` - if ( - !event.request.headers.authorization - || event.request.headers.authorization.value !== "Basic ${basicAuth}" - ) { - return { - statusCode: 401, - headers: { - "www-authenticate": { value: "Basic" } - } - }; - }`, -} -``` - -To deploy this, you need to first set the `USERNAME` and `PASSWORD` secrets. - -```bash -sst secret set USERNAME my-username -sst secret set PASSWORD my-password -``` - -If you are deploying this to preview environments, you might want to set the secrets using -the [`--fallback`](/docs/reference/cli#secret) flag. -```ts title="sst.config.ts" -const username = new sst.Secret("USERNAME"); -const password = new sst.Secret("PASSWORD"); -const basicAuth = $resolve([username.value, password.value]).apply( - ([username, password]) => - Buffer.from(`${username}:${password}`).toString("base64") -); - -new sst.aws.Nextjs("MyWeb", { - server: { - // Don't password protect prod - edge: $app.stage !== "production" - ? { - viewerRequest: { - injection: $interpolate` - if ( - !event.request.headers.authorization - || event.request.headers.authorization.value !== "Basic ${basicAuth}" - ) { - return { - statusCode: 401, - headers: { - "www-authenticate": { value: "Basic" } - } - }; - }`, - }, - } - : undefined, - }, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-nextjs-basic-auth). - - ---- -## AWS Next.js container with Redis - -Creates a hit counter app with Next.js and Redis. - -This deploys Next.js as a Fargate service to ECS and it's linked to Redis. - -```ts title="sst.config.ts" {3} -new sst.aws.Service("MyService", { - cluster, - link: [redis], - loadBalancer: { - ports: [{ listen: "80/http", forward: "3000/http" }], - }, - dev: { - command: "npm run dev", - }, -}); -``` - -Since our Redis cluster is in a VPC, we’ll need a tunnel to connect to it from our local -machine. - -```bash "sudo" -sudo npx sst tunnel install -``` - -This needs _sudo_ to create a network interface on your machine. You’ll only need to do this -once on your machine. - -To start your app locally run. - -```bash -npx sst dev -``` - -Now if you go to `http://localhost:3000` you’ll see a counter update as you refresh the page. - -Finally, you can deploy it by: - -1. Setting `output: "standalone"` in your `next.config.mjs` file. -2. Adding a `Dockerfile` that's included in this example. -3. Running `npx sst deploy --stage production`. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { bastion: true }); -const redis = new sst.aws.Redis("MyRedis", { vpc }); -const cluster = new sst.aws.Cluster("MyCluster", { vpc }); - -new sst.aws.Service("MyService", { - cluster, - link: [redis], - loadBalancer: { - ports: [{ listen: "80/http", forward: "3000/http" }], - }, - dev: { - command: "npm run dev", - }, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-nextjs-redis). - - ---- -## AWS Next.js streaming - -An example of how to use streaming Next.js RSC. Uses `Suspense` to stream an async component. - -```tsx title="app/page.tsx" -Loading...}> - - -``` - -For this demo we also need to make sure the route is not statically built. - -```ts title="app/page.tsx" -export const dynamic = "force-dynamic"; -``` - -This is deployed with OpenNext, which needs a config to enable streaming. - -```ts title="open-next.config.ts" {4} -export default { - default: { - override: { - wrapper: "aws-lambda-streaming" - } - } -}; -``` - -You should see the _friends_ section load after a 3 second delay. - -:::note -Safari handles streaming differently than other browsers. -::: - -Safari uses a [different heuristic](https://bugs.webkit.org/show_bug.cgi?id=252413) to -determine when to stream data. You need to render _enough_ initial HTML to trigger streaming. -This is typically only a problem for demo apps. -```ts title="sst.config.ts" -new sst.aws.Nextjs("MyWeb"); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-nextjs-stream). - - ---- -## AWS OpenSearch local - -In this example, we connect to a locally running OpenSearch process for dev. While -on deploy, we use AWS' OpenSearch Service. - -We use the [`docker run`](https://docs.docker.com/reference/cli/docker/container/run/) -CLI to start a local container with OpenSearch. You don't have to use Docker, you can use -any other way to run OpenSearch locally. - -```bash -docker run \ - --rm \ - -p 9200:9200 \ - -v $(pwd)/.sst/storage/opensearch:/usr/share/opensearch/data \ - -e discovery.type=single-node \ - -e plugins.security.disabled=true \ - -e OPENSEARCH_INITIAL_ADMIN_PASSWORD=^Passw0rd^ \ - opensearchproject/opensearch:2.17.0 -``` - -The data is saved to the `.sst/storage` directory. So if you restart the dev server, the -data will still be there. - -We then configure the `dev` property of the `OpenSearch` component with the settings for -the local OpenSearch instance. - -```ts title="sst.config.ts" -dev: { - url: "http://localhost:9200", - username: "admin", - password: "^Passw0rd^" -} -``` - -By providing the `dev` prop for OpenSearch, SST will use the local OpenSearch process and -not deploy a new OpenSearch domain when running `sst dev`. - -It also allows us to access the local process through a Resource `link` without having -to conditionally check if we are running locally. - -```ts title="index.ts" -const client = new Client({ - node: Resource.MySearch.url, - auth: { - username: Resource.MySearch.username, - password: Resource.MySearch.password, - }, -}); -``` - -The above will work in both `sst dev` and `sst deploy`. -```ts title="sst.config.ts" -const search = new sst.aws.OpenSearch("MySearch", { - dev: { - url: "http://localhost:9200", - username: "admin", - password: "^Passw0rd^", - }, -}); - -new sst.aws.Function("MyApp", { - handler: "index.handler", - url: true, - link: [search], -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-open-search-local). - - ---- -## AWS OpenSearch - -In this example we create a new OpenSearch domain, link it to a function, and -then query it. - -Start by creating a new OpenSearch domain. - -```ts title="sst.config.ts" -const search = new sst.aws.OpenSearch("MySearch"); -``` - -Once linked to a function, we can connect to it. - -```ts title="index.ts" -import { Resource } from "sst"; -import { Client } from "@opensearch-project/opensearch"; - -const client = new Client({ - node: Resource.MySearch.url, - auth: { - username: Resource.MySearch.username, - password: Resource.MySearch.password - } -}); -``` - -This is using the [OpenSearch JS SDK](https://docs.opensearch.org/docs/latest/clients/javascript/index) to connect to the OpenSearch domain.. -```ts title="sst.config.ts" -const search = new sst.aws.OpenSearch("MySearch"); -const app = new sst.aws.Function("MyApp", { - handler: "index.handler", - url: true, - link: [search], -}); - -return { - app: app.url, - url: search.url, - username: search.username, - password: search.password, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-open-search). - - ---- -## AWS Postgres local - -In this example, we connect to a locally running Postgres instance for dev. While -on deploy, we use RDS. - -We use the [`docker run`](https://docs.docker.com/reference/cli/docker/container/run/) CLI -to start a local container with Postgres. You don't have to use Docker, you can use -Postgres.app or any other way to run Postgres locally. - -```bash -docker run \ - --rm \ - -p 5432:5432 \ - -v $(pwd)/.sst/storage/postgres:/var/lib/postgresql/data \ - -e POSTGRES_USER=postgres \ - -e POSTGRES_PASSWORD=password \ - -e POSTGRES_DB=local \ - postgres:16.4 -``` - -The data is saved to the `.sst/storage` directory. So if you restart the dev server, the -data will still be there. - -We then configure the `dev` property of the `Postgres` component with the settings for the -local Postgres instance. - -```ts title="sst.config.ts" -dev: { - username: "postgres", - password: "password", - database: "local", - port: 5432, -} -``` - -By providing the `dev` prop for Postgres, SST will use the local Postgres instance and -not deploy a new RDS database when running `sst dev`. - -It also allows us to access the database through a Resource `link` without having to -conditionally check if we are running locally. - -```ts title="index.ts" -const pool = new Pool({ - host: Resource.MyPostgres.host, - port: Resource.MyPostgres.port, - user: Resource.MyPostgres.username, - password: Resource.MyPostgres.password, - database: Resource.MyPostgres.database, -}); -``` - -The above will work in both `sst dev` and `sst deploy`. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { nat: "ec2" }); - -const rds = new sst.aws.Postgres("MyPostgres", { - dev: { - username: "postgres", - password: "password", - database: "local", - host: "localhost", - port: 5432, - }, - vpc, -}); - -new sst.aws.Function("MyFunction", { - vpc, - url: true, - link: [rds], - handler: "index.handler", -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-postgres-local). - - ---- -## Prisma in Lambda - -To use Prisma in a Lambda function you need to - -- Generate the Prisma Client with the right architecture -- Copy the generated client to the function -- Run the function inside a VPC - -You can set the architecture using the `binaryTargets` option in `prisma/schema.prisma`. - -```prisma title="prisma/schema.prisma" -// For x86 -binaryTargets = ["native", "rhel-openssl-3.0.x"] -// For ARM -// binaryTargets = ["native", "linux-arm64-openssl-3.0.x"] -``` - -You can also switch to ARM, just make sure to also change the function architecture in your -`sst.config.ts`. - -```ts title="sst.config.ts" -{ - // For ARM - architecture: "arm64" -} -``` - -To generate the client, you need to run `prisma generate` when you make changes to the -schema. - -Since this [needs to be done on every deploy](https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/vercel-caching-issue#a-custom-postinstall-script), we add a `postinstall` script to the `package.json`. - -```json title="package.json" -"scripts": { - "postinstall": "prisma generate" -} -``` - -This runs the command on `npm install`. - -We then need to copy the generated client to the function when we deploy. - -```ts title="sst.config.ts" -{ - copyFiles: [{ from: "node_modules/.prisma/client/" }] -} -``` - -Our function also needs to run inside a VPC, since Prisma doesn't support the Data API. - -```ts title="sst.config.ts" -{ - vpc -} -``` - -#### Prisma in serverless environments - -Prisma is [not great in serverless environments](https://www.prisma.io/docs/orm/prisma-client/setup-and-configuration/databases-connections#serverless-environments-faas). For a couple of reasons: - -1. It doesn't support Data API, so you need to manage the connection pool on your own. -2. Without the Data API, your functions need to run inside a VPC. - - You cannot use `sst dev` without [connecting to the VPC](/docs/live#using-a-vpc). -3. Due to the internal architecture of their client, it's also has slower cold starts. - -Instead we recommend using [Drizzle](https://orm.drizzle.team). This example is here for -reference for people that are already using Prisma. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { nat: "managed" }); -const rds = new sst.aws.Postgres("MyPostgres", { vpc }); - -const api = new sst.aws.Function("MyApi", { - vpc, - url: true, - link: [rds], - // For ARM - // architecture: "arm64", - handler: "index.handler", - copyFiles: [{ from: "node_modules/.prisma/client/" }], -}); - -return { - api: api.url, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-prisma-lambda). - - ---- -## Puppeteer in Lambda - -To use Puppeteer in a Lambda function you need: - -1. [`puppeteer-core`](https://www.npmjs.com/package/puppeteer-core) -2. Chromium - - In `sst dev`, we'll use a locally installed Chromium version. - - In `sst deploy`, we'll use the [`@sparticuz/chromium`](https://github.com/sparticuz/chromium) package. It comes with a pre-built binary for Lambda. - -#### Chromium version - -Since Puppeteer has a preferred version of Chromium, we'll need to check the version of -Chrome that a given version of Puppeteer supports. Head over to the -[Puppeteer's Chromium Support page](https://pptr.dev/chromium-support) and check which -versions work together. - -For example, Puppeteer v23.1.1 supports Chrome for Testing 127.0.6533.119. So, we'll use the -v127 of `@sparticuz/chromium`. - -```bash -npm install puppeteer-core@23.1.1 @sparticuz/chromium@127.0.0 -``` - -#### Install Chromium locally - -To use this locally, you'll need to install Chromium. - -```bash -npx @puppeteer/browsers install chromium@latest --path /tmp/localChromium -``` - -Once installed you'll see the location of the Chromium binary, `/tmp/localChromium/chromium/mac_arm-1350406/chrome-mac/Chromium.app/Contents/MacOS/Chromium`. - -Update this in your Lambda function. - -```ts title="index.ts" -// This is the path to the local Chromium binary -const YOUR_LOCAL_CHROMIUM_PATH = "/tmp/localChromium/chromium/mac_arm-1350406/chrome-mac/Chromium.app/Contents/MacOS/Chromium"; -``` - -You'll notice we are using the right binary with the `SST_DEV` environment variable. - -```ts title="index.ts" {4-6} -const browser = await puppeteer.launch({ - args: chromium.args, - defaultViewport: chromium.defaultViewport, - executablePath: process.env.SST_DEV - ? YOUR_LOCAL_CHROMIUM_PATH - : await chromium.executablePath(), - headless: chromium.headless, -}); -``` - -#### Deploy - -We don't need a layer to deploy this because `@sparticuz/chromium` comes with a pre-built -binary for Lambda. - -:::note -As of writing this, `arm64` is not supported by `@sparticuz/chromium`. -::: - -We just need to set it in the [`nodejs.install`](/docs/component/aws/function#nodejs-install). - -```ts title="sst.config.ts" -{ - nodejs: { - install: ["@sparticuz/chromium"] - } -} -``` - -And on deploy, SST will use the right binary. - -:::tip -You don't need to use a Lambda layer to use Puppeteer. -::: - -We are giving our function more memory and a longer timeout since running Puppeteer can -take a while. -```ts title="sst.config.ts" -const api = new sst.aws.Function("MyFunction", { - url: true, - memory: "2 GB", - timeout: "15 minutes", - handler: "index.handler", - nodejs: { - install: ["@sparticuz/chromium"], - }, -}); - -return { - url: api.url, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-puppeteer). - - ---- -## AWS Lambda Python container - -Python Lambda function that use large dependencies like `numpy` and `pandas`, can -hit the 250MB Lambda package limit. To work around this, you can deploy them -as a container image to Lambda. - -:::tip -Container images on Lambda have a limit of 10GB. -::: - -In this example, we deploy two functions as container image. - -```ts title="sst.config.ts" {2-4} -const base = new sst.aws.Function("PythonFn", { - python: { - container: true, - }, - handler: "./functions/src/functions/api.handler", - runtime: "python3.11", - link: [linkableValue], - url: true, -}); -``` - -Now when you run `sst deploy`, it uses a built-in Dockerfile to build the image -and deploy it. You'll need to have the Docker daemon running. - -:::note -You need to have the Docker daemon running locally. -::: - -To use a custom Dockerfile, you can place a `Dockerfile` in the root of the -uv workspace for your function. - -```ts title="sst.config.ts" {5} -const custom = new sst.aws.Function("PythonFnCustom", { - python: { - container: true, - }, - handler: "./custom_dockerfile/src/custom_dockerfile/api.handler", - runtime: "python3.11", - link: [linkableValue], - url: true, -}); -``` - -Here we have a `Dockerfile` in the `custom_dockerfile/` directory. - -```dockerfile title="custom_dockerfile/Dockerfile" -# The python version to use is supplied as an arg from SST -ARG PYTHON_VERSION=3.11 - -# Use an official AWS Lambda base image for Python -FROM public.ecr.aws/lambda/python:${PYTHON_VERSION} - -# ... -``` - -The project structure looks something like this. - -```txt {5} -β”œβ”€β”€ sst.config.ts -β”œβ”€β”€ pyproject.toml -└── custom_dockerfile - β”œβ”€β”€ pyproject.toml - β”œβ”€β”€ Dockerfile - └── src - └── custom_dockerfile - └── api.py -``` - -Locally, you want to set the Python version in your `pyproject.toml` to make sure -that `sst dev` uses the same version as `sst deploy`. -```ts title="sst.config.ts" -const linkableValue = new sst.Linkable("MyLinkableValue", { - properties: { - foo: "Hello World", - }, -}); - -const base = new sst.aws.Function("PythonFn", { - python: { - container: true, - }, - handler: "./functions/src/functions/api.handler", - runtime: "python3.11", - link: [linkableValue], - url: true, -}); - -const custom = new sst.aws.Function("PythonFnCustom", { - python: { - container: true, - }, - handler: "./custom_dockerfile/src/custom_dockerfile/api.handler", - runtime: "python3.11", - link: [linkableValue], - url: true, -}); - -return { - base: base.url, - custom: custom.url, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-python-container). - - ---- -## AWS Lambda Python Hugging Face - -Uses a Python Lambda container image to deploy a lightweight -[Hugging Face](https://huggingface.co/) model. - -Uses the [transformers](https://github.com/huggingface/transformers) library to -generate text using the -[TinyStories-33M](https://huggingface.co/roneneldan/TinyStories-33M) model. The -backend is the pytorch cpu runtime. - -:::note -This is not a production ready example. -::: - -This example also shows how it is possible to use custom index resolution to get -dependencies from a private pypi server such as the pytorch cpu link. This -example also shows how to use a custom Dockerfile to handle complex builds such -as installing pytorch and pruning the build size. -```ts title="sst.config.ts" -new sst.aws.Function("MyPythonFunction", { - python: { - container: true, - }, - handler: "functions/src/functions/api.handler", - runtime: "python3.12", - timeout: "60 seconds", - url: true, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-python-huggingface). - - ---- -## AWS Lambda Python - -SST uses [uv](https://docs.astral.sh/uv/) to manage your Python runtime, make -sure you have it [installed](https://docs.astral.sh/uv/getting-started/installation/). - -Any [uv workspace](https://docs.astral.sh/uv/concepts/projects/workspaces/#workspace-sources) -package can be built and deployed as a Lambda function using SST. Drop-in mode -is currently not supported. - -:::note -Builds currently do not tree shake so lots of workspaces can make the build -larger than necessary. -::: - -In this example we deploy a handler from the `functions/` directory. It depends -on shared code from another uv workspace in the `core/` directory. - -```txt -β”œβ”€β”€ sst.config.ts -β”œβ”€β”€ pyproject.toml -β”œβ”€β”€ core -β”‚ β”œβ”€β”€ pyproject.toml -β”‚ └── src -β”‚ └── core -β”‚ └── __init__.py -└── functions - β”œβ”€β”€ pyproject.toml - └── src - └── functions - β”œβ”€β”€ __init__.py - └── api.py -``` - -The `handler` is the path to the handler file and the name of the handler function -in it. - -```ts title="sst.config.ts" {2} -new sst.aws.Function("MyPythonFunction", { - handler: "functions/src/functions/api.handler", - runtime: "python3.11", - link: [linkableValue], - url: true, -}); -``` - -SST will traverse up from the handler path and look for the nearest -`pyproject.toml`. And will throw an error if it can't find one. - -To access linked resources, you can use the SST SDK. - -```py title="functions/src/functions/api.py" {1} -from sst import Resource - -def handler(event, context): - print(Resource.MyLinkableValue.foo) -``` - -Where the `sst` package can be added to your `pyproject.toml`. - -```toml title="functions/pyproject.toml" -[tool.uv.sources] -sst = { git = "https://github.com/sst/sst.git", subdirectory = "sdk/python", branch = "dev" } -``` - -You also want to set the Python version in your `pyproject.toml` to the same -version as the one in Lambda. - -```toml title="functions/pyproject.toml" -requires-python = "==3.11.*" -``` - -This makes sure that your functions work the same in `sst dev` as `sst deploy`. -```ts title="sst.config.ts" -const linkableValue = new sst.Linkable("MyLinkableValue", { - properties: { - foo: "Hello World", - }, -}); - -new sst.aws.Function("MyPythonFunction", { - handler: "functions/src/functions/api.handler", - runtime: "python3.11", - link: [linkableValue], - url: true, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-python). - - ---- -## Subscribe to queues - -Create an SQS queue, subscribe to it, and publish to it from a function. -```ts title="sst.config.ts" -const queue = new sst.aws.Queue("MyQueue"); -queue.subscribe("subscriber.handler"); - -const app = new sst.aws.Function("MyApp", { - handler: "publisher.handler", - link: [queue], - url: true, -}); - -return { - app: app.url, - queue: queue.url, -}; -``` - -The subscriber will read messages from the queue in batches. This array of messages exists on the `Records` property of the `SQSEvent`. - -```ts title="subscriber.ts" -import type { SQSEvent, SQSHandler } from "aws-lambda"; - -export const handler: SQSHandler = async (event: SQSEvent) => { - for (const record of event.Records){ - // Message bodies are always strings - console.log(record.body) - } - return; -}; -``` - -By default, all messages in the batch become visible in the queue again if an error occurs. This can lead to unnecessary extra processing and messages being processed more than once. The solution is to enable [partial batch responsese](https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-errorhandling.html#services-sqs-batchfailurereporting) and return which specific messages within the batch should be made visible again in the queue. - -Update the queue subscriber. - -```ts title="sst.config.ts" -queue.subscribe("subscriber.handler", { - batch: { - partialResponses: true, - } -}); -``` - -Then update the handler to return the failed items. - -```ts title="subscriber.ts" -import type { SQSEvent, SQSHandler } from "aws-lambda"; - -export const handler: SQSHandler = async (event: SQSEvent) => { - const batchItemFailures = [] - for (const record of event.Records){ - try { - console.log(record.body) - if (Math.random() < 0.1){ - throw new Error("An error occurred") - } - } - catch (e) { - batchItemFailures.push({ itemIdentifier: record.messageId }); - } - } - - // Failed items will be made visible in the queue again - return { batchItemFailures }; -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-queue). - - ---- -## AWS Redis local - -In this example, we connect to a local Docker Redis instance for dev. While on deploy, we use -Redis ElastiCache. - -We use the [`docker run`](https://docs.docker.com/reference/cli/docker/container/run/) CLI -to start a local Redis server. You don't have to use Docker, you can run it locally any way -you want. - -```bash -docker run \ - --rm \ - -p 6379:6379 \ - -v $(pwd)/.sst/storage/redis:/data \ - redis:latest -``` - -The data is persisted to the `.sst/storage` directory. So if you restart the dev server, -the data will still be there. - -We then configure the `dev` property of the `Redis` component with the settings for the -local Redis server. - -```ts title="sst.config.ts" -dev: { - host: "localhost", - port: 6379 -} -``` - -By providing the `dev` prop for Redis, SST will use the local Redis server and -not deploy a new Redis ElastiCache cluster when running `sst dev`. - -It also allows us to access Redis through a Reosurce `link`. - -```ts title="index.ts" -const client = Resource.MyRedis.host === "localhost" - ? new Redis({ - host: Resource.MyRedis.host, - port: Resource.MyRedis.port, - }) - : new Cluster( - [{ - host: Resource.MyRedis.host, - port: Resource.MyRedis.port, - }], - { - redisOptions: { - tls: { checkServerIdentity: () => undefined }, - username: Resource.MyRedis.username, - password: Resource.MyRedis.password, - }, - }, - ); -``` - -The local Redis server is running in `standalone` mode, whereas on deploy it'll be in -`cluster` mode. So our Lambda function needs to connect using the right config. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { nat: "managed" }); - -const redis = new sst.aws.Redis("MyRedis", { - dev: { - host: "localhost", - port: 6379, - }, - vpc, -}); - -new sst.aws.Function("MyApp", { - vpc, - url: true, - link: [redis], - handler: "index.handler", -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-redis-local). - - ---- -## AWS Remix container with Redis - -Creates a hit counter app with Remix and Redis. - -This deploys Remix as a Fargate service to ECS and it's linked to Redis. - -```ts title="sst.config.ts" {3} -new sst.aws.Service("MyService", { - cluster, - link: [redis], - loadBalancer: { - ports: [{ listen: "80/http", forward: "3000/http" }], - }, - dev: { - command: "npm run dev", - }, -}); -``` - -Since our Redis cluster is in a VPC, we’ll need a tunnel to connect to it from our local -machine. - -```bash "sudo" -sudo npx sst tunnel install -``` - -This needs _sudo_ to create a network interface on your machine. You’ll only need to do this -once on your machine. - -To start your app locally run. - -```bash -npx sst dev -``` - -Now if you go to `http://localhost:5173` you’ll see a counter update as you refresh the page. - -Finally, you can deploy it by adding the `Dockerfile` that's included in this example and -running `npx sst deploy --stage production`. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { bastion: true }); -const redis = new sst.aws.Redis("MyRedis", { vpc }); -const cluster = new sst.aws.Cluster("MyCluster", { vpc }); - -new sst.aws.Service("MyService", { - cluster, - link: [redis], - loadBalancer: { - ports: [{ listen: "80/http", forward: "3000/http" }], - }, - dev: { - command: "npm run dev", - }, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-remix-redis). - - ---- -## AWS Remix streaming - -Follows the [Remix Streaming](https://remix.run/docs/en/main/guides/streaming) guide to create -an app that streams data. - -Uses the `defer` utility to stream data through the `loader` function. - -```tsx title="app/routes/_index.tsx" -return defer({ - spongebob, - friends: friendsPromise, -}); -``` - -Then uses the the `Suspense` and `Await` components to render the data. - -```tsx title="app/routes/_index.tsx" -Loading...}> - - { /* ... */ } - - -``` - -You should see the _friends_ section load after a 3 second delay. - -:::note -Safari handles streaming differently than other browsers. -::: - -Safari uses a [different heuristic](https://bugs.webkit.org/show_bug.cgi?id=252413) to -determine when to stream data. You need to render _enough_ initial HTML to trigger streaming. -This is typically only a problem for demo apps. - -Streaming works out of the box with the `Remix` component. -```ts title="sst.config.ts" -new sst.aws.Remix("MyWeb"); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-remix-stream). - - ---- -## Router and bucket - -Creates a router that serves static files from the `public` folder of a given bucket. -```ts title="sst.config.ts" -// Create a bucket that CloudFront can access -const bucket = new sst.aws.Bucket("MyBucket", { - access: "cloudfront", -}); - -// Upload the image to the `public` folder -new aws.s3.BucketObjectv2("MyImage", { - bucket: bucket.name, - key: "public/spongebob.svg", - contentType: "image/svg+xml", - source: $asset("spongebob.svg"), -}); - -const router = new sst.aws.Router("MyRouter", { - routes: { - "/*": { - bucket, - rewrite: { regex: "^/(.*)$", to: "/public/$1" }, - }, - }, -}); - -return { - image: $interpolate`${router.url}/spongebob.svg`, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-router-bucket). - - ---- -## Router and function URL - -Creates a router that routes all requests to a function with a URL. -```ts title="sst.config.ts" -const api = new sst.aws.Function("MyApi", { - handler: "api.handler", - url: true, -}); -const bucket = new sst.aws.Bucket("MyBucket", { - access: "public", -}); -const router = new sst.aws.Router("MyRouter", { - domain: "router.ion.dev.sst.dev", - routes: { - "/api/*": api.url, - "/*": $interpolate`https://${bucket.domain}`, - }, -}); - -return { - router: router.url, - bucket: bucket.domain, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-router). - - ---- -## AWS Cluster Service Discovery - -In this example, we are connecting to a service running on a cluster using its AWS Cloud -Map service host name. This is useful for service discovery. - -We are deploying a service to a cluster in a VPC. And we can access it within the VPC using -the service's cloud map hostname. - -```ts title="lambda.ts" -const reponse = await fetch(`http://${Resource.MyService.service}`); -``` - -Here we are accessing it through a Lambda function that's linked to the service and is -deployed to the same VPC. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { nat: "ec2" }); - -const cluster = new sst.aws.Cluster("MyCluster", { vpc }); -const service = new sst.aws.Service("MyService", { cluster }); - -new sst.aws.Function("MyFunction", { - vpc, - url: true, - link: [service], - handler: "lambda.handler", -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-service-discovery). - - ---- -## Sharp in Lambda - -Uses the [Sharp](https://sharp.pixelplumbing.com/) library to resize images. In this example, -it resizes a `logo.png` local file to 100x100 pixels. - -```json title="sst.config.ts" -{ - nodejs: { install: ["sharp"] } -} -``` - -We don't need a layer to deploy this because `sharp` comes with a pre-built binary for Lambda. -This is handled by [`nodejs.install`](/docs/component/aws/function#nodejs-install). - -:::tip -You don't need to use a Lambda layer to use Sharp. -::: - -In dev, this uses the sharp npm package locally. - -```json title="package.json" -{ - "dependencies": { - "sharp": "^0.33.5" - } -} -``` - -On deploy, SST will use the right binary from the sharp package for the target Lambda -architecture. -```ts title="sst.config.ts" -const func = new sst.aws.Function("MyFunction", { - url: true, - handler: "index.handler", - nodejs: { install: ["sharp"] }, - copyFiles: [{ from: "logo.png" }], -}); - -return { - url: func.url, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-sharp). - - ---- -## AWS SolidStart WebSocket endpoint - -Deploys a SolidStart app with a [WebSocket endpoint](https://docs.solidjs.com/solid-start/advanced/websocket) -in a container to AWS. - -Uses the experimental WebSocket support in Nitro. - -```ts title="app.config.ts" {4} -export default defineConfig({ - server: { - experimental: { - websocket: true, - }, - }, -}).addRouter({ - name: "ws", - type: "http", - handler: "./src/ws.ts", - target: "server", - base: "/ws", -}); -``` - -Once deployed you can test the `/ws` endpoint and it'll send a message back after a 3s delay. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { bastion: true }); -const cluster = new sst.aws.Cluster("MyCluster", { vpc }); - -new sst.aws.Service("MyService", { - cluster, - loadBalancer: { - ports: [{ listen: "80/http", forward: "3000/http" }], - }, - dev: { - command: "npm run dev", - }, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-solid-container-ws). - - ---- -## AWS static site basic auth - -This deploys a simple static site and adds basic auth to it. - -This is useful for dev environments where you want to share a static site with your team but -ensure that it's not publicly accessible. - -This works by injecting some code into a CloudFront function that checks the basic auth -header and matches it against the `USERNAME` and `PASSWORD` secrets. - -```ts title="sst.config.ts" -{ - injection: $interpolate` - if ( - !event.request.headers.authorization - || event.request.headers.authorization.value !== "Basic ${basicAuth}" - ) { - return { - statusCode: 401, - headers: { - "www-authenticate": { value: "Basic" } - } - }; - }`, -} -``` - -To deploy this, you need to first set the `USERNAME` and `PASSWORD` secrets. - -```bash -sst secret set USERNAME my-username -sst secret set PASSWORD my-password -``` - -If you are deploying this to preview environments, you might want to set the secrets using -the [`--fallback`](/docs/reference/cli#secret) flag. -```ts title="sst.config.ts" -const username = new sst.Secret("USERNAME"); -const password = new sst.Secret("PASSWORD"); -const basicAuth = $resolve([username.value, password.value]).apply( - ([username, password]) => - Buffer.from(`${username}:${password}`).toString("base64") -); - -new sst.aws.StaticSite("MySite", { - path: "site", - // Don't password protect prod - edge: $app.stage !== "production" - ? { - viewerRequest: { - injection: $interpolate` - if ( - !event.request.headers.authorization - || event.request.headers.authorization.value !== "Basic ${basicAuth}" - ) { - return { - statusCode: 401, - headers: { - "www-authenticate": { value: "Basic" } - } - }; - }`, - }, - } - : undefined, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-static-site-basic-auth). - - ---- -## AWS static site - -Deploy a simple HTML file as a static site with S3 and CloudFront. The website is stored in -the `site/` directory. -```ts title="sst.config.ts" -new sst.aws.StaticSite("MySite", { - path: "site", -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-static-site). - - ---- -## AWS SvelteKit container with Redis - -Creates a hit counter app with SvelteKit and Redis. - -This deploys SvelteKit as a Fargate service to ECS and it's linked to Redis. - -```ts title="sst.config.ts" {3} -new sst.aws.Service("MyService", { - cluster, - link: [redis], - loadBalancer: { - ports: [{ listen: "80/http", forward: "3000/http" }], - }, - dev: { - command: "npm run dev", - }, -}); -``` - -Since our Redis cluster is in a VPC, we’ll need a tunnel to connect to it from our local -machine. - -```bash "sudo" -sudo npx sst tunnel install -``` - -This needs _sudo_ to create a network interface on your machine. You’ll only need to do this -once on your machine. - -To start your app locally run. - -```bash -npx sst dev -``` - -Now if you go to `http://localhost:5173` you’ll see a counter update as you refresh the page. - -Finally, you can deploy it by adding the `Dockerfile` that's included in this example and -running `npx sst deploy --stage production`. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { bastion: true }); -const redis = new sst.aws.Redis("MyRedis", { vpc }); -const cluster = new sst.aws.Cluster("MyCluster", { vpc }); - -new sst.aws.Service("MyService", { - cluster, - link: [redis], - loadBalancer: { - ports: [{ listen: "80/http", forward: "3000/http" }], - }, - dev: { - command: "npm run dev", - }, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-svelte-redis). - - ---- -## Swift in Lambda - -Deploys a simple Swift application to Lambda using the `al2023` runtime. - -:::note -Building this function requires Docker. -::: - -Check out the README in the repo for more details. -```ts title="sst.config.ts" -const swift = new sst.aws.Function("Swift", { - runtime: "provided.al2023", - architecture: process.arch === "arm64" ? "arm64" : "x86_64", - bundle: build("app"), - handler: "bootstrap", - url: true, -}); -const router = new sst.aws.Router("SwiftRouter", { - routes: { - "/*": swift.url, - }, - domain: "swift.dev.sst.dev", -}); -return { - url: router.url, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-swift). - - ---- -## T3 Stack in AWS - -Deploy [T3 stack](https://create.t3.gg) with Drizzle and Postgres to AWS. - -This example was created using `create-t3-app` and the following options: tRPC, Drizzle, -no auth, Tailwind, Postgres, and the App Router. - -Instead of a local database, we'll be using an RDS Postgres database. - -```ts title="src/server/db/index.ts" {2-6} -const pool = new Pool({ - host: Resource.MyPostgres.host, - port: Resource.MyPostgres.port, - user: Resource.MyPostgres.username, - password: Resource.MyPostgres.password, - database: Resource.MyPostgres.database, -}); -``` - -Similarly, for Drizzle Kit. - -```ts title="drizzle.config.ts" {8-12} -export default { - schema: "./src/server/db/schema.ts", - dialect: "postgresql", - dbCredentials: { - ssl: { - rejectUnauthorized: false, - }, - host: Resource.MyPostgres.host, - port: Resource.MyPostgres.port, - user: Resource.MyPostgres.username, - password: Resource.MyPostgres.password, - database: Resource.MyPostgres.database, - }, - tablesFilter: ["aws-t3_*"], -} satisfies Config; -``` - -In our Next.js app we can access our Postgres database because we [link them](/docs/linking/) -both. We don't need to use our `.env` files. - -```ts title="sst.config.ts" {5} - const rds = new sst.aws.Postgres("MyPostgres", { vpc, proxy: true }); - - new sst.aws.Nextjs("MyWeb", { - vpc, - link: [rds] - }); -``` - -To run this in dev mode run: - -```bash -npm install -npx sst dev -``` - -It'll take a few minutes to deploy the database and the VPC. - -This also starts a tunnel to let your local machine connect to the RDS Postgres database. -Make sure you have it installed, you only need to do this once for your local machine. - -```bash -sudo npx sst tunnel install -``` - -Now in a new terminal you can run the database migrations. - -```bash -npm run db:push -``` - -We also have the Drizzle Studio start automatically in dev mode under the **Studio** tab. - -```ts title="sst.config.ts" -new sst.x.DevCommand("Studio", { - link: [rds], - dev: { - command: "npx drizzle-kit studio", - }, -}); -``` - -And to make sure our credentials are available, we update our `package.json` -with the [`sst shell`](/docs/reference/cli) CLI. - -```json title="package.json" -"db:generate": "sst shell drizzle-kit generate", -"db:migrate": "sst shell drizzle-kit migrate", -"db:push": "sst shell drizzle-kit push", -"db:studio": "sst shell drizzle-kit studio", -``` - -So running `npm run db:push` will run Drizzle Kit with the right credentials. - -To deploy this to production run: - -```bash -npx sst deploy --stage production -``` - -Then run the migrations. - -```bash -npx sst shell --stage production npx drizzle-kit push -``` - -If you are running this locally, you'll need to have a tunnel running. - -```bash -npx sst tunnel --stage production -``` - -If you are doing this in a CI/CD pipeline, you'd want your build containers to be in the -same VPC. -```ts title="sst.config.ts" -const vpc = new sst.aws.Vpc("MyVpc", { bastion: true, nat: "ec2" }); -const rds = new sst.aws.Postgres("MyPostgres", { vpc, proxy: true }); - -new sst.aws.Nextjs("MyWeb", { - vpc, - link: [rds] -}); - -new sst.x.DevCommand("Studio", { - link: [rds], - dev: { - command: "npx drizzle-kit studio", - }, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-t3). - - ---- -## AWS Task Cron - -Use the [`Task`](/docs/component/aws/task) and [`Cron`](/docs/component/aws/cron) components -for long running background tasks. - -We have a node script that we want to run in `index.mjs`. It'll be deployed as a -Docker container using `Dockerfile`. - -It'll be invoked by a cron job that runs every 2 minutes. - -```ts title="sst.config.ts" -new sst.aws.Cron("MyCron", { - task, - schedule: "rate(2 minutes)" -}); -``` - -When this is run in `sst dev`, the task is executed locally using `dev.command`. - -```ts title="sst.config.ts" -dev: { - command: "node index.mjs" -} -``` - -To deploy, you need the Docker daemon running. -```ts title="sst.config.ts" -const bucket = new sst.aws.Bucket("MyBucket"); -const vpc = new sst.aws.Vpc("MyVpc"); - -const cluster = new sst.aws.Cluster("MyCluster", { vpc }); -const task = new sst.aws.Task("MyTask", { - cluster, - link: [bucket], - dev: { - command: "node index.mjs", - }, -}); - -new sst.aws.Cron("MyCron", { - task, - schedule: "rate(2 minutes)", -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-task-cron). - - ---- -## AWS Task - -Use the [`Task`](/docs/component/aws/task) component to run background tasks. - -We have a node script that we want to run in `image/index.mjs`. It'll be deployed as a -Docker container using `image/Dockerfile`. - -We also have a function that the task is linked to. It uses the [SDK](/docs/reference/sdk/) -to start the task. - -```ts title="index.ts" {5} -import { Resource } from "sst"; -import { task } from "sst/aws/task"; - -export const handler = async () => { - const ret = await task.run(Resource.MyTask); - return { - statusCode: 200, - body: JSON.stringify(ret, null, 2), - }; -}; -``` - -When this is run in `sst dev`, the task is executed locally using `dev.command`. - -```ts title="sst.config.ts" -dev: { - command: "node index.mjs" -} -``` - -To deploy, you need the Docker daemon running. -```ts title="sst.config.ts" -const bucket = new sst.aws.Bucket("MyBucket"); -const vpc = new sst.aws.Vpc("MyVpc", { nat: "ec2" }); - -const cluster = new sst.aws.Cluster("MyCluster", { vpc }); - -const task = new sst.aws.Task("MyTask", { - cluster, - link: [bucket], - image: { - context: "image", - }, - dev: { - command: "node index.mjs", - }, -}); - -new sst.aws.Function("MyApp", { - vpc, - url: true, - link: [task], - handler: "index.handler", -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-task). - - ---- -## Subscribe to topics - -Create an SNS topic, publish to it from a function, and subscribe to it with a function and a queue. -```ts title="sst.config.ts" -const queue = new sst.aws.Queue("MyQueue"); -queue.subscribe("subscriber.handler"); - -const topic = new sst.aws.SnsTopic("MyTopic"); -topic.subscribe("MySubscriber1", "subscriber.handler", {}); -topic.subscribeQueue("MySubscriber2", queue.arn); - -const app = new sst.aws.Function("MyApp", { - handler: "publisher.handler", - link: [topic], - url: true, -}); - -return { - app: app.url, - topic: topic.name, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-topic). - - ---- -## Vector search - -Store and search for vector data using the Vector component. Includes a seeder API that -uses an LLM to generate embeddings for some movies and optionally their posters. - -Once seeded, you can call the search API to query the vector database. -```ts title="sst.config.ts" -const OpenAiApiKey = new sst.Secret("OpenAiApiKey"); -const vector = new sst.aws.Vector("MyVectorDB", { - dimension: 1536, -}); - -const seeder = new sst.aws.Function("Seeder", { - handler: "index.seeder", - link: [OpenAiApiKey, vector], - copyFiles: [ - { from: "iron-man.jpg", to: "iron-man.jpg" }, - { - from: "black-widow.jpg", - to: "black-widow.jpg", - }, - { - from: "spider-man.jpg", - to: "spider-man.jpg", - }, - { from: "thor.jpg", to: "thor.jpg" }, - { - from: "captain-america.jpg", - to: "captain-america.jpg", - }, - ], - url: true, -}); - -const app = new sst.aws.Function("MyApp", { - handler: "index.app", - link: [OpenAiApiKey, vector], - url: true, -}); - -return { seeder: seeder.url, app: app.url }; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-vector). - - ---- -## React SPA with Vite - -Deploy a React single-page app (SPA) with Vite to S3 and CloudFront. -```ts title="sst.config.ts" -new sst.aws.StaticSite("Web", { - build: { - command: "pnpm run build", - output: "dist", - }, -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/aws-vite). - - ---- -## Cloudflare Cron - -This example creates a Cloudflare Worker that runs on a schedule. -```ts title="sst.config.ts" -const cron = new sst.cloudflare.Cron("Cron", { - job: "index.ts", - schedules: ["* * * * *"] -}); - -return {}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/cloudflare-cron). - - ---- -## Cloudflare KV - -This example creates a Cloudflare KV namespace and links it to a worker. Now you can use the -SDK to interact with the KV namespace in your worker. -```ts title="sst.config.ts" -const storage = new sst.cloudflare.Kv("MyStorage"); -const worker = new sst.cloudflare.Worker("Worker", { - url: true, - link: [storage], - handler: "index.ts", -}); - -return { - url: worker.url, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/cloudflare-kv). - - ---- -## Link multiple secrets - -You might have multiple secrets that need to be used across your app. It can be tedious to -create a new secret and link it to each function or resource. - -A common pattern to addresses this is to create an object with all your secrets and then -link them all at once. Now when you have a new secret, you can add it to the object and -it will be automatically available to all your resources. -```ts title="sst.config.ts" -// Manage all secrets together -const secrets = { - secret1: new sst.Secret("Secret1", "some-secret-value-1"), - secret2: new sst.Secret("Secret2", "some-secret-value-2"), -}; -const allSecrets = Object.values(secrets); - -const bucket = new sst.aws.Bucket("MyBucket"); - -const api = new sst.aws.Function("MyApi", { - link: [bucket, ...allSecrets], - handler: "index.handler", - url: true, -}); - -return { - url: api.url, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/secret-link-all). - - ---- -## Default function props - -Set default props for all the functions in your app using the global [`$transform`](/docs/reference/global/#transform). -```ts title="sst.config.ts" -$transform(sst.aws.Function, (args) => { - args.runtime = "nodejs14.x"; - args.environment = { - FOO: "BAR", - }; -}); -new sst.aws.Function("MyFunction", { - handler: "index.ts", -}); -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/sst-$transform). - - ---- -## Vercel domains - -Creates a router that uses domains purchased through and hosted in your Vercel account. -Ensure the `VERCEL_API_TOKEN` and `VERCEL_TEAM_ID` environment variables are set. -```ts title="sst.config.ts" -const router = new sst.aws.Router("MyRouter", { - domain: { - name: "ion.sst.moe", - dns: sst.vercel.dns({ domain: "sst.moe" }), - }, - routes: { - "/*": "https://sst.dev", - }, -}); -return { - router: router.url, -}; -``` - -View the [full example](https://github.com/sst/sst/tree/dev/examples/vercel-domain). diff --git a/www/src/content/docs/docs/iam-credentials.mdx b/www/src/content/docs/docs/iam-credentials.mdx index 2fa0f28d83..7c87eaf66d 100644 --- a/www/src/content/docs/docs/iam-credentials.mdx +++ b/www/src/content/docs/docs/iam-credentials.mdx @@ -149,8 +149,10 @@ Let's start with an IAM policy you can _copy and paste_. "s3:PutBucketNotification", "s3:PutBucketPolicy", "s3:DeleteObject", + "s3:DeleteObjectVersion", "s3:GetObject", "s3:ListBucket", + "s3:ListBucketVersions", "s3:PutObject" ], "Resource": [ @@ -267,9 +269,12 @@ There are a couple of different things being bootstrapped and these are the perm "s3:CreateBucket", "s3:PutBucketVersioning", "s3:PutBucketNotification", + "s3:PutBucketPolicy", "s3:DeleteObject", + "s3:DeleteObjectVersion", "s3:GetObject", "s3:ListBucket", + "s3:ListBucketVersions", "s3:PutObject" ], "Resource": [ @@ -287,6 +292,8 @@ There are a couple of different things being bootstrapped and these are the perm "Action": [ "s3:CreateBucket", "s3:PutBucketVersioning", + "s3:PutBucketNotification", + "s3:PutBucketPolicy", "s3:DeleteObject", "s3:GetObject", "s3:ListBucket", @@ -371,7 +378,9 @@ The SST CLI also makes some AWS SDK calls to your account. Here are the IAM perm "ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath", - "ssm:PutParameter" + "ssm:PutParameter", + "ssm:AddTagsToResource", + "ssm:ListTagsForResource" ], "Resource": [ "arn:aws:ssm:us-east-1:112233445566:parameter/sst/*" @@ -379,18 +388,16 @@ The SST CLI also makes some AWS SDK calls to your account. Here are the IAM perm } ``` -- And permissions to connect to the IoT endpoint in `sst dev` to run your functions [_Live_](/docs/live). +- And permissions to connect to the AppSync endpoint in `sst dev` to run your functions [_Live_](/docs/live). ```json { "Sid": "LiveLambdaSocketConnection", "Effect": "Allow", "Action": [ - "iot:DescribeEndpoint", - "iot:Connect", - "iot:Subscribe", - "iot:Publish", - "iot:Receive" + "appsync:EventSubscribe", + "appsync:EventPublish", + "appsync:EventConnect" ], "Resource": [ "*" diff --git a/www/src/content/docs/docs/index.mdx b/www/src/content/docs/docs/index.mdx index 6b487771ea..310ee4fa1a 100644 --- a/www/src/content/docs/docs/index.mdx +++ b/www/src/content/docs/docs/index.mdx @@ -268,13 +268,57 @@ Learn more about our [monorepo setup](/docs/set-up-a-monorepo/). ## CLI -To make this all work, SST comes with a [CLI](/docs/reference/cli/). You can install it as a part of your Node project. +To make this all work, SST comes with a [CLI](/docs/reference/cli/). For JavaScript projects, install SST locally in your project so the CLI version is tracked with your app. -```bash -npm install sst -``` + + + ```bash + npm install sst + ``` + + + ```bash + pnpm add sst + ``` + + + ```bash + bun add sst + ``` + + + ```bash + yarn add sst + ``` + + + +You can then run the CLI with your package manager. + + + + ```bash + npx sst dev + ``` + + + ```bash + pnpm exec sst dev + ``` + + + ```bash + bun sst dev + ``` + + + ```bash + yarn sst dev + ``` + + -Or if you are not using Node, you can install it globally. +If you are not using JavaScript, you can install the CLI globally. ```bash curl -fsSL https://sst.dev/install | bash @@ -385,7 +429,7 @@ While SST is open-source and free to use, we also have the [Console](/docs/conso #### Next steps -1. [Learn about the SST workflow](/docs/workflow/) +1. [Learn the SST basics](/docs/basics/) 2. Create your first SST app - [Build a Next.js app in AWS](/docs/start/aws/nextjs/) - [Deploy Bun in a container to AWS](/docs/start/aws/bun/) diff --git a/www/src/content/docs/docs/linking.mdx b/www/src/content/docs/docs/linking.mdx index 208c52c0d5..6ccf0e8609 100644 --- a/www/src/content/docs/docs/linking.mdx +++ b/www/src/content/docs/docs/linking.mdx @@ -174,8 +174,8 @@ Resource links are only available on the server-side of your frontend. If you wa When you run `sst dev` or `sst deploy`, it generates the types to access the linked resources. These are generated as: -2. A `sst-env.d.ts` file in the project root with types for **all** the linked resources in the app. -1. A `sst-env.d.ts` file in the same directory of the nearest `package.json` of the function or frontend that's _receiving_ the links. This references the root `sst-env.d.ts` file. +1. A `sst-env.d.ts` file in the project root with types for **all** the linked resources in the app. +2. A `sst-env.d.ts` file in the same directory of the nearest `package.json` of the function or frontend that's _receiving_ the links. This is usually just a small reference shim to the root `sst-env.d.ts` file. You can check the generated `sst-env.d.ts` types into source control. This will let your teammates see the types without having to run `sst dev` when they pull your changes. @@ -278,3 +278,38 @@ You can also modify the links SST creates. For example, you might want to change This overrides the existing link and lets you create your own. Read more about [`sst.Linkable.wrap`](/docs/component/linkable/#static-wrap). + +--- + +### Link integration with external providers + +If you want to pass links to compute not managed by SST, like a native ECS task definition or a Kubernetes pod, use `Linkable.env()`. It converts your linked resources into `SST_RESOURCE_*` environment variables so `Resource.MyResource` works at runtime. + +```ts title="sst.config.ts" +const bucket = new sst.aws.Bucket("MyBucket"); + +const environment = sst.Linkable.env([bucket]); +``` + +This returns an `Output>` that you can pass to any provider that accepts environment variables. + +```ts title="sst.config.ts" +new kubernetes.apps.v1.Deployment("MyDeployment", { + spec: { + template: { + spec: { + containers: [{ + name: "app", + image: "my-image", + env: sst.Linkable.env([bucket]).apply((env) => + // Kubernetes requires environment variables to be an array of objects + Object.entries(env).map(([name, value]) => ({ name, value: String(value) })), + ), + }], + }, + }, + }, +}); +``` + +Read more about [`sst.Linkable.env`](/docs/component/linkable/#static-env). [Check out an example](/docs/examples/#aws-linkable-env). diff --git a/www/src/content/docs/docs/migrate-from-v2.mdx b/www/src/content/docs/docs/migrate-from-v2.mdx index b0495794d4..0f11d68eea 100644 --- a/www/src/content/docs/docs/migrate-from-v2.mdx +++ b/www/src/content/docs/docs/migrate-from-v2.mdx @@ -7,7 +7,7 @@ import config from '../../../../config.ts'; import { Tabs, TabItem } from '@astrojs/starlight/components'; -This guide will help you migrate your SST v2 apps to v3. We look at the major differences between v2 and v3 below. But to get a quick intro, we recommend reading the [What is SST](/docs/) and [Workflow](/docs/workflow/) docs. +This guide will help you migrate your SST v2 apps to v3. We look at the major differences between v2 and v3 below. But to get a quick intro, we recommend reading the [What is SST](/docs/) and [Basics](/docs/basics/) docs. :::tip We recently [migrated our demo notes app](https://github.com/sst/demo-notes-app/pull/8/files) from v2 to v3. You use these changes as reference. @@ -31,9 +31,8 @@ While the goal with v3 is to support most of what's in v2, there are a few thing | Construct | GitHub Issue | |----------|-------| -| `Auth` | [In beta](https://github.com/sst/sst/issues/4893) | | `Script` | [#811](https://github.com/sst/sst/issues/4323) | -| `Function` non-Node.js runtimes | [Python](https://github.com/sst/sst/issues/4669), [Container](https://github.com/sst/sst/issues/4462), [Custom](https://github.com/sst/sst/issues/4826) | +| `Function` non-Node.js runtimes | [Container](https://github.com/sst/sst/issues/4462), [Custom](https://github.com/sst/sst/issues/4826) | Feel free to let us know via the linked GitHub issues if these are blockers for you. It'll help us prioritize this list. @@ -273,7 +272,7 @@ For example, in the [monorepo notes app](https://github.com/sst/demo-notes-app/p return { UserPool: auth.userPool.id, - Region: aws.getRegionOutput().name, + Region: aws.getRegionOutput().region, IdentityPool: auth.identityPool.id, UserPoolClient: auth.userPoolClient.id, }; @@ -532,7 +531,7 @@ There are a couple of global variables, `$app` and `$dev` that replace the `app` 3. `$dev === true` tells you if you are in dev mode. Used to be `app.mode === "dev`. 4. `$dev === false` tells you if it's being deployed. Used to be `app.mode === "deploy`. 5. There is no `app.mode === remove` replacement since your components are not evaluated on `sst remove`. -6. There is no `app.region` since in v3 you can deploy resources to different regions or AWS profiles or _providers_. To get the default AWS provider you can use `aws.getRegionOutput().name`. +6. There is no `app.region` since in v3 you can deploy resources to different regions or AWS profiles or _providers_. To get the default AWS provider you can use `aws.getRegionOutput().region`. --- diff --git a/www/src/content/docs/docs/migrate-from-v3.mdx b/www/src/content/docs/docs/migrate-from-v3.mdx new file mode 100644 index 0000000000..261d9b7d02 --- /dev/null +++ b/www/src/content/docs/docs/migrate-from-v3.mdx @@ -0,0 +1,120 @@ +--- +title: Migrate From v3 +description: Migrate your SST v3 apps to v4. +--- + +import TestimonialWall from "../../../components/TestimonialWall.astro"; + +SST v4 upgrades the underlying [Pulumi AWS provider](https://www.pulumi.com/registry/packages/aws/) from v6 to v7. This guide covers migrating your SST v3 apps to v4. + +:::tip +SST components are already updated for v7. Most users only need to run `sst refresh` and deploy. +::: + +For the full list of upstream changes, refer to the [Pulumi AWS v7 migration guide](https://www.pulumi.com/registry/packages/aws/how-to-guides/7-0-migration). + +--- + +## Breaking changes + +No code changes are needed if you are only using SST components without transforms or direct `@pulumi/aws` usage. Otherwise, refer to the [Pulumi AWS v7 migration guide](https://www.pulumi.com/registry/packages/aws/how-to-guides/7-0-migration) for the full list of breaking changes. + +The internal changes to SST components were minimal: mostly S3 resource renames (dropping the `V2` suffix) and switching from `tags` to `tagsAll`. You can see the full list of changes in the [upgrade PR](https://github.com/anomalyco/sst/pull/6259/). + +--- + +## Migration steps + +When you update SST and the AWS provider version changes, `sst deploy` will be blocked until you migrate your state: + +1. **Install the latest v4** + + Make sure you're on the latest v4 version before running any commands. + +2. **Update your config** + + If you have any breaking changes from above, update your `sst.config.ts` accordingly. + +3. **Review changes** + + Run `sst diff` to preview what will change. This is a **one-way migration**, so it's worth reviewing first. + + ```bash frame="none" + sst diff + ``` + +4. **Migrate state** + + Run `sst refresh` to migrate your state. + + :::caution + Do not use the `--target` flag during migration. The state refresh must cover all resources to ensure a consistent migration. + ::: + + ```bash frame="none" + sst refresh + ``` + + Repeat for each stage. + + ```bash frame="none" + sst refresh --stage production + ``` + + If the stage was deployed using `sst dev`, use the `--dev` flag. + + ```bash frame="none" + sst refresh --dev + ``` + + If you [share resources across stages](/docs/share-across-stages), run `sst refresh` on the stage where the resource is created first β€” not the stage that references it via `.get()`. + +5. **Deploy** + + Once refreshed, deploy as usual. + + ```bash frame="none" + sst deploy + ``` + +That's it β€” once refreshed and deployed, your app is fully migrated to v4. + +--- + +## Upgrade testimonials + +Upgrading your infra framework can feel scary. Here's what teams have shared in Discord after migrating: + + diff --git a/www/src/content/docs/docs/planetscale.mdx b/www/src/content/docs/docs/planetscale.mdx new file mode 100644 index 0000000000..16cf900c53 --- /dev/null +++ b/www/src/content/docs/docs/planetscale.mdx @@ -0,0 +1,150 @@ +--- +title: PlanetScale +description: Learn how to use SST with PlanetScale +--- + +[PlanetScale](https://planetscale.com) is a cloud database platform built for speed and scalability. This guide covers how to set it up with SST. + +## Install + +Add the PlanetScale provider to your SST app. Learn more about [providers](/docs/providers). + +```bash +sst add planetscale +``` + +This adds the provider to your `sst.config.ts`. + +```ts title="sst.config.ts" {3} +{ + providers: { + planetscale: "1.0.0", + }, +} +``` + +Then set the `PLANETSCALE_SERVICE_TOKEN` and `PLANETSCALE_SERVICE_TOKEN_ID` environment variables. You can create a service token in the PlanetScale dashboard under [Settings > Service tokens](https://app.planetscale.com/~/settings/service-tokens). The token needs at least the following permissions for the upcoming examples: + +- `connect_branch` +- `connect_production_branch` +- `create_branch` +- `delete_branch` +- `delete_branch_password` +- `read_branch` +- `read_database` + +--- + +## Reference a database + +Reference an existing PlanetScale Vitess database directly from your SST config. + +```ts title="sst.config.ts" +const db = planetscale.getDatabaseVitessOutput({ + id: "mydb", + organization: "myorg", +}); +``` + +:::note +This guide uses the Vitess resources. PlanetScale also supports Postgres with equivalent resources and functions: `PostgresBranch`, `PostgresBranchRole`, `getDatabasePostgresOutput`, and `getPostgresBranchOutput`. +::: + +--- + +## Branch per stage + +PlanetScale supports database branching natively. Combined with SST, every stage and every pull request can get its own isolated database branch automatically. + +Conditionally create or reference a branch based on the current stage. + +```ts title="sst.config.ts" +const branch = + $app.stage === "production" + ? planetscale.getVitessBranchOutput({ + id: db.defaultBranch, + organization: db.organization, + database: db.name, + }) + : new planetscale.VitessBranch("DatabaseBranch", { + database: db.name, + organization: db.organization, + name: $app.stage, + parentBranch: db.defaultBranch, + }); +``` + +In production, it references the existing default branch. In any other stage, it creates a new branch from it. So `sst deploy --stage pr-42` creates a `pr-42` branch in PlanetScale. + +:::tip +Configure [Autodeploy](/docs/console/#autodeploy) in the SST Console to do this automatically on every pull request. +::: + +This is a pattern used in production by [terminal.shop](https://github.com/terminaldotshop/terminal) and [OpenCode](https://github.com/anomalyco/opencode). + +--- + +## Link to your app + +Create a password for the branch and wrap it in a `sst.Linkable`. + +```ts title="sst.config.ts" +const password = new planetscale.VitessBranchPassword("DatabasePassword", { + database: db.name, + organization: db.organization, + branch: branch.name, + role: "admin", + name: `${$app.name}-${$app.stage}`, +}); + +export const database = new sst.Linkable("Database", { + properties: { + host: password.accessHostUrl, + username: password.username, + password: password.plainText, + database: db.name, + port: 3306, + }, +}); +``` + +The [`Linkable`](/docs/component/linkable) component lets you wrap arbitrary values and make them available to any function or service you link it to. Learn more about [linking resources](/docs/linking). + +--- + +## Connect to the database + +Link the database to any function or service. + +```ts title="sst.config.ts" +new sst.aws.Function("Api", { + handler: "src/api.handler", + link: [database], +}); +``` + +Then access the credentials in a type-safe way through `Resource`. For example, with [Drizzle ORM](https://orm.drizzle.team). + +```ts title="src/drizzle.ts" +import { drizzle } from "drizzle-orm/planetscale-serverless"; +import { Resource } from "sst"; + +export const db = drizzle({ + connection: { + host: Resource.Database.host, + username: Resource.Database.username, + password: Resource.Database.password, + }, +}); +``` + +You can use any other ORM or database driver the same way. + +--- + +## Examples + +Check out the full examples: + +- [PlanetScale with Drizzle and MySQL](/docs/examples/#aws-planetscale-drizzle-mysql) +- [PlanetScale with Drizzle and Postgres](/docs/examples/#aws-planetscale-drizzle-postgres) diff --git a/www/src/content/docs/docs/providers.mdx b/www/src/content/docs/docs/providers.mdx index 1700721169..9a371d1fd9 100644 --- a/www/src/content/docs/docs/providers.mdx +++ b/www/src/content/docs/docs/providers.mdx @@ -114,8 +114,11 @@ Most providers will read your credentials from the environment. For example, for ```bash export CLOUDFLARE_API_TOKEN=aaaaaaaa_aaaaaaaaaaaa_aaaaaaaa +export CLOUDFLARE_DEFAULT_ACCOUNT_ID=aaaaaaaa_aaaaaaaaaaaa_aaaaaaaa ``` +Follow the [Cloudflare guide](/docs/cloudflare/) for the recommended setup. + However, some providers also allow you to pass in the credentials through the config. ```ts title="sst.config.ts" @@ -193,7 +196,7 @@ new sst.aws.Function("MyFunction, { handler: "src/lambda.handler", environment: { ACCOUNT: aws.getCallerIdentityOutput({}).accountId, - REGION: aws.getRegionOutput().name + REGION: aws.getRegionOutput().region } } ``` diff --git a/www/src/content/docs/docs/reference/sdk.mdx b/www/src/content/docs/docs/reference/sdk.mdx index 25260801a7..b961d7c0dc 100644 --- a/www/src/content/docs/docs/reference/sdk.mdx +++ b/www/src/content/docs/docs/reference/sdk.mdx @@ -100,8 +100,11 @@ SST uses [uv](https://docs.astral.sh/uv/) to package your Python functions. Your To use the SDK, add it to your `pyproject.toml`. ```toml title="functions/pyproject.toml" +[project] +dependencies = ["sst-sdk"] + [tool.uv.sources] -sst = { git = "https://github.com/sst/sst.git", subdirectory = "sdk/python", branch = "dev" } +sst-sdk = { git = "https://github.com/anomalyco/sst.git", subdirectory = "sdk/python", branch = "dev" } ``` And in your function, import the `resource` module and access the linked resource. @@ -120,7 +123,7 @@ const bucket = new sst.aws.Bucket("MyBucket"); new sst.aws.Function("MyFunction", { handler: "functions/src/functions/api.handler", - runtime: "python3.11", + runtime: "python3.13", link: [bucket] }); ``` @@ -171,8 +174,8 @@ Client functions are currently **not supported** in the Go SDK. Use the SST Rust SDK package in your Rust functions or container applications. -```toml title="Cargo.toml" -sst_sdk = "0.1.0" +```bash +cargo install sst_sdk ``` In your runtime, use the `Resource::get()` function to access linked resources as a typesafe struct, or a `serde_json::Value`. diff --git a/www/src/content/docs/docs/set-up-a-monorepo.mdx b/www/src/content/docs/docs/set-up-a-monorepo.mdx index eb8c55f237..8622123b08 100644 --- a/www/src/content/docs/docs/set-up-a-monorepo.mdx +++ b/www/src/content/docs/docs/set-up-a-monorepo.mdx @@ -76,7 +76,7 @@ The `packages/` directory includes the following: defined as modules. For example, we have an `Example` module. ```ts title="packages/core/src/example/index.ts" - export module Example { + export namespace Example { export function hello() { return "Hello, world!"; } diff --git a/www/src/content/docs/docs/start/cloudflare/hono.mdx b/www/src/content/docs/docs/start/cloudflare/hono.mdx index 5b67cfb87b..ac07fdd382 100644 --- a/www/src/content/docs/docs/start/cloudflare/hono.mdx +++ b/www/src/content/docs/docs/start/cloudflare/hono.mdx @@ -9,7 +9,7 @@ We are going to build an API with Hono, add an R2 bucket for file uploads, and d You can [view the source](https://github.com/sst/sst/tree/dev/examples/cloudflare-hono) of this example in our repo. ::: -Before you get started, make sure to [Create your Cloudflare API token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/). +Before you get started, [create your Cloudflare API token](/docs/cloudflare/#credentials). --- @@ -37,9 +37,9 @@ Select the defaults and pick **Cloudflare**. This'll create a `sst.config.ts` fi --- -#### Set the Cloudflare API token +#### Set the Cloudflare credentials -You can save your Cloudflare API token in a `.env` file or just set it directly. +Set the token and account ID in your shell or `.env` file. ```bash export CLOUDFLARE_API_TOKEN=aaaaaaaa_aaaaaaaaaaaa_aaaaaaaa @@ -193,4 +193,3 @@ npx sst deploy --stage production ``` You can use any stage name here but it's good to create a new stage for production. - diff --git a/www/src/content/docs/docs/start/cloudflare/trpc.mdx b/www/src/content/docs/docs/start/cloudflare/trpc.mdx index 4b5aad431a..08bf0f893c 100644 --- a/www/src/content/docs/docs/start/cloudflare/trpc.mdx +++ b/www/src/content/docs/docs/start/cloudflare/trpc.mdx @@ -9,7 +9,7 @@ We are going to build a [tRPC](https://trpc.io) API, a simple client, and deploy You can [view the source](https://github.com/sst/sst/tree/dev/examples/cloudflare-trpc) of this example in our repo. ::: -Before you get started, make sure to [Create your Cloudflare API token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/). +Before you get started, [create your Cloudflare API token](/docs/cloudflare/#credentials). --- @@ -37,7 +37,7 @@ Select the defaults and pick **Cloudflare**. This'll create a `sst.config.ts` fi --- -#### Set the Cloudflare API token +#### Set the Cloudflare credentials You can save your Cloudflare API token in a `.env` file or just set it directly. @@ -208,4 +208,3 @@ npx sst deploy --stage production ``` You can use any stage name here but it's good to create a new stage for production. - diff --git a/www/src/content/docs/docs/start/cloudflare/worker.mdx b/www/src/content/docs/docs/start/cloudflare/worker.mdx index 6a08c6e7c4..d05450a2a5 100644 --- a/www/src/content/docs/docs/start/cloudflare/worker.mdx +++ b/www/src/content/docs/docs/start/cloudflare/worker.mdx @@ -9,7 +9,7 @@ We are going to build an API with a Cloudflare Worker, add an R2 bucket for file You can [view the source](https://github.com/sst/sst/tree/dev/examples/cloudflare-worker) of this example in our repo. ::: -Before you get started, make sure to [Create your Cloudflare API token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/). +Before you get started, [create your Cloudflare API token](/docs/cloudflare/#credentials). --- @@ -37,9 +37,9 @@ Select the defaults and pick **Cloudflare**. This'll create a `sst.config.ts` fi --- -#### Set the Cloudflare API token +#### Set the Cloudflare credentials -You can save your Cloudflare API token in a `.env` file or just set it directly. +Set the token and account ID in your shell or `.env` file. ```bash export CLOUDFLARE_API_TOKEN=aaaaaaaa_aaaaaaaaaaaa_aaaaaaaa @@ -188,4 +188,3 @@ npx sst deploy --stage production ``` You can use any stage name here but it's good to create a new stage for production. - diff --git a/www/src/content/docs/docs/upgrade-aws-databases.mdx b/www/src/content/docs/docs/upgrade-aws-databases.mdx new file mode 100644 index 0000000000..ab24113a3a --- /dev/null +++ b/www/src/content/docs/docs/upgrade-aws-databases.mdx @@ -0,0 +1,108 @@ +--- +title: Upgrade AWS Databases +description: How to upgrade your database and minimize downtime. +--- + +Sometimes database components like [`Postgres`](/docs/component/aws/postgres/) and [`Mysql`](/docs/component/aws/mysql/) need to be upgraded as your application scales. + +When you change fields like `version` or `instance`, SST applies the update on the next `sst deploy`. + +By default, AWS performs the upgrade in place. This restarts the database and causes temporary downtime until your application can reconnect. + +This guide covers the different strategies to minimize that downtime. + +--- + +## Multi-AZ + +[`Postgres`](/docs/component/aws/postgres/) and [`Mysql`](/docs/component/aws/mysql/) support Multi-AZ deployments. AWS maintains a standby replica in a different availability zone. During the upgrade, changes are applied to the standby first, then it fails over. This reduces downtime to around 60-120 seconds instead of the full upgrade duration. + +```ts title="sst.config.ts" {4} +const database = new sst.aws.Postgres("MyDatabase", { + vpc, + version: "17", + multiAz: true, +}); +``` + +Multi-AZ roughly doubles the cost since a standby instance is always running. You can enable it temporarily for the upgrade and disable it after. + +:::caution +Major version upgrades restart both instances, making Multi-AZ ineffective for reducing downtime. +::: + +Enabling [RDS Proxy](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy.html) through the `proxy` property can help further reduce failover downtime by detecting the new primary directly instead of waiting for DNS propagation. This can bring downtime down to just a few seconds. + +--- + +## Blue/Green + +[`Postgres`](/docs/component/aws/postgres/) and [`Mysql`](/docs/component/aws/mysql/) support [AWS RDS Blue/Green deployments](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/blue-green-deployments.html) through the `blueGreen` property. This creates a staging copy of your database, applies the changes there, and switches over with near-zero downtime. + +```ts title="sst.config.ts" {4} +const database = new sst.aws.Postgres("MyDatabase", { + vpc, + version: "17", + blueGreen: true, +}); +``` + +Your database endpoint stays the same throughout, so no application changes are needed. The overall deploy takes significantly longer but actual downtime is near zero. + +:::caution +Blue/Green deployments are not compatible with databases that have read replicas or `proxy` enabled. +::: + +Learn more in [this Pulumi article](https://www.pulumi.com/blog/aws-rds-blue-green-deployment-updates). + +--- + +## Upgrading steps + +Here's the recommended workflow for upgrading a production database. + +### 1. Check compatibility + +Not all version jumps are allowed. You might need to go through intermediate versions. + +- [PostgreSQL upgrade paths](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_UpgradeDBInstance.PostgreSQL.html) +- [MySQL upgrade paths](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_UpgradeDBInstance.MySQL.html) +- [Aurora PostgreSQL upgrade paths](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_UpgradeDBInstance.PostgreSQL.html) +- [Aurora MySQL upgrade paths](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Updates.MajorVersionUpgrade.html) + +### 2. Pick a strategy + +Use in-place if some downtime is fine, Multi-AZ to reduce it, or Blue/Green for the shortest switchover when supported. + +| Component | In-place | Multi-AZ | Blue/Green | +| --- | --- | --- | --- | +| [`Postgres`](/docs/component/aws/postgres/) | βœ“ | βœ“ | βœ“ | +| [`Mysql`](/docs/component/aws/mysql/) | βœ“ | βœ“ | βœ“ | +| [`Aurora`](/docs/component/aws/aurora/) | βœ“ | βœ• | βœ• | +| [`Redis`](/docs/component/aws/redis/) | βœ“ | βœ• | βœ• | +| [`OpenSearch`](/docs/component/aws/open-search/) | βœ“ | βœ• | βœ• | + +### 3. Verify in a test stage + +Apply the change in a test stage first to ensure the upgrade works as expected. + +```diff lang="ts" title="sst.config.ts" + const database = new sst.aws.Postgres("MyDatabase", { + vpc, +- version: "16", ++ version: "17", ++ blueGreen: true, + }); +``` + +### 4. Deploy to production + +Finally, deploy the upgrade to your production stage. + +```ts title="sst.config.ts" +const database = new sst.aws.Postgres("MyDatabase", { + vpc, + version: "17", + blueGreen: true, +}); +``` diff --git a/www/src/content/docs/docs/workflow.mdx b/www/src/content/docs/docs/workflow.mdx deleted file mode 100644 index 9461f19a9f..0000000000 --- a/www/src/content/docs/docs/workflow.mdx +++ /dev/null @@ -1,396 +0,0 @@ ---- -title: Workflow -description: The basic workflow of building apps with SST. ---- - -import { Tabs, TabItem } from '@astrojs/starlight/components'; -import VideoAside from "../../../components/VideoAside.astro"; - -The main difference between working on SST versus any other framework is that everything related to your app is all **defined in code**. - -1. SST **automatically manages** the resources in AWS (or any provider) defined in your app. -2. You don't need to **make any manual changes** to them in your cloud provider's console. - -This idea of _automating everything_ can feel unfamiliar at first. So let's go through the workflow and look at some basic concepts. - ---- - -## Setup - -Before you start working on your app, there are a couple of things we recommend setting up. - -Starting with your code editor. - ---- - -### Editor - -SST apps are configured through a file called `sst.config.ts`. It's a TypeScript file and it can work with your editor to type check and autocomplete your code. It can also show you inline help. - - - - ![Editor typecheck](../../../assets/docs/workflow/editor-typecheck.png) - - - ![Editor autocomplete](../../../assets/docs/workflow/editor-autocomplete.png) - - - ![Editor help](../../../assets/docs/workflow/editor-help.png) - - - -Most modern editors; VS Code and Neovim included, should do the above automatically. But you should start by making sure that your editor has been set up. - ---- - -### Credentials - -SST apps are deployed to your infrastructure. So whether you are deploying to AWS, or Cloudflare, or any other cloud provider, make sure you have their credentials configured locally. - -Learn more about how to [configure your AWS credentials](/docs/iam-credentials/). - ---- - -### Console - -SST also comes with a [Console](/docs/console/). It shows you all your apps, the resources in them, lets you configure _git push to deploy_, and also send you alerts for when there are any issues. - -While it is optional, we recommend creating a free account and linking it to your AWS account. Learn more about the [SST Console](/docs/console/). - ---- - -## sst.config.ts - -Now that you are ready to work on your app and your `sst.config.ts`, let's take a look at what it means to _configure everything in code_. - - ---- - -### IaC - -Infrastructure as Code or _IaC_ is a process of automating the management of infrastructure through code. Rather than doing it manually through a console or user interface. - -:::tip -You won't need to use the AWS Console to configure your SST app. -::: - -Say your app has a Function and an S3 bucket, you would define that in your `sst.config.ts`. - -```ts title="sst.config.ts" -const bucket = new sst.aws.Bucket("MyBucket"); - -new sst.aws.Function("MyFunction", { - handler: "index.handler" -}); -``` - -You won't need to go to the Lambda and S3 parts of the AWS Console. SST will do the work for you. - -In the above snippets, `sst.aws.Function` and `sst.aws.Bucket` are called Components. Learn more about [Components](/docs/components/). - ---- - -### Resources - -The reason this works is because when SST deploys the above app, it'll convert it into a set of commands. These then call AWS with your credentials to create the underlying resources. So the above components get transformed into a list of low level resources in AWS. - -:::tip -You are not directly responsible for the low level resources that SST creates. -::: - -If you log in to your AWS Console you can see what gets created internally. While these might look a little intimidating, they are all managed by SST and you are not directly responsible for them. - -SST will create, track, and remove all the low level resources defined in your app. - ---- - -#### Exceptions - -There are some exceptions to this. You might have resources that are not defined in your SST config. These could include the following resources: - -1. **Previously created** - - You might've previously created some resources by hand that you would like to use in your new SST app. You can import these resources into your app. Moving forward, SST will manage them for you. Learn more about [importing resources](/docs/import-resources/). - -2. **Externally managed** - - You might have resources that are managed by a different team. In this case, you don't want SST to manage them. You simply want to reference them in your app. Learn more about [referencing resources](/docs/reference-resources/). - -3. **Shared across stages** - - If you are creating preview environments, you might not want to make copies of certain resources, like your database. You might want to share these across stages. Learn more about [sharing across stages](/docs/share-across-stages/). - ---- - -### Linking - -Let's say you wanted your function from the above example to upload a file to the S3 bucket, you'd need to hardcode the name of the bucket in your API. - - - -SST avoids this by allowing you to **link resources** together. - -```ts title="sst.config.ts" {3} -new sst.aws.Function("MyFunction", { - handler: "index.handler", - link: [bucket] -}); -``` - -Now in your function you can access the bucket using SST's [SDK](/docs/reference/sdk/). - -```ts title="index.ts" "Resource.MyBucket.name" -import { Resource } from "sst"; - -console.log(Resource.MyBucket.name); -``` - -There's a difference between the two snippets above. One is your **infrastructure code** and the other is your **runtime code**. One is run while creating your app, while the other runs when your users use your app. - -:::tip -You can access your infrastructure in your runtime using the SST SDK. -::: - -The _link_ allows you to access your **infrastructure** in your **runtime code**. Learn more about [resource linking](/docs/linking/). - ---- - -### State - -When you make a change to your `sst.config.ts`, like we did above. SST only deploys the changes. - -```diff lang="ts" title="sst.config.ts" -new sst.aws.Function("MyFunction", { - handler: "index.handler", -+ link: [bucket] -}); -``` - -It does this by maintaining a _state_ of your app. The state is a tree of all the resources in your app and all their properties. - -The state is stored in a file locally and backed up to a bucket in your AWS (or Cloudflare) account. - -:::tip -You can view the state of your app and its history in the SST Console. -::: - -A word of caution, if for some reason you delete your state locally and in your provider, SST won't be able to manage the resources anymore. To SST this app won't exist anymore. - -:::danger -Do not delete the bucket that stores your app's state. -::: - -To fix this, you'll have to manually re-import all those resources back into your app. Learn more about [how state works](/docs/state/). - ---- - -#### Out of sync - -We mentioned above that you are not responsible for the low level resources that SST creates. But this isn't just a point of convenience; it's something you should not do. - -:::caution -Do not manually make changes to the low level resources that SST creates. -::: - -The reason for this is that, SST only applies the diffs when your `sst.config.ts` changes. So if you manually change the resources, it'll be out of sync with your state. - -You can fix some of this by running [`sst refresh`](reference/cli/#refresh) but in general you should avoid doing this. - ---- - -## App - -So now that we know how IaC works, a lot of the workflow and concepts will begin to make sense. Starting with the key parts of an app. - ---- - -### Name - -Every app has a name. The name is used as a namespace. It allows SST to deploy multiple apps to the same cloud provider account, while isolating the resources in an app. - -If you change the name of your app in your `sst.config.ts`, SST will create a completely new set of resources for it. It **does not** rename the resources. - -:::caution -To rename an app, you'll need to remove the resources from the old one and deploy to the new one. -::: - -So if you: - -1. Create an app with the name `my-sst-app` in your `sst.config.ts` and deploy it. -2. Rename the app in your `sst.config.ts` to `my-new-sst-app` and deploy again. - -You will now have two apps in your AWS account called `my-sst-app` and `my-new-sst-app`. - -If you want to rename your app, you'll need to [remove](/docs/workflow/#remove) the old app first and then deploy a new one with the new name. - ---- - -### Stage - -An app can have multiple stages. A stage is like an _environment_, it's a separate version of your app. For example, you might have a dev stage, a production stage, or a personal stage. - -It's useful to have multiple versions of your app because it lets you make changes and test in one version while your users continue to use the other. - -You create a new stage by deploying to it with the `--stage ` CLI option. The stage name is used as a namespace to create a new version of your app. It's similar to how the app name is used as a namespace. - -:::caution -To rename a stage, you'll need to [remove](/docs/workflow/#remove) the resources from the old one and deploy to the new one. -::: - -Similar to app names, stages cannot be renamed. So if you wanted to rename a `development` stage to `dev`; you'll need to first remove `development` and then deploy `dev`. - ---- - -#### Personal stages - -By default, if no stage is passed in, SST creates a stage using the username in your computer. This is called a **personal stage**. Personal stages are typically used in _dev_ mode and every developer on your team should use their own personal stage. - -We'll look at this in detail below. - ---- - -### Region - -Most resources that are created in AWS (and many other providers) belong to a specific region. So when you deploy your app, it's deployed to a specific region. - -:::caution -To switch regions, you'll need to [remove](/docs/workflow/#remove) the resources from one region and deploy to the new one. -::: - -For AWS, the region comes from your AWS credentials but it can be specified in the `sst.config.ts`. - -```ts title="sst.config.ts" {5-7} -export default $config({ - app(input) { - return { - name: "my-sst-app", - providers: { - aws: { region: "us-west-2" } - } - }; - } -}); -``` - -Similar to the app and stage, if you want to switch regions; you'll need to remove your app in the old region and deploy it to the new one. - ---- - -## Commands - -Now with the above background let's look at the workflow of building an SST app. - -Let's say you've created an app by running. - -```bash -sst init -``` - ---- - -### Dev - -To start with, you'll run your app in dev. - -```bash -sst dev -``` - -This deploys your app to your _personal_ stage in _dev mode_. It brings up a multiplexer that deploys your app, runs your functions, creates a tunnel, and starts your frontend and container services. - - - -It deploys your app a little differently and is optimized for local development. - -1. It runs the functions in your app [_Live_](/docs/live/) by deploying a **_stub_ version**. These proxy any requests to your local machine. -2. It **does not deploy** your frontends or container services. Instead, it starts them locally. -3. It also creates a [_tunnel_](/docs/reference/cli#tunnel) that allows them to connect to any resources that are deployed in a VPC. - -:::note -Only use `sst dev` in your personal stage. -::: - -For this reason we recommend only using your personal stage for local development. And instead deploying to a separate stage when you want to share your app with your users. - -Learn more about [`sst dev`](/docs/reference/cli/#dev). - ---- - -### Deploy - -Once you are ready to go to production you can run. - -```bash -sst deploy --stage production -``` - -You can use any stage name for production here. - ---- - -### Remove - -If you want to remove your app and all the resources in it, you can run. - -```bash -sst remove --stage -``` - -You want to be careful while running this command because it permanently removes all the resources from your AWS (or cloud provider) account. - -:::caution -Be careful while running `sst remove` since it permanently removes all your resources. -::: - -To prevent accidental removal, our template `sst.config.ts` comes with the following. - -```ts title="sst.config.ts" -removal: input?.stage === "production" ? "retain" : "remove", -``` - -This is telling SST that if the stage is called `production` then on remove, retain critical resources like buckets and databases. This should avoid any accidental data loss. - - - -Learn more about [removal policies](/docs/reference/config/#removal). - ---- - -## With a team - -This workflow really shines when working with a team. Let's look at what it looks like with a basic git workflow. - - - -1. Every developer on the team uses `sst dev` to work in their own isolated personal stage. -2. You commit your changes to a branch called `dev`. -3. Any changes to the `dev` branch are auto-deployed using `sst deploy --stage dev`. -4. Your team tests changes made to the `dev` stage of your app. -5. If they look good, `dev` is merged into a branch called `production`. -6. And any changes to the `production` branch are auto-deployed to the `production` stage with `sst deploy --stage production`. - -In this setup, you have a separate stage per developer, a _dev_ stage for testing, and a _production_ stage. - ---- - -### Autodeploy - -To have a branch automatically deploy to a stage when commits are pushed to it, you need to configure GitHub Actions. - -![SST Console Autodeploy](../../../assets/docs/workflow/sst-console-autodeploy.png) - -Or you can connect your repo to the SST Console and it'll auto-deploy your app for you. Learn more about [Autodeploy](/docs/console/#autodeploy). - ---- - -### PR environments - -You can also set it up to create preview environments. - -So when a pull request (say PR#12) is created, you auto-deploy a new stage using `sst deploy --stage pr-12`. And once the PR is merged, the preview environment or stage gets removed using `sst remove --stage pr-12`. - -Just like above, you can configure this using GitHub Actions or let the SST Console do it for you. - ---- - -And there you have it. You are now ready to build apps the _SST way_. diff --git a/www/src/content/docs/dummy/markdown.mdx b/www/src/content/docs/dummy/markdown.mdx index 43d6d1c7ab..555130ba8e 100644 --- a/www/src/content/docs/dummy/markdown.mdx +++ b/www/src/content/docs/dummy/markdown.mdx @@ -439,3 +439,11 @@ Astro helps you build faster websites with [β€œIslands Architecture”](https:// ## Right Sidebar ## Overflow + +```ts +new sst.cloudflare.StaticSite("Website", { + domain: "example.com", + notFound: "single-page-application", + trailingSlash: "auto", +}); +``` diff --git a/www/src/pages/docs/[...slug].md.ts b/www/src/pages/docs/[...slug].md.ts index e22c7e0cb5..0e714c5430 100644 --- a/www/src/pages/docs/[...slug].md.ts +++ b/www/src/pages/docs/[...slug].md.ts @@ -15,13 +15,41 @@ export async function getStaticPaths() { })); } +async function buildExamplesCatalog(): Promise { + const docs = await getCollection("docs"); + const examples = docs + .filter((doc) => doc.id.startsWith("docs/examples/")) + .sort((a, b) => a.id.localeCompare(b.id)); + + const lines = examples.map((doc) => { + const slug = doc.id.replace(/\.mdx?$/, "").replace(/^docs\//, ""); + return `- [${doc.data.title}](/docs/${slug}/)`; + }); + + return lines.join("\n"); +} + export const GET: APIRoute = async ({ params }) => { const slug = params.slug!; const entry = await getEntry("docs", `docs/${slug}`); if (!entry?.body) return new Response("Not found", { status: 404 }); - const cleaned = cleanMarkdown(entry.body); - const markdown = `# ${entry.data.title} + let content: string; + if (slug === "examples") { + // Serve as a catalog of links to individual example pages + const catalog = await buildExamplesCatalog(); + content = `# ${entry.data.title} + +${entry.data.description || ""} + +Source: https://sst.dev/docs/${slug} + +--- + +${catalog}`; + } else { + const cleaned = cleanMarkdown(entry.body); + content = `# ${entry.data.title} ${entry.data.description || ""} @@ -30,8 +58,9 @@ Source: https://sst.dev/docs/${slug} --- ${cleaned}`; + } - return new Response(markdown, { + return new Response(content, { headers: { "Content-Type": "text/markdown; charset=utf-8", "Cache-Control": "public,max-age=0,s-maxage=86400,stale-while-revalidate=86400", diff --git a/www/src/pages/llms-full.txt.ts b/www/src/pages/llms-full.txt.ts index 8c999685b2..29dc377eb8 100644 --- a/www/src/pages/llms-full.txt.ts +++ b/www/src/pages/llms-full.txt.ts @@ -6,6 +6,7 @@ export const GET: APIRoute = async () => { const docs = await getCollection("docs"); const filtered = docs .filter((doc) => doc.id.startsWith("docs/")) + .filter((doc) => doc.id.replace(/\.mdx?$/, "") !== "docs/examples") .sort((a, b) => a.id.localeCompare(b.id)); const pages = filtered.map((doc) => { diff --git a/www/src/pages/llms.txt.ts b/www/src/pages/llms.txt.ts index 116e064b37..a010dba2a2 100644 --- a/www/src/pages/llms.txt.ts +++ b/www/src/pages/llms.txt.ts @@ -5,6 +5,7 @@ export const GET: APIRoute = async () => { const docs = await getCollection("docs"); const filtered = docs .filter((doc) => doc.id.startsWith("docs/")) + .filter((doc) => !doc.id.startsWith("docs/examples/")) .sort((a, b) => a.id.localeCompare(b.id)); const links = filtered diff --git a/www/src/styles/markdown.css b/www/src/styles/markdown.css index bc76d153dc..24d7474b2e 100644 --- a/www/src/styles/markdown.css +++ b/www/src/styles/markdown.css @@ -19,7 +19,7 @@ } .sl-markdown-content :is(.starlight-aside, .expressive-code) + :is(.starlight-aside, .expressive-code) { - margin-top: calc(var(--paragraph-spacing) + 1.125rem); + margin-top: var(--paragraph-spacing); } /* Space around sections */ .sl-markdown-content :not(h1, h2, h3, h4, h5, h6) diff --git a/www/src/util/markdown.ts b/www/src/util/markdown.ts index 02e1a8115d..c5160f843a 100644 --- a/www/src/util/markdown.ts +++ b/www/src/util/markdown.ts @@ -1,55 +1,76 @@ +import changelog from "../data/changelog.json"; + +function renderChangelog(): string { + return (changelog as Array<{ tag: string; body: string }>) + .map((r) => `## ${r.tag.replace(/^v/, "")}\n\n${r.body}`) + .join("\n\n"); +} + +function stripImportsAndExports(source: string): string { + return source.replace(/^import\s+.*$/gm, "").replace(/^export\s+.*$/gm, ""); +} + export function cleanMarkdown(source: string): string { - return ( - source - // Remove import statements - .replace(/^import\s+.*$/gm, "") - // Remove export statements - .replace(/^export\s+.*$/gm, "") - // Remove JSX comments {/* ... */} (single and multiline) - .replace(/\{\/\*[\s\S]*?\*\/\}/g, "") - // Remove self-closing tags - .replace(/]*\/>/g, "") - // Remove self-closing tags - .replace(/]*\/>/g, "") - // Remove self-closing tags - .replace(/]*\/>/g, "") - // Remove self-closing tags - .replace(/]*\/>/g, "") - // Remove tsdoc component tags (opening and closing) - .replace(/<\/?(?:Section|Segment|InlineSection)(?:\s+[^>]*)?>/g, "") - // Convert content to just content - .replace(/]*>([\s\S]*?)<\/NestedTitle>/g, "$1") - // Remove
and
- .replace(//g, "") - .replace(/^<\/div>\s*$/gm, "") - // Merge adjacent tags into one (for type expressions like Input) - .replace(/<\/code>/g, "") - // Convert innermost content β†’ `content` - .replace(/([^<]*)<\/code>/g, "`$1`") - // Strip remaining outer and from nested structures - .replace(//g, "") - // Convert plain content β†’ `content` - .replace(/([^<]*)<\/code>/g, "`$1`") - .replace(/<\/code>/g, "") - // Remove

and

wrapper tags - .replace(/<\/?p>/g, "") - // Decode common HTML entities - .replace(/</g, "<") - .replace(/>/g, ">") - .replace(/&/g, "&") - .replace(/“/g, '"') - .replace(/”/g, '"') - .replace(/{/g, "{") - .replace(/}/g, "}") - .replace(/{/g, "{") - .replace(/}/g, "}") - // Convert / to labeled sections - .replace(//g, "") - .replace(/<\/Tabs>/g, "") - .replace(//g, "**$1**\n") - .replace(/<\/TabItem>/g, "") - // Collapse 3+ consecutive blank lines to 2 - .replace(/\n{3,}/g, "\n\n") - .trim() - ); + return source + .split(/(```[\s\S]*?```)/g) + .map((part, i) => + // Odd indices are code blocks β€” leave them alone + i % 2 === 1 ? part : stripImportsAndExports(part) + ) + .join("") + // Remove JSX comments {/* ... */} (single and multiline) + .replace(/\{\/\*[\s\S]*?\*\/\}/g, "") + // Remove self-closing tags + .replace(/]*\/>/g, "") + // Remove self-closing tags + .replace(/]*\/>/g, "") + // Remove self-closing tags + .replace(/]*\/>/g, "") + // Remove self-closing tags + .replace(/]*\/>/g, "") + // Remove tsdoc component tags (opening and closing) + .replace(/<\/?(?:Section|Segment|InlineSection)(?:\s+[^>]*)?>/g, "") + // Convert content to just content + .replace(/]*>([\s\S]*?)<\/NestedTitle>/g, "$1") + // Remove
and
+ .replace(//g, "") + .replace(/^<\/div>\s*$/gm, "") + // Merge adjacent tags into one (for type expressions like Input) + .replace(/<\/code>/g, "") + // Convert innermost content β†’ `content` + .replace(/([^<]*)<\/code>/g, "`$1`") + // Strip remaining outer and from nested structures + .replace(//g, "") + // Convert plain content β†’ `content` + .replace(/([^<]*)<\/code>/g, "`$1`") + .replace(/<\/code>/g, "") + // Remove

and

wrapper tags + .replace(/<\/?p>/g, "") + // Decode common HTML entities + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/&/g, "&") + .replace(/“/g, '"') + .replace(/”/g, '"') + .replace(/{/g, "{") + .replace(/}/g, "}") + .replace(/{/g, "{") + .replace(/}/g, "}") + // Convert / to labeled sections + .replace(//g, "") + .replace(/<\/Tabs>/g, "") + .replace(//g, "**$1**\n") + .replace(/<\/TabItem>/g, "") + // Ensure blank line before and after code blocks, horizontal rules, and headings + .replace(/([^\n])\n(```)/g, "$1\n\n$2") + .replace(/(```)\n([^\n])/g, "$1\n\n$2") + .replace(/([^\n])\n(---)\n/g, "$1\n\n$2\n") + .replace(/\n(---)\n([^\n])/g, "\n$1\n\n$2") + .replace(/([^\n])\n(#{1,6} )/g, "$1\n\n$2") + .replace(/\n(#{1,6} .+)\n([^\n])/g, "\n$1\n\n$2") + // Replace component + .replace(//g, renderChangelog()) + // Collapse 3+ consecutive blank lines to 2 + .replace(/\n{3,}/g, "\n\n") + .trim(); }