diff --git a/.github/ISSUE_TEMPLATE/BugReport.yml b/.github/ISSUE_TEMPLATE/BugReport.yml new file mode 100644 index 0000000..372d818 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BugReport.yml @@ -0,0 +1,63 @@ +--- +name: Bug +description: File a bug/issue +labels: [bug, triage] +assignees: + - michenriksen +body: + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the bug you encountered. + options: + - label: I have searched the existing issues + required: true + - type: textarea + attributes: + label: Current Behavior + description: A concise description of what you're experiencing. + validations: + required: false + - type: textarea + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: false + - type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. In this environment... + 2. With this config... + 3. Run '...' + 4. See error... + validations: + required: false + - type: textarea + attributes: + label: Environment + description: | + examples: + - **OS**: macOS 13.3.1 + - **Architecture**: arm + - **Gokiburi**: v0.1.x + - **Browser**: Google Chrome 113.0.xxxx.xx + value: | + - OS: + - Architecture: + - Gokiburi: + - Browser: + render: markdown + validations: + required: false + - type: textarea + attributes: + label: Anything else? + description: | + Links? References? Anything that will give more context about the issue you are encountering! + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..a8ac5aa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,12 @@ +--- +blank_issues_enabled: false +contact_links: + - name: 🛟 Help & Support + url: https://github.com/michenriksen/gokiburi/discussions/categories/q-a + about: Please ask and answer questions here. + - name: 💡 Feature Requests & Ideas + url: https://github.com/michenriksen/gokiburi/discussions/categories/ideas + about: Please add feature requests and ideas here. + - name: 🛡️ Vulnerability Disclosure + url: https://michenriksen.com/ + about: Please e-mail me if you think you have found a vulnerability or other sensitive issue. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..48be7d5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,38 @@ +--- +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" + assignees: + - michenriksen + labels: + - deps + - gomod + - automerge + commit-message: + prefix: "chore(deps)" + - package-ecosystem: "npm" + directory: "/web/app" + schedule: + interval: "weekly" + assignees: + - michenriksen + labels: + - deps + - npm + - automerge + commit-message: + prefix: "chore(deps)" + - package-ecosystem: GitHub-actions + directory: "/" + schedule: + interval: "weekly" + assignees: + - michenriksen + labels: + - deps + - ghactions + commit-message: + prefix: "chore(ci)" diff --git a/.github/images/gokiburi_dark.png b/.github/images/gokiburi_dark.png new file mode 100644 index 0000000..59c13b3 Binary files /dev/null and b/.github/images/gokiburi_dark.png differ diff --git a/.github/images/gokiburi_light.png b/.github/images/gokiburi_light.png new file mode 100644 index 0000000..6b23123 Binary files /dev/null and b/.github/images/gokiburi_light.png differ diff --git a/.github/images/social_preview.png b/.github/images/social_preview.png new file mode 100644 index 0000000..5f6c82f Binary files /dev/null and b/.github/images/social_preview.png differ diff --git a/.github/images/web_ui_coverage.png b/.github/images/web_ui_coverage.png new file mode 100644 index 0000000..fd26099 Binary files /dev/null and b/.github/images/web_ui_coverage.png differ diff --git a/.github/images/web_ui_overview.png b/.github/images/web_ui_overview.png new file mode 100644 index 0000000..232aefb Binary files /dev/null and b/.github/images/web_ui_overview.png differ diff --git a/.github/images/web_ui_settings.png b/.github/images/web_ui_settings.png new file mode 100644 index 0000000..6d21a34 Binary files /dev/null and b/.github/images/web_ui_settings.png differ diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml new file mode 100644 index 0000000..ae3e78c --- /dev/null +++ b/.github/workflows/audit.yml @@ -0,0 +1,148 @@ +--- +name: Security Audit + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "10 13 * * 1" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + run: + runs-on: ubuntu-latest + outputs: + go_changes: ${{ steps.filter.outputs.go_changes }} + fe_changes: ${{ steps.filter.outputs.fe_changes }} + gh_actions_changes: ${{ steps.filter.outputs.gh_actions_changes }} + steps: + - uses: actions/checkout@v3 + + - name: Check modified files + uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + go_changes: + - '**.go' + - 'go.mod' + fe_changes: + - 'web/app/**.ts' + - 'web/app/**.js' + - 'web/app/**.cjs' + - 'web/app/**.json' + - 'web/app/**.svelte' + - 'web/app/package.json' + gha_changes: + - '.github/**.yml' + + - name: Install Python + if: >- + ${{ github.actor != 'dependabot[bot]' && + !contains('["push", "pull_request"]', github.event_name) || + needs.check_changes.outputs.go_changes == 'true' || + needs.check_changes.outputs.fe_changes == 'true' || + needs.check_changes.outputs.gha_changes == 'true' }} + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install Semgrep + if: >- + ${{ github.actor != 'dependabot[bot]' && + !contains('["push", "pull_request"]', github.event_name) || + needs.check_changes.outputs.go_changes == 'true' || + needs.check_changes.outputs.fe_changes == 'true' || + needs.check_changes.outputs.gha_changes == 'true' }} + run: | + python -m pip install --upgrade pip + pip install semgrep + + - name: Install Go + if: >- + ${{ github.actor != 'dependabot[bot]' && + !contains('["push", "pull_request"]', github.event_name) || + needs.check_changes.outputs.go_changes == 'true' }} + uses: actions/setup-go@v4 + with: + go-version: "stable" + + - uses: actions/cache@v3 + if: >- + ${{ github.actor != 'dependabot[bot]' && + !contains('["push", "pull_request"]', github.event_name) || + needs.check_changes.outputs.go_changes == 'true' }} + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Install govulncheck + if: >- + ${{ github.actor != 'dependabot[bot]' && + !contains('["push", "pull_request"]', github.event_name) || + needs.check_changes.outputs.go_changes == 'true' }} + run: go install golang.org/x/vuln/cmd/govulncheck@latest + + - name: Create Dummy Frontend Build + if: >- + ${{ github.actor != 'dependabot[bot]' && + !contains('["push", "pull_request"]', github.event_name) || + needs.check_changes.outputs.go_changes == 'true' }} + run: mkdir web/app/build && touch web/app/build/dummy.txt + + - name: Install Go Dependencies + if: >- + ${{ github.actor != 'dependabot[bot]' && + !contains('["push", "pull_request"]', github.event_name) || + needs.check_changes.outputs.go_changes == 'true' }} + run: go mod tidy + + - name: Audit Go Code + if: >- + ${{ github.actor != 'dependabot[bot]' && + !contains('["push", "pull_request"]', github.event_name) || + needs.check_changes.outputs.go_changes == 'true' }} + run: make audit + + - name: Install Node + if: >- + ${{ github.actor != 'dependabot[bot]' && + !contains('["push", "pull_request"]', github.event_name) || + needs.check_changes.outputs.fe_changes == 'true' }} + uses: actions/setup-node@v3 + with: + node-version: "latest" + cache: "npm" + cache-dependency-path: "web/app/package-lock.json" + + - name: Install NPM dependencies + if: >- + ${{ github.actor != 'dependabot[bot]' && + !contains('["push", "pull_request"]', github.event_name) || + needs.check_changes.outputs.fe_changes == 'true' }} + run: npm ci && git diff --exit-code + working-directory: web/app + + - name: Audit Frontend Code + if: >- + ${{ github.actor != 'dependabot[bot]' && + !contains('["push", "pull_request"]', github.event_name) || + needs.check_changes.outputs.fe_changes == 'true' }} + run: make audit-fe + + - name: Audit GitHub Actions + if: >- + ${{ github.actor != 'dependabot[bot]' && + !contains('["push", "pull_request"]', github.event_name) || + needs.check_changes.outputs.gha_changes == 'true' }} + run: make audit-gha diff --git a/.github/workflows/build-fe.yml b/.github/workflows/build-fe.yml new file mode 100644 index 0000000..a96d3dc --- /dev/null +++ b/.github/workflows/build-fe.yml @@ -0,0 +1,62 @@ +--- +name: Build Frontend + +permissions: + contents: read + +on: + push: + branches: [main] + paths: + - "web/app/**.json" + - "web/app/**.cjs" + - "web/app/**.ts" + - "web/app/**.js" + - "web/app/**.svelte" + pull_request: + branches: [main] + paths: + - "web/app/**.json" + - "web/app/**.cjs" + - "web/app/**.ts" + - "web/app/**.js" + - "web/app/**.svelte" + schedule: + - cron: "0 10 * * 1" + workflow_call: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + run: + name: Build Frontend + runs-on: ubuntu-latest + timeout-minutes: 5 + strategy: + fail-fast: true + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: "latest" + cache: "npm" + cache-dependency-path: "web/app/package-lock.json" + + - name: Install NPM dependencies + run: npm ci && git diff --exit-code + working-directory: web/app + + - name: make lint-fe + run: make lint-fe + + - name: make build-fe + run: make build-fe + + - name: make test-fe + run: make test-fe diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..a636285 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,90 @@ +--- +name: Build + +permissions: + contents: read + +on: + push: + branches: [main] + paths: + - "go.mod" + - "**.go" + pull_request: + branches: [main] + paths: + - "go.mod" + - "**.go" + schedule: + - cron: "0 10 * * 1" + workflow_call: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 5 + strategy: + fail-fast: true + matrix: + go: ["stable", "oldstable"] + + steps: + - name: Check out code + uses: actions/checkout@v3 + + - name: Install Go + uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go }} + check-latest: true + + - uses: actions/cache@v3 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Go Tidy + run: go mod tidy && git diff --exit-code + + - name: Create Dummy Frontend Build + run: mkdir web/app/build && touch web/app/build/dummy.txt + + - name: Go Vet + run: go vet ./... + + - name: Go Mod + run: go mod download + + - name: Go Mod Verify + run: go mod verify + + - name: Go Build + run: go build -o /dev/null ./... + + - name: make test + run: make test + + lint: + name: Lint + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/setup-go@v4 + with: + go-version: "stable" + - uses: actions/checkout@v3 + + - name: Create Dummy Frontend Build + run: mkdir web/app/build && touch web/app/build/dummy.txt + + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..07edf73 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,66 @@ +--- +name: Release + +permissions: + contents: write + +on: + workflow_dispatch: + inputs: + version: + description: "Release version (e.g., v1.0.0)" + required: true + +jobs: + run: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - run: git fetch --force --tags + + - name: Create release + run: | + git config --global user.email "mchnrksn@gmail.com" + git config --global user.name "Michael Henriksen" + git tag -a "$VERSION" -m "$VERSION" + env: + VERSION: ${{ github.event.inputs.version }} + + - uses: actions/setup-go@v4 + with: + go-version: stable + + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: "latest" + cache: "npm" + cache-dependency-path: "web/app/package-lock.json" + + - name: Install NPM dependencies + run: npm ci && git diff --exit-code + working-directory: web/app + + - name: Install Syft + run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v4 + with: + distribution: goreleaser + version: latest + args: release --clean + env: + VERSION: ${{ github.event.inputs.version }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GOPATH: ${{ github.workspace }}/go + + - name: Notify Go proxy about new release + run: go list -m "github.com/michenriksen/gokiburi@$VERSION" || true + env: + GOPROXY: proxy.golang.org + VERSION: ${{ github.event.inputs.version }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8812756 --- /dev/null +++ b/.gitignore @@ -0,0 +1,125 @@ +# Created by https://www.toptal.com/developers/gitignore/api/go,vim,visualstudiocode,macos,linux +# Edit at https://www.toptal.com/developers/gitignore?templates=go,vim,visualstudiocode,macos,linux + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +!internal/pkg/coverparser/testdata/numbers/coverprofile.out +!internal/pkg/runner/testdata/coverprofile.out + +# Dependency directories (remove the comment below to include it) +vendor/ + +# Go workspace file +go.work + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/go,vim,visualstudiocode,macos,linux + +/bin +/.go +/.licenses* +/coverage/ +/dist +/dependencies.csv +*.spdx.sbom + diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..2352d88 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,217 @@ +--- +run: + go: "1.20" + +issues: + exclude-rules: + - path: _test\.go + linters: + - errcheck + - forcetypeassert + - gosec + - revive + - wsl + +linters-settings: + depguard: + list-type: denylist + packages-with-error-message: + - github.com/stretchr/testify/assert: "use github.com/stretchr/testify/require for faster tests" + - github.com/davecgh/go-spew: "spew is only for temporary debugging" + additional-guards: + - list-type: denylist + packages-with-error-message: + - github.com/stretchr/testify: "testify must only be used in test files" + ignore-file-rules: + - "**/*_test.go" + - list-type: denylist + packages-with-error-message: + - github.com/brianvoe/gofakeit: "gofakeit must only be used in test files" + ignore-file-rules: + - "**/*_test.go" + + forbidigo: + forbid: + - 'fmt\.Print.*' + - 'log\.Print.*' + + gofumpt: + extra-rules: true + + goimports: + local-prefixes: github.com/michenriksen/gokiburi + + nolintlint: + require-explanation: true + require-specific: true + + wrapcheck: + ignorePackageGlobs: + - github.com/labstack/echo/* + - github.com/invopop/validation + - encoding/json + + revive: + rules: + - name: argument-limit + severity: warning + disabled: false + arguments: [4] + - name: atomic + severity: warning + disabled: false + - name: bool-literal-in-expr + severity: warning + disabled: false + - name: cognitive-complexity + severity: warning + disabled: false + arguments: [20] + - name: comment-spacings + severity: warning + disabled: false + arguments: + - nolint + - "#nosec" + - name: constant-logical-expr + severity: warning + disabled: false + - name: context-as-argument + severity: warning + disabled: false + - name: cyclomatic + severity: warning + disabled: false + arguments: [20] + - name: context-keys-type + severity: warning + disabled: false + - name: datarace + severity: warning + disabled: false + - name: deep-exit + severity: warning + disabled: false + - name: defer + severity: warning + disabled: false + - name: duplicated-imports + severity: warning + disabled: false + - name: early-return + severity: warning + disabled: false + - name: empty-block + severity: warning + disabled: false + - name: error-return + severity: warning + disabled: false + - name: error-strings + severity: warning + disabled: false + - name: errorf + severity: warning + disabled: false + - name: function-result-limit + severity: warning + disabled: false + arguments: [3] + - name: identical-branches + severity: warning + disabled: false + - name: if-return + severity: warning + disabled: false + - name: increment-decrement + severity: warning + disabled: false + - name: import-shadowing + severity: warning + disabled: false + - name: line-length-limit + severity: warning + disabled: false + arguments: [120] + - name: modifies-parameter + severity: warning + disabled: false + - name: modifies-value-receiver + severity: warning + disabled: false + - name: optimize-operands-order + severity: warning + disabled: false + - name: range + severity: warning + disabled: false + - name: range-val-in-closure + severity: warning + disabled: false + - name: range-val-address + severity: warning + disabled: false + - name: redefines-builtin-id + severity: warning + disabled: false + - name: string-of-int + severity: warning + disabled: false + - name: struct-tag + severity: warning + disabled: false + - name: var-naming + severity: warning + disabled: false + - name: var-declaration + severity: warning + disabled: false + - name: unconditional-recursion + severity: warning + disabled: false + - name: unnecessary-stmt + severity: warning + disabled: false + - name: unreachable-code + severity: warning + disabled: false + - name: unused-parameter + severity: warning + disabled: false + - name: unused-receiver + severity: warning + disabled: false + - name: use-any + severity: warning + disabled: false + +linters: + disable-all: true + enable: + - bidichk + - depguard + - errcheck + - errname + - forcetypeassert + - gocheckcompilerdirectives + - gocritic + - godot + - gofumpt + - goimports + - gosec + - gosimple + - ineffassign + - makezero + - misspell + - nolintlint + - prealloc + - revive + - staticcheck + - tenv + - testpackage + - thelper + - typecheck + - unused + - usestdlibvars + - wrapcheck + - wsl diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..77e1531 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,90 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com +--- +project_name: gokiburi +report_sizes: true +env: + - BUILD_COMMIT={{ .FullCommit }} + - BUILD_TIME={{ .Date }} +before: + hooks: + - make clean + - go mod tidy + - make dep-csv + - make sbom + - make build-fe +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm64 + goarm: + - 6 + - 7 + ignore: + - goos: darwin + goarch: arm64 + goarm: 6 + - goos: darwin + goarch: arm64 + goarm: 7 + mod_timestamp: "{{ .CommitTimestamp }}" + flags: + - "-trimpath" + asmflags: + - "all=-trimpath={{ .Env.GOPATH }}" + gcflags: + - "all=-trimpath={{ .Env.GOPATH }}" + ldflags: + - "-s -w" + - "-X github.com/michenriksen/gokiburi/internal/gokiburi.buildVersion={{ .Version }}" + - "-X github.com/michenriksen/gokiburi/internal/gokiburi.buildCommit={{ .FullCommit }}" + - "-X github.com/michenriksen/gokiburi/internal/gokiburi.buildTime={{ .Date }}" + +archives: + - format: tar.gz + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + format: zip + files: + - README.md + - LICENSE.md + - dependencies.csv + - "{{ .ProjectName }}.spdx.sbom" + +checksum: + name_template: "checksums.txt" +snapshot: + name_template: "{{ incpatch .Version }}-next" +changelog: + sort: asc + filters: + exclude: + - "^chore" + - "^docs" + - "^test" + - "merge conflict" + - "Merge pull request" + - "Merge remote-tracking branch" + - "Merge branch" + +release: + draft: true + replace_existing_draft: true +# The lines beneath this are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/use them. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj diff --git a/.syft.yaml b/.syft.yaml new file mode 100644 index 0000000..66221b5 --- /dev/null +++ b/.syft.yaml @@ -0,0 +1,4 @@ +--- +golang: + search-local-mod-cache-licenses: true + search-remote-licenses: true diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..286eb3e --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Michael Henriksen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..515b44b --- /dev/null +++ b/Makefile @@ -0,0 +1,134 @@ +DBG_MAKEFILE ?= +ifeq ($(DBG_MAKEFILE),1) + $(warning ***** starting Makefile for goal(s) "$(MAKECMDGOALS)") + $(warning ***** $(shell date)) +else + MAKEFLAGS += -s +endif + +BIN ?= gokiburi + +APP_NAME ?= $(BIN) + +ifeq ($(VERSION),) + VERSION := $(shell git log -1 --format="%ad-%h" --date=format-local:"%Y%m%d%H%M%S" --abbrev=12) +endif + +DBG ?= + +MAKEFLAGS += --no-builtin-rules +MAKEFLAGS += --warn-undefined-variables +.SUFFIXES: + +OS := $(if $(GOOS),$(GOOS),$(shell go env GOOS)) +ARCH := $(if $(GOARCH),$(GOARCH),$(shell go env GOARCH)) + +TAG := $(VERSION)__$(OS)_$(ARCH) + +BIN_EXTENSION := +ifeq ($(OS), windows) + BIN_EXTENSION := .exe +endif + +SHELL := /usr/bin/env bash -o errexit -o pipefail -o nounset + +GOFLAGS ?= +HTTP_PROXY ?= +HTTPS_PROXY ?= + +export BIN +export BIN_EXTENSION +export APP_NAME +export VERSION +export DBG +export OS +export ARCH +export GOFLAGS +export HTTP_PROXY +export HTTPS_PROXY + +build: # @HELP builds binary to ./build directory for one platform ($OS/$ARCH) +build: build-fe + scripts/build.sh + +build-fe: # @HELP builds frontend, as defined in ./scripts/build-fe.sh +build-fe: + scripts/build-fe.sh + +version: # @HELP outputs the version string +version: + echo $(VERSION) + +version-next: # @HELP outputs the next version string based on commits. +version-next: + svu next + +test-all: # @HELP runs tests for Go and frontend +test-all: test test-fe + +test: # @HELP runs tests, as defined in ./scripts/test.sh +test: + scripts/test.sh ./... + +test-fe: # @HELP runs frontend tests, as defined in ./scripts/test-fe.sh +test-fe: + scripts/test-fe.sh + +lint-all: # @HELP runs linting for Go and frontend +lint-all: lint lint-fe + +lint: # @HELP runs linting, as defined in ./scripts/lint.sh +lint: + scripts/lint.sh ./... + +audit-all: # @HELP runs Go, frontend, and GitHub Actions security audits +audit-all: + scripts/audit.sh + +audit: # @HELP runs Go security audits, as defined in ./scripts/audit.sh +audit: + scripts/audit.sh go + +audit-fe: # @HELP runs frontend security audits, as defined in ./scripts/audit.sh +audit-fe: + scripts/audit.sh fe + +audit-gha: # @HELP runs GitHub Actions security audits, as defined in ./scripts/audit.sh +audit-gha: + scripts/audit.sh gha + +lint-fe: # @HELP runs frontend linting, as defined in ./scripts/lint-fe.sh +lint-fe: + scripts/lint-fe.sh + +clean: # @HELP removes build artifacts +clean: + rm -rf ./bin + rm -rf ./dist + rm -rf ./web/app/build + rm -f ./dependencies.csv + rm -f ./gokiburi.spdx.sbom + +dep-csv: # @HELP generates CVS of dependencies to ./dependencies.csv +dep-csv: + scripts/dep-csv.sh + +sbom: # @HELP generates SBOM file to ./gokiburi.spdx.sbom +sbom: + scripts/sbom.sh + +help: # @HELP prints this message +help: + echo "VARIABLES:" + echo " BIN = $(BIN)" + echo " OS = $(OS)" + echo " ARCH = $(ARCH)" + echo " DBG = $(DBG)" + echo " GOFLAGS = $(GOFLAGS)" + echo + echo "TARGETS:" + grep -E '^.*: *# *@HELP' $(MAKEFILE_LIST) \ + | awk ' \ + BEGIN {FS = ": *# *@HELP"}; \ + { printf " %-30s %s\n", $$1, $$2 }; \ + ' diff --git a/README.md b/README.md new file mode 100644 index 0000000..e9b3b03 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# Gokiburi: Automatic Test Runs for Go Projects + +
+
+
+
+ + + + Pictogram of a person shocked by seeing a cockroach on the ground + +
+
+
+
+ +Gokiburi is a powerful and user-friendly testing tool designed to enhance the developer experience in Go projects. It automatically runs tests in real-time by monitoring file changes, ensuring that your code remains robust and reliable throughout the development process. With Gokiburi, you can focus on writing code while it takes care of running tests and keeping you informed about your project's health. + +## Highlights + +- **Real-time monitoring**: Gokiburi keeps an eye on your Go project files and automatically triggers tests for the package where the file belongs as soon as a change is detected. +- **Sleek web UI**: Easily monitor and sift through test results using Gokiburi's intuitive web interface. Gain insights into your project's code coverage and quickly identify areas that need improvement. +- **Configurable notifications**: Stay informed about your project's health with customizable browser and sound notifications. Gokiburi will promptly notify you if something isn’t right, allowing you to fix the issue faster and more efficiently. + +## Usage + +> **Note** +> Gokiburi is currently a **work in progress** and should be regarded as beta software. Although I've been using it personally without any major problems, it may still contain bugs and quirks. If you encounter any issues or odd behavior, please don't hesitate to [create a new issue](https://github.com/michenriksen/gokiburi/issues/new). + +Begin using Gokiburi with these steps: + +1. Head to your Go project's root directory in a terminal. +2. Launch Gokiburi by entering the `gokiburi` command: + +```shell +~/src/github.com/example/project $ gokiburi +``` + +Gokiburi will keep an eye on directories, monitoring for any changes in `.go` source files. When a modification or new file is detected, Gokiburi automatically runs tests for the package specified in the affected file. + +You can, of course, use Gokiburi as a basic test runner and monitor the results on your terminal. However, its true potential shines when utilizing the web UI, which is accessible by default at [http://localhost:9393/]. + +The web UI offers a comprehensive view of all test results, complete with search and filter capabilities. This allows you to focus on the specific results you're interested in: + +![Gokiburi web UI](.github/images/web_ui_overview.png) + +By clicking the gear icon button, you can access Gokiburi's web UI settings. Here, among other options, you can activate sound and browser notifications. This way, you'll be instantly alerted when something goes wrong, even when you're busy in your editor: + +![Gokiburi web UI settings](.github/images/web_ui_settings.png) + +Gokiburi runs tests with code coverage by default. When you click the coverage button for a package, it opens the coverage report view. This helps you effortlessly pinpoint parts of your code with robust coverage, as well as areas that could use improvement: + +![Gokiburi coverage report](.github/images/web_ui_coverage.png) + +## Installation + +It’s recommended to install the most recent pre-compiled binary release for your operating system and architecture from the [releases page](https://github.com/michenriksen/gokiburi/releases). + +If you have Go installed, it’s also possible to install the latest development version with the command: + +```shell +$ go install github.com/michenriksen/gokiburi@latest +``` + +The `@latest` tag can also be replaced with a specific release tag if you would rather not install the latest code. diff --git a/cmd/root/command.go b/cmd/root/command.go new file mode 100644 index 0000000..6f92e53 --- /dev/null +++ b/cmd/root/command.go @@ -0,0 +1,76 @@ +package root + +import ( + "os" + "os/signal" + "path/filepath" + "syscall" + "time" + + "github.com/michenriksen/gokiburi/internal/gokiburi" + "github.com/michenriksen/gokiburi/internal/pkg/config" + + "github.com/spf13/cobra" +) + +// Run root command. +func Run() { + cobra.CheckErr(Build().Execute()) +} + +// Build root command. +// +// Registers usage information and command flags. +func Build() *cobra.Command { + app := gokiburi.New() + + cmd := &cobra.Command{ + Use: "gokiburi [flags] [dir]", + Version: gokiburi.Version(), + Args: cobra.RangeArgs(0, 1), + PreRun: func(cmd *cobra.Command, args []string) { + cfg, err := config.Load(cmd) + cobra.CheckErr(err) + + dir, err := os.Getwd() + cobra.CheckErr(err) + + if len(args) == 1 { + dir, err = filepath.Abs(args[0]) + cobra.CheckErr(err) + } + + cobra.CheckErr(app.Init(cfg, dir, gokiburi.InitLogger)) + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + + go func() { + <-c + app.Cancel() + os.Exit(1) //nolint:revive // call to `os.Exit` is intended here. + }() + }, + Run: func(cmd *cobra.Command, args []string) { + app.Run() + }, + } + + cmd.PersistentFlags().Bool("viper", true, "use Viper for configuration") + cmd.PersistentFlags().Bool("debug", false, "log debugging information") + cmd.PersistentFlags().Bool("json", false, "log in JSON format") + cmd.PersistentFlags().Bool("quiet", false, "log only warnings and errors") + + cmd.Flags().String("shuffle", "off", "randomize the execution order of tests (off,on,N)") + cmd.Flags().String("covermode", "count", "mode for coverage analysis (set,count,atomic)") + cmd.Flags().Bool("race", false, "enable data race detector") + cmd.Flags().Bool("short", false, "tell long-running tests to shorten their runtime") + cmd.Flags().Duration("timeout", 10*time.Minute, "timeout for test runs") + cmd.Flags().StringSlice("skip-paths", nil, "additional paths for watcher to skip") + cmd.Flags().String("listen-address", "127.0.0.1", "address to run web server on") + cmd.Flags().Int("listen-port", 9393, "port to run web server on") + + cmd.SetVersionTemplate(gokiburi.VersionTemplate()) + + return cmd +} diff --git a/dependencies.csv.tmpl b/dependencies.csv.tmpl new file mode 100644 index 0000000..581f258 --- /dev/null +++ b/dependencies.csv.tmpl @@ -0,0 +1,5 @@ +"Package","Version","Type","Licences" +{{- range .Artifacts}} +"{{.Name}}","{{.Version}}","{{.Type}}","{{.Licenses}}" +{{- end}} + diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..b306d6a --- /dev/null +++ b/docs/CODE_OF_CONDUCT.md @@ -0,0 +1,131 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at `mchnrksn+coc[at]gmail[dot]com`. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..cc52453 --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,48 @@ +# Release Runbook + +This document describes the steps needed to release a new version of Gokiburi: + +## 1. Preparations + +1. [ ] Ensure latest code is fetched: `git pull --rebase origin main` +2. [ ] Tidy go dependencies: `go mod tidy` +3. [ ] Tidy frontend dependencies in `web/app`: `npm install; npm prune` +4. [ ] Check for vulnerable code and dependencies: `make audit` +5. [ ] Check code is correctly formatted: `make lint-all` +6. [ ] Check all tests pass: `make test-all` +7. [ ] Build new snapshot binary: `goreleaser build --snapshot --single-target` +8. [ ] Run binary: `./dist/gokiburi_*/gokiburi` +9. [ ] Confirm web UI loads on `http://localhost:9393/` + +## 2. Manual Acceptance Test + +1. [ ] Confirm no errors in developer console +2. [ ] Toggle on sound notifications in app bar +3. [ ] Open settings and confirm sound notifications are active for all events +4. [ ] Press **Run All Tests** app bar button +5. [ ] Confirm sound notification is played +6. [ ] Click on test and confirm output and start time are shown +7. [ ] Click on coverage button and confirm code coverage is rendered +8. [ ] Perform search and confirm tests are filtered +9. [ ] Modify other filter options and confirm tests are filtered +10. [ ] Open settings and disable sound notifications for passing tests +11. [ ] Modify source file and confirm package tests are run +12. [ ] Confirm sound notification is NOT played +13. [ ] Press **Pause Automatic Test Runs** app bar button +14. [ ] Modify source file and confirm package tests are NOT run +15. [ ] Press **Resume Automatic Test Runs** app bar button +16. [ ] Confirm state goes back to **Live** +17. [ ] Press **Clear All Results** and confirm test results are cleared +18. [ ] Confirm no new errors in developer console +19. [ ] Reload UI and confirm test results are cleared +20. [ ] Press `Ctrl+C` in terminal and confirm application shuts down with no errors + +## 3. Release + +1. [ ] Get next semantic release version: `make version-next` +2. [ ] Run [Release workflow] with next version +3. [ ] Go to [Releases page] and verify new draft release +4. [ ] Edit release and check **Create a discussion for this release** and press the **Publish release** button + +[Release workflow]: https://github.com/michenriksen/gokiburi/actions/workflows/release.yml +[Releases page]: https://github.com/michenriksen/gokiburi/releases diff --git a/docs/THANKS.md b/docs/THANKS.md new file mode 100644 index 0000000..0f6d197 --- /dev/null +++ b/docs/THANKS.md @@ -0,0 +1,17 @@ +# Thanks :bow: + +Here is a list of individuals and projects, in no specific order, that I'd like to express my gratitude to for making the development of Gokiburi a smooth process: + +- https://github.com/fsnotify/fsnotify file system monitoring +- https://github.com/spf13/cobra CLI framework +- https://github.com/invopop/validation data validation +- https://github.com/charmbracelet/log CLI logging +- https://github.com/stretchr/testify unit testing toolkit +- https://github.com/labstack/echo web server framework +- https://kit.svelte.dev/ frontend framework +- https://www.skeleton.dev/ UI toolkit +- https://tailwindcss.com/ CSS framework +- https://github.com/Templarian/MaterialDesign-JS UI icons +- https://howlerjs.com/ web audio library +- https://freesound.org/people/Eponn/packs/35313/ notification sounds +- https://github.com/smartystreets/goconvey/ inspiration for Gokiburi diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8452606 --- /dev/null +++ b/go.mod @@ -0,0 +1,52 @@ +module github.com/michenriksen/gokiburi + +go 1.19 + +require ( + github.com/aidarkhanov/nanoid/v2 v2.0.5 + github.com/charmbracelet/lipgloss v0.7.1 + github.com/charmbracelet/log v0.2.1 + github.com/dustin/go-humanize v1.0.1 + github.com/fsnotify/fsnotify v1.6.0 + github.com/invopop/validation v0.3.0 + github.com/labstack/echo/v4 v4.10.2 + github.com/spf13/cobra v1.7.0 + github.com/spf13/viper v1.15.0 + github.com/stretchr/testify v1.8.2 + golang.org/x/net v0.9.0 + golang.org/x/tools v0.8.0 +) + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/labstack/gommon v0.4.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.1 // indirect + github.com/pelletier/go-toml/v2 v2.0.6 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/spf13/afero v1.9.3 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.4.2 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/crypto v0.6.0 // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/text v0.9.0 // indirect + golang.org/x/time v0.3.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cc3a857 --- /dev/null +++ b/go.sum @@ -0,0 +1,544 @@ +cloud.google.com/go v0.26.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.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/aidarkhanov/nanoid/v2 v2.0.5 h1:HLx5RyDuvOZ6YxlhYTxSU8Il+q7xVKmXM62MfSxziN0= +github.com/aidarkhanov/nanoid/v2 v2.0.5/go.mod h1:YF/U48D1yA3AoGGUdRrCV95J/KJBShvR9TyLqQwdtlI= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= +github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= +github.com/charmbracelet/log v0.2.1 h1:1z7jpkk4yKyjwlmKmKMM5qnEDSpV32E7XtWhuv0mTZE= +github.com/charmbracelet/log v0.2.1/go.mod h1:GwFfjewhcVDWLrpAbY5A0Hin9YOlEn40eWT4PNaxFT4= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +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/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +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.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/validation v0.3.0 h1:o260kbjXzoBO/ypXDSSrCLL7SxEFUXBsX09YTE9AxZw= +github.com/invopop/validation v0.3.0/go.mod h1:qIBG6APYLp2Wu3/96p3idYjP8ffTKVmQBfKiZbw0Hts= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pty v1.1.1/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/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M= +github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k= +github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= +github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +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.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.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +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.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= +github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= +github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= +github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= +github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= +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/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/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.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +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-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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/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-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/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-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= +golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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= +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= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/gokiburi/app.go b/internal/gokiburi/app.go new file mode 100644 index 0000000..850f28c --- /dev/null +++ b/internal/gokiburi/app.go @@ -0,0 +1,293 @@ +package gokiburi + +import ( + "context" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "time" + + "github.com/charmbracelet/log" + + "github.com/michenriksen/gokiburi/internal/pkg/config" + "github.com/michenriksen/gokiburi/internal/pkg/coverparser" + "github.com/michenriksen/gokiburi/internal/pkg/runner" + "github.com/michenriksen/gokiburi/internal/pkg/server" + "github.com/michenriksen/gokiburi/internal/pkg/state" + "github.com/michenriksen/gokiburi/internal/pkg/watcher" +) + +const ( + logKeyPath = "path" + logKeyOp = "op" + logKeyErr = "error" +) + +// Option configures an [App]. +type Option func(*App) + +// App for gokiburi. +// +// Acts as the central point of the application. +type App struct { + ctx context.Context + ctxCancel context.CancelFunc + state state.State + dir string + config *config.Config + stdout io.Writer + stderr io.Writer + stdin io.Reader + logger *log.Logger + watcher *watcher.Watcher + runner *runner.Runner + server *server.Server + parser *coverparser.Parser +} + +// New App returns a new app. +// +// Call [App.Init] to further initialize the application. +func New(opts ...Option) *App { + ctx, cancel := context.WithCancel(context.Background()) + + app := &App{ + ctx: ctx, + ctxCancel: cancel, + state: state.Init, + stdout: os.Stdout, + stderr: os.Stderr, + stdin: os.Stdin, + } + + for _, opt := range opts { + opt(app) + } + + return app +} + +// Init application with configuration, root directory, and initializer functions. +func (a *App) Init(cfg *config.Config, dir string, inits ...Initializer) error { + a.config = cfg + a.dir = dir + + for _, initFunc := range inits { + if err := initFunc(a); err != nil { + return fmt.Errorf("initializing app: %w", err) + } + } + + return nil +} + +func (a *App) Run() { + a.logger.Info("starting gokiburi...", "version", Version()) + a.logger.Debug("debugging is enabled") + + a.watcher = watcher.New(a.ctx, a.dir, + watcher.WithLogger(a.logger.WithPrefix("watcher")), + watcher.WithSkipPaths(a.config.SkipPaths...), + ) + a.runner = runner.New(a.ctx, a.dir, + runner.WithLogger(a.logger.WithPrefix("runner")), + runner.WithCovermode(a.config.Covermode), + runner.WithShuffle(a.config.Shuffle), + runner.WithRaceDetection(a.config.RaceDetection), + runner.WithShort(a.config.Short), + runner.WithTimeout(a.config.Timeout), + ) + a.server = server.New(a.ctx, a.dir, + server.WithLogger(a.logger.WithPrefix("server")), + ) + a.parser = coverparser.New(a.ctx, a.dir, + coverparser.WithLogger(a.logger.WithPrefix("coverparser")), + ) + + a.startServer() + a.startWatcher() + a.setState(state.Ready) + + for { + select { + case cmd, ok := <-a.server.Commands: + if !ok { + continue + } + + a.handleCommand(cmd) + case event, ok := <-a.watcher.Events: + if !ok { + continue + } + + a.handleEvent(event) + } + } +} + +func (a *App) Cancel() { + a.stdout.Write([]byte("\r")) //nolint:errcheck // we don't care if this write fails. + a.logger.Warn("caught interrupt; shutting down...") + a.setState(state.Closing) + + a.ctxCancel() + a.server.Close() + + time.Sleep(2 * time.Second) +} + +func (a *App) handleCommand(cmd *server.Command) { + switch cmd.Instruction { + case server.Pause: + a.logger.Info("pausing automatic test runs") + a.setState(state.Paused) + case server.Resume: + a.logger.Info("resuming automatic test runs") + a.setState(state.Ready) + case server.RunTests: + a.runTests(cmd.Data) + } +} + +func (a *App) handleEvent(event *watcher.EventBatch) { + for path, op := range event.Events { + a.logger.Debug("watcher event", logKeyPath, path, logKeyOp, op) + } + + pkgMap := make(map[string]struct{}) + + for _, path := range event.Paths() { + if _, ok := pkgMap[path]; ok { + continue + } + + pkg, err := a.runner.PackageForFile(path) + if err != nil { + a.logger.Error("error determining package for file", logKeyErr, err) + continue + } + + pkgMap[pkg] = struct{}{} + } + + pkgs := make([]string, 0, len(pkgMap)) + + for pkg := range pkgMap { + pkgs = append(pkgs, pkg) + } + + a.runTests(pkgs...) +} + +func (a *App) setState(s state.State) { + a.state = s + a.server.SetState(s) +} + +func (a *App) startServer() { + go func() { + if err := a.server.Serve(a.config.ListenAddress, a.config.ListenPort); err != nil { + if a.state != state.Closing { + a.logger.Fatal("server error", logKeyErr, err) + } + } + }() +} + +func (a *App) startWatcher() { + if err := a.watcher.Watch(); err != nil { + a.logger.Fatal("error starting watcher", logKeyErr, err) + } +} + +func (a *App) runTests(pkgs ...string) { //nolint:revive // logic is easy enough to follow. + if a.state != state.Ready { + a.logger.Debug("skipping test run when state is not ready", "state", a.state) + return + } + + go func() { + a.setState(state.Running) + defer a.setState(state.Ready) + + result, err := a.runner.Run(pkgs...) + if err != nil { + a.logErrorAndNotify(err, "test runner failed with error") + + return + } + + if result.Error != "" { + a.logger.Info("skipping coverage report parsing for result with error") + a.addResult(result) + + if err = result.Close(); err != nil { + a.logger.Error("error closing test result", logKeyErr, err) + } + + return + } + + if result.Tests == 0 { + a.logger.Info("skipping coverage report parsing for result with no tests") + a.addResult(result) + + if err = result.Close(); err != nil { + a.logger.Error("error closing test result", logKeyErr, err) + } + + return + } + + rdir := result.Dir() + + f, err := os.Open(filepath.Join(rdir, "coverprofile.out")) + if err != nil { + a.logErrorAndNotify(err, "failed to open coverage profile for test result") + + return + } + + defer f.Close() + + report, err := a.parser.Parse(f) + if err != nil { + a.logErrorAndNotify(err, "coverage profile parser failed with error") + + return + } + + data, err := json.Marshal(report) + if err != nil { + a.logErrorAndNotify(err, "failed to encode coverage report to JSON") + + return + } + + if err := os.WriteFile(filepath.Join(rdir, "report.json"), data, 0o600); err != nil { + a.logErrorAndNotify(err, "failed writing coverage report to file") + + return + } + + a.addResult(result) + }() +} + +func (a *App) addResult(r *runner.Result) { + if r == nil { + return + } + + a.server.AddResult(r) +} + +func (a *App) logErrorAndNotify(err error, format string, args ...any) { + msg := fmt.Sprintf(format, args...) + + a.logger.Error(msg, logKeyErr, err) + a.server.SendNotification("error", msg) +} diff --git a/internal/gokiburi/inits.go b/internal/gokiburi/inits.go new file mode 100644 index 0000000..ae791d7 --- /dev/null +++ b/internal/gokiburi/inits.go @@ -0,0 +1,39 @@ +package gokiburi + +import "github.com/charmbracelet/log" + +// Initializer initializes part of an [App]. +type Initializer func(*App) error + +// InitLogger for [App]. +// +// Initializes the app logger configured according to the app configuration. +func InitLogger(app *App) error { + log.InfoLevelStyle = logInfoLevelStyle + log.WarnLevelStyle = logWarnLevelStyle + log.ErrorLevelStyle = logErrorLevelStyle + log.FatalLevelStyle = logFatalLevelStyle + log.KeyStyle = logKeyStyle + log.PrefixStyle = logPrefixStyle + + level := log.InfoLevel + + if app.config.Debug { + level = log.DebugLevel + } else if app.config.Quiet { + level = log.WarnLevel + } + + app.logger = log.NewWithOptions(app.stdout, log.Options{ + Level: level, + ReportCaller: app.config.Debug, + ReportTimestamp: true, + Prefix: "gokiburi", + }) + + if app.config.JSON { + app.logger.SetFormatter(log.JSONFormatter) + } + + return nil +} diff --git a/internal/gokiburi/styles.go b/internal/gokiburi/styles.go new file mode 100644 index 0000000..8887da5 --- /dev/null +++ b/internal/gokiburi/styles.go @@ -0,0 +1,28 @@ +package gokiburi + +import ( + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/log" +) + +var ( + logInfoLevelStyle = log.InfoLevelStyle.Copy(). + Foreground(lipgloss.AdaptiveColor{Light: "#7D79F6", Dark: "#514DC1"}) + + logWarnLevelStyle = log.WarnLevelStyle.Copy(). + Foreground(lipgloss.AdaptiveColor{Light: "#FF8700", Dark: "#FFAF00"}) + + logErrorLevelStyle = log.ErrorLevelStyle.Copy(). + Foreground(lipgloss.AdaptiveColor{Light: "#FF6F91", Dark: "#C74665"}) + + logFatalLevelStyle = log.FatalLevelStyle.Copy(). + Foreground(lipgloss.AdaptiveColor{Light: "#FF4672", Dark: "#ED567A"}) + + logKeyStyle = log.KeyStyle.Copy(). + Foreground(lipgloss.AdaptiveColor{Light: "#FFFDF5", Dark: "#FFFDF5"}). + Bold(false) + + logPrefixStyle = log.PrefixStyle.Copy(). + Foreground(lipgloss.AdaptiveColor{Light: "#FFFDF5", Dark: "#FFFDF5"}). + Bold(true) +) diff --git a/internal/gokiburi/version.go b/internal/gokiburi/version.go new file mode 100644 index 0000000..ad5bcf8 --- /dev/null +++ b/internal/gokiburi/version.go @@ -0,0 +1,74 @@ +package gokiburi + +import ( + "fmt" + "runtime" + "time" +) + +const versionTemplate = `{{with .Name}}{{printf "%%s:" .}}{{end}} + Version: {{printf "%%s" .Version}} + Go Version: %s + Git Commit: %s + Released: %s + OS/Arch: %s +` + +// Build information set by the compiler. +var ( + buildVersion = "" + buildTime = "" + buildCommit = "" + buildGoVersion = "" + buildGoOSArch = "" +) + +// Version of the application. +// +// Returns `0.0.0-dev` if no version is set. +func Version() string { + if buildVersion == "" { + return "0.0.0-dev" + } + + return buildVersion +} + +// VersionTemplate for the Cobra CLI framework. +func VersionTemplate() string { + return fmt.Sprintf(versionTemplate, + BuildGoVersion(), BuildCommit(), BuildTime(), BuildGoOSArch(), + ) +} + +func BuildTime() string { + if buildTime == "" { + return time.Now().UTC().Format(time.RFC3339) + } + + return buildTime +} + +func BuildCommit() string { + if buildCommit == "" { + return "HEAD" + } + + return buildCommit +} + +func BuildGoVersion() string { + if buildGoVersion == "" { + return runtime.Version() + } + + return buildGoVersion +} + +func BuildGoOSArch() string { + if buildGoOSArch == "" { + return runtime.GOOS + "/" + runtime.GOARCH + } + + return buildGoOSArch +} diff --git a/internal/pkg/command/runner.go b/internal/pkg/command/runner.go new file mode 100644 index 0000000..15f1e1a --- /dev/null +++ b/internal/pkg/command/runner.go @@ -0,0 +1,40 @@ +package command + +import ( + "context" + "errors" + "os/exec" +) + +// Runner function runs an external command. +// +// Command is executed in a content-aware fashion and with `dir` set as its +// working directory. +// +// Returns the combined output from stdout and stderr, exit code, or error +// if execution fails. +// +// If exit code indicates failure (anything other than 0), it is not treated +// as an execution error, and returned error will be nil. +type Runner func(ctx context.Context, dir, name string, args ...string) (out []byte, exitCode int, err error) + +// DefaultRunner for running an external command. +// +// Uses the `os/exec` package to run the command. +var DefaultRunner = func(ctx context.Context, dir, name string, args ...string) (out []byte, exitCode int, err error) { + cmd := exec.CommandContext(ctx, name, args...) //#nosec:G204 // no security risk. + cmd.Dir = dir + + out, err = cmd.CombinedOutput() + if err != nil { + var exitError *exec.ExitError + + if errors.As(err, &exitError) { + return out, cmd.ProcessState.ExitCode(), nil + } + + return out, cmd.ProcessState.ExitCode(), err + } + + return out, cmd.ProcessState.ExitCode(), nil +} diff --git a/internal/pkg/command/runner_test.go b/internal/pkg/command/runner_test.go new file mode 100644 index 0000000..981b8ee --- /dev/null +++ b/internal/pkg/command/runner_test.go @@ -0,0 +1,32 @@ +package command_test + +import ( + "context" + "testing" + + "github.com/michenriksen/gokiburi/internal/pkg/command" + + "github.com/stretchr/testify/require" +) + +func TestDefaultRunner(t *testing.T) { + out, exitCode, err := command.DefaultRunner(context.Background(), ".", "echo", "Hello, World!") + + require.NoError(t, err) + require.Equal(t, "Hello, World!\n", string(out)) + require.Equal(t, 0, exitCode) +} + +func TestDefaultRunner_ExitCode(t *testing.T) { + _, exitCode, err := command.DefaultRunner(context.Background(), ".", "false") + + require.NoError(t, err) + require.Equal(t, 1, exitCode) +} + +func TestDefaultRunner_NotFound(t *testing.T) { + _, exitCode, err := command.DefaultRunner(context.Background(), ".", "5tgh52s7g7o") + + require.Error(t, err) + require.Equal(t, -1, exitCode) +} diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go new file mode 100644 index 0000000..a0e6fcb --- /dev/null +++ b/internal/pkg/config/config.go @@ -0,0 +1,150 @@ +package config + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/invopop/validation" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +const ( + configName = "gokiburi" + envPrefix = "gokiburi" + envVPDebug = "GOKIBURI_DEBUG_VIPER" +) + +var shuffleRegexp = regexp.MustCompile(`^(on|off|\d+)$`) + +// Config for the application. +type Config struct { + CfgFile string + UseViper bool `mapstructure:"viper"` + Debug bool + JSON bool + Quiet bool + Shuffle string + Covermode string + RaceDetection bool `mapstructure:"race"` + Short bool + Timeout time.Duration + Run string + Skip string + SkipPaths []string `mapstructure:"skip-paths"` + ListenAddress string `mapstructure:"listen-address"` + ListenPort int `mapstructure:"listen-port"` +} + +// Validate configuration. +func (c Config) Validate() error { + return validation.ValidateStruct(&c, + validation.Field(&c.Shuffle, validation.Match(shuffleRegexp). + Error("must be on, off, or a number")), + validation.Field(&c.Covermode, validation.In("set", "count", "atomic"). + Error("must be one of set, count, or atomic")), + validation.Field(&c.ListenPort, validation.Min(0), validation.Max(65535)), + ) +} + +// Load configuration with flag values from command. +// +// Looks for viper configuration files at the current location, in order: +// +// - Current working directory +// - `gokiburi` directory inside directory returned by [os.UserConfigDir] +// - /etc/gokiburi/ +// +// the configuration file must be named `gokiburi` and have an extension +// supported by Viper, e.g. `yml`, `toml`, or `json`. +// +// If a command line flag is set, it will override the value will take +// precedence over the configuration file value. +func Load(cmd *cobra.Command) (*Config, error) { + cfg := &Config{} + + if err := cfg.Load(cmd); err != nil { + return nil, fmt.Errorf("loading configuration: %w", err) + } + + if err := cfg.Validate(); err != nil { + return nil, fmt.Errorf("invalid configuration: %w", err) + } + + return cfg, nil +} + +// Load configuration with flag values from command. +// +// Looks for viper configuration files at the current location, in order: +// +// - Current working directory +// - `gokiburi` directory inside directory returned by [os.UserConfigDir] +// - /etc/gokiburi/ +// +// the configuration file must be named `gokiburi` and have an extension +// supported by Viper, e.g. `yml`, `toml`, or `json`. +// +// If a command line flag is set, the value will take precedence over the +// configuration file value. +func (c *Config) Load(cmd *cobra.Command) error { + v := viper.New() + + if err := v.BindPFlag("useViper", cmd.Flag("viper")); err != nil { + return fmt.Errorf("binding viper flag: %w", err) + } + + if err := v.BindPFlags(cmd.PersistentFlags()); err != nil { + return fmt.Errorf("binding persistent command flags: %w", err) + } + + if err := v.BindPFlags(cmd.Flags()); err != nil { + return fmt.Errorf("binding command flags: %w", err) + } + + if err := c.readInConfig(v); err != nil { + return err + } + + c.CfgFile = v.ConfigFileUsed() + + if _, ok := os.LookupEnv(envVPDebug); ok { + v.Debug() + } + + if err := v.Unmarshal(c); err != nil { + return fmt.Errorf("unmarshaling viper: %w", err) + } + + return nil +} + +func (*Config) readInConfig(v *viper.Viper) error { + cfgDir, err := os.UserConfigDir() + if err != nil { + return fmt.Errorf("getting user configuration directory: %w", err) + } + + v.SetConfigName(configName) + v.AddConfigPath(".") + v.AddConfigPath(filepath.Join(cfgDir, configName)) + v.AddConfigPath(filepath.Join("/etc", configName)) + + v.SetEnvPrefix(envPrefix) + v.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + + if err = v.ReadInConfig(); err != nil { + if errors.As(err, &viper.ConfigFileNotFoundError{}) { + return nil + } + + return fmt.Errorf("reading configuration file %q: %w", v.ConfigFileUsed(), err) + } + + return nil +} diff --git a/internal/pkg/config/config_test.go b/internal/pkg/config/config_test.go new file mode 100644 index 0000000..e074d45 --- /dev/null +++ b/internal/pkg/config/config_test.go @@ -0,0 +1,112 @@ +package config_test + +import ( + "testing" + "time" + + "github.com/invopop/validation" + + "github.com/michenriksen/gokiburi/internal/pkg/config" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" +) + +func TestLoad(t *testing.T) { + cmd := &cobra.Command{} + cmd.PersistentFlags().Bool("viper", false, "") + cmd.PersistentFlags().Bool("debug", true, "") + cmd.PersistentFlags().Bool("json", true, "") + cmd.PersistentFlags().Bool("quiet", true, "") + cmd.Flags().String("shuffle", "on", "") + cmd.Flags().String("covermode", "count", "") + cmd.Flags().Bool("race", true, "") + cmd.Flags().Bool("short", true, "") + cmd.Flags().Duration("timeout", 42*time.Second, "") + cmd.Flags().StringSlice("skip-paths", []string{"path1", "path2"}, "") + cmd.Flags().String("listen-address", "127.0.0.1", "") + cmd.Flags().Int("listen-port", 8080, "") + + cfg, err := config.Load(cmd) + + require.NoError(t, err) + require.True(t, cfg.Debug) + require.True(t, cfg.JSON) + require.True(t, cfg.Quiet) + require.Equal(t, "on", cfg.Shuffle) + require.Equal(t, "count", cfg.Covermode) + require.True(t, cfg.RaceDetection) + require.True(t, cfg.Short) + require.Equal(t, 42*time.Second, cfg.Timeout) + require.Equal(t, []string{"path1", "path2"}, cfg.SkipPaths) + require.Equal(t, "127.0.0.1", cfg.ListenAddress) + require.Equal(t, 8080, cfg.ListenPort) +} + +func TestLoad_NumericShuffleValue(t *testing.T) { + cmd := &cobra.Command{} + cmd.Flags().Bool("viper", false, "") + cmd.Flags().String("shuffle", "1337", "") + + cfg, err := config.Load(cmd) + + require.NoError(t, err) + require.NotNil(t, cfg) + require.Equal(t, "1337", cfg.Shuffle) +} + +func TestLoad_InvalidShuffleValue(t *testing.T) { + cmd := &cobra.Command{} + cmd.Flags().Bool("viper", false, "") + cmd.Flags().String("shuffle", "wut", "") + + cfg, err := config.Load(cmd) + + var errs validation.Errors + require.ErrorAs(t, err, &errs) + + require.Equal(t, "Shuffle: must be on, off, or a number.", errs.Error()) + require.Nil(t, cfg) +} + +func TestLoad_InvalidCovermodeValue(t *testing.T) { + cmd := &cobra.Command{} + cmd.Flags().Bool("viper", false, "") + cmd.Flags().String("covermode", "lol", "") + + cfg, err := config.Load(cmd) + + var errs validation.Errors + require.ErrorAs(t, err, &errs) + + require.Equal(t, "Covermode: must be one of set, count, or atomic.", errs.Error()) + require.Nil(t, cfg) +} + +func TestLoad_ListenPortLessThanZero(t *testing.T) { + cmd := &cobra.Command{} + cmd.Flags().Bool("viper", false, "") + cmd.Flags().String("listen-port", "-1", "") + + cfg, err := config.Load(cmd) + + var errs validation.Errors + require.ErrorAs(t, err, &errs) + + require.Equal(t, "ListenPort: must be no less than 0.", errs.Error()) + require.Nil(t, cfg) +} + +func TestLoad_ListenPortTooHigh(t *testing.T) { + cmd := &cobra.Command{} + cmd.Flags().Bool("viper", false, "") + cmd.Flags().String("listen-port", "65536", "") + + cfg, err := config.Load(cmd) + + var errs validation.Errors + require.ErrorAs(t, err, &errs) + + require.Equal(t, "ListenPort: must be no greater than 65535.", errs.Error()) + require.Nil(t, cfg) +} diff --git a/internal/pkg/coverparser/cover.go b/internal/pkg/coverparser/cover.go new file mode 100644 index 0000000..956be06 --- /dev/null +++ b/internal/pkg/coverparser/cover.go @@ -0,0 +1,58 @@ +package coverparser + +import ( + "time" +) + +// Mode of a coverage report. +type Mode string + +// Coverage modes supported by Go. +const ( + ModeSet Mode = "set" // does this statement run? + ModeCount Mode = "count" // how many times does this statement run? + ModeAtomic Mode = "atomic" // like count, but correct in multithreaded tests. +) + +// Report of code coverage in a test run. +// +// Holds coverage profile data and contents of tested files. +type Report struct { + Mode Mode `json:"mode"` + Profiles []Profile `json:"profiles"` + Time time.Time `json:"time"` +} + +// Profile of a single file in coverage report. +type Profile struct { + FileName string `json:"filename"` // Name of file. + Package string `json:"package"` // Package where file belongs. + Path string `json:"path"` // Absolute path to file. + Content []byte `json:"content"` // Content of file at time of testing. + Size int `json:"size"` // File size. + Coverage float64 `json:"coverage"` // Coverage percentage of file. + LineCount int `json:"lineCount"` // Number of lines in file. + Boundaries []ProfileBoundary `json:"boundaries"` // Boundaries map coverage of the content. +} + +// ProfileBoundary represents the position in a source file of the beginning +// and end of a block as reported by the coverage profile. +// +// User interfaces can use this to mark coverage of a file, e.g. by wrapping +// file content in HTML tags between start and end offsets. +type ProfileBoundary struct { + Offset int `json:"offset"` // Location as a byte offset in the content. + Start bool `json:"start"` // Is this the start of a block? + Count int `json:"count"` // Amount of times block was invoked in tests. + Norm float64 `json:"norm"` // Coverage normalized to [0..1]. + Index int `json:"index"` // Order in content. +} + +// pkg describes a single package, compatible with the JSON output from 'go list'; see 'go help list'. +type pkg struct { + ImportPath string + Dir string + Error *struct { + Err string + } +} diff --git a/internal/pkg/coverparser/parser.go b/internal/pkg/coverparser/parser.go new file mode 100644 index 0000000..2b5a81e --- /dev/null +++ b/internal/pkg/coverparser/parser.go @@ -0,0 +1,266 @@ +package coverparser + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "os" + "path" + "path/filepath" + "strings" + "time" + + "github.com/charmbracelet/log" + "golang.org/x/tools/cover" + + "github.com/michenriksen/gokiburi/internal/pkg/command" +) + +const defaultGoBin = "go" + +var ( + // ErrNoProfiles is returned by [Parser.Parse] if profile contains no data. + ErrNoProfiles = errors.New("cover profile contains no coverage data") + + // ErrNoPackages is returned by [Parser.Parse] if profile contains no package data. + ErrNoPackages = errors.New("cover profile contains no package coverage data") +) + +// Option configures a [Parser]. +type Option func(*Parser) + +// Parser parses Go test coverage profile data. +type Parser struct { + ctx context.Context + root string + logger *log.Logger + goBin string + cmdRunner command.Runner +} + +// New parser for Go test coverage profile data. +func New(ctx context.Context, rootDir string, opts ...Option) *Parser { + p := &Parser{ + ctx: ctx, + root: rootDir, + logger: log.New(io.Discard), + goBin: defaultGoBin, + cmdRunner: command.DefaultRunner, + } + + for _, opt := range opts { + opt(p) + } + + return p +} + +// Parse test coverage data from Go coverprofile data. +// +// Parses the content of a file created by the `go test -coverprofile=...` +// command and returns a report with coverage data and contents of tested files. +// +// Uses [pkg.go.dev/golang.org/x/tools/cover] under the hood to parse the +// coverprofile data. +func (p *Parser) Parse(coverprofile io.Reader) (*Report, error) { + profiles, err := cover.ParseProfilesFromReader(coverprofile) + if err != nil { + return nil, fmt.Errorf("parsing cover profile data: %w", err) + } + + if len(profiles) == 0 { + return nil, ErrNoProfiles + } + + report := &Report{ + Mode: Mode(profiles[0].Mode), + Time: time.Now(), + } + + pkgs, err := p.profilePkgs(profiles) + if err != nil { + return nil, fmt.Errorf("getting packages from profiles: %w", err) + } + + if len(pkgs) == 0 { + return nil, ErrNoPackages + } + + for _, cp := range profiles { + profile := Profile{ + FileName: cp.FileName, + Package: path.Dir(cp.FileName), + } + + profile.Path, err = p.pkgFile(pkgs, cp.FileName) + if err != nil { + return nil, fmt.Errorf("getting absolute path for %s: %w", cp.FileName, err) + } + + profile.Content, err = os.ReadFile(profile.Path) + if err != nil { + return nil, fmt.Errorf("reading file %s: %w", profile.Path, err) + } + + profile.Size = len(profile.Content) + profile.Boundaries = p.profileBoundaries(profile.Content, cp) + profile.Coverage = p.percentCovered(cp) + profile.LineCount = p.lineCount(profile.Content) + + report.Profiles = append(report.Profiles, profile) + } + + return report, nil +} + +// profilePkgs returns a map of packages in cover profiles. +// +// Adapted from `findPkgs` function in go `src/cmd/cover/func.go:180` at +// commit `d922c0a`. +func (p *Parser) profilePkgs(profiles []*cover.Profile) (map[string]*pkg, error) { + pkgs := make(map[string]*pkg) + + var list []string + + for _, profile := range profiles { + if strings.HasPrefix(profile.FileName, ".") || filepath.IsAbs(profile.FileName) { + // Relative or absolute path. + continue + } + + pkg := path.Dir(profile.FileName) + + if _, ok := pkgs[pkg]; !ok { + pkgs[pkg] = nil + + list = append(list, pkg) + } + } + + if len(list) == 0 { + return pkgs, nil + } + + args := append([]string{"list", "-e", "-json"}, list...) + + out, exitCode, err := p.cmdRunner(p.ctx, p.root, p.goBin, args...) + if err != nil { + return nil, fmt.Errorf("running go list command: %w", err) + } + + if exitCode != 0 { + return nil, fmt.Errorf("non-zero exit code from go list command: %d", exitCode) + } + + dec := json.NewDecoder(bytes.NewReader(out)) + + for { + var pkg pkg + + err := dec.Decode(&pkg) + if err == io.EOF { + break + } + + if err != nil { + return nil, fmt.Errorf("decoding go list json: %w", err) + } + + pkgs[pkg.ImportPath] = &pkg + } + + return pkgs, nil +} + +// pkgFile returns absolute path to a file inside a package. +// +// Adapted from `findFile` function in go `src/cmd/cover/func.go:226` at +// commit `d922c0a`. +func (*Parser) pkgFile(pkgs map[string]*pkg, name string) (string, error) { + if strings.HasPrefix(name, ".") || filepath.IsAbs(name) { + // Relative or absolute path. + return name, nil + } + + pkg := pkgs[path.Dir(name)] + + if pkg != nil { + if pkg.Dir != "" { + return filepath.Join(pkg.Dir, path.Base(name)), nil + } + + if pkg.Error != nil { + return "", errors.New(pkg.Error.Err) + } + } + + return "", fmt.Errorf("no package for %s in go list output", name) +} + +// profileBoundaries converts [cover.Boundary] structs in a profile to +// [ProfileBoundary] structs from this package. +// +// This is done to control how a report is marshalled to JSON. +func (*Parser) profileBoundaries(src []byte, profile *cover.Profile) []ProfileBoundary { + cbs := profile.Boundaries(src) + boundaries := make([]ProfileBoundary, len(cbs)) + + for i, cb := range cbs { + boundaries[i] = ProfileBoundary{ + Offset: cb.Offset, + Start: cb.Start, + Count: cb.Count, + Norm: cb.Norm, + Index: cb.Index, + } + } + + return boundaries +} + +// percentCovered calculates the file coverage percentage from a profile. +func (*Parser) percentCovered(profile *cover.Profile) float64 { + var total, covered int64 + + for _, b := range profile.Blocks { + total += int64(b.NumStmt) + + if b.Count > 0 { + covered += int64(b.NumStmt) + } + } + + if total == 0 { + return 0 + } + + percent := float64(covered) / float64(total) * 100 + ratio := math.Pow(10, float64(1)) + + return math.Round(percent*ratio) / ratio +} + +// lineCount returns the number of lines in a byte slice. +func (*Parser) lineCount(b []byte) int { + return bytes.Count(b, []byte{'\n'}) + 1 +} + +// WithLogger configures [Parser] with a logger. +func WithLogger(logger *log.Logger) Option { + return func(p *Parser) { + p.logger = logger + } +} + +// WithGoBinPath configures [Parser] to use path as go binary for commands. +// +// By default, `go` is used. +func WithGoBinPath(goBinPath string) Option { + return func(p *Parser) { + p.goBin = goBinPath + } +} diff --git a/internal/pkg/coverparser/parser_test.go b/internal/pkg/coverparser/parser_test.go new file mode 100644 index 0000000..7dbedd1 --- /dev/null +++ b/internal/pkg/coverparser/parser_test.go @@ -0,0 +1,103 @@ +package coverparser_test + +import ( + "context" + "math" + "os" + "path" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/michenriksen/gokiburi/internal/pkg/coverparser" + "github.com/michenriksen/gokiburi/internal/pkg/util/testutil" +) + +func TestParser_Parse(t *testing.T) { + f := testutil.OpenFile(t, "testdata", "numbers", "coverprofile.out") + defer f.Close() + + parser := coverparser.New(context.Background(), "./testdata") + report, err := parser.Parse(f) + require.NoError(t, err) + + expectedContent := testutil.ReadFile(t, "testdata", "numbers", "numbers.go") + + require.Equal(t, coverparser.ModeAtomic, report.Mode) + require.NotZero(t, report.Time) + require.Len(t, report.Profiles, 1) + + profile := report.Profiles[0] + require.True(t, strings.HasSuffix(profile.FileName, "testdata/numbers/numbers.go")) + require.True(t, strings.HasSuffix(profile.Path, "testdata/numbers/numbers.go")) + require.True(t, strings.HasSuffix(profile.Package, "testdata/numbers")) + require.Equal(t, float64(100), profile.Coverage) + require.Equal(t, 11, profile.LineCount) + require.Equal(t, len(expectedContent), profile.Size) + require.Equal(t, expectedContent, profile.Content) + require.Len(t, profile.Boundaries, 6) // 3 pairs of start/end boundaries. + + bb := profile.Boundaries + + // First boundary starts at opening `{` for `IntMin` function. + require.Equal(t, 0, bb[0].Index) + require.True(t, bb[0].Start) + require.Equal(t, 156, bb[0].Offset) + require.Equal(t, 5, bb[0].Count) + require.Equal(t, 1.0, bb[0].Norm) + + // First boundary ends at opening `{` for `if a < b` condition. + require.Equal(t, 1, bb[1].Index) + require.False(t, bb[1].Start) + require.Equal(t, 168, bb[1].Offset) + require.Equal(t, 0, bb[1].Count) + require.Equal(t, 0.0, bb[1].Norm) + + // Second boundary starts at opening `{` for `if a < b` condition. + require.Equal(t, 2, bb[2].Index) + require.True(t, bb[2].Start) + require.Equal(t, 168, bb[2].Offset) + require.Equal(t, 2, bb[2].Count) + require.Equal(t, 0.43, math.Round(bb[2].Norm*100)/100) + + // Second boundary ends after `return a` statement. + require.Equal(t, 3, bb[3].Index) + require.False(t, bb[3].Start) + require.Equal(t, 183, bb[3].Offset) + require.Equal(t, 0, bb[3].Count) + require.Equal(t, 0.0, bb[3].Norm) + + // Third boundary starts closing `}` for `if a < b` condition. + require.Equal(t, 4, bb[4].Index) + require.True(t, bb[4].Start) + require.Equal(t, 185, bb[4].Offset) + require.Equal(t, 3, bb[4].Count) + require.Equal(t, 0.68, math.Round(bb[4].Norm*100)/100) + + // Third boundary ends after `return b` statement. + require.Equal(t, 5, bb[5].Index) + require.False(t, bb[5].Start) + require.Equal(t, 193, bb[5].Offset) + require.Equal(t, 0, bb[5].Count) + require.Equal(t, 0.0, bb[5].Norm) +} + +func TestParser_Parse_BadCoverprofile(t *testing.T) { + f, err := os.Open(path.Join("testdata", "coverprofile.out.bad")) + require.NoError(t, err) + + parser := coverparser.New(context.Background(), "./testdata") + report, err := parser.Parse(f) + + require.ErrorContains(t, err, "parsing cover profile data:") + require.Nil(t, report) +} + +func TestParser_Parse_NoProfiles(t *testing.T) { + parser := coverparser.New(context.Background(), "./testdata") + report, err := parser.Parse(strings.NewReader("mode: atomic\n")) + + require.ErrorIs(t, err, coverparser.ErrNoProfiles) + require.Nil(t, report) +} diff --git a/internal/pkg/coverparser/testdata/coverprofile.out.bad b/internal/pkg/coverparser/testdata/coverprofile.out.bad new file mode 100644 index 0000000..8b1592b --- /dev/null +++ b/internal/pkg/coverparser/testdata/coverprofile.out.bad @@ -0,0 +1,3 @@ +mode: atomic +github.com/michenriksen/gokiburi/internal/pkg/coverparser/testdata/main.go:Oops,what am I doing here!?,6.11 1 5 + diff --git a/internal/pkg/coverparser/testdata/numbers/coverprofile.out b/internal/pkg/coverparser/testdata/numbers/coverprofile.out new file mode 100644 index 0000000..3a9f7d4 --- /dev/null +++ b/internal/pkg/coverparser/testdata/numbers/coverprofile.out @@ -0,0 +1,4 @@ +mode: atomic +github.com/michenriksen/gokiburi/internal/pkg/coverparser/testdata/numbers/numbers.go:5.27,6.11 1 5 +github.com/michenriksen/gokiburi/internal/pkg/coverparser/testdata/numbers/numbers.go:6.11,8.3 1 2 +github.com/michenriksen/gokiburi/internal/pkg/coverparser/testdata/numbers/numbers.go:9.2,9.10 1 3 diff --git a/internal/pkg/coverparser/testdata/numbers/numbers.go b/internal/pkg/coverparser/testdata/numbers/numbers.go new file mode 100644 index 0000000..72127e6 --- /dev/null +++ b/internal/pkg/coverparser/testdata/numbers/numbers.go @@ -0,0 +1,10 @@ +// Package numbers is a test package for the coverparser package. +package numbers + +// IntMin returns the minimum of two integers. +func IntMin(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/internal/pkg/coverparser/testdata/numbers/numbers_test.go b/internal/pkg/coverparser/testdata/numbers/numbers_test.go new file mode 100644 index 0000000..17f94c8 --- /dev/null +++ b/internal/pkg/coverparser/testdata/numbers/numbers_test.go @@ -0,0 +1,29 @@ +package numbers + +import ( + "fmt" + "testing" +) + +func TestIntMin(t *testing.T) { + tests := []struct { + a, b int + want int + }{ + {0, 1, 0}, + {1, 0, 0}, + {2, -2, -2}, + {0, -1, -1}, + {-1, 0, -1}, + } + + for _, tt := range tests { + testname := fmt.Sprintf("%d,%d", tt.a, tt.b) + t.Run(testname, func(t *testing.T) { + ans := IntMin(tt.a, tt.b) + if ans != tt.want { + t.Errorf("got %d, want %d", ans, tt.want) + } + }) + } +} diff --git a/internal/pkg/runner/parser.go b/internal/pkg/runner/parser.go new file mode 100644 index 0000000..60abb47 --- /dev/null +++ b/internal/pkg/runner/parser.go @@ -0,0 +1,209 @@ +package runner + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "regexp" + "strconv" + "strings" + "time" + + "github.com/charmbracelet/log" +) + +const floatBitSize = 64 + +var percentageRegexp = regexp.MustCompile(`(\d+(?:\.\d+)?)%`) + +// Package is the result of tests in a single package. +type Package struct { + Time time.Time `json:"time"` + Name string `json:"name"` + Pass bool `json:"pass"` + Passed int `json:"passed"` + Skipped int `json:"skipped"` + Failed int `json:"failed"` + Coverage float64 `json:"coverage"` + Elapsed float64 `json:"elapsed"` + Tests []*Test `json:"tests"` + testMap map[string]*Test +} + +// Test result of a single test. +type Test struct { + Time time.Time `json:"time"` + Name string `json:"name"` + Package string `json:"package"` + Pass bool `json:"pass"` + Skip bool `json:"skip"` + Elapsed float64 `json:"elapsed"` + Output string `json:"output"` +} + +type parser struct { + logger *log.Logger +} + +func (p *parser) parse(out []byte) ([]*Package, error) { + pkgMap := make(map[string]*Package) + lineNum := 0 + scanner := bufio.NewScanner(bytes.NewReader(out)) + scanner.Split(bufio.ScanLines) + + for scanner.Scan() { + lineNum++ + + line := scanner.Bytes() + event := goTestEvent{} + + if err := json.Unmarshal(line, &event); err != nil { + p.logger.Error("error decoding line as Go test event", logKeyErr, err, logKeyLineNum, lineNum) + p.logger.Debug(string(line), logKeyLineNum, lineNum) + + continue + } + + if event.Package == "" { + p.logger.Debug("ignoring non-test related event", "event", event) + + continue + } + + p.ensurePkgAndTest(event, pkgMap) + + if err := p.handleEvent(event, pkgMap); err != nil { + p.logger.Error("error handling event", logKeyErr, err, logKeyLineNum, lineNum) + p.logger.Debug(string(line), logKeyLineNum, lineNum) + } + } + + return p.makePkgSlice(pkgMap), nil +} + +func (p *parser) ensurePkgAndTest(e goTestEvent, pkgMap map[string]*Package) { + if _, ok := pkgMap[e.Package]; !ok { + p.logger.Debug("new package", "pkg", e.Package) + + pkgMap[e.Package] = &Package{ + Time: e.Time, + Name: e.Package, + testMap: make(map[string]*Test), + } + } + + if e.Test != "" { + if _, ok := pkgMap[e.Package].testMap[e.Test]; !ok { + p.logger.Debug("new test", "pkg", e.Package, "test", e.Test) + + pkgMap[e.Package].testMap[e.Test] = &Test{ + Name: e.Test, + Package: e.Package, + Time: e.Time, + } + } + } +} + +func (*parser) makePkgSlice(pkgMap map[string]*Package) []*Package { + pkgs := make([]*Package, 0, len(pkgMap)) + + for _, pkg := range pkgMap { + tests := make([]*Test, 0, len(pkg.testMap)) + + for _, test := range pkg.testMap { + tests = append(tests, test) + } + + pkg.Tests = tests + pkg.testMap = nil + + pkgs = append(pkgs, pkg) + } + + return pkgs +} + +func (p *parser) handleEvent(e goTestEvent, pkgMap map[string]*Package) error { + if e.Test == "" { + return p.handlePackageEvent(e, pkgMap) + } + + return p.handleTestEvent(e, pkgMap) +} + +func (p *parser) handlePackageEvent(e goTestEvent, pkgMap map[string]*Package) error { + pkg, ok := pkgMap[e.Package] + if !ok { + return fmt.Errorf("unknown package: %s", e.Package) + } + + switch e.Action { + case "pass": + pkg.Pass = true + pkg.Elapsed = e.Elapsed + case "fail": + pkg.Pass = false + pkg.Elapsed = e.Elapsed + case "output": + if !strings.Contains(e.Output, "coverage:") { + return nil + } + + if match := percentageRegexp.FindStringSubmatch(e.Output); len(match) == 2 { + c, err := strconv.ParseFloat(match[1], floatBitSize) + if err != nil { + return fmt.Errorf("parsing %q as float64: %w", match[1], err) + } + + pkg.Coverage = c + } + default: + p.logger.Debug("ignoring package event", "action", e.Action) + } + + return nil +} + +func (p *parser) handleTestEvent(e goTestEvent, pkgMap map[string]*Package) error { + pkg, ok := pkgMap[e.Package] + if !ok { + return fmt.Errorf("unknown package: %s", e.Package) + } + + test, ok := pkgMap[e.Package].testMap[e.Test] + if !ok { + return fmt.Errorf("unknown test for package %s: %s", e.Package, e.Test) + } + + switch e.Action { + case "pass": + test.Pass = true + test.Elapsed = e.Elapsed + pkg.Passed++ + case "fail": + test.Pass = false + test.Elapsed = e.Elapsed + pkg.Failed++ + case "skip": + test.Skip = true + test.Elapsed = e.Elapsed + pkg.Skipped++ + case "output": + test.Output += e.Output + default: + p.logger.Debug("ignoring test event", "action", e.Action) + } + + return nil +} + +type goTestEvent struct { + Time time.Time + Action string + Package string + Test string + Elapsed float64 + Output string +} diff --git a/internal/pkg/runner/runner.go b/internal/pkg/runner/runner.go new file mode 100644 index 0000000..ccdbfef --- /dev/null +++ b/internal/pkg/runner/runner.go @@ -0,0 +1,435 @@ +package runner + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/aidarkhanov/nanoid/v2" + "github.com/charmbracelet/log" + + "github.com/michenriksen/gokiburi/internal/pkg/command" +) + +const ( + logKeyLineNum = "lineNum" + logKeyErr = "error" + + defaultCovermode = "atomic" + defaultTimeout = 10 * time.Minute + defaultGoBin = "go" + + coverDirPerm = 0o755 +) + +// Result of a `go test` run. +type Result struct { + UUID string `json:"uuid"` + Error string `json:"error"` + Pass bool `json:"pass"` + Start time.Time `json:"start"` + Duration time.Duration `json:"duration"` + ExitCode int `json:"exitCode"` + Targets []string `json:"targets"` + Passed int `json:"passed"` + Failed int `json:"failed"` + Skipped int `json:"skipped"` + Tests int `json:"tests"` + Packages []*Package `json:"packages"` + dir string +} + +// Close Result. +// +// Deletes the associated coverage directory if it exists. +func (r *Result) Close() error { + if r.dir == "" { + return nil + } + + if err := os.RemoveAll(r.dir); err != nil { + return fmt.Errorf("deleting test result directory %q: %w", r.dir, err) + } + + r.dir = "" + + return nil +} + +// Dir containing files relevant for the test result. +// +// Directory contains `coverprofile.out` generated by [Runner.Run] as well +// as `report.json` which is a code coverage report generated by +// [coverparser.Parser]. +func (r Result) Dir() string { + return r.dir +} + +// UUIDFunc for generating a UUID for a [Result]. +// +// By default, [nanoid.New] is used for UUID generation, but function can be +// replaced for testing purposes. +type UUIDFunc func() (string, error) + +// Option configures a [Runner]. +type Option func(*Runner) + +// Runner runs tests and parses the results. +type Runner struct { + ctx context.Context + root string + logger *log.Logger + parser *parser + shuffle string + covermode string + raceDetection bool + short bool + timeout time.Duration + goBin string + coverDir string + coverDirOnce sync.Once + runnerFunc command.Runner + uuidFunc UUIDFunc +} + +// New Runner for running tests. +// +// Runs `go test` on directories for Go source files and parses the output for +// easy consumption. +// +// The root directory should be the root directory of a Go project with tests. +func New(ctx context.Context, rootDir string, opts ...Option) *Runner { + r := &Runner{ + ctx: ctx, + root: rootDir, + logger: log.New(io.Discard), + parser: &parser{}, + covermode: defaultCovermode, + timeout: defaultTimeout, + goBin: defaultGoBin, + runnerFunc: command.DefaultRunner, + uuidFunc: nanoid.New, + } + + for _, opt := range opts { + opt(r) + } + + r.parser.logger = r.logger + + return r +} + +// Run tests for packages. +// +// Runs `go test` on given packages and parses the results. +func (r *Runner) Run(pkgs ...string) (*Result, error) { + var ( + out []byte + err error + ) + + result := &Result{ + Pass: true, + Start: time.Now(), + } + + if result.UUID, err = r.uuidFunc(); err != nil { + return nil, fmt.Errorf("generating UUID for test run result: %w", err) + } + + if result.Targets, err = r.processTargets(pkgs); err != nil { + return nil, err + } + + if result.dir, err = r.mkdirCover(result.UUID); err != nil { + return nil, err + } + + r.logger.Info("running tests...") + + args := r.mkArgs(result) + + r.logger.Debug("command", "cmd", r.goBin, "args", strings.Join(args, " ")) + + out, result.ExitCode, err = r.runnerFunc(r.ctx, r.root, r.goBin, args...) + result.Duration = time.Since(result.Start) + + if err != nil || result.ExitCode != 0 { + if err := r.handleError(err, out); err != nil { + result.Error = err.Error() + result.Pass = false + + return result, err + } + } + + r.logger.Debug("command finished", "dur", result.Duration, "exitCode", result.ExitCode) + + result.Packages, err = r.parser.parse(out) + if err != nil { + return nil, fmt.Errorf("parsing output of go test command: %w", err) + } + + for _, pkg := range result.Packages { + for _, test := range pkg.Tests { + result.Tests++ + + switch { + case test.Pass: + result.Passed++ + case test.Skip: + result.Skipped++ + case !test.Pass: + result.Failed++ + } + } + } + + if result.Failed != 0 { + result.Pass = false + } + + r.handleResult(result) + + return result, nil +} + +func (r *Runner) PackageForFile(name string) (string, error) { + args := []string{"list", "-find", "-f", "{{.ImportPath}}", filepath.Dir(name)} + + r.logger.Debug("command", "cmd", r.goBin, "args", strings.Join(args, " ")) + + out, exitCode, err := r.runnerFunc(r.ctx, r.root, r.goBin, args...) + if err != nil { + return "", fmt.Errorf("running go list command: %w", err) + } + + if exitCode != 0 { + return "", fmt.Errorf("running go list command: exit code: %s", out) + } + + return strings.TrimSpace(string(out)), nil +} + +func (r *Runner) handleResult(result *Result) { + logger := r.logger.With( + "pass", result.Pass, + "tests", result.Tests, + "packages", len(result.Packages), + "passed", result.Passed, + "skipped", result.Skipped, + "failed", result.Failed, + "dur", result.Duration.Round(time.Millisecond), + ) + + switch { + case result.Tests == 0: + logger.Info("no tests found") + case result.Pass: + logger.Info("tests passed") + default: + logger.With("exitCode", result.ExitCode).Warn("tests failed") + } +} + +func (r *Runner) handleError(err error, out []byte) error { + var reason string + + switch { + case errors.Is(err, context.Canceled): + reason = "canceled" + case errors.Is(err, context.DeadlineExceeded): + reason = "timeout" + case errors.Is(err, exec.ErrNotFound): + reason = fmt.Sprintf("go binary %q was not found", r.goBin) + case bytes.Contains(out, []byte("panic: test timed out")): + reason = "timeout" + case bytes.Contains(out, []byte("[build failed]")): + reason = "build failed" + default: + return nil + } + + return fmt.Errorf(reason) +} + +func (r *Runner) mkdirCover(uuid string) (string, error) { + r.coverDirOnce.Do(func() { + if r.coverDir == "" { + var err error + + r.coverDir, err = os.MkdirTemp("", "gokiburi-coverage") + if err != nil { + panic(fmt.Errorf("creating temporary directory for test coverage files: %w", err)) + } + } + }) + + name := filepath.Join(r.coverDir, uuid) + + if err := os.MkdirAll(name, coverDirPerm); err != nil { + return "", fmt.Errorf("creating coverage dir %q: %w", name, err) + } + + return name, nil +} + +func (r *Runner) mkArgs(result *Result) []string { + args := []string{ + "test", "-json", "-cover", + "-coverprofile", path.Join(result.dir, "coverprofile.out"), + } + + if r.shuffle != "" { + args = append(args, "-shuffle", r.shuffle) + } + + if r.raceDetection { + args = append(args, "-race", "-covermode", "atomic") + } else { + args = append(args, "-covermode", r.covermode) + } + + if r.short { + args = append(args, "-short") + } + + if r.timeout != 0 { + args = append(args, "-timeout", r.timeout.String()) + } + + return append(args, result.Targets...) +} + +func (r *Runner) processTargets(targets []string) ([]string, error) { + targetMap := make(map[string]struct{}) + + validPkgs, err := r.validPackages() + if err != nil { + return nil, fmt.Errorf("determining valid packages: %w", err) + } + + for _, t := range targets { + if _, ok := validPkgs[t]; ok { + targetMap[t] = struct{}{} + continue + } + + r.logger.Warnf("invalid package: %q", t) + } + + tt := make([]string, 0, len(targetMap)) + + for t := range targetMap { + tt = append(tt, t) + } + + return tt, nil +} + +func (r *Runner) validPackages() (map[string]struct{}, error) { + pkgs := make(map[string]struct{}) + pkgs["./..."] = struct{}{} + + args := []string{"list", "./..."} + + r.logger.Debug("command", "cmd", r.goBin, "args", strings.Join(args, " ")) + + out, exitCode, err := r.runnerFunc(r.ctx, r.root, r.goBin, args...) + if err != nil { + return nil, fmt.Errorf("running go list command: %w", err) + } + + if exitCode != 0 { + return nil, fmt.Errorf("running go list command: exit code %d", exitCode) + } + + for _, pkg := range strings.Fields(string(out)) { + pkgs[pkg] = struct{}{} + } + + return pkgs, nil +} + +// WithLogger configures [Runner] with a logger. +func WithLogger(logger *log.Logger) Option { + return func(r *Runner) { + r.logger = logger + } +} + +// WithGoBinPath configures [Runner] to use path as go binary for running tests. +// +// By default, `go` is used. +func WithGoBinPath(goBinPath string) Option { + return func(r *Runner) { + r.goBin = goBinPath + } +} + +// WithRunnerFunc configures [Runner] to use function for running commands. +// +// By default, commands are run with the `os/exec` package, but the runner +// function can be replaced for testing purposes. +func WithRunnerFunc(rf command.Runner) Option { + return func(r *Runner) { + r.runnerFunc = rf + } +} + +// WithUUIDFunc configures [Runner] to use function for UUID generation. +// +// By default, [nanoid.New] is used for UUID generation, but function can be +// replaced for testing purposes. +func WithUUIDFunc(uf UUIDFunc) Option { + return func(r *Runner) { + r.uuidFunc = uf + } +} + +// WithShuffle configures [Runner] to run tests with `-shuffle` flag. +func WithShuffle(val string) Option { + return func(r *Runner) { + r.shuffle = val + } +} + +// WithCovermode configures [Runner] to run tests with `-covermode` flag. +func WithCovermode(val string) Option { + return func(r *Runner) { + r.covermode = val + } +} + +// WithRaceDetection configures [Runner] to run tests with `-race` flag. +// +// Note: Enabling race detection will force the covermode to be `atomic`. +func WithRaceDetection(enabled bool) Option { + return func(r *Runner) { + r.raceDetection = enabled + } +} + +// WithShort configures [Runner] to run tests with `-short` flag. +func WithShort(enabled bool) Option { + return func(r *Runner) { + r.short = enabled + } +} + +// WithTimeout configures [Runner] to use duration for test run timeout. +func WithTimeout(dur time.Duration) Option { + return func(r *Runner) { + r.timeout = dur + } +} diff --git a/internal/pkg/runner/runner_test.go b/internal/pkg/runner/runner_test.go new file mode 100644 index 0000000..6af2d99 --- /dev/null +++ b/internal/pkg/runner/runner_test.go @@ -0,0 +1,424 @@ +package runner_test + +import ( + "context" + "os" + "path" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/michenriksen/gokiburi/internal/pkg/command" + "github.com/michenriksen/gokiburi/internal/pkg/runner" + "github.com/michenriksen/gokiburi/internal/pkg/util/testutil" +) + +const testTarget = "./..." + +func TestRunner_Run(t *testing.T) { + runnerFunc := mockRunnerFunc(t, testutil.ReadFile(t, "testdata", "testoutput.json"), 1, nil) + r := runner.New(context.Background(), "./testdata", + runner.WithRunnerFunc(runnerFunc), + runner.WithUUIDFunc(mockUUIDFunc), + ) + + result, err := r.Run(testTarget) + + require.NoError(t, err) + require.NotNil(t, result) + + defer func() { + if dir := result.Dir(); dir != "" { + os.RemoveAll(dir) + } + }() + + require.Equal(t, "deadbeef", result.UUID) + require.Equal(t, []string{testTarget}, result.Targets) + require.NotZero(t, result.Duration) + require.Empty(t, result.Error) + require.False(t, result.Pass) + require.Equal(t, 3, result.Tests) + require.Equal(t, 1, result.Passed) + require.Equal(t, 1, result.Skipped) + require.Equal(t, 1, result.Failed) + require.Len(t, result.Packages, 1) + + pkg := result.Packages[0] + + require.True(t, strings.HasSuffix(pkg.Name, "testdata/numbers")) + require.False(t, pkg.Pass) + require.Equal(t, 1, pkg.Passed) + require.Equal(t, 1, pkg.Skipped) + require.Equal(t, 1, pkg.Failed) + require.Len(t, pkg.Tests, 3) + require.Equal(t, 0.29, pkg.Elapsed) + require.Equal(t, 66.7, pkg.Coverage) + + passed := testByName(t, "TestIntMinBasic", pkg.Tests) + skipped := testByName(t, "TestIntMinTableDriven", pkg.Tests) + failed := testByName(t, "TestIntMinFailing", pkg.Tests) + + require.Equal(t, pkg.Name, passed.Package) + require.True(t, passed.Pass) + require.False(t, passed.Skip) + require.Equal(t, 0.0, passed.Elapsed) + require.Equal(t, "=== RUN TestIntMinBasic\n--- PASS: TestIntMinBasic (0.00s)\n", passed.Output) + + require.Equal(t, pkg.Name, skipped.Package) + require.False(t, skipped.Pass) + require.True(t, skipped.Skip) + require.Equal(t, 0.0, skipped.Elapsed) + require.Equal(t, "=== RUN TestIntMinTableDriven\n numbers_test.go:16: skipping table driven test\n--- SKIP: TestIntMinTableDriven (0.00s)\n", skipped.Output) + + require.Equal(t, pkg.Name, failed.Package) + require.False(t, failed.Pass) + require.False(t, failed.Skip) + require.Equal(t, 0.0, failed.Elapsed) + require.Equal(t, "=== RUN TestIntMinFailing\n numbers_test.go:41: failing test\n--- FAIL: TestIntMinFailing (0.00s)\n", failed.Output) +} + +// TestRunner_Run_Integration runs the actual tests inside ./testdata/ and +// asserts that results are as expected. +func TestRunner_Run_Integration(t *testing.T) { + testutil.SetIntegrationTest(t) + + r := runner.New(context.Background(), "./testdata", + runner.WithUUIDFunc(mockUUIDFunc), + ) + + result, err := r.Run(testTarget) + + require.NoError(t, err) + require.NotNil(t, result) + + defer func() { + if dir := result.Dir(); dir != "" { + os.RemoveAll(dir) + } + }() + + require.Equal(t, "deadbeef", result.UUID) + require.Equal(t, []string{testTarget}, result.Targets) + require.NotZero(t, result.Duration) + require.Empty(t, result.Error) + require.False(t, result.Pass) + require.Equal(t, 3, result.Tests) + require.Equal(t, 1, result.Passed) + require.Equal(t, 1, result.Skipped) + require.Equal(t, 1, result.Failed) + require.Len(t, result.Packages, 1) + + pkg := result.Packages[0] + + require.True(t, strings.HasSuffix(pkg.Name, "testdata/numbers")) + require.False(t, pkg.Pass) + require.Equal(t, 1, pkg.Passed) + require.Equal(t, 1, pkg.Skipped) + require.Equal(t, 1, pkg.Failed) + require.Len(t, pkg.Tests, 3) + require.NotZero(t, pkg.Elapsed) + require.Equal(t, 66.7, pkg.Coverage) + + passed := testByName(t, "TestIntMinBasic", pkg.Tests) + skipped := testByName(t, "TestIntMinTableDriven", pkg.Tests) + failed := testByName(t, "TestIntMinFailing", pkg.Tests) + + require.Equal(t, pkg.Name, passed.Package) + require.True(t, passed.Pass) + require.False(t, passed.Skip) + require.Equal(t, "=== RUN TestIntMinBasic\n--- PASS: TestIntMinBasic (0.00s)\n", passed.Output) + + require.Equal(t, pkg.Name, skipped.Package) + require.False(t, skipped.Pass) + require.True(t, skipped.Skip) + require.Equal(t, "=== RUN TestIntMinTableDriven\n numbers_test.go:16: skipping table driven test\n--- SKIP: TestIntMinTableDriven (0.00s)\n", skipped.Output) + + require.Equal(t, pkg.Name, failed.Package) + require.False(t, failed.Pass) + require.False(t, failed.Skip) + require.Equal(t, "=== RUN TestIntMinFailing\n numbers_test.go:41: failing test\n--- FAIL: TestIntMinFailing (0.00s)\n", failed.Output) +} + +func TestRunner_Run_NoTests(t *testing.T) { + runnerFunc := mockRunnerFunc(t, testutil.ReadFile(t, "testdata", "testoutput-no-tests.json"), 1, nil) + r := runner.New(context.Background(), "./testdata", + runner.WithRunnerFunc(runnerFunc), + runner.WithUUIDFunc(mockUUIDFunc), + ) + + result, err := r.Run(testTarget) + + require.NoError(t, err) + require.NotNil(t, result) + + defer func() { + if dir := result.Dir(); dir != "" { + os.RemoveAll(dir) + } + }() + + require.Equal(t, "deadbeef", result.UUID) + require.Equal(t, []string{testTarget}, result.Targets) + require.NotZero(t, result.Duration) + require.Empty(t, result.Error) + require.True(t, result.Pass) + require.Equal(t, 0, result.Tests) + require.Equal(t, 0, result.Passed) + require.Equal(t, 0, result.Skipped) + require.Equal(t, 0, result.Failed) + require.Len(t, result.Packages, 1) + + pkg := result.Packages[0] + + require.True(t, strings.HasSuffix(pkg.Name, "testdata/numbers")) + require.True(t, pkg.Pass) + require.Equal(t, 0, pkg.Passed) + require.Equal(t, 0, pkg.Skipped) + require.Equal(t, 0, pkg.Failed) + require.Len(t, pkg.Tests, 0) + require.Equal(t, 0.0, pkg.Elapsed) + require.Equal(t, 0.0, pkg.Coverage) +} + +func TestRunner_Run_AllPass(t *testing.T) { + runnerFunc := mockRunnerFunc(t, testutil.ReadFile(t, "testdata", "testoutput-all-pass.json"), 0, nil) + r := runner.New(context.Background(), "./testdata", + runner.WithRunnerFunc(runnerFunc), + runner.WithUUIDFunc(mockUUIDFunc), + ) + + result, err := r.Run(testTarget) + + require.NoError(t, err) + require.NotNil(t, result) + + defer func() { + if dir := result.Dir(); dir != "" { + os.RemoveAll(dir) + } + }() + + require.Equal(t, "deadbeef", result.UUID) + require.Equal(t, []string{testTarget}, result.Targets) + require.NotZero(t, result.Duration) + require.Empty(t, result.Error) + require.True(t, result.Pass) + require.Equal(t, 7, result.Tests) + require.Equal(t, 7, result.Passed) + require.Equal(t, 0, result.Skipped) + require.Equal(t, 0, result.Failed) + require.Len(t, result.Packages, 1) + + pkg := result.Packages[0] + + require.True(t, strings.HasSuffix(pkg.Name, "testdata/numbers")) + require.True(t, pkg.Pass) + require.Equal(t, 7, pkg.Passed) + require.Equal(t, 0, pkg.Skipped) + require.Equal(t, 0, pkg.Failed) + require.Len(t, pkg.Tests, 7) + require.Equal(t, 0.0, pkg.Elapsed) + require.Equal(t, 100.0, pkg.Coverage) +} + +func TestRunner_Run_DataRace(t *testing.T) { + runnerFunc := mockRunnerFunc(t, testutil.ReadFile(t, "testdata", "testoutput-data-race.json"), 66, nil) + r := runner.New(context.Background(), "./testdata", + runner.WithRunnerFunc(runnerFunc), + runner.WithUUIDFunc(mockUUIDFunc), + ) + + result, err := r.Run(testTarget) + + require.NoError(t, err) + require.NotNil(t, result) + + defer func() { + if dir := result.Dir(); dir != "" { + os.RemoveAll(dir) + } + }() + + require.Equal(t, "deadbeef", result.UUID) + require.Equal(t, []string{testTarget}, result.Targets) + require.NotZero(t, result.Duration) + require.Empty(t, result.Error) + require.False(t, result.Pass) + require.Equal(t, 1, result.Tests) + require.Equal(t, 0, result.Passed) + require.Equal(t, 0, result.Skipped) + require.Equal(t, 1, result.Failed) + require.Len(t, result.Packages, 1) + + pkg := result.Packages[0] + + require.True(t, strings.HasSuffix(pkg.Name, "testdata/numbers")) + require.False(t, pkg.Pass) + require.Equal(t, 0, pkg.Passed) + require.Equal(t, 0, pkg.Skipped) + require.Equal(t, 1, pkg.Failed) + require.Len(t, pkg.Tests, 1) + require.Equal(t, 0.258, pkg.Elapsed) + require.Equal(t, 40.0, pkg.Coverage) + + failed := testByName(t, "TestCounterIncrement", pkg.Tests) + + require.Equal(t, pkg.Name, failed.Package) + require.False(t, failed.Pass) + require.False(t, failed.Skip) + require.Contains(t, failed.Output, "WARNING: DATA RACE") + require.Contains(t, failed.Output, "Read at 0x00c0000182e8") + require.Contains(t, failed.Output, "Previous write at 0x00c0000182e8") + require.Contains(t, failed.Output, "race detected during execution of test") +} + +func TestRunner_Run_BuildFailed(t *testing.T) { + runnerFunc := mockRunnerFunc(t, testutil.ReadFile(t, "testdata", "testoutput-build-failed.txt"), 1, nil) + r := runner.New(context.Background(), "./testdata", + runner.WithRunnerFunc(runnerFunc), + runner.WithUUIDFunc(mockUUIDFunc), + ) + + result, err := r.Run(testTarget) + + require.ErrorContains(t, err, "build failed") + require.NotNil(t, result) + + defer func() { + if dir := result.Dir(); dir != "" { + os.RemoveAll(dir) + } + }() + + require.Equal(t, "deadbeef", result.UUID) + require.Equal(t, []string{testTarget}, result.Targets) + require.NotZero(t, result.Duration) + require.Equal(t, "build failed", result.Error) + require.False(t, result.Pass) + require.Equal(t, 0, result.Tests) + require.Equal(t, 0, result.Passed) + require.Equal(t, 0, result.Skipped) + require.Equal(t, 0, result.Failed) + require.Len(t, result.Packages, 0) +} + +func TestRunner_Run_FlagConfig(t *testing.T) { + mockRf := func(ctx context.Context, dir, name string, args ...string) ([]byte, int, error) { + if args[0] == "list" { + return []byte(testTarget), 0, nil + } + + argStr := strings.Join(args, " ") + require.Contains(t, argStr, " -short ") + require.Contains(t, argStr, " -shuffle on ") + require.Contains(t, argStr, " -race ") + require.Contains(t, argStr, " -timeout 10s ") + + return testutil.ReadFile(t, "testdata", "testoutput.json"), 0, nil + } + + r := runner.New(context.Background(), "./testdata", + runner.WithRunnerFunc(mockRf), + runner.WithShort(true), + runner.WithShuffle("on"), + runner.WithRaceDetection(true), + runner.WithTimeout(10*time.Second), + ) + + result, err := r.Run(testTarget) + + require.NoError(t, err) + require.NotNil(t, result) +} + +func TestResult_Close(t *testing.T) { + runnerFunc := mockRunnerFunc(t, testutil.ReadFile(t, "testdata", "testoutput.json"), 1, nil) + r := runner.New(context.Background(), "./testdata", + runner.WithRunnerFunc(runnerFunc), + runner.WithUUIDFunc(mockUUIDFunc), + ) + + result, err := r.Run(testTarget) + + require.NoError(t, err) + require.NotNil(t, result) + + require.DirExists(t, result.Dir()) + require.NoError(t, result.Close()) + require.NoDirExists(t, result.Dir()) + require.Empty(t, result.Dir()) +} + +func TestResult_Close_DirAlreadyRemoved(t *testing.T) { + runnerFunc := mockRunnerFunc(t, testutil.ReadFile(t, "testdata", "testoutput.json"), 1, nil) + r := runner.New(context.Background(), "./testdata", + runner.WithRunnerFunc(runnerFunc), + runner.WithUUIDFunc(mockUUIDFunc), + ) + + result, err := r.Run(testTarget) + + require.NoError(t, err) + require.NotNil(t, result) + + require.DirExists(t, result.Dir()) + require.NoError(t, os.RemoveAll(result.Dir())) + require.NoError(t, result.Close()) + require.Empty(t, result.Dir()) +} + +func mockUUIDFunc() (string, error) { + return "deadbeef", nil +} + +func mockRunnerFunc(t *testing.T, out []byte, exitCode int, returnErr error) command.Runner { + t.Helper() + + return func(ctx context.Context, dir, name string, args ...string) ([]byte, int, error) { + if args[0] == "list" { + return []byte(testTarget), 0, nil + } + + var cpDest string + + // Find destination for the coverage profile. + for i, arg := range args { + if arg == "-coverprofile" { + cpDest = args[i+1] + break + } + } + + if cpDest == "" { + t.Fatal("coverprofile arg not found") + } + + cp, err := os.ReadFile(path.Join("testdata", "coverprofile.out")) + if err != nil { + t.Fatalf("error reading testdata cover profile: %v", err) + } + + if err := os.WriteFile(cpDest, cp, 0o644); err != nil { + t.Fatalf("error writing coverage file to %q: %v", cpDest, err) + } + + return out, exitCode, returnErr + } +} + +func testByName(t *testing.T, name string, tests []*runner.Test) *runner.Test { + t.Helper() + + for _, test := range tests { + if test.Name == name { + return test + } + } + + t.Fatalf("could not find test %q", name) + + return nil +} diff --git a/internal/pkg/runner/testdata/coverprofile.out b/internal/pkg/runner/testdata/coverprofile.out new file mode 100644 index 0000000..9687cb5 --- /dev/null +++ b/internal/pkg/runner/testdata/coverprofile.out @@ -0,0 +1,4 @@ +mode: atomic +github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers/numbers.go:5.27,6.11 1 1 +github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers/numbers.go:6.11,8.3 1 0 +github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers/numbers.go:9.2,9.10 1 1 diff --git a/internal/pkg/runner/testdata/numbers/counter.go b/internal/pkg/runner/testdata/numbers/counter.go new file mode 100644 index 0000000..827d11d --- /dev/null +++ b/internal/pkg/runner/testdata/numbers/counter.go @@ -0,0 +1,13 @@ +package numbers + +type Counter struct { + value int +} + +func (c *Counter) Increment() { + c.value++ +} + +func (c *Counter) Value() int { + return c.value +} diff --git a/internal/pkg/runner/testdata/numbers/counter_test.go b/internal/pkg/runner/testdata/numbers/counter_test.go new file mode 100644 index 0000000..8d74ff6 --- /dev/null +++ b/internal/pkg/runner/testdata/numbers/counter_test.go @@ -0,0 +1,25 @@ +package numbers + +import ( + "sync" + "testing" +) + +func TestCounterIncrement(t *testing.T) { + c := &Counter{} + var wg sync.WaitGroup + + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + c.Increment() + }() + } + + wg.Wait() + + if c.Value() != 100 { + t.Errorf("Expected counter value to be 100, got %d", c.Value()) + } +} diff --git a/internal/pkg/runner/testdata/numbers/numbers.go b/internal/pkg/runner/testdata/numbers/numbers.go new file mode 100644 index 0000000..4dd318f --- /dev/null +++ b/internal/pkg/runner/testdata/numbers/numbers.go @@ -0,0 +1,10 @@ +// Package Numbers is a test package for testing the runner package. +package numbers + +// IntMin returns the minimum of two integers. +func IntMin(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/internal/pkg/runner/testdata/numbers/numbers_test.go b/internal/pkg/runner/testdata/numbers/numbers_test.go new file mode 100644 index 0000000..47f804f --- /dev/null +++ b/internal/pkg/runner/testdata/numbers/numbers_test.go @@ -0,0 +1,38 @@ +package numbers + +import ( + "fmt" + "testing" +) + +func TestIntMinBasic(t *testing.T) { + ans := IntMin(2, -2) + if ans != -2 { + t.Errorf("IntMin(2, -2) = %d; want -2", ans) + } +} + +func TestIntMinTableDriven(t *testing.T) { + t.Skip("skipping table driven test") + + tests := []struct { + a, b int + want int + }{ + {0, 1, 0}, + {1, 0, 0}, + {2, -2, -2}, + {0, -1, -1}, + {-1, 0, -1}, + } + + for _, tt := range tests { + testname := fmt.Sprintf("%d,%d", tt.a, tt.b) + t.Run(testname, func(t *testing.T) { + ans := IntMin(tt.a, tt.b) + if ans != tt.want { + t.Errorf("got %d, want %d", ans, tt.want) + } + }) + } +} diff --git a/internal/pkg/runner/testdata/testoutput-all-pass.json b/internal/pkg/runner/testdata/testoutput-all-pass.json new file mode 100644 index 0000000..9723ec5 --- /dev/null +++ b/internal/pkg/runner/testdata/testoutput-all-pass.json @@ -0,0 +1,33 @@ +{"Time":"2023-01-01T13:37:00.557861Z","Action":"start","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers"} +{"Time":"2023-01-01T13:37:00.557925Z","Action":"run","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinBasic"} +{"Time":"2023-01-01T13:37:00.557931Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinBasic","Output":"=== RUN TestIntMinBasic\n"} +{"Time":"2023-01-01T13:37:00.557939Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinBasic","Output":"--- PASS: TestIntMinBasic (0.00s)\n"} +{"Time":"2023-01-01T13:37:00.557945Z","Action":"pass","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinBasic","Elapsed":0} +{"Time":"2023-01-01T13:37:00.557952Z","Action":"run","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven"} +{"Time":"2023-01-01T13:37:00.557956Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven","Output":"=== RUN TestIntMinTableDriven\n"} +{"Time":"2023-01-01T13:37:00.55796Z","Action":"run","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven/0,1"} +{"Time":"2023-01-01T13:37:00.557963Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven/0,1","Output":"=== RUN TestIntMinTableDriven/0,1\n"} +{"Time":"2023-01-01T13:37:00.557969Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven/0,1","Output":"--- PASS: TestIntMinTableDriven/0,1 (0.00s)\n"} +{"Time":"2023-01-01T13:37:00.557973Z","Action":"pass","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven/0,1","Elapsed":0} +{"Time":"2023-01-01T13:37:00.557977Z","Action":"run","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven/1,0"} +{"Time":"2023-01-01T13:37:00.55798Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven/1,0","Output":"=== RUN TestIntMinTableDriven/1,0\n"} +{"Time":"2023-01-01T13:37:00.557984Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven/1,0","Output":"--- PASS: TestIntMinTableDriven/1,0 (0.00s)\n"} +{"Time":"2023-01-01T13:37:00.557987Z","Action":"pass","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven/1,0","Elapsed":0} +{"Time":"2023-01-01T13:37:00.557991Z","Action":"run","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven/2,-2"} +{"Time":"2023-01-01T13:37:00.558004Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven/2,-2","Output":"=== RUN TestIntMinTableDriven/2,-2\n"} +{"Time":"2023-01-01T13:37:00.558008Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven/2,-2","Output":"--- PASS: TestIntMinTableDriven/2,-2 (0.00s)\n"} +{"Time":"2023-01-01T13:37:00.558012Z","Action":"pass","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven/2,-2","Elapsed":0} +{"Time":"2023-01-01T13:37:00.558015Z","Action":"run","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven/0,-1"} +{"Time":"2023-01-01T13:37:00.558019Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven/0,-1","Output":"=== RUN TestIntMinTableDriven/0,-1\n"} +{"Time":"2023-01-01T13:37:00.558033Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven/0,-1","Output":"--- PASS: TestIntMinTableDriven/0,-1 (0.00s)\n"} +{"Time":"2023-01-01T13:37:00.558036Z","Action":"pass","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven/0,-1","Elapsed":0} +{"Time":"2023-01-01T13:37:00.558038Z","Action":"run","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven/-1,0"} +{"Time":"2023-01-01T13:37:00.55804Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven/-1,0","Output":"=== RUN TestIntMinTableDriven/-1,0\n"} +{"Time":"2023-01-01T13:37:00.558043Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven/-1,0","Output":"--- PASS: TestIntMinTableDriven/-1,0 (0.00s)\n"} +{"Time":"2023-01-01T13:37:00.558046Z","Action":"pass","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven/-1,0","Elapsed":0} +{"Time":"2023-01-01T13:37:00.558049Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven","Output":"--- PASS: TestIntMinTableDriven (0.00s)\n"} +{"Time":"2023-01-01T13:37:00.558051Z","Action":"pass","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven","Elapsed":0} +{"Time":"2023-01-01T13:37:00.558054Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Output":"PASS\n"} +{"Time":"2023-01-01T13:37:00.558056Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Output":"\tgithub.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers\tcoverage: 100.0% of statements\n"} +{"Time":"2023-01-01T13:37:00.558067Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Output":"ok \tgithub.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers\t(cached)\tcoverage: 100.0% of statements\n"} +{"Time":"2023-01-01T13:37:00.55807Z","Action":"pass","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Elapsed":0} diff --git a/internal/pkg/runner/testdata/testoutput-build-failed.txt b/internal/pkg/runner/testdata/testoutput-build-failed.txt new file mode 100644 index 0000000..7a4f035 --- /dev/null +++ b/internal/pkg/runner/testdata/testoutput-build-failed.txt @@ -0,0 +1,3 @@ +# github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers [github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers.test] +./numbers_test.go:9:16: cannot use "2" (untyped string constant) as int value in argument to IntMin +FAIL github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers [build failed] diff --git a/internal/pkg/runner/testdata/testoutput-data-race.json b/internal/pkg/runner/testdata/testoutput-data-race.json new file mode 100644 index 0000000..97c23ed --- /dev/null +++ b/internal/pkg/runner/testdata/testoutput-data-race.json @@ -0,0 +1,43 @@ +{"Time":"2023-01-01T13:37:00.295491Z","Action":"start","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers"} +{"Time":"2023-01-01T13:37:00.547881Z","Action":"run","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement"} +{"Time":"2023-01-01T13:37:00.548031Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":"=== RUN TestCounterIncrement\n"} +{"Time":"2023-01-01T13:37:00.548739Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":"==================\n"} +{"Time":"2023-01-01T13:37:00.548772Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":"WARNING: DATA RACE\n"} +{"Time":"2023-01-01T13:37:00.548783Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":"Read at 0x00c0000182e8 by goroutine 21:\n"} +{"Time":"2023-01-01T13:37:00.548806Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers.(*Counter).Increment()\n"} +{"Time":"2023-01-01T13:37:00.548864Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" /Users/michael/src/github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers/counter.go:8 +0xb4\n"} +{"Time":"2023-01-01T13:37:00.548881Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers.TestCounterIncrement.func1()\n"} +{"Time":"2023-01-01T13:37:00.548891Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" /Users/michael/src/github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers/counter_test.go:16 +0x64\n"} +{"Time":"2023-01-01T13:37:00.5489Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":"\n"} +{"Time":"2023-01-01T13:37:00.548918Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":"Previous write at 0x00c0000182e8 by goroutine 7:\n"} +{"Time":"2023-01-01T13:37:00.548926Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers.(*Counter).Increment()\n"} +{"Time":"2023-01-01T13:37:00.548937Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" /Users/michael/src/github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers/counter.go:8 +0xc4\n"} +{"Time":"2023-01-01T13:37:00.548947Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers.TestCounterIncrement.func1()\n"} +{"Time":"2023-01-01T13:37:00.548955Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" /Users/michael/src/github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers/counter_test.go:16 +0x64\n"} +{"Time":"2023-01-01T13:37:00.548965Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":"\n"} +{"Time":"2023-01-01T13:37:00.549006Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":"Goroutine 21 (running) created at:\n"} +{"Time":"2023-01-01T13:37:00.549013Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers.TestCounterIncrement()\n"} +{"Time":"2023-01-01T13:37:00.54902Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" /Users/michael/src/github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers/counter_test.go:14 +0x6c\n"} +{"Time":"2023-01-01T13:37:00.549082Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" testing.tRunner()\n"} +{"Time":"2023-01-01T13:37:00.54915Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" /usr/local/go/src/testing/testing.go:1576 +0x180\n"} +{"Time":"2023-01-01T13:37:00.549182Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" testing.(*T).Run.func1()\n"} +{"Time":"2023-01-01T13:37:00.549192Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" /usr/local/go/src/testing/testing.go:1629 +0x40\n"} +{"Time":"2023-01-01T13:37:00.549201Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":"\n"} +{"Time":"2023-01-01T13:37:00.549268Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":"Goroutine 7 (finished) created at:\n"} +{"Time":"2023-01-01T13:37:00.549288Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers.TestCounterIncrement()\n"} +{"Time":"2023-01-01T13:37:00.5493Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" /Users/michael/src/github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers/counter_test.go:14 +0x6c\n"} +{"Time":"2023-01-01T13:37:00.54931Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" testing.tRunner()\n"} +{"Time":"2023-01-01T13:37:00.549333Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" /usr/local/go/src/testing/testing.go:1576 +0x180\n"} +{"Time":"2023-01-01T13:37:00.549364Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" testing.(*T).Run.func1()\n"} +{"Time":"2023-01-01T13:37:00.549407Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" /usr/local/go/src/testing/testing.go:1629 +0x40\n"} +{"Time":"2023-01-01T13:37:00.549428Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":"==================\n"} +{"Time":"2023-01-01T13:37:00.549672Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" counter_test.go:23: Expected counter value to be 100, got 99\n"} +{"Time":"2023-01-01T13:37:00.549708Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":" testing.go:1446: race detected during execution of test\n"} +{"Time":"2023-01-01T13:37:00.549788Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Output":"--- FAIL: TestCounterIncrement (0.00s)\n"} +{"Time":"2023-01-01T13:37:00.549804Z","Action":"fail","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestCounterIncrement","Elapsed":0} +{"Time":"2023-01-01T13:37:00.549833Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Output":" testing.go:1446: race detected during execution of test\n"} +{"Time":"2023-01-01T13:37:00.549845Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Output":"FAIL\n"} +{"Time":"2023-01-01T13:37:00.552453Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Output":"\tgithub.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers\tcoverage: 40.0% of statements\n"} +{"Time":"2023-01-01T13:37:00.553484Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Output":"exit status 1\n"} +{"Time":"2023-01-01T13:37:00.553517Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Output":"FAIL\tgithub.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers\t0.258s\n"} +{"Time":"2023-01-01T13:37:00.553535Z","Action":"fail","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Elapsed":0.258} diff --git a/internal/pkg/runner/testdata/testoutput-no-tests.json b/internal/pkg/runner/testdata/testoutput-no-tests.json new file mode 100644 index 0000000..380ea03 --- /dev/null +++ b/internal/pkg/runner/testdata/testoutput-no-tests.json @@ -0,0 +1,6 @@ +{"Time":"2023-01-01T13:37:00.879004Z","Action":"start","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers"} +{"Time":"2023-01-01T13:37:00.879054Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Output":"testing: warning: no tests to run\n"} +{"Time":"2023-01-01T13:37:00.879063Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Output":"PASS\n"} +{"Time":"2023-01-01T13:37:00.879068Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Output":"\tgithub.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers\tcoverage: 0.0% of statements\n"} +{"Time":"2023-01-01T13:37:00.879074Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Output":"ok \tgithub.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers\t(cached)\tcoverage: 0.0% of statements [no tests to run]\n"} +{"Time":"2023-01-01T13:37:00.87908Z","Action":"pass","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Elapsed":0} diff --git a/internal/pkg/runner/testdata/testoutput.json b/internal/pkg/runner/testdata/testoutput.json new file mode 100644 index 0000000..297df0d --- /dev/null +++ b/internal/pkg/runner/testdata/testoutput.json @@ -0,0 +1,19 @@ +{"Time":"2023-01-01T13:37:00.636011Z","Action":"start","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers"} +{"Time":"2023-01-01T13:37:00.920877Z","Action":"run","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinBasic"} +{"Time":"2023-01-01T13:37:00.921056Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinBasic","Output":"=== RUN TestIntMinBasic\n"} +{"Time":"2023-01-01T13:37:00.921157Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinBasic","Output":"--- PASS: TestIntMinBasic (0.00s)\n"} +{"Time":"2023-01-01T13:37:00.921198Z","Action":"pass","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinBasic","Elapsed":0} +{"Time":"2023-01-01T13:37:00.921249Z","Action":"run","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven"} +{"Time":"2023-01-01T13:37:00.92127Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven","Output":"=== RUN TestIntMinTableDriven\n"} +{"Time":"2023-01-01T13:37:00.921288Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven","Output":" numbers_test.go:16: skipping table driven test\n"} +{"Time":"2023-01-01T13:37:00.921323Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven","Output":"--- SKIP: TestIntMinTableDriven (0.00s)\n"} +{"Time":"2023-01-01T13:37:00.921344Z","Action":"skip","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinTableDriven","Elapsed":0} +{"Time":"2023-01-01T13:37:00.921363Z","Action":"run","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinFailing"} +{"Time":"2023-01-01T13:37:00.921381Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinFailing","Output":"=== RUN TestIntMinFailing\n"} +{"Time":"2023-01-01T13:37:00.921403Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinFailing","Output":" numbers_test.go:41: failing test\n"} +{"Time":"2023-01-01T13:37:00.921424Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinFailing","Output":"--- FAIL: TestIntMinFailing (0.00s)\n"} +{"Time":"2023-01-01T13:37:00.921444Z","Action":"fail","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Test":"TestIntMinFailing","Elapsed":0} +{"Time":"2023-01-01T13:37:00.921462Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Output":"FAIL\n"} +{"Time":"2023-01-01T13:37:00.924682Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Output":"\tgithub.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers\tcoverage: 66.7% of statements\n"} +{"Time":"2023-01-01T13:37:00.92586Z","Action":"output","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Output":"FAIL\tgithub.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers\t0.289s\n"} +{"Time":"2023-01-01T13:37:00.925885Z","Action":"fail","Package":"github.com/michenriksen/gokiburi/internal/pkg/runner/testdata/numbers","Elapsed":0.29} diff --git a/internal/pkg/server/api.go b/internal/pkg/server/api.go new file mode 100644 index 0000000..b7f1f1d --- /dev/null +++ b/internal/pkg/server/api.go @@ -0,0 +1,92 @@ +package server + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + + "github.com/labstack/echo/v4" + + "github.com/michenriksen/gokiburi/internal/pkg/state" +) + +func (s *Server) resultsHandler(c echo.Context) error { + return c.JSON(http.StatusOK, s.getResults()) +} + +func (s *Server) pauseHandler(c echo.Context) error { + if s.state != state.Ready { + // Only allow client to pause if current state is Ready. + return c.NoContent(http.StatusForbidden) + } + + s.sendCommand(newCommand(Pause, "")) + + return c.NoContent(http.StatusAccepted) +} + +func (s *Server) resumeHandler(c echo.Context) error { + if s.state != state.Paused { + // Only allow client to resume if current state is Paused. + return c.NoContent(http.StatusForbidden) + } + + s.sendCommand(newCommand(Resume, "")) + + return c.NoContent(http.StatusAccepted) +} + +func (s *Server) clearResultsHandler(c echo.Context) error { + s.clearResults() + return c.NoContent(http.StatusOK) +} + +func (s *Server) runHandler(c echo.Context) error { + req := runRequest{} + + if ok, err := bindAndValidate(c, &req); !ok { + return err + } + + s.logger.Debug("emitting RunTests command", "data", req.Package) + + s.Commands <- newCommand(RunTests, req.Package) + + return c.NoContent(http.StatusAccepted) +} + +func (s *Server) reportHandler(c echo.Context) error { + uuid := c.Param("uuid") + if !validUUID(uuid) { + return c.NoContent(http.StatusBadRequest) + } + + result := s.resultByUUID(uuid) + if result == nil { + return c.JSON(http.StatusNotFound, fmt.Errorf("no test result with UUID %s", uuid)) + } + + rpath := filepath.Join(result.Dir(), "report.json") + + report, err := os.Open(rpath) + if err != nil { + if os.IsNotExist(err) { + s.logger.Error("can't find coverage report for result", logKeyUUID, uuid, "path", rpath) + + return c.JSON( + http.StatusNotFound, + fmt.Errorf("coverage report for result with UUID %s was not found", uuid), + ) + } + + s.logger.Error("failed to read coverage report for result", logKeyUUID, uuid, logKeyError, err) + + return c.JSON( + http.StatusInternalServerError, + fmt.Errorf("failed to read coverage report for result with UUID %s", uuid), + ) + } + + return c.Stream(200, "application/json", report) +} diff --git a/internal/pkg/server/commands.go b/internal/pkg/server/commands.go new file mode 100644 index 0000000..7cb6696 --- /dev/null +++ b/internal/pkg/server/commands.go @@ -0,0 +1,23 @@ +package server + +// Instruction from user to the App. +type Instruction int + +const ( + Pause Instruction = iota + Resume + RunTests +) + +// Command from user to the App. +type Command struct { + Instruction Instruction + Data string +} + +func newCommand(instruction Instruction, data string) *Command { + return &Command{ + Instruction: instruction, + Data: data, + } +} diff --git a/internal/pkg/server/middleware.go b/internal/pkg/server/middleware.go new file mode 100644 index 0000000..d862bf3 --- /dev/null +++ b/internal/pkg/server/middleware.go @@ -0,0 +1,21 @@ +package server + +import ( + "github.com/labstack/echo/v4" +) + +// apiHeaders adds response headers to disable browser caching for API +// endpoints. +func apiHeaders() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + res := c.Response() + + res.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + res.Header().Set("Pragma", "no-cache") + res.Header().Set("Expires", "0") + + return next(c) + } + } +} diff --git a/internal/pkg/server/requests.go b/internal/pkg/server/requests.go new file mode 100644 index 0000000..8c06e17 --- /dev/null +++ b/internal/pkg/server/requests.go @@ -0,0 +1,47 @@ +package server + +import ( + "net/http" + "regexp" + + "github.com/invopop/validation" + "github.com/labstack/echo/v4" +) + +var ( + uuidRegexp = regexp.MustCompile(`^[\w_-]{21}$`) + pkgRegexp = regexp.MustCompile(`^[\w\.\/_-]+$`) +) + +type runRequest struct { + Package string `json:"package"` +} + +func (r runRequest) Validate() error { + validation.ErrorTag = "json" + + return validation.ValidateStruct(&r, + validation.Field(&r.Package, + validation.Required, + validation.Match(pkgRegexp).Error("must be a valid package name"), + ), + ) +} + +func validUUID(s string) bool { + return uuidRegexp.MatchString(s) +} + +func bindAndValidate(c echo.Context, i any) (ok bool, err error) { + if err = c.Bind(i); err != nil { + return false, c.NoContent(http.StatusBadRequest) + } + + if vi, ok := i.(validation.Validatable); ok { + if err = vi.Validate(); err != nil { + return false, c.JSON(http.StatusUnprocessableEntity, err) + } + } + + return true, nil +} diff --git a/internal/pkg/server/responses.go b/internal/pkg/server/responses.go new file mode 100644 index 0000000..c292619 --- /dev/null +++ b/internal/pkg/server/responses.go @@ -0,0 +1,7 @@ +package server + +type notification struct { + Type string `json:"type"` + Body string `json:"body"` + Tag string `json:"tag"` +} diff --git a/internal/pkg/server/server.go b/internal/pkg/server/server.go new file mode 100644 index 0000000..bbdc599 --- /dev/null +++ b/internal/pkg/server/server.go @@ -0,0 +1,308 @@ +package server + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/sha1" // #nosec:G505 // SHA1 is used for non-sensitive notification tags. + "encoding/base64" + "errors" + "fmt" + "io" + "io/fs" + "mime" + "net" + "net/http" + "path" + "strconv" + "sync" + "time" + + "github.com/charmbracelet/log" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "golang.org/x/net/websocket" + + "github.com/michenriksen/gokiburi/internal/pkg/runner" + "github.com/michenriksen/gokiburi/internal/pkg/state" + "github.com/michenriksen/gokiburi/web" +) + +const ( + defaultMaxResults = 50 + + logKeyUUID = "uuid" + logKeyError = "error" + + cspFmt = "default-src 'self' 'nonce-%s'; style-src 'self' 'unsafe-inline'; img-src 'self' data:" +) + +// Option configures a [Server]. +type Option func(*Server) + +// Server for serving web UI and API. +type Server struct { + Commands chan *Command + ctx context.Context + root string + router *echo.Echo + state state.State + logger *log.Logger + results []*runner.Result + resultsMu sync.RWMutex + wsConn *websocket.Conn + wsMu sync.Mutex + maxResults int +} + +// New Server for serving web UI and API. +func New(ctx context.Context, rootDir string, opts ...Option) *Server { + router := echo.New() + router.HideBanner = true + router.HidePort = true + + router.Use(middleware.Recover()) + router.Use(middleware.Secure()) + + s := &Server{ + Commands: make(chan *Command, 1), + ctx: ctx, + root: rootDir, + router: router, + state: state.Init, + logger: log.New(io.Discard), + maxResults: defaultMaxResults, + } + + for _, opt := range opts { + opt(s) + } + + return s +} + +// Serve requests on host and port. +func (s *Server) Serve(host string, port int) error { + if err := s.registerAPIHandlers(); err != nil { + return fmt.Errorf("registering API handlers: %w", err) + } + + if err := s.registerHandlers(); err != nil { + return fmt.Errorf("registering static asset handler: %w", err) + } + + address := net.JoinHostPort(host, strconv.Itoa(port)) + + s.logger.Info(fmt.Sprintf("web server listening on %s", address), "url", "http://"+address+"/") + + if err := s.router.Start(address); err != nil { + return fmt.Errorf("starting router on %s: %w", address, err) + } + + return nil +} + +// Close server. +func (s *Server) Close() error { + s.logger.Info("shutting down") + + close(s.Commands) + + s.clearResults() + + if err := s.router.Shutdown(context.Background()); err != nil { + return fmt.Errorf("closing router: %w", err) + } + + return nil +} + +// AddResult to be served via the API. +// +// Prepends the result to the internal result slice. Slice is trimmed if its +// length exceeds configured max results. +func (s *Server) AddResult(r *runner.Result) { + if r.Error != "" { + s.sendClientMessage(newClientMessage("resultError", r)) + return + } + + if r.Tests == 0 { + s.sendClientMessage(newClientMessage("resultEmpty", r)) + return + } + + s.resultsMu.Lock() + defer s.resultsMu.Unlock() + + // Prepend result to beginning of slice. + s.results = append(s.results, nil) + copy(s.results[1:], s.results) + s.results[0] = r + + // Trim slice if length has become too big. + if len(s.results) > s.maxResults { + for i := s.maxResults; i < len(s.results); i++ { + s.logger.Debug(fmt.Sprintf("closing and trimming old result at index %d", i), "uuid", s.results[i].UUID) + s.results[i].Close() + } + + s.results = s.results[0:s.maxResults] + } + + s.sendClientMessage(newClientMessage("result", r)) +} + +// SetState for server. +func (s *Server) SetState(newState state.State) { + s.state = newState + s.sendClientMessage(newClientMessage("state", s.state)) +} + +// SendNotification to display as a toaster message in the web UI. +func (s *Server) SendNotification(kind, bodyFmt string, args ...any) { + body := fmt.Sprintf(bodyFmt, args...) + sha := sha1.New() //#nosec:G401 // We don't need a cryptographically secure hash notification tags. + sha.Sum([]byte(time.Now().String())) + sha.Sum([]byte(kind)) + sha.Sum([]byte(body)) + + s.sendClientMessage(newClientMessage("notification", ¬ification{ + kind, + body, + base64.RawURLEncoding.EncodeToString(sha.Sum(nil)), + })) +} + +// Reset server to initial state. +// +// Sets the state to [state.Init], clears results, and closes all long poll +// subscriptions. +// +// Note: This is mainly used for testing purposes. +func (s *Server) Reset() { + s.state = state.Init + s.clearResults() +} + +func (s *Server) getResults() []*runner.Result { + s.resultsMu.RLock() + defer s.resultsMu.RUnlock() + + c := make([]*runner.Result, len(s.results)) + copy(c, s.results) + + return c +} + +func (s *Server) resultByUUID(uuid string) *runner.Result { + for _, r := range s.getResults() { + if r.UUID == uuid { + return r + } + } + + return nil +} + +func (s *Server) clearResults() { + s.resultsMu.Lock() + defer s.resultsMu.Unlock() + + for _, result := range s.results { + result.Close() + } + + s.logger.Info("cleared current test results") + + s.results = nil +} + +func (s *Server) sendCommand(cmd *Command) { + s.Commands <- cmd +} + +func (*Server) indexHandler(c echo.Context) error { + index, err := web.StaticAssetFS.ReadFile("app/build/index.html") + if err != nil { + return fmt.Errorf("reading index.html: %w", err) + } + + nonce, err := genNonce() + if err != nil { + return fmt.Errorf("generating nonce: %w", err) + } + + modified := bytes.ReplaceAll(index, []byte("/dev/null 2>&1; then + echo "semgrep is not installed. See https://semgrep.dev/docs/getting-started/" + exit 1 +fi + +if ! command -v "govulncheck" >/dev/null 2>&1; then + echo "govulncheck is not installed. See https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck" + exit 1 +fi + +semgrepgoexcluded=( + "go.lang.security.audit.crypto.bad_imports.insecure-module-used" + "go.lang.security.audit.crypto.use_of_weak_crypto.use-of-sha1" +) + +# semgrepfeexcluded=() + +semgrepgoargs=("--config" "p/golang") +semgrepfeargs=("--config" "p/typescript" "--exclude" "skeleton") + +for rule in "${semgrepgoexcluded[@]}"; do + semgrepgoargs+=("--exclude-rule" "$rule") +done + +# for rule in "${semgrepfeexcluded[@]}"; do +# semgrepfeargs+=("--exclude-rule" "$rule") +# done + +tmpfile=$(mktemp) +exitstatus="" + +if [[ -z "${1:-}" || "${1:-}" == "go" ]]; then + echo -n "auditing go dependencies with govulncheck: " + govulncheck ./... > "$tmpfile" 2>&1 || exitstatus=$? + if [ "$exitstatus" != "" ]; then + echo "FAIL" + cat "$tmpfile" + echo + exit 1 + fi + echo "PASS" + rm "$tmpfile" + + echo -n "auditing go files with semgrep: " + ERRS=$(semgrep scan --quiet "${semgrepgoargs[@]}" . 2>&1 || true) + if [ "$ERRS" != "" ]; then + echo "FAIL" + echo "$ERRS" + echo + exit 1 + fi + echo "PASS" +fi + +if [[ -z "${1:-}" || "${1:-}" == "fe" ]]; then + ( + cd web/app/src + tmpfile=$(mktemp) + echo -n "auditing frontend dependencies with npm audit: " + npm audit > "$tmpfile" 2>&1 || exitstatus=$? + if [ "$exitstatus" != "" ]; then + echo "FAIL" + echo "$ERRS" + echo + exit 1 + fi + echo "PASS" + rm "$tmpfile" + ) + + echo -n "auditing frontend files with semgrep: " + ERRS=$(semgrep scan --quiet "${semgrepfeargs[@]}" web/app/src 2>&1 || true) + if [ "$ERRS" != "" ]; then + echo "FAIL" + echo "$ERRS" + echo + exit 1 + fi + echo "PASS" +fi + +if [[ -z "${1:-}" || "${1:-}" == "gha" ]]; then + echo -n "auditing GitHub Actions with semgrep: " + ERRS=$(semgrep scan --quiet --config "p/github-actions" .github 2>&1 || true) + if [ "$ERRS" != "" ]; then + echo "FAIL" + echo "$ERRS" + echo + exit 1 + fi + echo "PASS" +fi diff --git a/scripts/build-fe.sh b/scripts/build-fe.sh new file mode 100755 index 0000000..9e7b1e7 --- /dev/null +++ b/scripts/build-fe.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +if [ "${APP_NAME:-}" = "" ]; then + echo "APP_NAME must be set" + exit 1 +fi + +if [ "${VERSION:-}" = "" ]; then + echo "VERSION must be set" + exit 1 +fi + +if [[ ${VERSION:0:1} == "v" ]]; then + version="${VERSION:1}" +else + version="$VERSION" +fi + +( + cd "$(dirname "${BASH_SOURCE[0]}")/../web/app" + + export PUBLIC_APP_NAME="$APP_NAME" + export PUBLIC_APP_VERSION="$version" + + tmpfile=$(mktemp) + exitstatus="" + + echo -n "building frontend: " + npm run build > "$tmpfile" 2>&1 || exitstatus=$? + if [ "$exitstatus" != "" ]; then + echo "FAIL" + cat "$tmpfile" + exit 1 + fi + echo "SUCCESS" + rm "$tmpfile" +) diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..87be5a8 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +if [ "${OS:-}" = "" ]; then + echo "OS must be set" + exit 1 +fi +if [ "${ARCH:-}" = "" ]; then + echo "ARCH must be set" + exit 1 +fi +if [ "${VERSION:-}" = "" ]; then + echo "VERSION must be set" + exit 1 +fi + +export CGO_ENABLED=0 +export GOARCH="$ARCH" +export GOOS="$OS" +export GO111MODULE=on + +if [[ "${DBG:-}" == 1 ]]; then + # Debugging - disable optimizations and inlining. + gogcflags="all=-N -l" + goasmflags="" + goldflags="" +else + # Not debugging - trim paths, disable symbols and DWARF. + goasmflags="all=-trimpath=$(go env GOPATH)" + gogcflags="all=-trimpath=$(go env GOPATH)" + goldflags="-s -w" +fi + +buildcommit=${BUILD_COMMIT:-$(git rev-parse HEAD)} +buildtime=${BUILD_TIME:-$(date -u +"%Y-%m-%dT%H:%M:%SZ")} +buildgoversion=${BUILD_GO_VERSION:-$(go version | awk '{print $3}')} +buildosarch="$OS/$ARCH" + +gokiburipkg="$(go list -m)/internal/gokiburi" + +echo -n "building $BIN v$VERSION for $OS/$ARCH: " +ERRS=$(go build -gcflags="$gogcflags" -asmflags="$goasmflags" -ldflags "-X $gokiburipkg.buildVersion=$VERSION -X $gokiburipkg.buildCommit=$buildcommit -X $gokiburipkg.buildTime=$buildtime -X $gokiburipkg.buildGoVersion=$buildgoversion -X $gokiburipkg.buildOSArch=$buildosarch $goldflags" -o "bin/$BIN$BIN_EXTENSION" main.go || true) +if [ "$ERRS" != "" ]; then + echo "FAIL" + echo "$ERRS" + echo + exit 1 +fi +echo "SUCCESS" diff --git a/scripts/dep-csv.sh b/scripts/dep-csv.sh new file mode 100755 index 0000000..aba2e59 --- /dev/null +++ b/scripts/dep-csv.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +if ! command -v "syft" >/dev/null 2>&1; then + echo "syft is not installed. https://github.com/anchore/syft/#installation" + exit 1 +fi + +tempfile=$(mktemp) + +echo -n "running syft: " +ERRS=$(syft --quiet -o template -t dependencies.csv.tmpl --file "$tempfile" --exclude "./build/*,./dist/*,./web/app/build/**/*" .) +if [ "$ERRS" != "" ]; then + echo "FAIL" + echo "$ERRS" + echo + exit 1 +fi +echo "SUCCESS" + +echo -n "sorting and de-duplicating CSV lines: " +(head -n 1 "$tempfile"; grep -v "^$" <(tail -n +2 "$tempfile") | sort) | uniq > "$tempfile.uniq" && mv "$tempfile.uniq" dependencies.csv +echo "SUCCESS" + +rm "$tempfile" diff --git a/scripts/lint-fe.sh b/scripts/lint-fe.sh new file mode 100755 index 0000000..8ff7081 --- /dev/null +++ b/scripts/lint-fe.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +( + cd "$(dirname "${BASH_SOURCE[0]}")/../web/app" + + echo -n "running prettier: " + ERRS=$(npx prettier --plugin-search-dir . --loglevel warn --check . 2>&1 || true) + if [ "$ERRS" != "" ]; then + echo "FAIL" + echo "$ERRS" + echo + exit 1 + fi + echo "PASS" + + echo -n "running eslint: " + ERRS=$(npx eslint --quiet 2>&1 || true) + if [ "$ERRS" != "" ]; then + echo "FAIL" + echo "$ERRS" + echo + exit 1 + fi + echo "PASS" +) diff --git a/scripts/lint.sh b/scripts/lint.sh new file mode 100755 index 0000000..df7335a --- /dev/null +++ b/scripts/lint.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +if ! command -v "golangci-lint" >/dev/null 2>&1; then + echo "golangci-lint is not installed. See https://golangci-lint.run/usage/install/#local-installation" + exit 1 +fi + +echo -n "running go vet: " +ERRS=$(go vet "$@" 2>&1 || true) +if [ "$ERRS" != "" ]; then + echo "FAIL" + echo "$ERRS" + echo + exit 1 +fi +echo "PASS" + +echo -n "running golangci-lint: " +ERRS=$(golangci-lint run "$@" 2>&1 || true) +if [ "$ERRS" != "" ]; then + echo "FAIL" + echo "$ERRS" + echo + exit 1 +fi +echo "PASS" diff --git a/scripts/sbom.sh b/scripts/sbom.sh new file mode 100755 index 0000000..8a6d165 --- /dev/null +++ b/scripts/sbom.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +if ! command -v "syft" >/dev/null 2>&1; then + echo "syft is not installed. https://github.com/anchore/syft/#installation" + exit 1 +fi + +echo -n "running syft: " +ERRS=$(syft --quiet -o spdx-json --file "$APP_NAME.spdx.sbom" --exclude "./build/*,./dist/*,./web/app/build/*" .) +if [ "$ERRS" != "" ]; then + echo "FAIL" + echo "$ERRS" + echo + exit 1 +fi +echo "SUCCESS" diff --git a/scripts/test-fe.sh b/scripts/test-fe.sh new file mode 100755 index 0000000..45369c4 --- /dev/null +++ b/scripts/test-fe.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +( + cd "$(dirname "${BASH_SOURCE[0]}")/../web/app" + + echo "running frontend tests:" + npx vitest --run --silent --no-color + echo +) diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..98accfa --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +echo "running tests:" +go test -shuffle on -cover -covermode atomic -race "$@" +echo diff --git a/web/app/.eslintignore b/web/app/.eslintignore new file mode 100644 index 0000000..89431da --- /dev/null +++ b/web/app/.eslintignore @@ -0,0 +1,14 @@ +.DS_Store +node_modules +/build +/src/skeleton +/.svelte-kit +/package +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/web/app/.eslintrc.cjs b/web/app/.eslintrc.cjs new file mode 100644 index 0000000..f934894 --- /dev/null +++ b/web/app/.eslintrc.cjs @@ -0,0 +1,46 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], + plugins: ['svelte3', '@typescript-eslint', 'simple-import-sort', 'import'], + ignorePatterns: ['*.cjs'], + overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], + settings: { + 'svelte3/typescript': () => require('typescript') + }, + parserOptions: { + sourceType: 'module', + ecmaVersion: 2020 + }, + env: { + browser: true, + es2017: true, + node: false + }, + rules: { + 'simple-import-sort/imports': [ + 'error', + { + groups: [ + // Side effect imports. + ['^\\u0000'], + + // Svelte imports. + [`^svelte(/.*)?$`, `^svelte(/.*)?\\u0000$`], + + // Skeleton imports. + [`^@skeletonlabs/`, `^@skeletonlabs/.+\\u0000$`], + + // Any other external imports. + [`^`, `\\u0000$`], + + [`^\\$lib/components/`], + [`^\\$lib/stores/`], + [`^\\$lib/services/`], + [`^\\$lib/common/`, `^\\$lib/common/(.+)\\u0000$`] + ] + } + ], + 'simple-import-sort/exports': 'error' + } +}; diff --git a/web/app/.gitignore b/web/app/.gitignore new file mode 100644 index 0000000..6635cf5 --- /dev/null +++ b/web/app/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/web/app/.npmrc b/web/app/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/web/app/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/web/app/.prettierignore b/web/app/.prettierignore new file mode 100644 index 0000000..77af666 --- /dev/null +++ b/web/app/.prettierignore @@ -0,0 +1,14 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +/src/skeleton +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/web/app/.prettierrc b/web/app/.prettierrc new file mode 100644 index 0000000..032c37e --- /dev/null +++ b/web/app/.prettierrc @@ -0,0 +1,9 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 120, + "plugins": ["prettier-plugin-svelte"], + "pluginSearchDirs": ["."], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/web/app/.vscode/settings.json b/web/app/.vscode/settings.json new file mode 100644 index 0000000..54a4399 --- /dev/null +++ b/web/app/.vscode/settings.json @@ -0,0 +1,77 @@ +{ + "tailwindCSS.classAttributes": [ + "class", + "accent", + "active", + "background", + "badge", + "border", + "borderColor", + "borderWidth", + "button", + "buttonBack", + "buttonClasses", + "buttonComplete", + "buttonNext", + "buttonTextNext", + "buttonTextPrevious", + "chips", + "color", + "cursor", + "display", + "element", + "fill", + "flex", + "gap", + "gridColumns", + "height", + "hover", + "invalid", + "justify", + "meter", + "padding", + "regionBody", + "regionCaption", + "regionCaret", + "regionCone", + "regionContent", + "regionControl", + "regionDefault", + "regionFoot", + "regionHead", + "regionHeader", + "regionIcon", + "regionInterface", + "regionInterfaceText", + "regionLabel", + "regionLead", + "regionLegend", + "regionList", + "regionNavigation", + "regionPage", + "regionPanel", + "regionRowHeadline", + "regionRowMain", + "regionTrail", + "rounded", + "select", + "shadow", + "slotDefault", + "slotFooter", + "slotHeader", + "slotLead", + "slotMessage", + "slotMeta", + "slotPageContent", + "slotPageFooter", + "slotPageHeader", + "slotSidebarLeft", + "slotSidebarRight", + "slotTrail", + "space", + "spacing", + "text", + "track", + "width" + ] +} diff --git a/web/app/README.md b/web/app/README.md new file mode 100644 index 0000000..5c91169 --- /dev/null +++ b/web/app/README.md @@ -0,0 +1,38 @@ +# create-svelte + +Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/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/web/app/package-lock.json b/web/app/package-lock.json new file mode 100644 index 0000000..29b2f0a --- /dev/null +++ b/web/app/package-lock.json @@ -0,0 +1,6549 @@ +{ + "name": "gokiburi", + "version": "0.0.1-dev", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gokiburi", + "version": "0.0.1-dev", + "devDependencies": { + "@floating-ui/dom": "^1.2.7", + "@mdi/js": "^7.2.96", + "@sveltejs/adapter-auto": "^2.0.1", + "@sveltejs/adapter-static": "^2.0.2", + "@sveltejs/kit": "^1.16.0", + "@sveltejs/vite-plugin-svelte": "^2.1.1", + "@tailwindcss/forms": "^0.5.3", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/svelte": "^3.2.2", + "@types/howler": "^2.2.7", + "@typescript-eslint/eslint-plugin": "^5.59.2", + "@typescript-eslint/parser": "^5.59.2", + "autoprefixer": "^10.4.14", + "date-fns": "^2.30.0", + "eslint": "^8.40.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-simple-import-sort": "^10.0.0", + "eslint-plugin-svelte3": "^4.0.0", + "howler": "^2.2.3", + "jest-websocket-mock": "^2.4.0", + "jsdom": "^22.0.0", + "postcss": "^8.4.23", + "prettier": "^2.8.8", + "prettier-plugin-svelte": "^2.10.0", + "svelte": "^3.58.0", + "svelte-check": "^3.3.0", + "svelte-material-icons": "^3.0.4", + "tailwindcss": "^3.3.2", + "tslib": "^2.5.0", + "typescript": "^5.0.4", + "vite": "^4.3.4", + "vitest": "^0.31.0", + "vitest-fetch-mock": "^0.2.2" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz", + "integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==", + "dev": true + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", + "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/runtime": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", + "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.18.tgz", + "integrity": "sha512-EmwL+vUBZJ7mhFCs5lA4ZimpUH3WMAoqvOIYhVQwdIgSpHC8ImHdsRyhHAVxpDYUSm0lWvd63z0XH1IlImS2Qw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.18.tgz", + "integrity": "sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.18.tgz", + "integrity": "sha512-x+0efYNBF3NPW2Xc5bFOSFW7tTXdAcpfEg2nXmxegm4mJuVeS+i109m/7HMiOQ6M12aVGGFlqJX3RhNdYM2lWg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.18.tgz", + "integrity": "sha512-6tY+djEAdF48M1ONWnQb1C+6LiXrKjmqjzPNPWXhu/GzOHTHX2nh8Mo2ZAmBFg0kIodHhciEgUBtcYCAIjGbjQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.18.tgz", + "integrity": "sha512-Qq84ykvLvya3dO49wVC9FFCNUfSrQJLbxhoQk/TE1r6MjHo3sFF2tlJCwMjhkBVq3/ahUisj7+EpRSz0/+8+9A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.18.tgz", + "integrity": "sha512-fw/ZfxfAzuHfaQeMDhbzxp9mc+mHn1Y94VDHFHjGvt2Uxl10mT4CDavHm+/L9KG441t1QdABqkVYwakMUeyLRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.18.tgz", + "integrity": "sha512-FQFbRtTaEi8ZBi/A6kxOC0V0E9B/97vPdYjY9NdawyLd4Qk5VD5g2pbWN2VR1c0xhzcJm74HWpObPszWC+qTew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.18.tgz", + "integrity": "sha512-jW+UCM40LzHcouIaqv3e/oRs0JM76JfhHjCavPxMUti7VAPh8CaGSlS7cmyrdpzSk7A+8f0hiedHqr/LMnfijg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.18.tgz", + "integrity": "sha512-R7pZvQZFOY2sxUG8P6A21eq6q+eBv7JPQYIybHVf1XkQYC+lT7nDBdC7wWKTrbvMXKRaGudp/dzZCwL/863mZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.18.tgz", + "integrity": "sha512-ygIMc3I7wxgXIxk6j3V00VlABIjq260i967Cp9BNAk5pOOpIXmd1RFQJQX9Io7KRsthDrQYrtcx7QCof4o3ZoQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.18.tgz", + "integrity": "sha512-bvPG+MyFs5ZlwYclCG1D744oHk1Pv7j8psF5TfYx7otCVmcJsEXgFEhQkbhNW8otDHL1a2KDINW20cfCgnzgMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.18.tgz", + "integrity": "sha512-oVqckATOAGuiUOa6wr8TXaVPSa+6IwVJrGidmNZS1cZVx0HqkTMkqFGD2HIx9H1RvOwFeWYdaYbdY6B89KUMxA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.18.tgz", + "integrity": "sha512-3dLlQO+b/LnQNxgH4l9rqa2/IwRJVN9u/bK63FhOPB4xqiRqlQAU0qDU3JJuf0BmaH0yytTBdoSBHrb2jqc5qQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.18.tgz", + "integrity": "sha512-/x7leOyDPjZV3TcsdfrSI107zItVnsX1q2nho7hbbQoKnmoeUWjs+08rKKt4AUXju7+3aRZSsKrJtaRmsdL1xA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.18.tgz", + "integrity": "sha512-cX0I8Q9xQkL/6F5zWdYmVf5JSQt+ZfZD2bJudZrWD+4mnUvoZ3TDDXtDX2mUaq6upMFv9FlfIh4Gfun0tbGzuw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.18.tgz", + "integrity": "sha512-66RmRsPlYy4jFl0vG80GcNRdirx4nVWAzJmXkevgphP1qf4dsLQCpSKGM3DUQCojwU1hnepI63gNZdrr02wHUA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.18.tgz", + "integrity": "sha512-95IRY7mI2yrkLlTLb1gpDxdC5WLC5mZDi+kA9dmM5XAGxCME0F8i4bYH4jZreaJ6lIZ0B8hTrweqG1fUyW7jbg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.18.tgz", + "integrity": "sha512-WevVOgcng+8hSZ4Q3BKL3n1xTv5H6Nb53cBrtzzEjDbbnOmucEVcZeGCsCOi9bAOcDYEeBZbD2SJNBxlfP3qiA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.18.tgz", + "integrity": "sha512-Rzf4QfQagnwhQXVBS3BYUlxmEbcV7MY+BH5vfDZekU5eYpcffHSyjU8T0xucKVuOcdCsMo+Ur5wmgQJH2GfNrg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.18.tgz", + "integrity": "sha512-Kb3Ko/KKaWhjeAm2YoT/cNZaHaD1Yk/pa3FTsmqo9uFh1D1Rfco7BBLIPdDOozrObj2sahslFuAQGvWbgWldAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.18.tgz", + "integrity": "sha512-0/xUMIdkVHwkvxfbd5+lfG7mHOf2FRrxNbPiKWg9C4fFrB8H0guClmaM3BFiRUYrznVoyxTIyC/Ou2B7QQSwmw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.18.tgz", + "integrity": "sha512-qU25Ma1I3NqTSHJUOKi9sAH1/Mzuvlke0ioMJRthLXKm7JiSKVwFghlGbDLOO2sARECGhja4xYfRAZNPAkooYg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", + "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.5.2", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz", + "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.2.6.tgz", + "integrity": "sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==", + "dev": true + }, + "node_modules/@floating-ui/dom": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.2.7.tgz", + "integrity": "sha512-DyqylONj1ZaBnzj+uBnVfzdjjCkFCL2aA9ESHLyUOGSqb03RpbLMImP1ekIQXYs4KLk9jAjJfZAU8hXfWSahEg==", + "dev": true, + "dependencies": { + "@floating-ui/core": "^1.2.6" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", + "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@jest/expect-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", + "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.4.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", + "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.25.16" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", + "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.4.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@mdi/js": { + "version": "7.2.96", + "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.2.96.tgz", + "integrity": "sha512-paR9M9ZT7rKbh2boksNUynuSZMHhqRYnEZOm/KrZTjQ4/FzyhjLHuvw/8XYzP+E7fS4+/Ms/82EN1pl/OFsiIA==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "dev": true + }, + "node_modules/@sinclair/typebox": { + "version": "0.25.24", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "dev": true + }, + "node_modules/@sveltejs/adapter-auto": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-2.0.1.tgz", + "integrity": "sha512-anxxYMcQy7HWSKxN4YNaVcgNzCHtNFwygq72EA1Xv7c+5gSECOJ1ez1PYoLciPiFa7A3XBvMDQXUFJ2eqLDtAA==", + "dev": true, + "dependencies": { + "import-meta-resolve": "^3.0.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^1.0.0" + } + }, + "node_modules/@sveltejs/adapter-static": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-2.0.2.tgz", + "integrity": "sha512-9wYtf6s6ew7DHUHMrt55YpD1FgV7oWql2IGsW5BXquLxqcY9vjrqCFo0TzzDpo+ZPZkW/v77k0eOP6tsAb8HmQ==", + "dev": true, + "peerDependencies": { + "@sveltejs/kit": "^1.5.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.16.2.tgz", + "integrity": "sha512-yxcpA4nvlVlJ+VyYnj0zD3QN05kfmoh4OyitlPrVG34nnZSHzFpE4eZ33X1A/tc9prslSFRhpM6rWngCs0nM8w==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@sveltejs/vite-plugin-svelte": "^2.1.1", + "@types/cookie": "^0.5.1", + "cookie": "^0.5.0", + "devalue": "^4.3.0", + "esm-env": "^1.0.0", + "kleur": "^4.1.5", + "magic-string": "^0.30.0", + "mime": "^3.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^2.0.2", + "tiny-glob": "^0.2.9", + "undici": "~5.22.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": "^16.14 || >=18" + }, + "peerDependencies": { + "svelte": "^3.54.0", + "vite": "^4.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.2.0.tgz", + "integrity": "sha512-KDtdva+FZrZlyug15KlbXuubntAPKcBau0K7QhAIqC5SAy0uDbjZwoexDRx0L0J2T4niEfC6FnA9GuQQJKg+Aw==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.0", + "svelte-hmr": "^0.15.1", + "vitefu": "^0.2.4" + }, + "engines": { + "node": "^14.18.0 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.54.0", + "vite": "^4.0.0" + } + }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.3.tgz", + "integrity": "sha512-y5mb86JUoiUgBjY/o6FJSFZSEttfb3Q5gllE4xoKjAAD+vBrnIhE4dViwUuow3va8mpH4s9jyUbUbrRGoRdc2Q==", + "dev": true, + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" + } + }, + "node_modules/@testing-library/dom": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz", + "integrity": "sha512-d9ULIT+a4EXLX3UU8FBjauG9NnsZHkHztXoIcTsOKoOw030fyjheN9svkTULjJxtYag9DZz5Jz5qkWZDPxTFwA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "^5.0.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.4.4", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "5.16.5", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz", + "integrity": "sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.0.1", + "@babel/runtime": "^7.9.2", + "@types/testing-library__jest-dom": "^5.9.1", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.5.6", + "lodash": "^4.17.15", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=8", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/svelte": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-3.2.2.tgz", + "integrity": "sha512-IKwZgqbekC3LpoRhSwhd0JswRGxKdAGkf39UiDXTywK61YyLXbCYoR831e/UUC6EeNW4hiHPY+2WuovxOgI5sw==", + "dev": true, + "dependencies": { + "@testing-library/dom": "^8.1.0" + }, + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "svelte": "3.x" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", + "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==", + "dev": true + }, + "node_modules/@types/chai": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", + "dev": true + }, + "node_modules/@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.1.tgz", + "integrity": "sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==", + "dev": true + }, + "node_modules/@types/howler": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@types/howler/-/howler-2.2.7.tgz", + "integrity": "sha512-PEZldwZqJJw1PWRTpupyC7ajVTZA8aHd8nB/Y0n6zRZi5u8ktYDntsHj13ltEiBRqWwF06pASxBEvCTxniG8eA==", + "dev": true + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.1.tgz", + "integrity": "sha512-tEuVcHrpaixS36w7hpsfLBLpjtMRJUE09/MHXn923LOVojDwyC14cWcfc0rDs0VEfUyYmt/+iX1kxxp+gZMcaQ==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.0.0.tgz", + "integrity": "sha512-cD2uPTDnQQCVpmRefonO98/PPijuOnnEy5oytWJFPY1N9aJCz2wJ5kSGWO+zJoed2cY2JxQh6yBuUq4vIn61hw==", + "dev": true + }, + "node_modules/@types/pug": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.6.tgz", + "integrity": "sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.3.13", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", + "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "node_modules/@types/testing-library__jest-dom": { + "version": "5.14.5", + "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz", + "integrity": "sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==", + "dev": true, + "dependencies": { + "@types/jest": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.2.tgz", + "integrity": "sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/type-utils": "5.59.2", + "@typescript-eslint/utils": "5.59.2", + "debug": "^4.3.4", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.2.tgz", + "integrity": "sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/typescript-estree": "5.59.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", + "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz", + "integrity": "sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.59.2", + "@typescript-eslint/utils": "5.59.2", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", + "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", + "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.2.tgz", + "integrity": "sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/typescript-estree": "5.59.2", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", + "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.59.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/expect": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.31.0.tgz", + "integrity": "sha512-Jlm8ZTyp6vMY9iz9Ny9a0BHnCG4fqBa8neCF6Pk/c/6vkUk49Ls6UBlgGAU82QnzzoaUs9E/mUhq/eq9uMOv/g==", + "dev": true, + "dependencies": { + "@vitest/spy": "0.31.0", + "@vitest/utils": "0.31.0", + "chai": "^4.3.7" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.31.0.tgz", + "integrity": "sha512-H1OE+Ly7JFeBwnpHTrKyCNm/oZgr+16N4qIlzzqSG/YRQDATBYmJb/KUn3GrZaiQQyL7GwpNHVZxSQd6juLCgw==", + "dev": true, + "dependencies": { + "@vitest/utils": "0.31.0", + "concordance": "^5.0.4", + "p-limit": "^4.0.0", + "pathe": "^1.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.31.0.tgz", + "integrity": "sha512-5dTXhbHnyUMTMOujZPB0wjFjQ6q5x9c8TvAsSPUNKjp1tVU7i9pbqcKPqntyu2oXtmVxKbuHCqrOd+Ft60r4tg==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.0", + "pathe": "^1.1.0", + "pretty-format": "^27.5.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.31.0.tgz", + "integrity": "sha512-IzCEQ85RN26GqjQNkYahgVLLkULOxOm5H/t364LG0JYb3Apg0PsYCHLBYGA006+SVRMWhQvHlBBCyuByAMFmkg==", + "dev": true, + "dependencies": { + "tinyspy": "^2.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.31.0.tgz", + "integrity": "sha512-kahaRyLX7GS1urekRXN2752X4gIgOGVX4Wo8eDUGUkTWlGpXzf5ZS6N9RUUS+Re3XEE8nVGqNyxkSxF5HXlGhQ==", + "dev": true, + "dependencies": { + "concordance": "^5.0.4", + "loupe": "^2.3.6", + "pretty-format": "^27.5.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", + "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", + "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/autoprefixer": { + "version": "10.4.14", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", + "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "dependencies": { + "browserslist": "^4.21.5", + "caniuse-lite": "^1.0.30001464", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/blueimp-md5": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", + "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dev": true, + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001482", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001482.tgz", + "integrity": "sha512-F1ZInsg53cegyjroxLNW9DmrEQ1SuGRTO1QlpA0o2/6OpQ0gFeDRoq1yFmnr8Sakn9qwwt9DmbxHB6w167OSuQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chai": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/concordance": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/concordance/-/concordance-5.0.4.tgz", + "integrity": "sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==", + "dev": true, + "dependencies": { + "date-time": "^3.1.0", + "esutils": "^2.0.3", + "fast-diff": "^1.2.0", + "js-string-escape": "^1.0.1", + "lodash": "^4.17.15", + "md5-hex": "^3.0.1", + "semver": "^7.3.2", + "well-known-symbols": "^2.0.0" + }, + "engines": { + "node": ">=10.18.0 <11 || >=12.14.0 <13 || >=14" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dev": true, + "dependencies": { + "node-fetch": "2.6.7" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "dev": true, + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/data-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/date-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz", + "integrity": "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==", + "dev": true, + "dependencies": { + "time-zone": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-equal": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.1.tgz", + "integrity": "sha512-lKdkdV6EOGoVn65XaOsPdH4rMxTZOnmFyuIkMjM1i5HHCbfjC97dawgTAy0deYNfuqUqW+Q5VrVaQYtUpSd6yQ==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.0", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.0", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.0.tgz", + "integrity": "sha512-n94yQo4LI3w7erwf84mhRUkUJfhLoCZiLyoOZ/QFsDbcWNZePrLwbQpvZBUG2TNxwV3VjCKPxkiiQA6pe3TrTA==", + "dev": true + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/diff-sequences": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "dev": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.385", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.385.tgz", + "integrity": "sha512-L9zlje9bIw0h+CwPQumiuVlfMcV4boxRjFIWDcLfFqTZNbkwOExBzfmswytHawObQX4OUhtNv8gIiB21kOurIg==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", + "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.2.0", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.17.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.18.tgz", + "integrity": "sha512-z1lix43jBs6UKjcZVKOw2xx69ffE2aG0PygLL5qJ9OS/gy0Ewd1gW/PUQIOIQGXBHWNywSc0floSKoMFF8aK2w==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.18", + "@esbuild/android-arm64": "0.17.18", + "@esbuild/android-x64": "0.17.18", + "@esbuild/darwin-arm64": "0.17.18", + "@esbuild/darwin-x64": "0.17.18", + "@esbuild/freebsd-arm64": "0.17.18", + "@esbuild/freebsd-x64": "0.17.18", + "@esbuild/linux-arm": "0.17.18", + "@esbuild/linux-arm64": "0.17.18", + "@esbuild/linux-ia32": "0.17.18", + "@esbuild/linux-loong64": "0.17.18", + "@esbuild/linux-mips64el": "0.17.18", + "@esbuild/linux-ppc64": "0.17.18", + "@esbuild/linux-riscv64": "0.17.18", + "@esbuild/linux-s390x": "0.17.18", + "@esbuild/linux-x64": "0.17.18", + "@esbuild/netbsd-x64": "0.17.18", + "@esbuild/openbsd-x64": "0.17.18", + "@esbuild/sunos-x64": "0.17.18", + "@esbuild/win32-arm64": "0.17.18", + "@esbuild/win32-ia32": "0.17.18", + "@esbuild/win32-x64": "0.17.18" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz", + "integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.3", + "@eslint/js": "8.40.0", + "@humanwhocodes/config-array": "^0.11.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.5.2", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", + "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", + "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.11.0", + "resolve": "^1.22.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.27.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", + "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.7.4", + "has": "^1.0.3", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.6", + "resolve": "^1.22.1", + "semver": "^6.3.0", + "tsconfig-paths": "^3.14.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-simple-import-sort": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-10.0.0.tgz", + "integrity": "sha512-AeTvO9UCMSNzIHRkg8S6c3RPy5YEwKWSQPx3DYghLedo2ZQxowPFLGDN1AZ2evfg6r6mjBSZSLxLFsWSu3acsw==", + "dev": true, + "peerDependencies": { + "eslint": ">=5.0.0" + } + }, + "node_modules/eslint-plugin-svelte3": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte3/-/eslint-plugin-svelte3-4.0.0.tgz", + "integrity": "sha512-OIx9lgaNzD02+MDFNLw0GEUbuovNcglg+wnd/UY0fbZmlQSz7GlQiQ1f+yX0XvC07XPcDOnFcichqI3xCwp71g==", + "dev": true, + "peerDependencies": { + "eslint": ">=8.0.0", + "svelte": "^3.2.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esm-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", + "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", + "dev": true + }, + "node_modules/espree": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", + "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", + "dev": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", + "dev": true + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/howler": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.3.tgz", + "integrity": "sha512-QM0FFkw0LRX1PR8pNzJVAY25JhIWvbKMBFM4gqk+QdV+kPXOhleWGCB6AiAF/goGjIHK2e/nIElplvjQwhr0jg==", + "dev": true + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-meta-resolve": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-3.0.0.tgz", + "integrity": "sha512-4IwhLhNNA8yy445rPjD/lWh++7hMDOml2eHtd58eG7h+qK3EryMuuRbsHGPikCoAgIkkDnckKfWSk2iDla/ejg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jest-diff": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", + "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/jest-get-type": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", + "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", + "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/jest-message-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", + "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.5.0", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/jest-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", + "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-websocket-mock": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jest-websocket-mock/-/jest-websocket-mock-2.4.0.tgz", + "integrity": "sha512-AOwyuRw6fgROXHxMOiTDl1/T4dh3fV4jDquha5N0csS/PNp742HeTZWPAuKppVRSQ8s3fUGgJHoyZT9JDO0hMA==", + "dev": true, + "dependencies": { + "jest-diff": "^28.0.2", + "mock-socket": "^9.1.0" + } + }, + "node_modules/jest-websocket-mock/node_modules/@jest/schemas": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", + "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.24.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-websocket-mock/node_modules/@sinclair/typebox": { + "version": "0.24.51", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", + "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", + "dev": true + }, + "node_modules/jest-websocket-mock/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-websocket-mock/node_modules/diff-sequences": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", + "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-websocket-mock/node_modules/jest-diff": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", + "integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^28.1.1", + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-websocket-mock/node_modules/jest-get-type": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", + "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-websocket-mock/node_modules/pretty-format": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", + "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", + "dev": true, + "dependencies": { + "@jest/schemas": "^28.1.3", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-websocket-mock/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-websocket-mock/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/jiti": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz", + "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-sdsl": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", + "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/js-string-escape": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", + "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.0.0.tgz", + "integrity": "sha512-p5ZTEb5h+O+iU02t0GfEjAnkdYPrQSkfuTSMkMYyIoMvUNEHsbG0bHHbfXIcfTqD2UfvjQX7mmgiFsyRwGscVw==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loupe": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", + "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/md5-hex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz", + "integrity": "sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==", + "dev": true, + "dependencies": { + "blueimp-md5": "^2.10.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mlly": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.2.0.tgz", + "integrity": "sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww==", + "dev": true, + "dependencies": { + "acorn": "^8.8.2", + "pathe": "^1.1.0", + "pkg-types": "^1.0.2", + "ufo": "^1.1.1" + } + }, + "node_modules/mock-socket": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.2.1.tgz", + "integrity": "sha512-aw9F9T9G2zpGipLLhSNh6ZpgUyUl4frcVmRN08uE1NWPWg43Wx6+sGPDbQ7E5iFZZDJW5b5bypMeAEHqTbIFag==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.4.tgz", + "integrity": "sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g==", + "dev": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", + "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.0.tgz", + "integrity": "sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, + "node_modules/postcss": { + "version": "8.4.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", + "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^2.1.1" + }, + "engines": { + "node": ">= 14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.12.tgz", + "integrity": "sha512-NdxGCAZdRrwVI1sy59+Wzrh+pMMHxapGnpfenDVlMEXoOcvt4pGE0JLK9YY2F5dLxcFYA/YbVQKhcGU+FtSYQg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-2.10.0.tgz", + "integrity": "sha512-GXMY6t86thctyCvQq+jqElO+MKdB09BkL3hexyGP3Oi8XLKRFaJP1ud/xlWCZ9ZIa2BxHka32zhHfcuU+XsRQg==", + "dev": true, + "peerDependencies": { + "prettier": "^1.16.4 || ^2.0.0", + "svelte": "^3.2.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.21.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.5.tgz", + "integrity": "sha512-a4NTKS4u9PusbUJcfF4IMxuqjFzjm6ifj76P54a7cKnvVzJaG12BLVR+hgU2YDGHzyMMQNxLAZWuALsn8q2oQg==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sander": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", + "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", + "dev": true, + "dependencies": { + "es6-promise": "^3.1.2", + "graceful-fs": "^4.1.3", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.2" + } + }, + "node_modules/sander/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/sirv": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.3.tgz", + "integrity": "sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sorcery": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz", + "integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.14", + "buffer-crc32": "^0.2.5", + "minimist": "^1.2.0", + "sander": "^0.5.0" + }, + "bin": { + "sorcery": "bin/sorcery" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.3.tgz", + "integrity": "sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==", + "dev": true + }, + "node_modules/stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "dependencies": { + "internal-slot": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", + "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.0.1.tgz", + "integrity": "sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==", + "dev": true, + "dependencies": { + "acorn": "^8.8.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/sucrase": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz", + "integrity": "sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "3.59.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.0.tgz", + "integrity": "sha512-Di1wVPwdWriw5pSyInMRpr5EZmwrzKxtDKv5aXu8A/WDUi59Y5bIvl42eLef0x1vwz+ZtrjdnT8nXir2bDqR/A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/svelte-check": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.3.1.tgz", + "integrity": "sha512-+Yb1F50M76JRPdZlxB8/blg75GiqKH/8QJTNtC3cKvxCbrRK7zpgmOg2oxem9n4eDAIllesm74guR3AnlAtNVg==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "chokidar": "^3.4.1", + "fast-glob": "^3.2.7", + "import-fresh": "^3.2.1", + "picocolors": "^1.0.0", + "sade": "^1.7.4", + "svelte-preprocess": "^5.0.3", + "typescript": "^5.0.3" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "peerDependencies": { + "svelte": "^3.55.0" + } + }, + "node_modules/svelte-hmr": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.1.tgz", + "integrity": "sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==", + "dev": true, + "engines": { + "node": "^12.20 || ^14.13.1 || >= 16" + }, + "peerDependencies": { + "svelte": ">=3.19.0" + } + }, + "node_modules/svelte-material-icons": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/svelte-material-icons/-/svelte-material-icons-3.0.4.tgz", + "integrity": "sha512-b126iWd/kMZgstYbGIocmGwN2txGFW+S+BY+d66yPRVSgcZgrnzRJWQbhn0OCy7dzzjkYfgI/i2PdWRukqu+RQ==", + "dev": true, + "peerDependencies": { + "svelte": "^3.0.0" + } + }, + "node_modules/svelte-preprocess": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.3.tgz", + "integrity": "sha512-GrHF1rusdJVbOZOwgPWtpqmaexkydznKzy5qIC2FabgpFyKN57bjMUUUqPRfbBXK5igiEWn1uO/DXsa2vJ5VHA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@types/pug": "^2.0.6", + "detect-indent": "^6.1.0", + "magic-string": "^0.27.0", + "sorcery": "^0.11.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">= 14.10.0" + }, + "peerDependencies": { + "@babel/core": "^7.10.2", + "coffeescript": "^2.5.1", + "less": "^3.11.3 || ^4.0.0", + "postcss": "^7 || ^8", + "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0", + "pug": "^3.0.0", + "sass": "^1.26.8", + "stylus": "^0.55.0", + "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "svelte": "^3.23.0", + "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "coffeescript": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "postcss-load-config": { + "optional": true + }, + "pug": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/svelte-preprocess/node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "node_modules/tailwindcss": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz", + "integrity": "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.12", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.18.2", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/time-zone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz", + "integrity": "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "dev": true, + "dependencies": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, + "node_modules/tinybench": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.0.tgz", + "integrity": "sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.5.0.tgz", + "integrity": "sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.0.tgz", + "integrity": "sha512-7eORpyqImoOvkQJCSkL0d0mB4NHHIFAy4b1u8PHdDa7SjGS2njzl6/lyGoZLm+eyYEtlUmFGE0rFj66SWxZgQQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", + "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/ufo": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.2.tgz", + "integrity": "sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==", + "dev": true + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.0.tgz", + "integrity": "sha512-fR9RXCc+6Dxav4P9VV/sp5w3eFiSdOjJYsbtWfd4s5L5C4ogyuVpdKIVHeW0vV1MloM65/f7W45nR9ZxwVdyiA==", + "dev": true, + "dependencies": { + "busboy": "^1.6.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.5.tgz", + "integrity": "sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==", + "dev": true, + "dependencies": { + "esbuild": "^0.17.5", + "postcss": "^8.4.23", + "rollup": "^3.21.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.31.0.tgz", + "integrity": "sha512-8x1x1LNuPvE2vIvkSB7c1mApX5oqlgsxzHQesYF7l5n1gKrEmrClIiZuOFbFDQcjLsmcWSwwmrWrcGWm9Fxc/g==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "mlly": "^1.2.0", + "pathe": "^1.1.0", + "picocolors": "^1.0.0", + "vite": "^3.0.0 || ^4.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitefu": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.4.tgz", + "integrity": "sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==", + "dev": true, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.31.0.tgz", + "integrity": "sha512-JwWJS9p3GU9GxkG7eBSmr4Q4x4bvVBSswaCFf1PBNHiPx00obfhHRJfgHcnI0ffn+NMlIh9QGvG75FlaIBdKGA==", + "dev": true, + "dependencies": { + "@types/chai": "^4.3.4", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "@vitest/expect": "0.31.0", + "@vitest/runner": "0.31.0", + "@vitest/snapshot": "0.31.0", + "@vitest/spy": "0.31.0", + "@vitest/utils": "0.31.0", + "acorn": "^8.8.2", + "acorn-walk": "^8.2.0", + "cac": "^6.7.14", + "chai": "^4.3.7", + "concordance": "^5.0.4", + "debug": "^4.3.4", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.0", + "pathe": "^1.1.0", + "picocolors": "^1.0.0", + "std-env": "^3.3.2", + "strip-literal": "^1.0.1", + "tinybench": "^2.4.0", + "tinypool": "^0.5.0", + "vite": "^3.0.0 || ^4.0.0", + "vite-node": "0.31.0", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@vitest/browser": "*", + "@vitest/ui": "*", + "happy-dom": "*", + "jsdom": "*", + "playwright": "*", + "safaridriver": "*", + "webdriverio": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "playwright": { + "optional": true + }, + "safaridriver": { + "optional": true + }, + "webdriverio": { + "optional": true + } + } + }, + "node_modules/vitest-fetch-mock": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/vitest-fetch-mock/-/vitest-fetch-mock-0.2.2.tgz", + "integrity": "sha512-XmH6QgTSjCWrqXoPREIdbj40T7i1xnGmAsTAgfckoO75W1IEHKR8hcPCQ7SO16RsdW1t85oUm6pcQRLeBgjVYQ==", + "dev": true, + "dependencies": { + "cross-fetch": "^3.0.6" + }, + "engines": { + "node": ">=14.14.0" + }, + "peerDependencies": { + "vitest": ">=0.16.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/well-known-symbols": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz", + "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/web/app/package.json b/web/app/package.json new file mode 100644 index 0000000..f32241d --- /dev/null +++ b/web/app/package.json @@ -0,0 +1,52 @@ +{ + "name": "gokiburi", + "version": "0.0.1-dev", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "test:unit": "vitest", + "lint": "prettier --plugin-search-dir . --check . && eslint .", + "format": "prettier --plugin-search-dir . --write ." + }, + "devDependencies": { + "@floating-ui/dom": "^1.2.7", + "@mdi/js": "^7.2.96", + "@sveltejs/adapter-auto": "^2.0.1", + "@sveltejs/adapter-static": "^2.0.2", + "@sveltejs/kit": "^1.16.0", + "@sveltejs/vite-plugin-svelte": "^2.1.1", + "@tailwindcss/forms": "^0.5.3", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/svelte": "^3.2.2", + "@types/howler": "^2.2.7", + "@typescript-eslint/eslint-plugin": "^5.59.2", + "@typescript-eslint/parser": "^5.59.2", + "autoprefixer": "^10.4.14", + "date-fns": "^2.30.0", + "eslint": "^8.40.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-simple-import-sort": "^10.0.0", + "eslint-plugin-svelte3": "^4.0.0", + "howler": "^2.2.3", + "jest-websocket-mock": "^2.4.0", + "jsdom": "^22.0.0", + "postcss": "^8.4.23", + "prettier": "^2.8.8", + "prettier-plugin-svelte": "^2.10.0", + "svelte": "^3.58.0", + "svelte-check": "^3.3.0", + "svelte-material-icons": "^3.0.4", + "tailwindcss": "^3.3.2", + "tslib": "^2.5.0", + "typescript": "^5.0.4", + "vite": "^4.3.4", + "vitest": "^0.31.0", + "vitest-fetch-mock": "^0.2.2" + }, + "type": "module" +} diff --git a/web/app/postcss.config.cjs b/web/app/postcss.config.cjs new file mode 100644 index 0000000..054c147 --- /dev/null +++ b/web/app/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/web/app/src/app.d.ts b/web/app/src/app.d.ts new file mode 100644 index 0000000..8f4d638 --- /dev/null +++ b/web/app/src/app.d.ts @@ -0,0 +1,9 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +// and what to do when importing types +declare namespace App { + // interface Locals {} + // interface PageData {} + // interface Error {} + // interface Platform {} +} diff --git a/web/app/src/app.html b/web/app/src/app.html new file mode 100644 index 0000000..eb8f054 --- /dev/null +++ b/web/app/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/web/app/src/app.postcss b/web/app/src/app.postcss new file mode 100644 index 0000000..abf7876 --- /dev/null +++ b/web/app/src/app.postcss @@ -0,0 +1,54 @@ +/*place global styles here */ +html, +body { + @apply h-full overflow-hidden; +} + +.tooltip { + position: relative; +} + +.tooltip:before { + @apply bg-surface-900 p-2 mt-2 text-sm text-center rounded max-w-sm whitespace-nowrap absolute z-50; + content: attr(data-tip); + position: absolute; + top: 100%; + opacity: 0; + transition: 0.3s opacity; +} + +.tooltip:hover:before { + opacity: 1; +} + +.tooltip.tooltip-left:before { + margin: initial; + @apply mr-2; + top: 50%; + right: 100%; + transform: translateY(-50%); +} + +.tooltip.tooltip-right:before { + margin: initial; + @apply ml-2; + top: 50%; + left: 100%; + transform: translateY(-50%); +} + +.tooltip:hover:before { + display: block; +} + +.tooltip-notifications:before { + left: -50px; +} + +.tooltip-runall:before { + left: -25px; +} + +.tooltip-collapse-sidebar:before { + left: -75px; +} diff --git a/web/app/src/index.test.ts b/web/app/src/index.test.ts new file mode 100644 index 0000000..5d40967 --- /dev/null +++ b/web/app/src/index.test.ts @@ -0,0 +1,7 @@ +import { describe, expect, it } from 'vitest'; + +describe('sum test', () => { + it('adds 1 + 2 to equal 3', () => { + expect(1 + 2).toBe(3); + }); +}); diff --git a/web/app/src/lib/common/types.ts b/web/app/src/lib/common/types.ts new file mode 100644 index 0000000..5039c3a --- /dev/null +++ b/web/app/src/lib/common/types.ts @@ -0,0 +1,249 @@ +/** + * Represents the result of a single Go test. + */ +export interface Test { + time: string; + name: string; + package: string; + pass: boolean; + skip: boolean; + elapsed: number; // Seconds. + output: string; +} + +/** + * Represents the test results of a Go package. + */ +export interface Package { + time: string; + name: string; + pass: boolean; + passed: number; + skipped: number; + failed: number; + coverage: number; + elapsed: number; // Seconds. + tests: Test[]; +} + +/** + * Represents the test results of one or more Go packages. + */ +export interface Result { + uuid: string; + error: string; + pass: boolean; + start: string; + duration: number; + exitCode: number; + targets: string[]; + passed: number; + failed: number; + skipped: number; + tests: number; + packages: Package[] | null; +} + +/** + * Represents the position in a source file of the beginning and + * end of a block as reported by the coverage profile. + */ +export interface ProfileBoundary { + offset: number; + start: boolean; + count: number; + norm: number; + index: number; +} + +/** + * Represents a profile of a single file in a coverage report. + */ +export interface Profile { + filename: string; + package: string; + path: string; + content: string; + size: number; + coverage: number; + lineCount: number; + boundaries: ProfileBoundary[]; +} + +/** + * Represents a coverage report for a test run. + */ +export interface Report { + mode: CoverageMode; + profiles: Profile[]; + time: string; +} + +/* + * Coverage modes supported by Go. + */ +type CoverageMode = 'set' | 'count' | 'atomic'; + +/** + * States the application can be in: + * + * - init: Initializing components. + * - ready: Ready and waiting to run tests. + * - paused: Automatic test runs is paused. + * - running: Test run is in progress. + * - closing: Shutting down. + * - offline: Backend API is unresponsive. + */ +export type State = 'init' | 'ready' | 'paused' | 'running' | 'closing' | 'offline'; + +/** + * Notification emitted by the backend in the status response. + */ +export interface ServerNotification { + type: NotificationType; + body: string; + tag: string; +} + +/** + * Response from the `GET /api/status` API endpoint. + */ +export interface Status { + state: State; + lastResult?: Result; + root?: string; + notification?: ServerNotification; +} + +/** + * WebSocket message kinds. + */ +export type WSMessageKind = + | 'keepalive' // Dummy message to keep connection alive. + | 'init' // initial message with UI initialization data. + | 'state' // Server state change. + | 'result' // New test result. + | 'resultError' // New test result with error. + | 'resultEmpty' // New test result with no tests. + | 'notification' // Server notification. + | 'connectionClosed'; // Connection closed by server. + +/** + * WebSocket message. + */ +export interface WSMessage { + kind: WSMessageKind; + data?: unknown; +} + +/** + * Metadata about the application. + */ +export interface Metadata { + appName: string; + version: string; + isDevVersion: boolean; + environment: string; + projectUrl: string; + issuesUrl: string; +} + +/** + * Possible values for browser notifications setting. + * + * - pass: Browser notification when tests pass. + * - fail: Browser notification when tests fail. + * - error: Browser notification when tests encounter errors. + * - all: Browser notification on all events. + * + * Error notifications will always be emitted. + */ +export type NotifyOnSetting = 'pass' | 'fail' | 'error' | 'all'; + +/** + * Possible values for sound notifications setting. + * + * - pass: Sound when tests pass. + * - fail: Sound when tests fail. + * - error: Sound when tests encounter errors. + * - all: Sound on all events. + * + * Sound notifications will always be emitted on error. + */ +export type AudioNotifyOnSetting = 'pass' | 'fail' | 'error' | 'all'; + +/** + * Possible values for code coverage theme. + * + * white-to-green: scale from white to green and red for no coverage. + * white-to-blue: scale from white to blue and yellow for no coverage (color blind friendly). + * heatmap: scale from white to berry red and blue for no coverage. + */ +export type CoverageThemeSetting = 'white-to-green' | 'white-to-blue' | 'heatmap'; + +/** + * Possible browser notification types. + * + * - pass: adds a green check mark icon to the notification. + * - fail: adds a red cross icon to the notification. + * - error: adds a red exclamation mark icon to the notification. + * - warning: adds a yellow exclamation mark icon to the notification. + * - info: adds a neutral information icon to the notification. + */ +export type NotificationType = 'pass' | 'fail' | 'error' | 'warning' | 'info'; + +/** + * Represents any object containing a `time` field expected to contain a + * parsable timestamp. + * + * Used for generic sorting functions, etc. + */ +export interface Timestamped { + time: string; +} + +/** + * Possible filter options for package status. + */ +export type FilterPackageStatus = 'passing' | 'failing' | 'noTests'; + +/** + * Possible filter options for test status. + */ +export type FilterTestStatus = 'passing' | 'failing' | 'skipped'; + +/** + * Possible filter options for coverage level. + */ +export type FilterCoverageLevel = 'high' | 'medium' | 'low' | 'none'; + +/** + * Settings for filtering a {@link Result}. + */ +export type FilterSettings = { + search: string; + packageStatus: FilterPackageStatus[]; + testStatus: FilterTestStatus[]; + coverage: FilterCoverageLevel[]; +}; + +export type ErrorType = Error | null | undefined | unknown; + +export type Placement = + | 'top' + | 'top-start' + | 'top-end' + | 'right' + | 'right-start' + | 'right-end' + | 'bottom' + | 'bottom-start' + | 'bottom-end' + | 'left' + | 'left-start' + | 'left-end'; + +export type DebounceFn = unknown, Args extends unknown[]>( + fn: T, + delay: number +) => (...args: Args) => void; diff --git a/web/app/src/lib/common/utils.spec.ts b/web/app/src/lib/common/utils.spec.ts new file mode 100644 index 0000000..99055dc --- /dev/null +++ b/web/app/src/lib/common/utils.spec.ts @@ -0,0 +1,86 @@ +import { escapeHtml, formatBytes, formatDuration, pluralize, sortByTime, truncateFilepath } from './utils'; +import { describe, expect, it } from 'vitest'; + +describe('escapeHtml', () => { + it('escapes special HTML characters', () => { + expect(escapeHtml(`&<>"'/`)).toEqual(`&<>"'/`); + expect(escapeHtml(``)).toEqual( + `<script>alert('xss')</script>` + ); + expect(escapeHtml(`Hello\nWorld!`)).toEqual(`Hello\n<strong>World!</strong>`); + }); +}); + +describe('formatDuration', () => { + it('formats nanoseconds to human readable format', () => { + expect(formatDuration(92_000_000_000)).toEqual('1m32s'); + expect(formatDuration(16_000_000_000)).toEqual('16s'); + expect(formatDuration(8_000_000)).toEqual('8ms'); + }); +}); + +describe('pluralize', () => { + it('pluralizes a word correctly from a number', () => { + expect(pluralize(-1, 'test', 'tests')).toEqual('-1 tests'); + expect(pluralize(0, 'test', 'tests')).toEqual('0 tests'); + expect(pluralize(1, 'test', 'tests')).toEqual('1 test'); + expect(pluralize(2, 'test', 'tests')).toEqual('2 tests'); + }); +}); + +describe('sortByTime', () => { + it('sorts an array of timestamped objects by time in ascending order', () => { + const objs = [ + { time: '2023-04-08T17:20:18.605327' }, + { time: '2023-04-08T17:20:17.605327' }, + { time: '2023-04-07T17:20:18.605327' } + ]; + + const sorted = objs.sort(sortByTime); + + expect(sorted[0].time).toEqual('2023-04-07T17:20:18.605327'); + expect(sorted[1].time).toEqual('2023-04-08T17:20:17.605327'); + expect(sorted[2].time).toEqual('2023-04-08T17:20:18.605327'); + }); +}); + +describe('formatBytes', () => { + it('formats bytes as human-readable text', () => { + expect(formatBytes(0)).toEqual('0 B'); + expect(formatBytes(500)).toEqual('500 B'); + expect(formatBytes(1024)).toEqual('1.0 KiB'); + expect(formatBytes(1024 * 1024)).toEqual('1.0 MiB'); + expect(formatBytes(1_572_864)).toEqual('1.5 MiB'); + }); +}); + +describe('truncateFilepath', () => { + it('truncates a filepath to a maximum length', () => { + const tt = [ + { + s: 'github.com/johndoe/project/warpcore/breach.go', + l: 40, + want: 'github.com/john…roject/warpcore/breach.go' + }, + { + s: 'github.com/johndoe/project/main.go', + l: 40, + want: 'github.com/johndoe/project/main.go' + }, + { + s: 'github.com/johndoe/project/main.go', + l: 0, + want: 'github.com/johndoe/project/main.go' + }, + { + s: 'github.com/johndoe/project/main.go', + l: -1, + want: 'github.com/johndoe/project/main.go' + } + ]; + + for (const tc of tt) { + expect(truncateFilepath(tc.s, tc.l)).toEqual(tc.want); + } + }); +}); diff --git a/web/app/src/lib/common/utils.ts b/web/app/src/lib/common/utils.ts new file mode 100644 index 0000000..44d8956 --- /dev/null +++ b/web/app/src/lib/common/utils.ts @@ -0,0 +1,266 @@ +import { get } from 'svelte/store'; + +import { metadata } from '$lib/stores/metadata'; + +import type { Timestamped } from '$lib/common/types'; + +const meta = get(metadata); + +/** + * Pattern for matching characters that need + * escaping in a HTML context. + */ +const unsafeHtmlCharRegexp = /[&<>"'/]/g; + +/** + * Special HTML characters mapped to their + * HTML entities. + */ +const entityMap: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/' +}; + +/** + * Format nanoseconds to a human readable duration. + * + * @param nanoseconds - Duration in nanoseconds. + * + * @returns A human-readable string representation of duration like `1m32s`. + * + * @throws Error if given a negative number. + */ +export function formatDuration(nanoseconds: number): string { + if (nanoseconds < 0) { + throw new Error('duration must be a positive number'); + } + + const timeUnits = [ + { unit: 'h', value: 60 * 60 * 1000 * 1000 * 1000 }, + { unit: 'm', value: 60 * 1000 * 1000 * 1000 }, + { unit: 's', value: 1000 * 1000 * 1000 }, + { unit: 'ms', value: 1000 * 1000 } + ]; + + let result = ''; + + for (const { unit, value } of timeUnits) { + if (nanoseconds >= value) { + const count = Math.floor(nanoseconds / value); + nanoseconds %= value; + result += `${count}${unit}`; + } + } + + if (!result) { + result = '0ms'; + } + + return result; +} + +/** + * Pluralize a word with number. + * + * Pluralizes a word correctly depending on the number given. + * + * @param num - A number of something. + * @param singular - Word in its singular form. + * @param plural - Word in its plural form. + * + * @returns A string consisting of number followed by the singular + * or plural word, like `0 bugs`, `1 feature`, `4 files`. + */ +export function pluralize(num: number, singular: string, plural: string): string { + let word = plural; + + if (num === 1) { + word = singular; + } + + return num + ' ' + word; +} + +/** + * Escape special HTML characters in a string. + * + * Makes an unsafe string safe to render directly in HTML by escaping + * special characters to their HTML entities. + * + * @param unsafe - Untrusted string. + * + * @returns A safe string with special HTML characters like `<`, `>`, `"`, etc. + * converted to their HTML entities. + */ +export function escapeHtml(unsafe: string): string { + return unsafe.replace(unsafeHtmlCharRegexp, (s: string) => { + return entityMap[s]; + }); +} + +/** + * Escape special characters in a string for use in a RegExp. + * + * Makes an unsafe string safe to use in a RegExp by escaping special + * characters. + * + * @param unsafe - Untrusted string. + * + * @returns A safe string with special RegExp characters like `*`, `+`, `?`, + * etc. converted to their escaped form. + */ +export function escapeRegExp(unsafe: string): string { + return unsafe.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string. +} + +/** + * Sort array of timestamped objects by time in descending order. + * + * Can be given to `Array.sort` as the sorter function. + * + * @param a - Object with a `time` field. + * @param b - Object with a `time` field. + * + * @returns + * -1 if a.time is older than b.time, 1 if a.time is earlier than + * b.time, and 0 if times are equal. + */ +export function sortByTime(a: Timestamped, b: Timestamped): number { + if (a.time < b.time) { + return -1; + } else if (a.time > b.time) { + return 1; + } else { + return 0; + } +} + +/** + * Format bytes as human-readable text. + * + * @remarks Adapted from {@link https://stackoverflow.com/a/14919494}. + * + * @param bytes - Number of bytes. + * @param si - True to use metric (SI) units, aka powers of 1000. False to use + * binary (IEC), aka powers of 1024. + * @param dp - Number of decimal places to display. + * + * @returns Formatted string. + */ +export function formatBytes(bytes: number, si = false, dp = 1): string { + const thresh = si ? 1000 : 1024; + + if (Math.abs(bytes) < thresh) { + return bytes + ' B'; + } + + const units = si + ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + let u = -1; + const r = 10 ** dp; + + do { + bytes /= thresh; + ++u; + } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1); + + return bytes.toFixed(dp) + ' ' + units[u]; +} + +/** + * Truncate a file path with an ellipsis in the middle if its length exceeds a + * specified maximum, ensuring that the base filename is never truncated. + * + * @param path - The file path to be truncated. + * @param maxLength - The maximum allowed length for the truncated file path. + * + * @returns + * Truncated file path with an ellipsis in the middle if the input length + * exceeds maxLength, or the original file path if it doesn't exceed maxLength + * or if the base filename's length is equal to or greater than maxLength. + */ +export function truncateFilepath(path: string, maxLength: number): string { + const lastSlashIndex = path.lastIndexOf('/'); + const dirname = path.slice(0, lastSlashIndex); + const basename = path.slice(lastSlashIndex + 1); + + if (path.length <= maxLength || basename.length >= maxLength) { + return path; + } + + const remainingLength = maxLength - basename.length - 1; // Subtract 1 for the ellipsis character + const halfRemainingLength = remainingLength / 2; + const start = dirname.slice(0, Math.ceil(halfRemainingLength)); + const end = dirname.slice(-Math.floor(halfRemainingLength)); + + return `${start}…${end}/${basename}`; +} + +/** + * Build a query string from key-value pairs. + * + * @param params - Key-value pairs to be encoded as a query string. + * + * @returns A query string, like `foo=bar&baz=qux`. + */ +export function buildQueryString(params: { [key: string]: string | number | boolean }): string { + const searchParams = new URLSearchParams(); + + for (const key in params) { + if (Object.prototype.hasOwnProperty.call(params, key)) { + searchParams.append(key, params[key].toString()); + } + } + + return searchParams.toString(); +} + +/** + * Debounce a function by the specified wait time. + * + * @param func - Function to debounce. + * @param wait - Milliseconds to wait before invoking the function. + * + * @returns A debounced version of the given function. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function debounce any>(func: T, wait: number): (...args: Parameters) => void { + let timeoutId: ReturnType | null = null; + + return (...args: Parameters) => { + if (timeoutId !== null) { + clearTimeout(timeoutId); + } + + timeoutId = setTimeout(() => { + func(...args); + timeoutId = null; + }, wait); + }; +} + +/** + * Sleep for the specified duration. + * + * @param duration - Milliseconds to sleep. + * @returns A promise that resolves after the specified duration. + */ +export function sleep(duration: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, duration); + }); +} + +/** + * Set the page title. + * + * @param title - Page title. + */ +export function setPageTitle(title: string): void { + document.title = `${title} | ${meta.appName}`; +} diff --git a/web/app/src/lib/components/AppBar.svelte b/web/app/src/lib/components/AppBar.svelte new file mode 100644 index 0000000..6a97a9d --- /dev/null +++ b/web/app/src/lib/components/AppBar.svelte @@ -0,0 +1,177 @@ + + + + +
+ {$metadata.appName} + {#key rootVal} + {#if rootVal} +
+ {rootVal} +
+ {:else} +
initializing...
+ {/if} + {/key} +
+
+ +
+ {#if audioContextUnlockedVal} + + {:else} + + {/if} + + +
+ +
+ {#if stateVal === 'paused'} +   + + {:else if stateVal === 'offline' || stateVal === 'closing'} +   + + {:else if stateVal === 'init'} +   + + {:else} +   + + {/if} +
+ +
+ {#if stateVal === 'paused'} + + {:else if stateVal === 'offline' || stateVal === 'closing'} + + {:else if stateVal === 'init'} + + {:else} + + {/if} +
+ + +
+ +
+ +
+ +
+
+
diff --git a/web/app/src/lib/components/CodeBlock.spec.ts b/web/app/src/lib/components/CodeBlock.spec.ts new file mode 100644 index 0000000..25b3f3c --- /dev/null +++ b/web/app/src/lib/components/CodeBlock.spec.ts @@ -0,0 +1,63 @@ +import { tick } from 'svelte'; + +import { render, screen } from '@testing-library/svelte'; +import { describe, expect, it } from 'vitest'; + +import CodeBlock from '$lib/components/CodeBlock.svelte'; + +import { coverageTheme } from '$lib/stores/settings'; + +describe('CodeBlock', () => { + describe('color legend', () => { + it('has a coverage scale', () => { + render(CodeBlock, { props: { code: '' } }); + + expect(screen.getByText('lowest')).toHaveClass('gokiburi-cov-1'); + + const scale = screen.getAllByText('※'); + expect(scale.length).toBe(10); + scale.forEach((el, i) => { + expect(el).toHaveClass(`gokiburi-cov-${i + 1}`); + }); + + expect(screen.getByText('highest')).toHaveClass('gokiburi-cov-10'); + }); + + it('has an example for no coverage', () => { + render(CodeBlock, { props: { code: '' } }); + + expect(screen.getByText('no coverage')).toHaveClass('gokiburi-cov-0'); + }); + + it('has an example for non-runnable code', () => { + render(CodeBlock, { props: { code: '' } }); + + expect(screen.getByText('non-runnable')).toHaveClass('!text-surface-400'); + }); + }); + + describe('code display', () => { + it('renders code as raw HTML', () => { + const { getByRole } = render(CodeBlock, { + props: { code: 'code' } + }); + + const code = getByRole('code'); + + expect(code.innerHTML).toEqual( + 'code' + ); + }); + + describe('when theme setting is changed', () => { + it('changes the theme', async () => { + const { getByRole } = render(CodeBlock, { props: { code: '' } }); + + coverageTheme.set('heatmap'); + await tick(); + + expect(getByRole('code')).toHaveClass('heatmap'); + }); + }); + }); +}); diff --git a/web/app/src/lib/components/CodeBlock.svelte b/web/app/src/lib/components/CodeBlock.svelte new file mode 100644 index 0000000..362f3b7 --- /dev/null +++ b/web/app/src/lib/components/CodeBlock.svelte @@ -0,0 +1,176 @@ + + +
+ +
{@html code}
+
+ + diff --git a/web/app/src/lib/components/Combobox.svelte b/web/app/src/lib/components/Combobox.svelte new file mode 100644 index 0000000..5c5ce74 --- /dev/null +++ b/web/app/src/lib/components/Combobox.svelte @@ -0,0 +1,101 @@ + + + + +
+ + {#if multiple && showToggleAll} + + + {#if values.length === 0} + + {:else} + + {/if} + + {toggleAllLabel} + + {/if} + {#each options as option (option.value)} + + + {#if values.includes(option.value)} + + {:else} + + {/if} + + {option.label} + + {/each} + +
diff --git a/web/app/src/lib/components/CoverageIcon.spec.ts b/web/app/src/lib/components/CoverageIcon.spec.ts new file mode 100644 index 0000000..dd9bcbb --- /dev/null +++ b/web/app/src/lib/components/CoverageIcon.spec.ts @@ -0,0 +1,208 @@ +import { tick } from 'svelte'; + +import { + mdiCircleOutline, + mdiCircleSlice1, + mdiCircleSlice2, + mdiCircleSlice3, + mdiCircleSlice4, + mdiCircleSlice5, + mdiCircleSlice6, + mdiCircleSlice7, + mdiCircleSlice8 +} from '@mdi/js'; +import { render } from '@testing-library/svelte'; +import { renderComponentToString } from '$lib/tests/helpers'; +import { afterEach, describe, expect, it } from 'vitest'; + +import CoverageIcon from '$lib/components/CoverageIcon.svelte'; +import SvgIcon from '$lib/components/SvgIcon.svelte'; + +import { coverageHighMin, coverageMediumMin, coverageUseColor } from '$lib/stores/settings'; + +afterEach(() => { + coverageUseColor.set('true'); +}); + +describe('CoverageIcon', () => { + describe('when coverage is 100 and above', () => { + it('renders coverage icon eight', async () => { + const results = render(CoverageIcon, { props: { percentage: 100 } }); + const icon = renderComponentToString(SvgIcon, { path: mdiCircleSlice8, size: '1.5em', class: 'inline-block' }); + expect(results.container.innerHTML).toContain(icon); + results.component.$set({ percentage: 105 }); + await tick(); + expect(results.container.innerHTML).toContain(icon); + }); + }); + + describe('when coverage is 87.5 and above', () => { + it('renders coverage icon seven', async () => { + const results = render(CoverageIcon, { props: { percentage: 87.5 } }); + const icon = renderComponentToString(SvgIcon, { path: mdiCircleSlice7, size: '1.5em', class: 'inline-block' }); + expect(results.container.innerHTML).toContain(icon); + results.component.$set({ percentage: 92 }); + await tick(); + expect(results.container.innerHTML).toContain(icon); + }); + }); + + describe('when coverage is 75 and above', () => { + it('renders coverage icon six', async () => { + const results = render(CoverageIcon, { props: { percentage: 75 } }); + const icon = renderComponentToString(SvgIcon, { path: mdiCircleSlice6, size: '1.5em', class: 'inline-block' }); + expect(results.container.innerHTML).toContain(icon); + results.component.$set({ percentage: 80 }); + await tick(); + expect(results.container.innerHTML).toContain(icon); + }); + }); + + describe('when coverage is 62.5 and above', () => { + it('renders coverage icon five', async () => { + const results = render(CoverageIcon, { props: { percentage: 62.5 } }); + const icon = renderComponentToString(SvgIcon, { path: mdiCircleSlice5, size: '1.5em', class: 'inline-block' }); + expect(results.container.innerHTML).toContain(icon); + results.component.$set({ percentage: 67 }); + await tick(); + expect(results.container.innerHTML).toContain(icon); + }); + }); + + describe('when coverage is 50 and above', () => { + it('renders coverage icon four', async () => { + const results = render(CoverageIcon, { props: { percentage: 50 } }); + const icon = renderComponentToString(SvgIcon, { path: mdiCircleSlice4, size: '1.5em', class: 'inline-block' }); + expect(results.container.innerHTML).toContain(icon); + results.component.$set({ percentage: 55 }); + await tick(); + expect(results.container.innerHTML).toContain(icon); + }); + }); + + describe('when coverage is 37.5 and above', () => { + it('renders coverage icon three', async () => { + const results = render(CoverageIcon, { props: { percentage: 37.5 } }); + const icon = renderComponentToString(SvgIcon, { path: mdiCircleSlice3, size: '1.5em', class: 'inline-block' }); + expect(results.container.innerHTML).toContain(icon); + results.component.$set({ percentage: 42 }); + await tick(); + expect(results.container.innerHTML).toContain(icon); + }); + }); + + describe('when coverage is 25 and above', () => { + it('renders coverage icon two', async () => { + const results = render(CoverageIcon, { props: { percentage: 25 } }); + const icon = renderComponentToString(SvgIcon, { path: mdiCircleSlice2, size: '1.5em', class: 'inline-block' }); + expect(results.container.innerHTML).toContain(icon); + results.component.$set({ percentage: 30 }); + await tick(); + expect(results.container.innerHTML).toContain(icon); + }); + }); + + describe('when coverage is 12.5 and above', () => { + it('renders coverage icon one', async () => { + const results = render(CoverageIcon, { props: { percentage: 12.5 } }); + const icon = renderComponentToString(SvgIcon, { path: mdiCircleSlice1, size: '1.5em', class: 'inline-block' }); + expect(results.container.innerHTML).toContain(icon); + results.component.$set({ percentage: 17 }); + await tick(); + expect(results.container.innerHTML).toContain(icon); + }); + }); + + describe('when coverage is 0 and above', () => { + it('renders coverage icon zero', async () => { + const results = render(CoverageIcon, { props: { percentage: 0 } }); + const icon = renderComponentToString(SvgIcon, { path: mdiCircleOutline, size: '1.5em', class: 'inline-block' }); + expect(results.container.innerHTML).toContain(icon); + results.component.$set({ percentage: 5 }); + await tick(); + expect(results.container.innerHTML).toContain(icon); + }); + }); + + describe('positive coverage', () => { + describe('when coverage is equal to or above positive coverage minimum', () => { + it('renders a green coverage icon', async () => { + coverageHighMin.set('80'); + const results = render(CoverageIcon, { props: { percentage: 80 } }); + expect(results.container.innerHTML).toContain(''); + results.component.$set({ percentage: 85 }); + await tick(); + expect(results.container.innerHTML).toContain(''); + }); + + describe('when use color setting is false', () => { + it('renders a neutral coverage icon', () => { + coverageHighMin.set('80'); + coverageUseColor.set('false'); + const { container } = render(CoverageIcon, { props: { percentage: 80 } }); + expect(container.innerHTML).toContain(''); + }); + }); + }); + + describe('when coverage is below positive coverage minimum', () => { + it('does not render a green coverage icon', () => { + coverageHighMin.set('80'); + const { container } = render(CoverageIcon, { props: { percentage: 75 } }); + expect(container.innerHTML).not.toContain(''); + }); + }); + }); + + describe('warning coverage', () => { + describe('when coverage is equal to or above warning coverage minimum', () => { + it('renders a yellow coverage icon', async () => { + coverageMediumMin.set('65'); + const results = render(CoverageIcon, { props: { percentage: 65 } }); + expect(results.container.innerHTML).toContain(''); + results.component.$set({ percentage: 65 }); + await tick(); + expect(results.container.innerHTML).toContain(''); + }); + + describe('when use color setting is false', () => { + it('renders a neutral coverage icon', () => { + coverageMediumMin.set('65'); + coverageUseColor.set('false'); + const { container } = render(CoverageIcon, { props: { percentage: 65 } }); + expect(container.innerHTML).toContain(''); + }); + }); + }); + + describe('when coverage is below warning coverage minimum', () => { + it('does not render a yellow coverage icon', () => { + coverageMediumMin.set('65'); + const { container } = render(CoverageIcon, { props: { percentage: 60 } }); + expect(container.innerHTML).not.toContain(''); + }); + }); + }); + + describe('negative coverage', () => { + describe('when coverage is below warning coverage minimum', () => { + it('renders a red coverage icon', async () => { + coverageMediumMin.set('65'); + const results = render(CoverageIcon, { props: { percentage: 40 } }); + expect(results.container.innerHTML).toContain(''); + results.component.$set({ percentage: 60 }); + await tick(); + expect(results.container.innerHTML).toContain(''); + }); + + describe('when use color setting is false', () => { + it('renders a neutral coverage icon', () => { + coverageMediumMin.set('65'); + coverageUseColor.set('false'); + const { container } = render(CoverageIcon, { props: { percentage: 40 } }); + expect(container.innerHTML).toContain(''); + }); + }); + }); + }); +}); diff --git a/web/app/src/lib/components/CoverageIcon.svelte b/web/app/src/lib/components/CoverageIcon.svelte new file mode 100644 index 0000000..65c2592 --- /dev/null +++ b/web/app/src/lib/components/CoverageIcon.svelte @@ -0,0 +1,89 @@ + + + + {#if percentage >= 100} + + {:else if percentage >= 87.5} + + {:else if percentage >= 75} + + {:else if percentage >= 62.5} + + {:else if percentage >= 50} + + {:else if percentage >= 37.5} + + {:else if percentage >= 25} + + {:else if percentage >= 12.5} + + {:else} + + {/if} + diff --git a/web/app/src/lib/components/CoverageReport.spec.ts b/web/app/src/lib/components/CoverageReport.spec.ts new file mode 100644 index 0000000..ed9b94d --- /dev/null +++ b/web/app/src/lib/components/CoverageReport.spec.ts @@ -0,0 +1,22 @@ +import { tick } from 'svelte'; + +import { render, screen } from '@testing-library/svelte'; +import coverageReportResponse from '$lib/tests/apiResponses/coverageReport.json' assert { type: 'JSON' }; +import { describe, expect, it, vi } from 'vitest'; +import createFetchMock from 'vitest-fetch-mock'; + +import CoverageReport from '$lib/components/CoverageReport.svelte'; + +const fetchMocker = createFetchMock(vi); + +fetchMocker.enableMocks(); + +afterEach(() => { + fetchMocker.resetMocks(); +}); + +describe('CoverageReport', () => { + it('fetches coverage report from the API', () => { + fetchMocker.mockResponseOnce(JSON.stringify(coverageReportResponse)); + }); +}); diff --git a/web/app/src/lib/components/CoverageReport.svelte b/web/app/src/lib/components/CoverageReport.svelte new file mode 100644 index 0000000..ff62a5f --- /dev/null +++ b/web/app/src/lib/components/CoverageReport.svelte @@ -0,0 +1,156 @@ + + +
+ {#if loading} +
+ +

Loading coverage profile...

+
+ {:else if error} +
+ +

Failed to load coverage report

+
+ Please check if {$metadata.appName} is still running and + try again. +
+
+ + {#if showErrorDetails} +
+
    +
  • Error: {errorMsg}
  • +
  • Result UUID: {uuid}
  • +
  • Package: {pkg}
  • +
+
+ {/if} +
+
+ {:else if profiles} +
+
+
+
+ {pkgs.length} + package +
+ +
+
+
+
+ {profiles.length} + file +
+ +
+
+ {#if profile} +
+
{profile.filename}
+
{profile.lineCount}L
+
{formatBytes(profile.size)}
+
+ + {profile.coverage}% ({report?.mode}) +
+
+ {/if} +
+ +
+ {#if profile} +
+ {/if} +
+ {/if} +
diff --git a/web/app/src/lib/components/OfflineAlert.spec.ts b/web/app/src/lib/components/OfflineAlert.spec.ts new file mode 100644 index 0000000..88f7107 --- /dev/null +++ b/web/app/src/lib/components/OfflineAlert.spec.ts @@ -0,0 +1,65 @@ +import { tick } from 'svelte'; + +import { render, screen } from '@testing-library/svelte'; +import { describe, expect, it } from 'vitest'; + +import OfflineAlert from '$lib/components/OfflineAlert.svelte'; + +import { state } from '$lib/stores/status'; + +describe('OfflineAlert', () => { + describe('when application state is init', () => { + it('is does not render', () => { + state.set('init'); + render(OfflineAlert, {}); + expect(screen.queryByText('Backend API Unresponsive')).toBeNull(); + }); + }); + + describe('when application state is ready', () => { + it('is does not render', () => { + state.set('ready'); + render(OfflineAlert, {}); + expect(screen.queryByText('Backend API Unresponsive')).toBeNull(); + }); + }); + + describe('when application state is running', () => { + it('is does not render', () => { + state.set('running'); + render(OfflineAlert, {}); + expect(screen.queryByText('Backend API Unresponsive')).toBeNull(); + }); + }); + + describe('when application state is paused', () => { + it('is does not render', () => { + state.set('paused'); + render(OfflineAlert, {}); + expect(screen.queryByText('Backend API Unresponsive')).toBeNull(); + }); + }); + + describe('when application state is offline', () => { + it('renders', () => { + state.set('offline'); + render(OfflineAlert, {}); + expect(screen.getByText('Backend API Unresponsive')).toBeInTheDocument(); + }); + + describe('when the application state is offline but changes to ready', () => { + it('does not render', async () => { + render(OfflineAlert, {}); + state.set('offline'); + await tick(); + + expect(screen.queryByText('Backend API Unresponsive')).toBeInTheDocument(); + + state.set('ready'); + await tick(); + + expect(screen.queryByText('Backend API Unresponsive')).toBeNull(); + }); + }); + }); +}); diff --git a/web/app/src/lib/components/OfflineAlert.svelte b/web/app/src/lib/components/OfflineAlert.svelte new file mode 100644 index 0000000..6d85f30 --- /dev/null +++ b/web/app/src/lib/components/OfflineAlert.svelte @@ -0,0 +1,30 @@ + + +{#if stateVal === 'offline'} +
+
+
Backend API Unresponsive
+
+{/if} diff --git a/web/app/src/lib/components/ResultButton.spec.ts b/web/app/src/lib/components/ResultButton.spec.ts new file mode 100644 index 0000000..0e32da8 --- /dev/null +++ b/web/app/src/lib/components/ResultButton.spec.ts @@ -0,0 +1,82 @@ +import { fireEvent, render } from '@testing-library/svelte'; +import { describe, expect, it, vi } from 'vitest'; + +import ResultButton from '$lib/components/ResultButton.svelte'; + +describe('ResultButton', () => { + describe('when given a passing result', () => { + it('renders as success variant', () => { + const { getByRole } = render(ResultButton, { props: { result: { uuid: 'deadbeef', pass: true } } }); + + expect(getByRole('button')).toHaveClass('variant-soft-success'); + }); + + describe('when set as active', () => { + it('renders as bordered success variant', () => { + const { getByRole } = render(ResultButton, { + props: { active: true, result: { uuid: 'deadbeef', pass: true } } + }); + + expect(getByRole('button')).toHaveClass('variant-ghost-success'); + }); + }); + + describe('when clicked', () => { + it('emits a click event with result UUID', () => { + const handleClick = vi.fn(); + const { component, getByRole } = render(ResultButton, { + props: { result: { uuid: 'deadbeef', pass: true } } + }); + component.$on('click', handleClick); + + const btn = getByRole('button'); + + fireEvent.click(btn); + + const expectedEvent = new CustomEvent('click', { + detail: 'deadbeef' + }); + + expect(handleClick).toHaveBeenCalledWith(expectedEvent); + }); + }); + }); + + describe('when given a failing result', () => { + it('renders as error variant', () => { + const { getByRole } = render(ResultButton, { props: { result: { uuid: 'deadbeef', pass: false } } }); + + expect(getByRole('button')).toHaveClass('variant-soft-error'); + }); + + describe('when set as active', () => { + it('renders as bordered error variant', () => { + const { getByRole } = render(ResultButton, { + props: { active: true, result: { uuid: 'deadbeef', pass: false } } + }); + + expect(getByRole('button')).toHaveClass('variant-ghost-error'); + }); + }); + + describe('when clicked', () => { + it('emits a click event with result UUID', () => { + const handleClick = vi.fn(); + const { component, getByRole } = render(ResultButton, { + props: { result: { uuid: 'deadbeef', pass: false } } + }); + component.$on('click', handleClick); + + const btn = getByRole('button'); + + fireEvent.click(btn); + + const expectedEvent = new CustomEvent('click', { + detail: 'deadbeef' + }); + + expect(handleClick).toHaveBeenCalledWith(expectedEvent); + }); + }); + }); +}); diff --git a/web/app/src/lib/components/ResultButton.svelte b/web/app/src/lib/components/ResultButton.svelte new file mode 100644 index 0000000..45c2a55 --- /dev/null +++ b/web/app/src/lib/components/ResultButton.svelte @@ -0,0 +1,40 @@ + + + diff --git a/web/app/src/lib/components/ResultFilter.svelte b/web/app/src/lib/components/ResultFilter.svelte new file mode 100644 index 0000000..4963c74 --- /dev/null +++ b/web/app/src/lib/components/ResultFilter.svelte @@ -0,0 +1,191 @@ + + +
+
+
+
+
+ +
+
+ +
+ filterPackageStatus.set(JSON.stringify(settings.packageStatus))} + /> + + filterTestStatus.set(JSON.stringify(settings.testStatus))} + /> + + filterCoverageLevel.set(JSON.stringify(settings.coverage))} + /> + + +
+
+
diff --git a/web/app/src/lib/components/ResultListBoxItem.spec.ts b/web/app/src/lib/components/ResultListBoxItem.spec.ts new file mode 100644 index 0000000..5824c89 --- /dev/null +++ b/web/app/src/lib/components/ResultListBoxItem.spec.ts @@ -0,0 +1,84 @@ +import { render } from '@testing-library/svelte'; +import { describe, expect, it, vi } from 'vitest'; + +import ResultListBoxItem from '$lib/components/ResultListBoxItem.svelte'; + +const startTime = '2023-01-01T13:37:00.511Z'; + +describe('ResultListBoxItem', () => { + describe('when given a passing result', () => { + it('renders as success variant', () => { + const { getByRole } = render(ResultListBoxItem, { + props: { result: { uuid: 'deadbeef', pass: true, start: startTime }, group: 'beefdead' } + }); + + expect(getByRole('option')).toHaveClass('variant-soft-success'); + }); + + it('displays result relative time', () => { + vi.setSystemTime('2023-01-01T11:37:00.511Z'); + + const { getByRole } = render(ResultListBoxItem, { + props: { result: { uuid: 'deadbeef', pass: true, start: startTime }, group: 'beefdead' } + }); + + expect(getByRole('option')).toHaveTextContent('2 hours'); + }); + + it('displays number of passing and total tests', () => { + const { getByRole } = render(ResultListBoxItem, { + props: { result: { uuid: 'deadbeef', pass: true, start: startTime, passed: 13, tests: 37 }, group: 'beefdead' } + }); + + expect(getByRole('option')).toHaveTextContent('13/37'); + }); + + describe('when selected', () => { + it('renders as bordered success variant', () => { + const { getByRole } = render(ResultListBoxItem, { + props: { result: { uuid: 'deadbeef', pass: true, start: startTime }, group: 'deadbeef' } + }); + + expect(getByRole('option')).toHaveClass('variant-ghost-success'); + }); + }); + }); + + describe('when given failed result', () => { + it('renders as error variant', () => { + const { getByRole } = render(ResultListBoxItem, { + props: { result: { uuid: 'deadbeef', pass: false, start: startTime }, group: 'beefdead' } + }); + + expect(getByRole('option')).toHaveClass('variant-soft-error'); + }); + + it('displays result relative time', () => { + vi.setSystemTime('2023-01-01T11:37:00.511Z'); + + const { getByRole } = render(ResultListBoxItem, { + props: { result: { uuid: 'deadbeef', pass: false, start: startTime }, group: 'beefdead' } + }); + + expect(getByRole('option')).toHaveTextContent('2 hours'); + }); + + it('displays number of failing and total tests', () => { + const { getByRole } = render(ResultListBoxItem, { + props: { result: { uuid: 'deadbeef', pass: false, start: startTime, failed: 13, tests: 37 }, group: 'beefdead' } + }); + + expect(getByRole('option')).toHaveTextContent('13/37'); + }); + + describe('when selected', () => { + it('renders as bordered error variant', () => { + const { getByRole } = render(ResultListBoxItem, { + props: { result: { uuid: 'deadbeef', pass: false, start: startTime }, group: 'deadbeef' } + }); + + expect(getByRole('option')).toHaveClass('variant-ghost-error'); + }); + }); + }); +}); diff --git a/web/app/src/lib/components/ResultListBoxItem.svelte b/web/app/src/lib/components/ResultListBoxItem.svelte new file mode 100644 index 0000000..1d8a50f --- /dev/null +++ b/web/app/src/lib/components/ResultListBoxItem.svelte @@ -0,0 +1,72 @@ + + + + + {#if result.pass} + + {:else if result.error} + {#if result.error == 'timeout'} + + {:else}j + + {/if} + {:else} + + {/if} + + + + + + {#if result.pass} + {result.passed}/{result.tests} + {:else if result.error} + 0/0 + {:else} + {result.failed}/{result.tests} + {/if} + + + {relativeTime} + diff --git a/web/app/src/lib/components/ResultPackageTable.svelte b/web/app/src/lib/components/ResultPackageTable.svelte new file mode 100644 index 0000000..44cb114 --- /dev/null +++ b/web/app/src/lib/components/ResultPackageTable.svelte @@ -0,0 +1,159 @@ + + +
+
+
+ {#if pkg.pass} + + {:else} + + {/if} +
+ +
+ {pkg.name} + {#if !hasTests}has no tests{/if} +
+ +
+ {#key pkg.coverage} + {#if hasTests} + + {#if coverageShowBadgesVal} + + {:else} + + {/if} + {/if} + {/key} +
+
+ +
+ {#if failingTests.length > 0 && filter.testStatus.includes('failing')} + + {/if} + + {#if passingTests.length > 0 && filter.testStatus.includes('passing')} + + {/if} + + {#if skippedTests.length > 0 && filter.testStatus.includes('skipped')} + + {/if} +
+ + {#if hasTests} +
+ {#key pkg.tests.length} +
{pluralize(pkg.tests.length, 'test', 'tests')}
+ {/key} + + {#key pkg.failed} +
{pkg.failed} failing
+ {/key} + + {#key pkg.skipped} +
{pkg.skipped} skipped
+ {/key} + + {#key pkg.elapsed} +
{pkg.elapsed}s
+ {/key} +
+ {/if} +
diff --git a/web/app/src/lib/components/ResultPackageTableGroup.svelte b/web/app/src/lib/components/ResultPackageTableGroup.svelte new file mode 100644 index 0000000..e5d5fdb --- /dev/null +++ b/web/app/src/lib/components/ResultPackageTableGroup.svelte @@ -0,0 +1,89 @@ + + +
+
(collapsed = !collapsed)} + on:keydown + > +
+ {header} + + + +
+
+ + {#if showTests} +
+ {#if preview.length > 0} + {#each preview as test} + + {/each} +
+
+
+ Load {pluralize(tests.length - preview.length, 'more test', 'more tests')} + + + +
+
+ {:else} + {#each tests as test (test.package + test.name)} + + {/each} + {/if} +
+ {/if} +
diff --git a/web/app/src/lib/components/ResultPackageTableRow.svelte b/web/app/src/lib/components/ResultPackageTableRow.svelte new file mode 100644 index 0000000..b9586d2 --- /dev/null +++ b/web/app/src/lib/components/ResultPackageTableRow.svelte @@ -0,0 +1,113 @@ + + +
+
(showDetails = !showDetails)} + on:keyup + > +
+ {#if test.skip} + + {:else if test.pass} + + {:else} + + {/if} +
+ +
+ {@html safeHighlightedTestName} +
+ +
+ {#key test.elapsed} + + {test.elapsed}s + + {/key} +
+
+ {#if showDetails} + {@const time = new Date(test.time)} +
+
+ +
+
+ +
+
+
+
+ +
+
+ at {format(time, 'HH:mm:ss')} + {#if !isToday(time)} + on {format(time, 'PP')} + {/if} + ({formatDistanceToNow(time)} ago) +
+
+ {/if} +
+ + diff --git a/web/app/src/lib/components/ResultStats.svelte b/web/app/src/lib/components/ResultStats.svelte new file mode 100644 index 0000000..2e8f194 --- /dev/null +++ b/web/app/src/lib/components/ResultStats.svelte @@ -0,0 +1,41 @@ + + +
+
+ + {pluralize(result.packages?.length || 0, 'Package', 'Packages')} +
+
+ + {pluralize(result.tests, 'Test', 'Tests')} +
+
0} class:!text-primary-600={result.failed == 0}> + + {result.failed} Failing +
+
0} class:!text-primary-600={result.skipped == 0}> + + {result.skipped} Skipped +
+
+ + {formatDuration(result.duration)} +
+
diff --git a/web/app/src/lib/components/Settings.svelte b/web/app/src/lib/components/Settings.svelte new file mode 100644 index 0000000..e84482a --- /dev/null +++ b/web/app/src/lib/components/Settings.svelte @@ -0,0 +1,341 @@ + + +
+
+
Settings
+ + runAllOnInit.set(runAllOnInitVal ? 'true' : 'false')} + active="bg-success-500" + > + Run all tests on initial load and resume + +
+ +
+
Sound notifications
+ {#if !audio.hasAudio()} +
Your browser does not support audio playback.
+ {:else} + + Play sound notifications + + + + + + +
+ Preview sounds: + + + +
+ {/if} +
+ +
+
Browser notifications
+ + notificationsActive.set(notificationsActiveVal ? 'true' : 'false')} + active="bg-success-500" + > + Send browser notifications + + + +
+ +
+
Code coverage
+ + + + coverageShowBadges.set(coverageShowBadgesVal ? 'true' : 'false')} + active="bg-success-500" + > + Show coverage badges + + +
+ coverageUseColor.set(coverageUseColorVal ? 'true' : 'false')} + active="bg-success-500" + disabled={!coverageShowBadgesVal} + > + Colorize coverage badges + +
+ +
+ +
+ +
+ +
+ +
+ + Low coverage badges when coverage is below {coverageMediumMinVal}% + + +
+
+
diff --git a/web/app/src/lib/components/Sidebar.svelte b/web/app/src/lib/components/Sidebar.svelte new file mode 100644 index 0000000..64c8fd7 --- /dev/null +++ b/web/app/src/lib/components/Sidebar.svelte @@ -0,0 +1,191 @@ + + +